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