View Javadoc

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