View Javadoc

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