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