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