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.proxy;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.InetAddress;
24  import java.net.URI;
25  import java.net.UnknownHostException;
26  import java.nio.ByteBuffer;
27  import java.util.Enumeration;
28  import java.util.HashSet;
29  import java.util.Iterator;
30  import java.util.Locale;
31  import java.util.Set;
32  import java.util.concurrent.Executor;
33  import java.util.concurrent.TimeUnit;
34  import java.util.concurrent.TimeoutException;
35  import javax.servlet.AsyncContext;
36  import javax.servlet.ServletConfig;
37  import javax.servlet.ServletContext;
38  import javax.servlet.ServletException;
39  import javax.servlet.UnavailableException;
40  import javax.servlet.http.HttpServlet;
41  import javax.servlet.http.HttpServletRequest;
42  import javax.servlet.http.HttpServletResponse;
43  
44  import org.eclipse.jetty.client.HttpClient;
45  import org.eclipse.jetty.client.api.ContentProvider;
46  import org.eclipse.jetty.client.api.Request;
47  import org.eclipse.jetty.client.api.Response;
48  import org.eclipse.jetty.client.api.Result;
49  import org.eclipse.jetty.client.util.InputStreamContentProvider;
50  import org.eclipse.jetty.http.HttpField;
51  import org.eclipse.jetty.http.HttpHeader;
52  import org.eclipse.jetty.http.HttpHeaderValue;
53  import org.eclipse.jetty.http.HttpVersion;
54  import org.eclipse.jetty.util.Callback;
55  import org.eclipse.jetty.util.HttpCookieStore;
56  import org.eclipse.jetty.util.log.Log;
57  import org.eclipse.jetty.util.log.Logger;
58  import org.eclipse.jetty.util.thread.QueuedThreadPool;
59  
60  /**
61   * Asynchronous ProxyServlet.
62   * <p/>
63   * Forwards requests to another server either as a standard web reverse proxy
64   * (as defined by RFC2616) or as a transparent reverse proxy.
65   * <p/>
66   * To facilitate JMX monitoring, the {@link HttpClient} instance is set as context attribute,
67   * prefixed with the servlet's name and exposed by the mechanism provided by
68   * {@link ServletContext#setAttribute(String, Object)}.
69   * <p/>
70   * The following init parameters may be used to configure the servlet:
71   * <ul>
72   * <li>hostHeader - forces the host header to a particular value</li>
73   * <li>viaHost - the name to use in the Via header: Via: http/1.1 &lt;viaHost&gt;</li>
74   * <li>whiteList - comma-separated list of allowed proxy hosts</li>
75   * <li>blackList - comma-separated list of forbidden proxy hosts</li>
76   * </ul>
77   * <p/>
78   * In addition, see {@link #createHttpClient()} for init parameters used to configure
79   * the {@link HttpClient} instance.
80   *
81   * @see ConnectHandler
82   */
83  public class ProxyServlet extends HttpServlet
84  {
85      private static final Set<String> HOP_HEADERS = new HashSet<>();
86      static
87      {
88          HOP_HEADERS.add("connection");
89          HOP_HEADERS.add("keep-alive");
90          HOP_HEADERS.add("proxy-authorization");
91          HOP_HEADERS.add("proxy-authenticate");
92          HOP_HEADERS.add("proxy-connection");
93          HOP_HEADERS.add("transfer-encoding");
94          HOP_HEADERS.add("te");
95          HOP_HEADERS.add("trailer");
96          HOP_HEADERS.add("upgrade");
97      }
98  
99      private final Set<String> _whiteList = new HashSet<>();
100     private final Set<String> _blackList = new HashSet<>();
101 
102     protected Logger _log;
103     private String _hostHeader;
104     private String _viaHost;
105     private HttpClient _client;
106     private long _timeout;
107 
108     @Override
109     public void init() throws ServletException
110     {
111         _log = createLogger();
112 
113         ServletConfig config = getServletConfig();
114 
115         _hostHeader = config.getInitParameter("hostHeader");
116 
117         _viaHost = config.getInitParameter("viaHost");
118         if (_viaHost == null)
119             _viaHost = viaHost();
120 
121         try
122         {
123             _client = createHttpClient();
124 
125             // Put the HttpClient in the context to leverage ContextHandler.MANAGED_ATTRIBUTES
126             getServletContext().setAttribute(config.getServletName() + ".HttpClient", _client);
127 
128             String whiteList = config.getInitParameter("whiteList");
129             if (whiteList != null)
130                 getWhiteListHosts().addAll(parseList(whiteList));
131 
132             String blackList = config.getInitParameter("blackList");
133             if (blackList != null)
134                 getBlackListHosts().addAll(parseList(blackList));
135         }
136         catch (Exception e)
137         {
138             throw new ServletException(e);
139         }
140     }
141 
142     public String getViaHost()
143     {
144         return _viaHost;
145     }
146 
147     public long getTimeout()
148     {
149         return _timeout;
150     }
151 
152     public void setTimeout(long timeout)
153     {
154         this._timeout = timeout;
155     }
156 
157     public Set<String> getWhiteListHosts()
158     {
159         return _whiteList;
160     }
161 
162     public Set<String> getBlackListHosts()
163     {
164         return _blackList;
165     }
166 
167     protected static String viaHost()
168     {
169         try
170         {
171             return InetAddress.getLocalHost().getHostName();
172         }
173         catch (UnknownHostException x)
174         {
175             return "localhost";
176         }
177     }
178 
179     protected HttpClient getHttpClient()
180     {
181         return _client;
182     }
183 
184     /**
185      * @return a logger instance with a name derived from this servlet's name.
186      */
187     protected Logger createLogger()
188     {
189         String servletName = getServletConfig().getServletName();
190         servletName = servletName.replace('-', '.');
191         if ((getClass().getPackage() != null) && !servletName.startsWith(getClass().getPackage().getName()))
192         {
193             servletName = getClass().getName() + "." + servletName;
194         }
195         return Log.getLogger(servletName);
196     }
197 
198     public void destroy()
199     {
200         try
201         {
202             _client.stop();
203         }
204         catch (Exception x)
205         {
206             if (_log.isDebugEnabled())
207                 _log.debug(x);
208         }
209     }
210 
211     /**
212      * Creates a {@link HttpClient} instance, configured with init parameters of this servlet.
213      * <p/>
214      * The init parameters used to configure the {@link HttpClient} instance are:
215      * <table>
216      * <thead>
217      * <tr>
218      * <th>init-param</th>
219      * <th>default</th>
220      * <th>description</th>
221      * </tr>
222      * </thead>
223      * <tbody>
224      * <tr>
225      * <td>maxThreads</td>
226      * <td>256</td>
227      * <td>The max number of threads of HttpClient's Executor.  If not set, or set to the value of "-", then the
228      * Jetty server thread pool will be used.</td>
229      * </tr>
230      * <tr>
231      * <td>maxConnections</td>
232      * <td>32768</td>
233      * <td>The max number of connections per destination, see {@link HttpClient#setMaxConnectionsPerDestination(int)}</td>
234      * </tr>
235      * <tr>
236      * <td>idleTimeout</td>
237      * <td>30000</td>
238      * <td>The idle timeout in milliseconds, see {@link HttpClient#setIdleTimeout(long)}</td>
239      * </tr>
240      * <tr>
241      * <td>timeout</td>
242      * <td>60000</td>
243      * <td>The total timeout in milliseconds, see {@link Request#timeout(long, TimeUnit)}</td>
244      * </tr>
245      * <tr>
246      * <td>requestBufferSize</td>
247      * <td>HttpClient's default</td>
248      * <td>The request buffer size, see {@link HttpClient#setRequestBufferSize(int)}</td>
249      * </tr>
250      * <tr>
251      * <td>responseBufferSize</td>
252      * <td>HttpClient's default</td>
253      * <td>The response buffer size, see {@link HttpClient#setResponseBufferSize(int)}</td>
254      * </tr>
255      * </tbody>
256      * </table>
257      *
258      * @return a {@link HttpClient} configured from the {@link #getServletConfig() servlet configuration}
259      * @throws ServletException if the {@link HttpClient} cannot be created
260      */
261     protected HttpClient createHttpClient() throws ServletException
262     {
263         ServletConfig config = getServletConfig();
264 
265         HttpClient client = newHttpClient();
266         
267         // Redirects must be proxied as is, not followed
268         client.setFollowRedirects(false);
269 
270         // Must not store cookies, otherwise cookies of different clients will mix
271         client.setCookieStore(new HttpCookieStore.Empty());
272 
273         Executor executor;
274         String value = config.getInitParameter("maxThreads");
275         if (value == null || "-".equals(value))
276         {
277             executor = (Executor)getServletContext().getAttribute("org.eclipse.jetty.server.Executor");
278             if (executor==null)
279                 throw new IllegalStateException("No server executor for proxy");
280         }
281         else
282         {
283             QueuedThreadPool qtp= new QueuedThreadPool(Integer.parseInt(value));
284             String servletName = config.getServletName();
285             int dot = servletName.lastIndexOf('.');
286             if (dot >= 0)
287                 servletName = servletName.substring(dot + 1);
288             qtp.setName(servletName);
289             executor=qtp;
290         }
291         
292         client.setExecutor(executor);
293 
294         value = config.getInitParameter("maxConnections");
295         if (value == null)
296             value = "256";
297         client.setMaxConnectionsPerDestination(Integer.parseInt(value));
298 
299         value = config.getInitParameter("idleTimeout");
300         if (value == null)
301             value = "30000";
302         client.setIdleTimeout(Long.parseLong(value));
303 
304         value = config.getInitParameter("timeout");
305         if (value == null)
306             value = "60000";
307         _timeout = Long.parseLong(value);
308 
309         value = config.getInitParameter("requestBufferSize");
310         if (value != null)
311             client.setRequestBufferSize(Integer.parseInt(value));
312 
313         value = config.getInitParameter("responseBufferSize");
314         if (value != null)
315             client.setResponseBufferSize(Integer.parseInt(value));
316 
317         try
318         {
319             client.start();
320 
321             // Content must not be decoded, otherwise the client gets confused
322             client.getContentDecoderFactories().clear();
323 
324             return client;
325         }
326         catch (Exception x)
327         {
328             throw new ServletException(x);
329         }
330     }
331 
332     /**
333      * @return a new HttpClient instance
334      */
335     protected HttpClient newHttpClient()
336     {
337         return new HttpClient();
338     }
339 
340     private Set<String> parseList(String list)
341     {
342         Set<String> result = new HashSet<>();
343         String[] hosts = list.split(",");
344         for (String host : hosts)
345         {
346             host = host.trim();
347             if (host.length() == 0)
348                 continue;
349             result.add(host);
350         }
351         return result;
352     }
353 
354     /**
355      * Checks the given {@code host} and {@code port} against whitelist and blacklist.
356      *
357      * @param host the host to check
358      * @param port the port to check
359      * @return true if it is allowed to be proxy to the given host and port
360      */
361     public boolean validateDestination(String host, int port)
362     {
363         String hostPort = host + ":" + port;
364         if (!_whiteList.isEmpty())
365         {
366             if (!_whiteList.contains(hostPort))
367             {
368                 if (_log.isDebugEnabled())
369                     _log.debug("Host {}:{} not whitelisted", host, port);
370                 return false;
371             }
372         }
373         if (!_blackList.isEmpty())
374         {
375             if (_blackList.contains(hostPort))
376             {
377                 if (_log.isDebugEnabled())
378                     _log.debug("Host {}:{} blacklisted", host, port);
379                 return false;
380             }
381         }
382         return true;
383     }
384 
385     @Override
386     protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException
387     {
388         final int requestId = getRequestId(request);
389 
390         URI rewrittenURI = rewriteURI(request);
391 
392         if (_log.isDebugEnabled())
393         {
394             StringBuffer uri = request.getRequestURL();
395             if (request.getQueryString() != null)
396                 uri.append("?").append(request.getQueryString());
397             if (_log.isDebugEnabled())
398                 _log.debug("{} rewriting: {} -> {}", requestId, uri, rewrittenURI);
399         }
400 
401         if (rewrittenURI == null)
402         {
403             onRewriteFailed(request, response);
404             return;
405         }
406 
407         final Request proxyRequest = _client.newRequest(rewrittenURI)
408                 .method(request.getMethod())
409                 .version(HttpVersion.fromString(request.getProtocol()));
410 
411         // Copy headers.
412 
413         // Any header listed by the Connection header must be removed:
414         // http://tools.ietf.org/html/rfc7230#section-6.1.
415         Set<String> hopHeaders = null;
416         Enumeration<String> connectionHeaders = request.getHeaders(HttpHeader.CONNECTION.asString());
417         while (connectionHeaders.hasMoreElements())
418         {
419             String value = connectionHeaders.nextElement();
420             String[] values = value.split(",");
421             for (String name : values)
422             {
423                 name = name.trim().toLowerCase(Locale.ENGLISH);
424                 if (hopHeaders == null)
425                     hopHeaders = new HashSet<>();
426                 hopHeaders.add(name);
427             }
428         }
429 
430         boolean hasContent = request.getContentLength() > 0 || request.getContentType() != null;
431         for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();)
432         {
433             String headerName = headerNames.nextElement();
434             String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
435 
436             if (HttpHeader.TRANSFER_ENCODING.is(headerName))
437                 hasContent = true;
438 
439             if (_hostHeader != null && HttpHeader.HOST.is(headerName))
440                 continue;
441 
442             // Remove hop-by-hop headers.
443             if (HOP_HEADERS.contains(lowerHeaderName))
444                 continue;
445             if (hopHeaders != null && hopHeaders.contains(lowerHeaderName))
446                 continue;
447 
448             for (Enumeration<String> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();)
449             {
450                 String headerValue = headerValues.nextElement();
451                 if (headerValue != null)
452                     proxyRequest.header(headerName, headerValue);
453             }
454         }
455 
456         // Force the Host header if configured
457         if (_hostHeader != null)
458             proxyRequest.header(HttpHeader.HOST, _hostHeader);
459 
460         // Add proxy headers
461         addViaHeader(proxyRequest);
462         addXForwardedHeaders(proxyRequest, request);
463 
464         final AsyncContext asyncContext = request.startAsync();
465         // We do not timeout the continuation, but the proxy request
466         asyncContext.setTimeout(0);
467         proxyRequest.timeout(getTimeout(), TimeUnit.MILLISECONDS);
468 
469         if (hasContent)
470             proxyRequest.content(proxyRequestContent(proxyRequest, request));
471 
472         customizeProxyRequest(proxyRequest, request);
473 
474         if (_log.isDebugEnabled())
475         {
476             StringBuilder builder = new StringBuilder(request.getMethod());
477             builder.append(" ").append(request.getRequestURI());
478             String query = request.getQueryString();
479             if (query != null)
480                 builder.append("?").append(query);
481             builder.append(" ").append(request.getProtocol()).append("\r\n");
482             for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();)
483             {
484                 String headerName = headerNames.nextElement();
485                 builder.append(headerName).append(": ");
486                 for (Enumeration<String> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();)
487                 {
488                     String headerValue = headerValues.nextElement();
489                     if (headerValue != null)
490                         builder.append(headerValue);
491                     if (headerValues.hasMoreElements())
492                         builder.append(",");
493                 }
494                 builder.append("\r\n");
495             }
496             builder.append("\r\n");
497 
498             _log.debug("{} proxying to upstream:{}{}{}{}",
499                     requestId,
500                     System.lineSeparator(),
501                     builder,
502                     proxyRequest,
503                     System.lineSeparator(),
504                     proxyRequest.getHeaders().toString().trim());
505         }
506 
507         proxyRequest.send(newProxyResponseListener(request, response));
508     }
509 
510     protected ContentProvider proxyRequestContent(final Request proxyRequest, final HttpServletRequest request) throws IOException
511     {
512         return new ProxyInputStreamContentProvider(proxyRequest, request, request.getInputStream());
513     }
514 
515     protected Response.Listener newProxyResponseListener(HttpServletRequest request, HttpServletResponse response)
516     {
517         return new ProxyResponseListener(request, response);
518     }
519 
520     protected void onClientRequestFailure(Request proxyRequest, HttpServletRequest request, Throwable failure)
521     {
522         if (_log.isDebugEnabled())
523             _log.debug(getRequestId(request) + " client request failure", failure);
524         proxyRequest.abort(failure);
525     }
526 
527     protected void onRewriteFailed(HttpServletRequest request, HttpServletResponse response) throws IOException
528     {
529         response.sendError(HttpServletResponse.SC_FORBIDDEN);
530     }
531 
532     protected Request addViaHeader(Request proxyRequest)
533     {
534         return proxyRequest.header(HttpHeader.VIA, "http/1.1 " + getViaHost());
535     }
536 
537     protected void addXForwardedHeaders(Request proxyRequest, HttpServletRequest request)
538     {
539         proxyRequest.header(HttpHeader.X_FORWARDED_FOR, request.getRemoteAddr());
540         proxyRequest.header(HttpHeader.X_FORWARDED_PROTO, request.getScheme());
541         proxyRequest.header(HttpHeader.X_FORWARDED_HOST, request.getHeader(HttpHeader.HOST.asString()));
542         proxyRequest.header(HttpHeader.X_FORWARDED_SERVER, request.getLocalName());
543     }
544 
545     protected void onResponseHeaders(HttpServletRequest request, HttpServletResponse response, Response proxyResponse)
546     {
547         for (HttpField field : proxyResponse.getHeaders())
548         {
549             String headerName = field.getName();
550             String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
551             if (HOP_HEADERS.contains(lowerHeaderName))
552                 continue;
553 
554             String newHeaderValue = filterResponseHeader(request, headerName, field.getValue());
555             if (newHeaderValue == null || newHeaderValue.trim().length() == 0)
556                 continue;
557 
558             response.addHeader(headerName, newHeaderValue);
559         }
560     }
561 
562     protected void onResponseContent(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, byte[] buffer, int offset, int length, Callback callback)
563     {
564         try
565         {
566             if (_log.isDebugEnabled())
567                 _log.debug("{} proxying content to downstream: {} bytes", getRequestId(request), length);
568             response.getOutputStream().write(buffer, offset, length);
569             callback.succeeded();
570         }
571         catch (Throwable x)
572         {
573             callback.failed(x);
574         }
575     }
576 
577     protected void onResponseSuccess(HttpServletRequest request, HttpServletResponse response, Response proxyResponse)
578     {
579         if (_log.isDebugEnabled())
580             _log.debug("{} proxying successful", getRequestId(request));
581         AsyncContext asyncContext = request.getAsyncContext();
582         asyncContext.complete();
583     }
584 
585     protected void onResponseFailure(HttpServletRequest request, HttpServletResponse response, Response proxyResponse, Throwable failure)
586     {
587         if (_log.isDebugEnabled())
588             _log.debug(getRequestId(request) + " proxying failed", failure);
589         if (response.isCommitted())
590         {
591             try
592             {
593                 // Use Jetty specific behavior to close connection.
594                 response.sendError(-1);
595                 AsyncContext asyncContext = request.getAsyncContext();
596                 asyncContext.complete();
597             }
598             catch (IOException x)
599             {
600                 if (_log.isDebugEnabled())
601                     _log.debug(getRequestId(request) + " could not close the connection", failure);
602             }
603         }
604         else
605         {
606             response.resetBuffer();
607             if (failure instanceof TimeoutException)
608                 response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT);
609             else
610                 response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
611             response.setHeader(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString());
612             AsyncContext asyncContext = request.getAsyncContext();
613             asyncContext.complete();
614         }
615     }
616 
617     protected int getRequestId(HttpServletRequest request)
618     {
619         return System.identityHashCode(request);
620     }
621 
622     protected URI rewriteURI(HttpServletRequest request)
623     {
624         if (!validateDestination(request.getServerName(), request.getServerPort()))
625             return null;
626 
627         StringBuffer uri = request.getRequestURL();
628         String query = request.getQueryString();
629         if (query != null)
630             uri.append("?").append(query);
631 
632         return URI.create(uri.toString());
633     }
634 
635     /**
636      * Extension point for subclasses to customize the proxy request.
637      * The default implementation does nothing.
638      *
639      * @param proxyRequest the proxy request to customize
640      * @param request the request to be proxied
641      */
642     protected void customizeProxyRequest(Request proxyRequest, HttpServletRequest request)
643     {
644     }
645 
646     /**
647      * Extension point for remote server response header filtering.
648      * The default implementation returns the header value as is.
649      * If null is returned, this header won't be forwarded back to the client.
650      *
651      * @param headerName the header name
652      * @param headerValue the header value
653      * @param request the request to proxy
654      * @return filteredHeaderValue the new header value
655      */
656     protected String filterResponseHeader(HttpServletRequest request, String headerName, String headerValue)
657     {
658         return headerValue;
659     }
660 
661     /**
662      * This convenience extension to {@link ProxyServlet} configures the servlet as a transparent proxy.
663      * This servlet is configured with the following init parameters:
664      * <ul>
665      * <li>proxyTo - a mandatory URI like http://host:80/context to which the request is proxied.</li>
666      * <li>prefix - an optional URI prefix that is stripped from the start of the forwarded URI.</li>
667      * </ul>
668      * <p/>
669      * For example, if a request is received at "/foo/bar", the 'proxyTo' parameter is "http://host:80/context"
670      * and the 'prefix' parameter is "/foo", then the request would be proxied to "http://host:80/context/bar".
671      */
672     public static class Transparent extends ProxyServlet
673     {
674         private final TransparentDelegate delegate = new TransparentDelegate(this);
675 
676         @Override
677         public void init(ServletConfig config) throws ServletException
678         {
679             super.init(config);
680             delegate.init(config);
681         }
682 
683         @Override
684         protected URI rewriteURI(HttpServletRequest request)
685         {
686             return delegate.rewriteURI(request);
687         }
688     }
689 
690     protected static class TransparentDelegate
691     {
692         private final ProxyServlet proxyServlet;
693         private String _proxyTo;
694         private String _prefix;
695 
696         protected TransparentDelegate(ProxyServlet proxyServlet)
697         {
698             this.proxyServlet = proxyServlet;
699         }
700 
701         protected void init(ServletConfig config) throws ServletException
702         {
703             _proxyTo = config.getInitParameter("proxyTo");
704             if (_proxyTo == null)
705                 throw new UnavailableException("Init parameter 'proxyTo' is required.");
706 
707             String prefix = config.getInitParameter("prefix");
708             if (prefix != null)
709             {
710                 if (!prefix.startsWith("/"))
711                     throw new UnavailableException("Init parameter 'prefix' must start with a '/'.");
712                 _prefix = prefix;
713             }
714 
715             // Adjust prefix value to account for context path
716             String contextPath = config.getServletContext().getContextPath();
717             _prefix = _prefix == null ? contextPath : (contextPath + _prefix);
718 
719             if (proxyServlet._log.isDebugEnabled())
720                 proxyServlet._log.debug(config.getServletName() + " @ " + _prefix + " to " + _proxyTo);
721         }
722 
723         protected URI rewriteURI(HttpServletRequest request)
724         {
725             String path = request.getRequestURI();
726             if (!path.startsWith(_prefix))
727                 return null;
728 
729             StringBuilder uri = new StringBuilder(_proxyTo);
730             if (_proxyTo.endsWith("/"))
731                 uri.setLength(uri.length() - 1);
732             String rest = path.substring(_prefix.length());
733             if (!rest.startsWith("/"))
734                 uri.append("/");
735             uri.append(rest);
736             String query = request.getQueryString();
737             if (query != null)
738                 uri.append("?").append(query);
739             URI rewrittenURI = URI.create(uri.toString()).normalize();
740 
741             if (!proxyServlet.validateDestination(rewrittenURI.getHost(), rewrittenURI.getPort()))
742                 return null;
743 
744             return rewrittenURI;
745         }
746     }
747 
748     protected class ProxyResponseListener extends Response.Listener.Adapter
749     {
750         private final HttpServletRequest request;
751         private final HttpServletResponse response;
752 
753         protected ProxyResponseListener(HttpServletRequest request, HttpServletResponse response)
754         {
755             this.request = request;
756             this.response = response;
757         }
758 
759         @Override
760         public void onBegin(Response proxyResponse)
761         {
762             response.setStatus(proxyResponse.getStatus());
763         }
764 
765         @Override
766         public void onHeaders(Response proxyResponse)
767         {
768             onResponseHeaders(request, response, proxyResponse);
769 
770             if (_log.isDebugEnabled())
771             {
772                 StringBuilder builder = new StringBuilder("\r\n");
773                 builder.append(request.getProtocol()).append(" ").append(response.getStatus()).append(" ").append(proxyResponse.getReason()).append("\r\n");
774                 for (String headerName : response.getHeaderNames())
775                 {
776                     builder.append(headerName).append(": ");
777                     for (Iterator<String> headerValues = response.getHeaders(headerName).iterator(); headerValues.hasNext();)
778                     {
779                         String headerValue = headerValues.next();
780                         if (headerValue != null)
781                             builder.append(headerValue);
782                         if (headerValues.hasNext())
783                             builder.append(",");
784                     }
785                     builder.append("\r\n");
786                 }
787                 _log.debug("{} proxying to downstream:{}{}{}{}{}",
788                         getRequestId(request),
789                         System.lineSeparator(),
790                         proxyResponse,
791                         System.lineSeparator(),
792                         proxyResponse.getHeaders().toString().trim(),
793                         System.lineSeparator(),
794                         builder);
795             }
796         }
797 
798         @Override
799         public void onContent(final Response proxyResponse, ByteBuffer content, final Callback callback)
800         {
801             byte[] buffer;
802             int offset;
803             int length = content.remaining();
804             if (content.hasArray())
805             {
806                 buffer = content.array();
807                 offset = content.arrayOffset();
808             }
809             else
810             {
811                 buffer = new byte[length];
812                 content.get(buffer);
813                 offset = 0;
814             }
815 
816             onResponseContent(request, response, proxyResponse, buffer, offset, length, new Callback()
817             {
818                 @Override
819                 public void succeeded()
820                 {
821                     callback.succeeded();
822                 }
823 
824                 @Override
825                 public void failed(Throwable x)
826                 {
827                     callback.failed(x);
828                     proxyResponse.abort(x);
829                 }
830             });
831         }
832 
833         @Override
834         public void onComplete(Result result)
835         {
836             if (result.isSucceeded())
837                 onResponseSuccess(request, response, result.getResponse());
838             else
839                 onResponseFailure(request, response, result.getResponse(), result.getFailure());
840             if (_log.isDebugEnabled())
841                 _log.debug("{} proxying complete", getRequestId(request));
842         }
843     }
844 
845     protected class ProxyInputStreamContentProvider extends InputStreamContentProvider
846     {
847         private final Request proxyRequest;
848         private final HttpServletRequest request;
849 
850         protected ProxyInputStreamContentProvider(Request proxyRequest, HttpServletRequest request, InputStream input)
851         {
852             super(input);
853             this.proxyRequest = proxyRequest;
854             this.request = request;
855         }
856 
857         @Override
858         public long getLength()
859         {
860             return request.getContentLength();
861         }
862 
863         @Override
864         protected ByteBuffer onRead(byte[] buffer, int offset, int length)
865         {
866             if (_log.isDebugEnabled())
867                 _log.debug("{} proxying content to upstream: {} bytes", getRequestId(request), length);
868             return onRequestContent(proxyRequest, request, buffer, offset, length);
869         }
870 
871         protected ByteBuffer onRequestContent(Request proxyRequest, final HttpServletRequest request, byte[] buffer, int offset, int length)
872         {
873             return super.onRead(buffer, offset, length);
874         }
875 
876         @Override
877         protected void onReadFailure(Throwable failure)
878         {
879             onClientRequestFailure(proxyRequest, request, failure);
880         }
881     }
882 }