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