View Javadoc

1   // ========================================================================
2   // Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses.
12  // ========================================================================
13  
14  package org.eclipse.jetty.servlets;
15  
16  import java.io.IOException;
17  import java.io.InputStream;
18  import java.io.OutputStream;
19  import java.net.InetSocketAddress;
20  import java.net.MalformedURLException;
21  import java.net.Socket;
22  import java.net.URI;
23  import java.net.URISyntaxException;
24  import java.util.Collections;
25  import java.util.Enumeration;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.StringTokenizer;
30  import javax.servlet.Servlet;
31  import javax.servlet.ServletConfig;
32  import javax.servlet.ServletContext;
33  import javax.servlet.ServletException;
34  import javax.servlet.ServletRequest;
35  import javax.servlet.ServletResponse;
36  import javax.servlet.UnavailableException;
37  import javax.servlet.http.HttpServletRequest;
38  import javax.servlet.http.HttpServletResponse;
39  
40  import org.eclipse.jetty.client.HttpClient;
41  import org.eclipse.jetty.client.HttpConnection;
42  import org.eclipse.jetty.client.HttpExchange;
43  import org.eclipse.jetty.continuation.Continuation;
44  import org.eclipse.jetty.continuation.ContinuationSupport;
45  import org.eclipse.jetty.http.HttpHeaderValues;
46  import org.eclipse.jetty.http.HttpHeaders;
47  import org.eclipse.jetty.http.HttpSchemes;
48  import org.eclipse.jetty.http.HttpURI;
49  import org.eclipse.jetty.http.PathMap;
50  import org.eclipse.jetty.io.Buffer;
51  import org.eclipse.jetty.io.EofException;
52  import org.eclipse.jetty.servlet.Holder;
53  import org.eclipse.jetty.util.HostMap;
54  import org.eclipse.jetty.util.IO;
55  import org.eclipse.jetty.util.Loader;
56  import org.eclipse.jetty.util.log.Log;
57  import org.eclipse.jetty.util.log.Logger;
58  import org.eclipse.jetty.util.thread.QueuedThreadPool;
59  import org.omg.CORBA._PolicyStub;
60  
61  /**
62   * Asynchronous Proxy Servlet.
63   * 
64   * Forward requests to another server either as a standard web proxy (as defined by RFC2616) or as a transparent proxy.
65   * <p>
66   * This servlet needs the jetty-util and jetty-client classes to be available to the web application.
67   * <p>
68   * To facilitate JMX monitoring, the "HttpClient", it's "ThreadPool" and the "Logger" are set as context attributes prefixed with the servlet name.
69   * <p>
70   * The following init parameters may be used to configure the servlet:
71   * <ul>
72   * <li>name - Name of Proxy servlet (default: "ProxyServlet"
73   * <li>maxThreads - maximum threads
74   * <li>maxConnections - maximum connections per destination
75   * <li>timeout - the period in ms the client will wait for a response from the proxied server
76   * <li>idleTimeout - the period in ms a connection to proxied server can be idle for before it is closed
77   * <li>requestHeaderSize - the size of the request header buffer (d. 6,144)
78   * <li>requestBufferSize - the size of the request buffer (d. 12,288)
79   * <li>responseHeaderSize - the size of the response header buffer (d. 6,144)
80   * <li>responseBufferSize - the size of the response buffer (d. 32,768)
81   * <li>HostHeader - Force the host header to a particular value 
82   * <li>whiteList - comma-separated list of allowed proxy destinations
83   * <li>blackList - comma-separated list of forbidden proxy destinations
84   * </ul>
85   * 
86   * @see org.eclipse.jetty.server.handler.ConnectHandler
87   */
88  public class ProxyServlet implements Servlet
89  {
90      protected Logger _log;
91      protected HttpClient _client;
92      protected String _hostHeader;
93  
94      protected HashSet<String> _DontProxyHeaders = new HashSet<String>();
95      {
96          _DontProxyHeaders.add("proxy-connection");
97          _DontProxyHeaders.add("connection");
98          _DontProxyHeaders.add("keep-alive");
99          _DontProxyHeaders.add("transfer-encoding");
100         _DontProxyHeaders.add("te");
101         _DontProxyHeaders.add("trailer");
102         _DontProxyHeaders.add("proxy-authorization");
103         _DontProxyHeaders.add("proxy-authenticate");
104         _DontProxyHeaders.add("upgrade");
105     }
106 
107     protected ServletConfig _config;
108     protected ServletContext _context;
109     protected HostMap<PathMap> _white = new HostMap<PathMap>();
110     protected HostMap<PathMap> _black = new HostMap<PathMap>();
111 
112     /* ------------------------------------------------------------ */
113     /*
114      * (non-Javadoc)
115      * 
116      * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
117      */
118     public void init(ServletConfig config) throws ServletException
119     {
120         _config = config;
121         _context = config.getServletContext();
122         
123         _hostHeader = config.getInitParameter("HostHeader");
124 
125         try
126         {
127             _log = createLogger(config);      
128 
129             _client = createHttpClient(config);
130             
131             if (_context != null)
132             {
133                 _context.setAttribute(config.getServletName() + ".Logger",_log);
134                 _context.setAttribute(config.getServletName() + ".ThreadPool",_client.getThreadPool());
135                 _context.setAttribute(config.getServletName() + ".HttpClient",_client);
136             }
137 
138             String white = config.getInitParameter("whiteList");
139             if (white != null)
140             {
141                 parseList(white,_white);
142             }
143             String black = config.getInitParameter("blackList");
144             if (black != null)
145             {
146                 parseList(black,_black);
147             }
148         }
149         catch (Exception e)
150         {
151             throw new ServletException(e);
152         }
153     }
154 
155     public void destroy()
156     {
157         try
158         {
159             _client.stop();
160         }
161         catch (Exception x)
162         {
163             _log.debug(x);
164         }
165     }
166     
167     
168     /**
169      * Create and return a logger based on the ServletConfig for use in the 
170      * proxy servlet
171      * 
172      * @param config
173      * @return Logger
174      */
175     protected Logger createLogger(ServletConfig config)
176     {
177         return Log.getLogger("org.eclipse.jetty.servlets." + config.getServletName());
178     }
179     
180     /**
181      * Create and return an HttpClient based on ServletConfig
182      * 
183      * By default this implementation will create an instance of the 
184      * HttpClient for use by this proxy servlet.
185      * 
186      * @param config 
187      * @return HttpClient 
188      * @throws Exception
189      */
190     protected HttpClient createHttpClient(ServletConfig config) throws Exception
191     {
192         HttpClient client = new HttpClient();
193         client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
194        
195         String t = config.getInitParameter("maxThreads");
196         
197         if (t != null)
198         {
199             client.setThreadPool(new QueuedThreadPool(Integer.parseInt(t)));
200         }
201         else
202         {
203             client.setThreadPool(new QueuedThreadPool());
204         }
205         
206         ((QueuedThreadPool)client.getThreadPool()).setName(config.getServletName());        
207 
208         t = config.getInitParameter("maxConnections");
209         
210         if (t != null)
211         {
212             client.setMaxConnectionsPerAddress(Integer.parseInt(t));
213         }
214        
215         t = config.getInitParameter("timeout");
216         
217         if ( t != null )
218         {
219             client.setTimeout(Long.parseLong(t));
220         }
221         
222         t = config.getInitParameter("idleTimeout");
223         
224         if ( t != null )
225         {
226             client.setIdleTimeout(Long.parseLong(t));
227         }
228         
229         t = config.getInitParameter("requestHeaderSize");
230         
231         if ( t != null )
232         {
233             client.setRequestHeaderSize(Integer.parseInt(t));
234         }
235         
236         t = config.getInitParameter("requestBufferSize");
237         
238         if ( t != null )
239         {
240             client.setRequestBufferSize(Integer.parseInt(t));
241         }
242         
243         t = config.getInitParameter("responseHeaderSize");
244         
245         if ( t != null )
246         {
247             client.setResponseHeaderSize(Integer.parseInt(t));
248         }
249         
250         t = config.getInitParameter("responseBufferSize");
251         
252         if ( t != null )
253         {
254             client.setResponseBufferSize(Integer.parseInt(t));
255         }
256         
257         client.start();
258         
259         return client;
260     }
261     
262     /* ------------------------------------------------------------ */
263     /**
264      * Helper function to process a parameter value containing a list of new entries and initialize the specified host map.
265      * 
266      * @param list
267      *            comma-separated list of new entries
268      * @param hostMap
269      *            target host map
270      */
271     private void parseList(String list, HostMap<PathMap> hostMap)
272     {
273         if (list != null && list.length() > 0)
274         {
275             int idx;
276             String entry;
277 
278             StringTokenizer entries = new StringTokenizer(list,",");
279             while (entries.hasMoreTokens())
280             {
281                 entry = entries.nextToken();
282                 idx = entry.indexOf('/');
283 
284                 String host = idx > 0?entry.substring(0,idx):entry;
285                 String path = idx > 0?entry.substring(idx):"/*";
286 
287                 host = host.trim();
288                 PathMap pathMap = hostMap.get(host);
289                 if (pathMap == null)
290                 {
291                     pathMap = new PathMap(true);
292                     hostMap.put(host,pathMap);
293                 }
294                 if (path != null)
295                 {
296                     pathMap.put(path,path);
297                 }
298             }
299         }
300     }
301 
302     /* ------------------------------------------------------------ */
303     /**
304      * Check the request hostname and path against white- and blacklist.
305      * 
306      * @param host
307      *            hostname to check
308      * @param path
309      *            path to check
310      * @return true if request is allowed to be proxied
311      */
312     public boolean validateDestination(String host, String path)
313     {
314         if (_white.size() > 0)
315         {
316             boolean match = false;
317 
318             Object whiteObj = _white.getLazyMatches(host);
319             if (whiteObj != null)
320             {
321                 List whiteList = (whiteObj instanceof List)?(List)whiteObj:Collections.singletonList(whiteObj);
322 
323                 for (Object entry : whiteList)
324                 {
325                     PathMap pathMap = ((Map.Entry<String, PathMap>)entry).getValue();
326                     if (match = (pathMap != null && (pathMap.size() == 0 || pathMap.match(path) != null)))
327                         break;
328                 }
329             }
330 
331             if (!match)
332                 return false;
333         }
334 
335         if (_black.size() > 0)
336         {
337             Object blackObj = _black.getLazyMatches(host);
338             if (blackObj != null)
339             {
340                 List blackList = (blackObj instanceof List)?(List)blackObj:Collections.singletonList(blackObj);
341 
342                 for (Object entry : blackList)
343                 {
344                     PathMap pathMap = ((Map.Entry<String, PathMap>)entry).getValue();
345                     if (pathMap != null && (pathMap.size() == 0 || pathMap.match(path) != null))
346                         return false;
347                 }
348             }
349         }
350 
351         return true;
352     }
353 
354     /* ------------------------------------------------------------ */
355     /*
356      * (non-Javadoc)
357      * 
358      * @see javax.servlet.Servlet#getServletConfig()
359      */
360     public ServletConfig getServletConfig()
361     {
362         return _config;
363     }
364 
365     /* ------------------------------------------------------------ */
366     /**
367      * Get the hostHeader.
368      * 
369      * @return the hostHeader
370      */
371     public String getHostHeader()
372     {
373         return _hostHeader;
374     }
375 
376     /* ------------------------------------------------------------ */
377     /**
378      * Set the hostHeader.
379      * 
380      * @param hostHeader
381      *            the hostHeader to set
382      */
383     public void setHostHeader(String hostHeader)
384     {
385         _hostHeader = hostHeader;
386     }
387 
388     /* ------------------------------------------------------------ */
389     /*
390      * (non-Javadoc)
391      * 
392      * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
393      */
394     public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
395     {
396         final int debug = _log.isDebugEnabled()?req.hashCode():0;
397 
398         final HttpServletRequest request = (HttpServletRequest)req;
399         final HttpServletResponse response = (HttpServletResponse)res;
400 
401         if ("CONNECT".equalsIgnoreCase(request.getMethod()))
402         {
403             handleConnect(request,response);
404         }
405         else
406         {
407             final InputStream in = request.getInputStream();
408             final OutputStream out = response.getOutputStream();
409 
410             final Continuation continuation = ContinuationSupport.getContinuation(request);
411 
412             if (!continuation.isInitial())
413                 response.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT); // Need better test that isInitial
414             else
415             {
416                 
417                 String uri = request.getRequestURI();
418                 if (request.getQueryString() != null)
419                     uri += "?" + request.getQueryString();
420 
421                 HttpURI url = proxyHttpURI(request.getScheme(),request.getServerName(),request.getServerPort(),uri);
422 
423                 if (debug != 0)
424                     _log.debug(debug + " proxy " + uri + "-->" + url);
425 
426                 if (url == null)
427                 {
428                     response.sendError(HttpServletResponse.SC_FORBIDDEN);
429                     return;
430                 }
431 
432                 HttpExchange exchange = new HttpExchange()
433                 {
434                     protected void onRequestCommitted() throws IOException
435                     {
436                     }
437 
438                     protected void onRequestComplete() throws IOException
439                     {
440                     }
441 
442                     protected void onResponseComplete() throws IOException
443                     {
444                         if (debug != 0)
445                             _log.debug(debug + " complete");
446                         continuation.complete();
447                     }
448 
449                     protected void onResponseContent(Buffer content) throws IOException
450                     {
451                         if (debug != 0)
452                             _log.debug(debug + " content" + content.length());
453                         content.writeTo(out);
454                     }
455 
456                     protected void onResponseHeaderComplete() throws IOException
457                     {
458                     }
459 
460                     protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException
461                     {
462                         if (debug != 0)
463                             _log.debug(debug + " " + version + " " + status + " " + reason);
464 
465                         if (reason != null && reason.length() > 0)
466                             response.setStatus(status,reason.toString());
467                         else
468                             response.setStatus(status);
469                     }
470 
471                     protected void onResponseHeader(Buffer name, Buffer value) throws IOException
472                     {
473                         String s = name.toString().toLowerCase();
474                         if (!_DontProxyHeaders.contains(s) || (HttpHeaders.CONNECTION_BUFFER.equals(name) && HttpHeaderValues.CLOSE_BUFFER.equals(value)))
475                         {
476                             if (debug != 0)
477                                 _log.debug(debug + " " + name + ": " + value);
478 
479                             response.addHeader(name.toString(),value.toString());
480                         }
481                         else if (debug != 0)
482                             _log.debug(debug + " " + name + "! " + value);
483                     }
484 
485                     protected void onConnectionFailed(Throwable ex)
486                     {
487                         handleOnConnectionFailed(ex,request,response);
488                         
489                         // it is possible this might trigger before the 
490                         // continuation.suspend()
491                         if (!continuation.isInitial())
492                         {
493                             continuation.complete();
494                         }
495                     }
496 
497                     protected void onException(Throwable ex)
498                     {
499                         if (ex instanceof EofException)
500                         {
501                             Log.ignore(ex);
502                             return;
503                         }
504                         handleOnException(ex,request,response);
505                         
506                         // it is possible this might trigger before the 
507                         // continuation.suspend()
508                         if (!continuation.isInitial())
509                         {
510                             continuation.complete();
511                         }
512                     }
513 
514                     protected void onExpire()
515                     {
516                         handleOnExpire(request,response);
517                         continuation.complete();
518                     }
519 
520                 };
521 
522                 exchange.setScheme(HttpSchemes.HTTPS.equals(request.getScheme())?HttpSchemes.HTTPS_BUFFER:HttpSchemes.HTTP_BUFFER);
523                 exchange.setMethod(request.getMethod());
524                 exchange.setURL(url.toString());
525                 exchange.setVersion(request.getProtocol());
526 
527 
528                 if (debug != 0)
529                     _log.debug(debug + " " + request.getMethod() + " " + url + " " + request.getProtocol());
530 
531                 // check connection header
532                 String connectionHdr = request.getHeader("Connection");
533                 if (connectionHdr != null)
534                 {
535                     connectionHdr = connectionHdr.toLowerCase();
536                     if (connectionHdr.indexOf("keep-alive") < 0 && connectionHdr.indexOf("close") < 0)
537                         connectionHdr = null;
538                 }
539 
540                 // force host
541                 if (_hostHeader != null)
542                     exchange.setRequestHeader("Host",_hostHeader);
543 
544                 // copy headers
545                 boolean xForwardedFor = false;
546                 boolean hasContent = false;
547                 long contentLength = -1;
548                 Enumeration<?> enm = request.getHeaderNames();
549                 while (enm.hasMoreElements())
550                 {
551                     // TODO could be better than this!
552                     String hdr = (String)enm.nextElement();
553                     String lhdr = hdr.toLowerCase();
554 
555                     if (_DontProxyHeaders.contains(lhdr))
556                         continue;
557                     if (connectionHdr != null && connectionHdr.indexOf(lhdr) >= 0)
558                         continue;
559                     if (_hostHeader != null && "host".equals(lhdr))
560                         continue;
561 
562                     if ("content-type".equals(lhdr))
563                         hasContent = true;
564                     else if ("content-length".equals(lhdr))
565                     {
566                         contentLength = request.getContentLength();
567                         exchange.setRequestHeader(HttpHeaders.CONTENT_LENGTH,Long.toString(contentLength));
568                         if (contentLength > 0)
569                             hasContent = true;
570                     }
571                     else if ("x-forwarded-for".equals(lhdr))
572                         xForwardedFor = true;
573 
574                     Enumeration<?> vals = request.getHeaders(hdr);
575                     while (vals.hasMoreElements())
576                     {
577                         String val = (String)vals.nextElement();
578                         if (val != null)
579                         {
580                             if (debug != 0)
581                                 _log.debug(debug + " " + hdr + ": " + val);
582 
583                             exchange.setRequestHeader(hdr,val);
584                         }
585                     }
586                 }
587 
588                 // Proxy headers
589                 exchange.setRequestHeader("Via","1.1 (jetty)");
590                 if (!xForwardedFor)
591                 {
592                     exchange.addRequestHeader("X-Forwarded-For",request.getRemoteAddr());
593                     exchange.addRequestHeader("X-Forwarded-Proto",request.getScheme());
594                     exchange.addRequestHeader("X-Forwarded-Host",request.getServerName());
595                     exchange.addRequestHeader("X-Forwarded-Server",request.getLocalName());
596                 }
597 
598                 if (hasContent)
599                     exchange.setRequestContentSource(in);
600 
601                 customizeExchange(exchange, request);     
602                 
603                 /*
604                  * we need to set the timeout on the continuation to take into
605                  * account the timeout of the HttpClient and the HttpExchange
606                  */
607                 long ctimeout = (_client.getTimeout() > exchange.getTimeout()) ? _client.getTimeout() : exchange.getTimeout();
608                 
609                 // continuation fudge factor of 1000, underlying components
610                 // should fail/expire first from exchange
611                 if ( ctimeout == 0 )
612                 {
613                     continuation.setTimeout(0);  // ideally never times out
614                 }
615                 else
616                 {    
617                     continuation.setTimeout(ctimeout + 1000);
618                 }
619                                 
620                 customizeContinuation(continuation);
621                 
622                 continuation.suspend(response);
623                 _client.send(exchange);
624 
625             }
626         }
627     }
628 
629     /* ------------------------------------------------------------ */
630     public void handleConnect(HttpServletRequest request, HttpServletResponse response) throws IOException
631     {
632         String uri = request.getRequestURI();
633 
634         String port = "";
635         String host = "";
636 
637         int c = uri.indexOf(':');
638         if (c >= 0)
639         {
640             port = uri.substring(c + 1);
641             host = uri.substring(0,c);
642             if (host.indexOf('/') > 0)
643                 host = host.substring(host.indexOf('/') + 1);
644         }
645 
646         // TODO - make this async!
647 
648         InetSocketAddress inetAddress = new InetSocketAddress(host,Integer.parseInt(port));
649 
650         // if (isForbidden(HttpMessage.__SSL_SCHEME,addrPort.getHost(),addrPort.getPort(),false))
651         // {
652         // sendForbid(request,response,uri);
653         // }
654         // else
655         {
656             InputStream in = request.getInputStream();
657             OutputStream out = response.getOutputStream();
658 
659             Socket socket = new Socket(inetAddress.getAddress(),inetAddress.getPort());
660 
661             response.setStatus(200);
662             response.setHeader("Connection","close");
663             response.flushBuffer();
664             // TODO prevent real close!
665 
666             IO.copyThread(socket.getInputStream(),out);
667             IO.copy(in,socket.getOutputStream());
668         }
669     }
670 
671     /* ------------------------------------------------------------ */
672     protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri) throws MalformedURLException
673     {
674         if (!validateDestination(serverName,uri))
675             return null;
676 
677         return new HttpURI(scheme + "://" + serverName + ":" + serverPort + uri);
678     }
679 
680     /*
681      * (non-Javadoc)
682      * 
683      * @see javax.servlet.Servlet#getServletInfo()
684      */
685     public String getServletInfo()
686     {
687         return "Proxy Servlet";
688     }
689 
690 
691     /**
692      * Extension point for subclasses to customize an exchange. Useful for setting timeouts etc. The default implementation does nothing.
693      * 
694      * @param exchange
695      * @param request
696      */
697     protected void customizeExchange(HttpExchange exchange, HttpServletRequest request)
698     {
699 
700     }
701 
702     /**
703      * Extension point for subclasses to customize the Continuation after it's initial creation in the service method. Useful for setting timeouts etc. The
704      * default implementation does nothing.
705      * 
706      * @param continuation
707      */
708     protected void customizeContinuation(Continuation continuation)
709     {
710 
711     }
712 
713     /**
714      * Extension point for custom handling of an HttpExchange's onConnectionFailed method. The default implementation delegates to
715      * {@link #handleOnException(Throwable, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
716      * 
717      * @param ex
718      * @param request
719      * @param response
720      */
721     protected void handleOnConnectionFailed(Throwable ex, HttpServletRequest request, HttpServletResponse response)
722     {
723         handleOnException(ex,request,response);
724     }
725 
726     /**
727      * Extension point for custom handling of an HttpExchange's onException method. The default implementation sets the response status to
728      * HttpServletResponse.SC_INTERNAL_SERVER_ERROR (503)
729      * 
730      * @param ex
731      * @param request
732      * @param response
733      */
734     protected void handleOnException(Throwable ex, HttpServletRequest request, HttpServletResponse response)
735     {
736         Log.warn(ex.toString());
737         Log.debug(ex);
738         if (!response.isCommitted())
739         {
740             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
741         }
742     }
743 
744     /**
745      * Extension point for custom handling of an HttpExchange's onExpire method. The default implementation sets the response status to
746      * HttpServletResponse.SC_GATEWAY_TIMEOUT (504)
747      * 
748      * @param request
749      * @param response
750      */
751     protected void handleOnExpire(HttpServletRequest request, HttpServletResponse response)
752     {
753         if (!response.isCommitted())
754         {
755             response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT);
756         }
757     }
758 
759     /**
760      * Transparent Proxy.
761      * 
762      * This convenience extension to ProxyServlet configures the servlet as a transparent proxy. The servlet is configured with init parameters:
763      * <ul>
764      * <li>ProxyTo - a URI like http://host:80/context to which the request is proxied.
765      * <li>Prefix - a URI prefix that is striped from the start of the forwarded URI.
766      * </ul>
767      * For example, if a request was received at /foo/bar and the ProxyTo was http://host:80/context and the Prefix was /foo, then the request would be proxied
768      * to http://host:80/context/bar
769      * 
770      */
771     public static class Transparent extends ProxyServlet
772     {
773         String _prefix;
774         String _proxyTo;
775 
776         public Transparent()
777         {
778         }
779 
780         public Transparent(String prefix, String host, int port)
781         {
782             this(prefix,"http",host,port,null);
783         }
784 
785         public Transparent(String prefix, String schema, String host, int port, String path)
786         {
787             try
788             {
789                 if (prefix != null)
790                 {
791                     _prefix = new URI(prefix).normalize().toString();
792                 }
793                 _proxyTo = new URI(schema,null,host,port,path,null,null).normalize().toString();
794             }
795             catch (URISyntaxException ex)
796             {
797                 _log.debug("Invalid URI syntax",ex);
798             }
799         }
800 
801         @Override
802         public void init(ServletConfig config) throws ServletException
803         {
804             super.init(config);
805 
806             String prefix = config.getInitParameter("Prefix");
807             _prefix = prefix == null?_prefix:prefix;
808 
809             // Adjust prefix value to account for context path
810             String contextPath = _context.getContextPath();
811             _prefix = _prefix == null?contextPath:(contextPath + _prefix);
812 
813             String proxyTo = config.getInitParameter("ProxyTo");
814             _proxyTo = proxyTo == null?_proxyTo:proxyTo;
815 
816             if (_proxyTo == null)
817                 throw new UnavailableException("ProxyTo parameter is requred.");
818 
819             if (!_prefix.startsWith("/"))
820                 throw new UnavailableException("Prefix parameter must start with a '/'.");
821 
822             _log.info(config.getServletName() + " @ " + _prefix + " to " + _proxyTo);
823         }
824 
825         @Override
826         protected HttpURI proxyHttpURI(final String scheme, final String serverName, int serverPort, final String uri) throws MalformedURLException
827         {
828             try
829             {
830                 if (!uri.startsWith(_prefix))
831                     return null;
832 
833                 URI dstUri = new URI(_proxyTo + uri.substring(_prefix.length())).normalize();
834 
835                 if (!validateDestination(dstUri.getHost(),dstUri.getPath()))
836                     return null;
837 
838                 return new HttpURI(dstUri.toString());
839             }
840             catch (URISyntaxException ex)
841             {
842                 throw new MalformedURLException(ex.getMessage());
843             }
844         }
845     }
846 }