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