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