001: /*
002: * JOSSO: Java Open Single Sign-On
003: *
004: * Copyright 2004-2008, Atricore, Inc.
005: *
006: * This is free software; you can redistribute it and/or modify it
007: * under the terms of the GNU Lesser General Public License as
008: * published by the Free Software Foundation; either version 2.1 of
009: * the License, or (at your option) any later version.
010: *
011: * This software is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: *
016: * You should have received a copy of the GNU Lesser General Public
017: * License along with this software; if not, write to the Free
018: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
019: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
020: */
021:
022: package org.josso.tc60.gateway.reverseproxy;
023:
024: import org.apache.catalina.Lifecycle;
025: import org.apache.catalina.LifecycleException;
026: import org.apache.catalina.LifecycleListener;
027: import org.apache.catalina.connector.Request;
028: import org.apache.catalina.connector.Response;
029: import org.apache.catalina.util.LifecycleSupport;
030: import org.apache.catalina.valves.ValveBase;
031: import org.apache.commons.httpclient.*;
032: import org.apache.commons.httpclient.methods.GetMethod;
033: import org.apache.commons.httpclient.methods.HeadMethod;
034: import org.apache.commons.httpclient.methods.PostMethod;
035: import org.apache.commons.httpclient.methods.PutMethod;
036: import org.josso.Lookup;
037: import org.josso.gateway.Constants;
038: import org.josso.gateway.reverseproxy.ProxyContextConfig;
039: import org.josso.gateway.reverseproxy.ReverseProxyConfiguration;
040:
041: import javax.servlet.http.HttpServletRequest;
042: import javax.servlet.http.HttpServletResponse;
043: import java.io.IOException;
044: import java.util.Enumeration;
045: import java.util.StringTokenizer;
046:
047: /**
048: * Reverse Proxy implementation using Tomcat Valves.
049: *
050: * @deprecated This component is no longer needed for N-Tier configurations.
051: *
052: * @author <a href="mailto:gbrigand@josso.org">Gianluca Brigandi</a>
053: * @version CVS $Id: ReverseProxyValve.java 405 2007-01-10 20:36:54Z sgonzalez $
054: */
055:
056: public class ReverseProxyValve extends ValveBase implements Lifecycle {
057:
058: // ----------------------------------------------------- Constants
059:
060: static final String METHOD_GET = "GET";
061: static final String METHOD_POST = "POST";
062: static final String METHOD_PUT = "PUT";
063: private final String METHOD_HEAD = "HEAD";
064:
065: // ----------------------------------------------------- Instance Variables
066: private String _configurationFileName;
067: private ReverseProxyConfiguration _rpc;
068: private boolean started;
069: private String _reverseProxyHost; // Reverse proxy host value.
070: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
071:
072: /**
073: * The descriptive information related to this implementation.
074: */
075: private static final String info = "org.josso.tc60.gateway.reverseproxy.ReverseProxyValve/1.0";
076:
077: // ------------------------------------------------------ Lifecycle Methods
078:
079: /**
080: * Add a lifecycle event listener to this component.
081: *
082: * @param listener The listener to add
083: */
084: public void addLifecycleListener(LifecycleListener listener) {
085:
086: lifecycle.addLifecycleListener(listener);
087:
088: }
089:
090: /**
091: * Get the lifecycle listeners associated with this lifecycle. If this
092: * Lifecycle has no listeners registered, a zero-length array is returned.
093: */
094: public LifecycleListener[] findLifecycleListeners() {
095:
096: return lifecycle.findLifecycleListeners();
097:
098: }
099:
100: /**
101: * Remove a lifecycle event listener from this component.
102: *
103: * @param listener The listener to remove
104: */
105: public void removeLifecycleListener(LifecycleListener listener) {
106:
107: lifecycle.removeLifecycleListener(listener);
108:
109: }
110:
111: /**
112: * Prepare for the beginning of active use of the public methods of this
113: * component. This method should be called after <code>configure()</code>,
114: * and before any of the public methods of the component are utilized.
115: *
116: * @throws LifecycleException if this component detects a fatal error
117: * that prevents this component from being used
118: */
119: public void start() throws LifecycleException {
120:
121: // Validate and update our current component state
122: if (started)
123: throw new LifecycleException("ReverseProxy already started");
124: lifecycle.fireLifecycleEvent(START_EVENT, null);
125: started = true;
126:
127: try {
128: _rpc = Lookup.getInstance()
129: .lookupReverseProxyConfiguration();
130: } catch (Exception e) {
131: throw new LifecycleException(e.getMessage(), e);
132: }
133:
134: log("Started");
135: }
136:
137: /**
138: * Gracefully terminate the active use of the public methods of this
139: * component. This method should be the last one called on a given
140: * instance of this component.
141: *
142: * @throws LifecycleException if this component detects a fatal error
143: * that needs to be reported
144: */
145: public void stop() throws LifecycleException {
146:
147: // Validate and update our current component state
148: if (!started)
149: throw new LifecycleException("ReverseProxy not started");
150: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
151: started = false;
152:
153: log("Stopped");
154:
155: }
156:
157: // ------------------------------------------------------------- Properties
158:
159: /**
160: * Sets reverse proxy configuration file name.
161: *
162: * @param configurationFileName configuration file name property value
163: */
164: public void setConfiguration(String configurationFileName) {
165: _configurationFileName = configurationFileName;
166: }
167:
168: /**
169: * Returns reverse proxy configuration file name.
170: *
171: * @return configuration property value
172: */
173: public String getConfiguration() {
174: return _configurationFileName;
175: }
176:
177: /**
178: * Return descriptive information about this Valve implementation.
179: */
180: public String getInfo() {
181: return (info);
182: }
183:
184: /**
185: * Intercepts Http request and redirects it to the configured SSO partner application.
186: *
187: * @param request The servlet request to be processed
188: * @param response The servlet response to be created
189: * in the current processing pipeline
190: * @exception IOException if an input/output error occurs
191: * @exception javax.servlet.ServletException if a servlet error occurs
192: */
193: public void invoke(Request request, Response response)
194: throws IOException, javax.servlet.ServletException {
195:
196: if (container.getLogger().isDebugEnabled())
197: container.getLogger().debug("ReverseProxyValve Acting.");
198:
199: ProxyContextConfig[] contexts = _rpc.getProxyContexts();
200:
201: // Create an instance of HttpClient.
202: HttpClient client = new HttpClient();
203:
204: HttpServletRequest hsr = (HttpServletRequest) request
205: .getRequest();
206: String uri = hsr.getRequestURI();
207:
208: String uriContext = null;
209:
210: StringTokenizer st = new StringTokenizer(uri.substring(1), "/");
211: while (st.hasMoreTokens()) {
212: String token = st.nextToken();
213: uriContext = "/" + token;
214: break;
215: }
216:
217: if (uriContext == null)
218: uriContext = uri;
219:
220: // Obtain the target host from the
221: String proxyForwardHost = null;
222: String proxyForwardUri = null;
223:
224: for (int i = 0; i < contexts.length; i++) {
225: if (contexts[i].getContext().equals(uriContext)) {
226: log("Proxy context mapped to host/uri: "
227: + contexts[i].getForwardHost()
228: + contexts[i].getForwardUri());
229: proxyForwardHost = contexts[i].getForwardHost();
230: proxyForwardUri = contexts[i].getForwardUri();
231: break;
232: }
233: }
234:
235: if (proxyForwardHost == null) {
236: log("URI '" + uri + "' can't be mapped to host");
237: getNext().invoke(request, response);
238: return;
239: }
240:
241: if (proxyForwardUri == null) {
242: // trim the uri context before submitting the http request
243: int uriTrailStartPos = uri.substring(1).indexOf("/") + 1;
244: proxyForwardUri = uri.substring(uriTrailStartPos);
245: } else {
246: int uriTrailStartPos = uri.substring(1).indexOf("/") + 1;
247: proxyForwardUri = proxyForwardUri
248: + uri.substring(uriTrailStartPos);
249: }
250:
251: // log ("Proxy request mapped to " + "http://" + proxyForwardHost + proxyForwardUri);
252:
253: HttpMethod method;
254:
255: // TODO: to be moved to a builder which instantiates and build concrete methods.
256: if (hsr.getMethod().equals(METHOD_GET)) {
257: // Create a method instance.
258: HttpMethod getMethod = new GetMethod(proxyForwardHost
259: + proxyForwardUri
260: + (hsr.getQueryString() != null ? ("?" + hsr
261: .getQueryString()) : ""));
262: method = getMethod;
263: } else if (hsr.getMethod().equals(METHOD_POST)) {
264: // Create a method instance.
265: PostMethod postMethod = new PostMethod(proxyForwardHost
266: + proxyForwardUri
267: + (hsr.getQueryString() != null ? ("?" + hsr
268: .getQueryString()) : ""));
269: postMethod.setRequestBody(hsr.getInputStream());
270: method = postMethod;
271: } else if (hsr.getMethod().equals(METHOD_HEAD)) {
272: // Create a method instance.
273: HeadMethod headMethod = new HeadMethod(proxyForwardHost
274: + proxyForwardUri
275: + (hsr.getQueryString() != null ? ("?" + hsr
276: .getQueryString()) : ""));
277: method = headMethod;
278: } else if (hsr.getMethod().equals(METHOD_PUT)) {
279: method = new PutMethod(proxyForwardHost
280: + proxyForwardUri
281: + (hsr.getQueryString() != null ? ("?" + hsr
282: .getQueryString()) : ""));
283: } else
284: throw new java.lang.UnsupportedOperationException(
285: "Unknown method : " + hsr.getMethod());
286:
287: // copy incoming http headers to reverse proxy request
288: Enumeration hne = hsr.getHeaderNames();
289: while (hne.hasMoreElements()) {
290: String hn = (String) hne.nextElement();
291:
292: // Map the received host header to the target host name
293: // so that the configured virtual domain can
294: // do the proper handling.
295: if (hn.equalsIgnoreCase("host")) {
296: method.addRequestHeader("Host", proxyForwardHost);
297: continue;
298: }
299:
300: Enumeration hvals = hsr.getHeaders(hn);
301: while (hvals.hasMoreElements()) {
302: String hv = (String) hvals.nextElement();
303: method.addRequestHeader(hn, hv);
304: }
305: }
306:
307: // Add Reverse-Proxy-Host header
308: String reverseProxyHost = getReverseProxyHost(request);
309: method.addRequestHeader(Constants.JOSSO_REVERSE_PROXY_HEADER,
310: reverseProxyHost);
311:
312: if (container.getLogger().isDebugEnabled())
313: container.getLogger().debug(
314: "Sending " + Constants.JOSSO_REVERSE_PROXY_HEADER
315: + " " + reverseProxyHost);
316:
317: // DO NOT follow redirects !
318: method.setFollowRedirects(false);
319:
320: // By default the httpclient uses HTTP v1.1. We are downgrading it
321: // to v1.0 so that the target server doesn't set a reply using chunked
322: // transfer encoding which doesn't seem to be handled properly.
323: // TODO: Check how to make chunked transfer encoding work.
324: client.getParams().setVersion(new HttpVersion(1, 0));
325:
326: // Execute the method.
327: int statusCode = -1;
328: try {
329: // execute the method.
330: statusCode = client.executeMethod(method);
331: } catch (HttpRecoverableException e) {
332: log("A recoverable exception occurred " + e.getMessage());
333: } catch (IOException e) {
334: log("Failed to connect.");
335: e.printStackTrace();
336: }
337:
338: // Check that we didn't run out of retries.
339: if (statusCode == -1) {
340: log("Failed to recover from exception.");
341: }
342:
343: // Read the response body.
344: byte[] responseBody = method.getResponseBody();
345:
346: // Release the connection.
347: method.releaseConnection();
348:
349: HttpServletResponse sres = (HttpServletResponse) response
350: .getResponse();
351:
352: // First thing to do is to copy status code to response, otherwise
353: // catalina will do it as soon as we set a header or some other part of the response.
354: sres.setStatus(method.getStatusCode());
355:
356: // copy proxy response headers to client response
357: Header[] responseHeaders = method.getResponseHeaders();
358: for (int i = 0; i < responseHeaders.length; i++) {
359: Header responseHeader = responseHeaders[i];
360: String name = responseHeader.getName();
361: String value = responseHeader.getValue();
362:
363: // Adjust the URL in the Location, Content-Location and URI headers on HTTP redirect responses
364: // This is essential to avoid by-passing the reverse proxy because of HTTP redirects on the
365: // backend servers which stay behind the reverse proxy
366: switch (method.getStatusCode()) {
367: case HttpStatus.SC_MOVED_TEMPORARILY:
368: case HttpStatus.SC_MOVED_PERMANENTLY:
369: case HttpStatus.SC_SEE_OTHER:
370: case HttpStatus.SC_TEMPORARY_REDIRECT:
371:
372: if ("Location".equalsIgnoreCase(name)
373: || "Content-Location".equalsIgnoreCase(name)
374: || "URI".equalsIgnoreCase(name)) {
375:
376: // Check that this redirect must be adjusted.
377: if (value.indexOf(proxyForwardHost) >= 0) {
378: String trail = value.substring(proxyForwardHost
379: .length());
380: value = getReverseProxyHost(request) + trail;
381: if (container.getLogger().isDebugEnabled())
382: container.getLogger().debug(
383: "Adjusting redirect header to "
384: + value);
385: }
386: }
387: break;
388:
389: } //end of switch
390: sres.addHeader(name, value);
391:
392: }
393:
394: // Sometimes this is null, when no body is returned ...
395: if (responseBody != null && responseBody.length > 0)
396: sres.getOutputStream().write(responseBody);
397:
398: sres.getOutputStream().flush();
399:
400: if (container.getLogger().isDebugEnabled())
401: container.getLogger().debug("ReverseProxyValve finished.");
402:
403: return;
404: }
405:
406: /**
407: * Return a String rendering of this object.
408: */
409: public String toString() {
410: StringBuffer sb = new StringBuffer("ReverseProxyValve[");
411: if (container != null)
412: sb.append(container.getName());
413: sb.append("]");
414: return (sb.toString());
415: }
416:
417: // ------------------------------------------------------ Protected Methods
418:
419: /**
420: * This method calculates the reverse-proxy-host header value.
421: */
422: protected String getReverseProxyHost(Request request) {
423: HttpServletRequest hsr = (HttpServletRequest) request
424: .getRequest();
425: if (_reverseProxyHost == null) {
426: synchronized (this ) {
427: String h = hsr.getProtocol().substring(0,
428: hsr.getProtocol().indexOf("/")).toLowerCase()
429: + "://"
430: + hsr.getServerName()
431: + (hsr.getServerPort() != 80 ? (":" + hsr
432: .getServerPort()) : "");
433: _reverseProxyHost = h;
434: }
435: }
436:
437: return _reverseProxyHost;
438:
439: }
440:
441: /**
442: * Log a message on the Logger associated with our Container (if any).
443: *
444: * @param message Message to be logged
445: */
446: protected void log(String message) {
447:
448: if (container != null) {
449: if (container.getLogger().isDebugEnabled())
450: container.getLogger().debug(
451: this .toString() + ": " + message);
452: } else
453: System.out.println(this .toString() + ": " + message);
454:
455: }
456:
457: /**
458: * Log a message on the Logger associated with our Container (if any).
459: *
460: * @param message Message to be logged
461: * @param throwable Associated exception
462: */
463: protected void log(String message, Throwable throwable) {
464:
465: if (container != null) {
466: if (container.getLogger().isDebugEnabled())
467: container.getLogger().debug(
468: this .toString() + ": " + message, throwable);
469: } else {
470: System.out.println(this .toString() + ": " + message);
471: throwable.printStackTrace(System.out);
472: }
473:
474: }
475:
476: }
|