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.tc50.gateway.reverseproxy;
023:
024: import org.apache.catalina.*;
025: import org.apache.catalina.util.LifecycleSupport;
026: import org.apache.catalina.valves.ValveBase;
027: import org.apache.commons.httpclient.*;
028: import org.apache.commons.httpclient.methods.GetMethod;
029: import org.apache.commons.httpclient.methods.HeadMethod;
030: import org.apache.commons.httpclient.methods.PostMethod;
031: import org.apache.commons.httpclient.methods.PutMethod;
032: import org.josso.Lookup;
033: import org.josso.gateway.Constants;
034: import org.josso.gateway.reverseproxy.ProxyContextConfig;
035: import org.josso.gateway.reverseproxy.ReverseProxyConfiguration;
036:
037: import javax.servlet.http.HttpServletRequest;
038: import javax.servlet.http.HttpServletResponse;
039: import java.io.IOException;
040: import java.util.Enumeration;
041: import java.util.StringTokenizer;
042:
043: /**
044: * Reverse Proxy implementation using Tomcat Valves.
045: *
046: * @deprecated This component is no longer needed for N-Tier configurations.
047: *
048: * @author <a href="mailto:gbrigand@josso.org">Gianluca Brigandi</a>
049: * @version CVS $Id: ReverseProxyValve.java 508 2008-02-18 13:32:29Z sgonzalez $
050: */
051:
052: public class ReverseProxyValve extends ValveBase implements Lifecycle {
053:
054: // ----------------------------------------------------- Constants
055:
056: static final String METHOD_GET = "GET";
057: static final String METHOD_POST = "POST";
058: static final String METHOD_PUT = "PUT";
059: private final String METHOD_HEAD = "HEAD";
060:
061: // ----------------------------------------------------- Instance Variables
062: private String _configurationFileName;
063: private ReverseProxyConfiguration _rpc;
064: private boolean started;
065: private String _reverseProxyHost; // Reverse proxy host value.
066: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
067:
068: /**
069: * The descriptive information related to this implementation.
070: */
071: private static final String info = "org.josso.tc50.gateway.reverseproxy.ReverseProxyValve/1.0";
072:
073: // ------------------------------------------------------ Lifecycle Methods
074:
075: /**
076: * Add a lifecycle event listener to this component.
077: *
078: * @param listener The listener to add
079: */
080: public void addLifecycleListener(LifecycleListener listener) {
081:
082: lifecycle.addLifecycleListener(listener);
083:
084: }
085:
086: /**
087: * Get the lifecycle listeners associated with this lifecycle. If this
088: * Lifecycle has no listeners registered, a zero-length array is returned.
089: */
090: public LifecycleListener[] findLifecycleListeners() {
091:
092: return lifecycle.findLifecycleListeners();
093:
094: }
095:
096: /**
097: * Remove a lifecycle event listener from this component.
098: *
099: * @param listener The listener to remove
100: */
101: public void removeLifecycleListener(LifecycleListener listener) {
102:
103: lifecycle.removeLifecycleListener(listener);
104:
105: }
106:
107: /**
108: * Prepare for the beginning of active use of the public methods of this
109: * component. This method should be called after <code>configure()</code>,
110: * and before any of the public methods of the component are utilized.
111: *
112: * @throws LifecycleException if this component detects a fatal error
113: * that prevents this component from being used
114: */
115: public void start() throws LifecycleException {
116:
117: // Validate and update our current component state
118: if (started)
119: throw new LifecycleException("ReverseProxy already started");
120: lifecycle.fireLifecycleEvent(START_EVENT, null);
121: started = true;
122:
123: try {
124: _rpc = Lookup.getInstance()
125: .lookupReverseProxyConfiguration();
126: } catch (Exception e) {
127: throw new LifecycleException(e.getMessage(), e);
128: }
129:
130: if (debug >= 1)
131: log("Started");
132: }
133:
134: /**
135: * Gracefully terminate the active use of the public methods of this
136: * component. This method should be the last one called on a given
137: * instance of this component.
138: *
139: * @throws LifecycleException if this component detects a fatal error
140: * that needs to be reported
141: */
142: public void stop() throws LifecycleException {
143:
144: // Validate and update our current component state
145: if (!started)
146: throw new LifecycleException("ReverseProxy not started");
147: lifecycle.fireLifecycleEvent(STOP_EVENT, null);
148: started = false;
149:
150: if (debug >= 1)
151: log("Stopped");
152:
153: }
154:
155: // ------------------------------------------------------------- Properties
156:
157: /**
158: * Sets reverse proxy configuration file name.
159: *
160: * @param configurationFileName configuration file name property value
161: */
162: public void setConfiguration(String configurationFileName) {
163: _configurationFileName = configurationFileName;
164: }
165:
166: /**
167: * Returns reverse proxy configuration file name.
168: *
169: * @return configuration property value
170: */
171: public String getConfiguration() {
172: return _configurationFileName;
173: }
174:
175: /**
176: * Return descriptive information about this Valve implementation.
177: */
178: public String getInfo() {
179: return (info);
180: }
181:
182: /**
183: * Intercepts Http request and redirects it to the configured SSO partner application.
184: *
185: * @param request The servlet request to be processed
186: * @param response The servlet response to be created
187: * @param valveContext The valve _context used to invoke the next valve
188: * in the current processing pipeline
189: * @exception IOException if an input/output error occurs
190: * @exception javax.servlet.ServletException if a servlet error occurs
191: */
192: public void invoke(Request request, Response response,
193: ValveContext valveContext) throws IOException,
194: javax.servlet.ServletException {
195:
196: if (debug >= 1)
197: log("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: valveContext.invokeNext(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 (debug >= 1)
313: log("Sending " + Constants.JOSSO_REVERSE_PROXY_HEADER + " "
314: + reverseProxyHost);
315:
316: // DO NOT follow redirects !
317: method.setFollowRedirects(false);
318:
319: // By default the httpclient uses HTTP v1.1. We are downgrading it
320: // to v1.0 so that the target server doesn't set a reply using chunked
321: // transfer encoding which doesn't seem to be handled properly.
322: // TODO: Check how to make chunked transfer encoding work.
323: client.getParams().setVersion(new HttpVersion(1, 0));
324:
325: // Execute the method.
326: int statusCode = -1;
327: try {
328: // execute the method.
329: statusCode = client.executeMethod(method);
330: } catch (HttpRecoverableException e) {
331: log("A recoverable exception occurred " + e.getMessage());
332: } catch (IOException e) {
333: log("Failed to connect.");
334: e.printStackTrace();
335: }
336:
337: // Check that we didn't run out of retries.
338: if (statusCode == -1) {
339: log("Failed to recover from exception.");
340: }
341:
342: // Read the response body.
343: byte[] responseBody = method.getResponseBody();
344:
345: // Release the connection.
346: method.releaseConnection();
347:
348: HttpServletResponse sres = (HttpServletResponse) response
349: .getResponse();
350:
351: // First thing to do is to copy status code to response, otherwise
352: // catalina will do it as soon as we set a header or some other part of the response.
353: sres.setStatus(method.getStatusCode());
354:
355: // copy proxy response headers to client response
356: Header[] responseHeaders = method.getResponseHeaders();
357: for (int i = 0; i < responseHeaders.length; i++) {
358: Header responseHeader = responseHeaders[i];
359: String name = responseHeader.getName();
360: String value = responseHeader.getValue();
361:
362: // Adjust the URL in the Location, Content-Location and URI headers on HTTP redirect responses
363: // This is essential to avoid by-passing the reverse proxy because of HTTP redirects on the
364: // backend servers which stay behind the reverse proxy
365: switch (method.getStatusCode()) {
366: case HttpStatus.SC_MOVED_TEMPORARILY:
367: case HttpStatus.SC_MOVED_PERMANENTLY:
368: case HttpStatus.SC_SEE_OTHER:
369: case HttpStatus.SC_TEMPORARY_REDIRECT:
370:
371: if ("Location".equalsIgnoreCase(name)
372: || "Content-Location".equalsIgnoreCase(name)
373: || "URI".equalsIgnoreCase(name)) {
374:
375: // Check that this redirect must be adjusted.
376: if (value.indexOf(proxyForwardHost) >= 0) {
377: String trail = value.substring(proxyForwardHost
378: .length());
379: value = getReverseProxyHost(request) + trail;
380: if (debug >= 1)
381: log("Adjusting redirect header to " + value);
382: }
383: }
384: break;
385:
386: } //end of switch
387: sres.addHeader(name, value);
388:
389: }
390:
391: // Sometimes this is null, when no body is returned ...
392: if (responseBody != null && responseBody.length > 0)
393: sres.getOutputStream().write(responseBody);
394:
395: sres.getOutputStream().flush();
396:
397: if (debug >= 1)
398: log("ReverseProxyValve finished.");
399:
400: return;
401: }
402:
403: /**
404: * Return a String rendering of this object.
405: */
406: public String toString() {
407: StringBuffer sb = new StringBuffer("ReverseProxyValve[");
408: if (container != null)
409: sb.append(container.getName());
410: sb.append("]");
411: return (sb.toString());
412: }
413:
414: // ------------------------------------------------------ Protected Methods
415:
416: /**
417: * This method calculates the reverse-proxy-host header value.
418: */
419: protected String getReverseProxyHost(Request request) {
420: HttpServletRequest hsr = (HttpServletRequest) request
421: .getRequest();
422: if (_reverseProxyHost == null) {
423: synchronized (this ) {
424: String h = hsr.getProtocol().substring(0,
425: hsr.getProtocol().indexOf("/")).toLowerCase()
426: + "://"
427: + hsr.getServerName()
428: + (hsr.getServerPort() != 80 ? (":" + hsr
429: .getServerPort()) : "");
430: _reverseProxyHost = h;
431: }
432: }
433:
434: return _reverseProxyHost;
435:
436: }
437:
438: /**
439: * Log a message on the Logger associated with our Container (if any).
440: *
441: * @param message Message to be logged
442: */
443: protected void log(String message) {
444:
445: Logger logger = container.getLogger();
446: if (logger != null)
447: logger.log(this .toString() + ": " + message);
448: else
449: System.out.println(this .toString() + ": " + message);
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: * @param throwable Associated exception
458: */
459: protected void log(String message, Throwable throwable) {
460:
461: Logger logger = container.getLogger();
462: if (logger != null)
463: logger.log(this .toString() + ": " + message, throwable);
464: else {
465: System.out.println(this .toString() + ": " + message);
466: throwable.printStackTrace(System.out);
467: }
468:
469: }
470:
471: }
|