1/* 2 * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/client/DefaultRequestDirector.java $ 3 * $Revision: 676023 $ 4 * $Date: 2008-07-11 09:40:56 -0700 (Fri, 11 Jul 2008) $ 5 * 6 * ==================================================================== 7 * Licensed to the Apache Software Foundation (ASF) under one 8 * or more contributor license agreements. See the NOTICE file 9 * distributed with this work for additional information 10 * regarding copyright ownership. The ASF licenses this file 11 * to you under the Apache License, Version 2.0 (the 12 * "License"); you may not use this file except in compliance 13 * with the License. You may obtain a copy of the License at 14 * 15 * http://www.apache.org/licenses/LICENSE-2.0 16 * 17 * Unless required by applicable law or agreed to in writing, 18 * software distributed under the License is distributed on an 19 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 20 * KIND, either express or implied. See the License for the 21 * specific language governing permissions and limitations 22 * under the License. 23 * ==================================================================== 24 * 25 * This software consists of voluntary contributions made by many 26 * individuals on behalf of the Apache Software Foundation. For more 27 * information on the Apache Software Foundation, please see 28 * <http://www.apache.org/>. 29 * 30 */ 31 32package org.apache.http.impl.client; 33 34import java.io.IOException; 35import java.io.InterruptedIOException; 36import java.net.URI; 37import java.net.URISyntaxException; 38import java.util.Locale; 39import java.util.Map; 40import java.util.concurrent.TimeUnit; 41 42import org.apache.commons.logging.Log; 43import org.apache.commons.logging.LogFactory; 44import org.apache.http.ConnectionReuseStrategy; 45import org.apache.http.Header; 46import org.apache.http.HttpEntity; 47import org.apache.http.HttpEntityEnclosingRequest; 48import org.apache.http.HttpException; 49import org.apache.http.HttpHost; 50import org.apache.http.HttpRequest; 51import org.apache.http.HttpResponse; 52import org.apache.http.ProtocolException; 53import org.apache.http.ProtocolVersion; 54import org.apache.http.auth.AuthScheme; 55import org.apache.http.auth.AuthScope; 56import org.apache.http.auth.AuthState; 57import org.apache.http.auth.AuthenticationException; 58import org.apache.http.auth.Credentials; 59import org.apache.http.auth.MalformedChallengeException; 60import org.apache.http.client.AuthenticationHandler; 61import org.apache.http.client.RequestDirector; 62import org.apache.http.client.CredentialsProvider; 63import org.apache.http.client.HttpRequestRetryHandler; 64import org.apache.http.client.NonRepeatableRequestException; 65import org.apache.http.client.RedirectException; 66import org.apache.http.client.RedirectHandler; 67import org.apache.http.client.UserTokenHandler; 68import org.apache.http.client.methods.AbortableHttpRequest; 69import org.apache.http.client.methods.HttpGet; 70import org.apache.http.client.params.ClientPNames; 71import org.apache.http.client.params.HttpClientParams; 72import org.apache.http.client.protocol.ClientContext; 73import org.apache.http.client.utils.URIUtils; 74import org.apache.http.conn.BasicManagedEntity; 75import org.apache.http.conn.ClientConnectionManager; 76import org.apache.http.conn.ClientConnectionRequest; 77import org.apache.http.conn.ConnectionKeepAliveStrategy; 78import org.apache.http.conn.ManagedClientConnection; 79import org.apache.http.conn.params.ConnManagerParams; 80import org.apache.http.conn.routing.BasicRouteDirector; 81import org.apache.http.conn.routing.HttpRoute; 82import org.apache.http.conn.routing.HttpRouteDirector; 83import org.apache.http.conn.routing.HttpRoutePlanner; 84import org.apache.http.conn.scheme.Scheme; 85import org.apache.http.entity.BufferedHttpEntity; 86import org.apache.http.message.BasicHttpRequest; 87import org.apache.http.params.HttpConnectionParams; 88import org.apache.http.params.HttpParams; 89import org.apache.http.params.HttpProtocolParams; 90import org.apache.http.protocol.ExecutionContext; 91import org.apache.http.protocol.HTTP; 92import org.apache.http.protocol.HttpContext; 93import org.apache.http.protocol.HttpProcessor; 94import org.apache.http.protocol.HttpRequestExecutor; 95 96/** 97 * Default implementation of {@link RequestDirector}. 98 * <br/> 99 * This class replaces the <code>HttpMethodDirector</code> in HttpClient 3. 100 * 101 * @author <a href="mailto:rolandw at apache.org">Roland Weber</a> 102 * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> 103 * 104 * <!-- empty lines to avoid svn diff problems --> 105 * @version $Revision: 676023 $ 106 * 107 * @since 4.0 108 */ 109public class DefaultRequestDirector implements RequestDirector { 110 111 private final Log log = LogFactory.getLog(getClass()); 112 113 /** The connection manager. */ 114 protected final ClientConnectionManager connManager; 115 116 /** The route planner. */ 117 protected final HttpRoutePlanner routePlanner; 118 119 /** The connection re-use strategy. */ 120 protected final ConnectionReuseStrategy reuseStrategy; 121 122 /** The keep-alive duration strategy. */ 123 protected final ConnectionKeepAliveStrategy keepAliveStrategy; 124 125 /** The request executor. */ 126 protected final HttpRequestExecutor requestExec; 127 128 /** The HTTP protocol processor. */ 129 protected final HttpProcessor httpProcessor; 130 131 /** The request retry handler. */ 132 protected final HttpRequestRetryHandler retryHandler; 133 134 /** The redirect handler. */ 135 protected final RedirectHandler redirectHandler; 136 137 /** The target authentication handler. */ 138 private final AuthenticationHandler targetAuthHandler; 139 140 /** The proxy authentication handler. */ 141 private final AuthenticationHandler proxyAuthHandler; 142 143 /** The user token handler. */ 144 private final UserTokenHandler userTokenHandler; 145 146 /** The HTTP parameters. */ 147 protected final HttpParams params; 148 149 /** The currently allocated connection. */ 150 protected ManagedClientConnection managedConn; 151 152 private int redirectCount; 153 154 private int maxRedirects; 155 156 private final AuthState targetAuthState; 157 158 private final AuthState proxyAuthState; 159 160 public DefaultRequestDirector( 161 final HttpRequestExecutor requestExec, 162 final ClientConnectionManager conman, 163 final ConnectionReuseStrategy reustrat, 164 final ConnectionKeepAliveStrategy kastrat, 165 final HttpRoutePlanner rouplan, 166 final HttpProcessor httpProcessor, 167 final HttpRequestRetryHandler retryHandler, 168 final RedirectHandler redirectHandler, 169 final AuthenticationHandler targetAuthHandler, 170 final AuthenticationHandler proxyAuthHandler, 171 final UserTokenHandler userTokenHandler, 172 final HttpParams params) { 173 174 if (requestExec == null) { 175 throw new IllegalArgumentException 176 ("Request executor may not be null."); 177 } 178 if (conman == null) { 179 throw new IllegalArgumentException 180 ("Client connection manager may not be null."); 181 } 182 if (reustrat == null) { 183 throw new IllegalArgumentException 184 ("Connection reuse strategy may not be null."); 185 } 186 if (kastrat == null) { 187 throw new IllegalArgumentException 188 ("Connection keep alive strategy may not be null."); 189 } 190 if (rouplan == null) { 191 throw new IllegalArgumentException 192 ("Route planner may not be null."); 193 } 194 if (httpProcessor == null) { 195 throw new IllegalArgumentException 196 ("HTTP protocol processor may not be null."); 197 } 198 if (retryHandler == null) { 199 throw new IllegalArgumentException 200 ("HTTP request retry handler may not be null."); 201 } 202 if (redirectHandler == null) { 203 throw new IllegalArgumentException 204 ("Redirect handler may not be null."); 205 } 206 if (targetAuthHandler == null) { 207 throw new IllegalArgumentException 208 ("Target authentication handler may not be null."); 209 } 210 if (proxyAuthHandler == null) { 211 throw new IllegalArgumentException 212 ("Proxy authentication handler may not be null."); 213 } 214 if (userTokenHandler == null) { 215 throw new IllegalArgumentException 216 ("User token handler may not be null."); 217 } 218 if (params == null) { 219 throw new IllegalArgumentException 220 ("HTTP parameters may not be null"); 221 } 222 this.requestExec = requestExec; 223 this.connManager = conman; 224 this.reuseStrategy = reustrat; 225 this.keepAliveStrategy = kastrat; 226 this.routePlanner = rouplan; 227 this.httpProcessor = httpProcessor; 228 this.retryHandler = retryHandler; 229 this.redirectHandler = redirectHandler; 230 this.targetAuthHandler = targetAuthHandler; 231 this.proxyAuthHandler = proxyAuthHandler; 232 this.userTokenHandler = userTokenHandler; 233 this.params = params; 234 235 this.managedConn = null; 236 237 this.redirectCount = 0; 238 this.maxRedirects = this.params.getIntParameter(ClientPNames.MAX_REDIRECTS, 100); 239 this.targetAuthState = new AuthState(); 240 this.proxyAuthState = new AuthState(); 241 } // constructor 242 243 244 private RequestWrapper wrapRequest( 245 final HttpRequest request) throws ProtocolException { 246 if (request instanceof HttpEntityEnclosingRequest) { 247 return new EntityEnclosingRequestWrapper( 248 (HttpEntityEnclosingRequest) request); 249 } else { 250 return new RequestWrapper( 251 request); 252 } 253 } 254 255 256 protected void rewriteRequestURI( 257 final RequestWrapper request, 258 final HttpRoute route) throws ProtocolException { 259 try { 260 261 URI uri = request.getURI(); 262 if (route.getProxyHost() != null && !route.isTunnelled()) { 263 // Make sure the request URI is absolute 264 if (!uri.isAbsolute()) { 265 HttpHost target = route.getTargetHost(); 266 uri = URIUtils.rewriteURI(uri, target); 267 request.setURI(uri); 268 } 269 } else { 270 // Make sure the request URI is relative 271 if (uri.isAbsolute()) { 272 uri = URIUtils.rewriteURI(uri, null); 273 request.setURI(uri); 274 } 275 } 276 277 } catch (URISyntaxException ex) { 278 throw new ProtocolException("Invalid URI: " + 279 request.getRequestLine().getUri(), ex); 280 } 281 } 282 283 284 // non-javadoc, see interface ClientRequestDirector 285 public HttpResponse execute(HttpHost target, HttpRequest request, 286 HttpContext context) 287 throws HttpException, IOException { 288 289 HttpRequest orig = request; 290 RequestWrapper origWrapper = wrapRequest(orig); 291 origWrapper.setParams(params); 292 HttpRoute origRoute = determineRoute(target, origWrapper, context); 293 294 RoutedRequest roureq = new RoutedRequest(origWrapper, origRoute); 295 296 long timeout = ConnManagerParams.getTimeout(params); 297 298 int execCount = 0; 299 300 boolean reuse = false; 301 HttpResponse response = null; 302 boolean done = false; 303 try { 304 while (!done) { 305 // In this loop, the RoutedRequest may be replaced by a 306 // followup request and route. The request and route passed 307 // in the method arguments will be replaced. The original 308 // request is still available in 'orig'. 309 310 RequestWrapper wrapper = roureq.getRequest(); 311 HttpRoute route = roureq.getRoute(); 312 313 // See if we have a user token bound to the execution context 314 Object userToken = context.getAttribute(ClientContext.USER_TOKEN); 315 316 // Allocate connection if needed 317 if (managedConn == null) { 318 ClientConnectionRequest connRequest = connManager.requestConnection( 319 route, userToken); 320 if (orig instanceof AbortableHttpRequest) { 321 ((AbortableHttpRequest) orig).setConnectionRequest(connRequest); 322 } 323 324 try { 325 managedConn = connRequest.getConnection(timeout, TimeUnit.MILLISECONDS); 326 } catch(InterruptedException interrupted) { 327 InterruptedIOException iox = new InterruptedIOException(); 328 iox.initCause(interrupted); 329 throw iox; 330 } 331 332 if (HttpConnectionParams.isStaleCheckingEnabled(params)) { 333 // validate connection 334 this.log.debug("Stale connection check"); 335 if (managedConn.isStale()) { 336 this.log.debug("Stale connection detected"); 337 managedConn.close(); 338 } 339 } 340 } 341 342 if (orig instanceof AbortableHttpRequest) { 343 ((AbortableHttpRequest) orig).setReleaseTrigger(managedConn); 344 } 345 346 // Reopen connection if needed 347 if (!managedConn.isOpen()) { 348 managedConn.open(route, context, params); 349 } 350 351 try { 352 establishRoute(route, context); 353 } catch (TunnelRefusedException ex) { 354 if (this.log.isDebugEnabled()) { 355 this.log.debug(ex.getMessage()); 356 } 357 response = ex.getResponse(); 358 break; 359 } 360 361 // Reset headers on the request wrapper 362 wrapper.resetHeaders(); 363 364 // Re-write request URI if needed 365 rewriteRequestURI(wrapper, route); 366 367 // Use virtual host if set 368 target = (HttpHost) wrapper.getParams().getParameter( 369 ClientPNames.VIRTUAL_HOST); 370 371 if (target == null) { 372 target = route.getTargetHost(); 373 } 374 375 HttpHost proxy = route.getProxyHost(); 376 377 // Populate the execution context 378 context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, 379 target); 380 context.setAttribute(ExecutionContext.HTTP_PROXY_HOST, 381 proxy); 382 context.setAttribute(ExecutionContext.HTTP_CONNECTION, 383 managedConn); 384 context.setAttribute(ClientContext.TARGET_AUTH_STATE, 385 targetAuthState); 386 context.setAttribute(ClientContext.PROXY_AUTH_STATE, 387 proxyAuthState); 388 389 // Run request protocol interceptors 390 requestExec.preProcess(wrapper, httpProcessor, context); 391 392 context.setAttribute(ExecutionContext.HTTP_REQUEST, 393 wrapper); 394 395 boolean retrying = true; 396 while (retrying) { 397 // Increment total exec count (with redirects) 398 execCount++; 399 // Increment exec count for this particular request 400 wrapper.incrementExecCount(); 401 if (wrapper.getExecCount() > 1 && !wrapper.isRepeatable()) { 402 throw new NonRepeatableRequestException("Cannot retry request " + 403 "with a non-repeatable request entity"); 404 } 405 406 try { 407 if (this.log.isDebugEnabled()) { 408 this.log.debug("Attempt " + execCount + " to execute request"); 409 } 410 response = requestExec.execute(wrapper, managedConn, context); 411 retrying = false; 412 413 } catch (IOException ex) { 414 this.log.debug("Closing the connection."); 415 managedConn.close(); 416 if (retryHandler.retryRequest(ex, execCount, context)) { 417 if (this.log.isInfoEnabled()) { 418 this.log.info("I/O exception ("+ ex.getClass().getName() + 419 ") caught when processing request: " 420 + ex.getMessage()); 421 } 422 if (this.log.isDebugEnabled()) { 423 this.log.debug(ex.getMessage(), ex); 424 } 425 this.log.info("Retrying request"); 426 } else { 427 throw ex; 428 } 429 430 // If we have a direct route to the target host 431 // just re-open connection and re-try the request 432 if (route.getHopCount() == 1) { 433 this.log.debug("Reopening the direct connection."); 434 managedConn.open(route, context, params); 435 } else { 436 // otherwise give up 437 retrying = false; 438 } 439 440 } 441 442 } 443 444 // Run response protocol interceptors 445 response.setParams(params); 446 requestExec.postProcess(response, httpProcessor, context); 447 448 449 // The connection is in or can be brought to a re-usable state. 450 reuse = reuseStrategy.keepAlive(response, context); 451 if(reuse) { 452 // Set the idle duration of this connection 453 long duration = keepAliveStrategy.getKeepAliveDuration(response, context); 454 managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS); 455 } 456 457 RoutedRequest followup = handleResponse(roureq, response, context); 458 if (followup == null) { 459 done = true; 460 } else { 461 if (reuse) { 462 this.log.debug("Connection kept alive"); 463 // Make sure the response body is fully consumed, if present 464 HttpEntity entity = response.getEntity(); 465 if (entity != null) { 466 entity.consumeContent(); 467 } 468 // entity consumed above is not an auto-release entity, 469 // need to mark the connection re-usable explicitly 470 managedConn.markReusable(); 471 } else { 472 managedConn.close(); 473 } 474 // check if we can use the same connection for the followup 475 if (!followup.getRoute().equals(roureq.getRoute())) { 476 releaseConnection(); 477 } 478 roureq = followup; 479 } 480 481 userToken = this.userTokenHandler.getUserToken(context); 482 context.setAttribute(ClientContext.USER_TOKEN, userToken); 483 if (managedConn != null) { 484 managedConn.setState(userToken); 485 } 486 } // while not done 487 488 489 // check for entity, release connection if possible 490 if ((response == null) || (response.getEntity() == null) || 491 !response.getEntity().isStreaming()) { 492 // connection not needed and (assumed to be) in re-usable state 493 if (reuse) 494 managedConn.markReusable(); 495 releaseConnection(); 496 } else { 497 // install an auto-release entity 498 HttpEntity entity = response.getEntity(); 499 entity = new BasicManagedEntity(entity, managedConn, reuse); 500 response.setEntity(entity); 501 } 502 503 return response; 504 505 } catch (HttpException ex) { 506 abortConnection(); 507 throw ex; 508 } catch (IOException ex) { 509 abortConnection(); 510 throw ex; 511 } catch (RuntimeException ex) { 512 abortConnection(); 513 throw ex; 514 } 515 } // execute 516 517 /** 518 * Returns the connection back to the connection manager 519 * and prepares for retrieving a new connection during 520 * the next request. 521 */ 522 protected void releaseConnection() { 523 // Release the connection through the ManagedConnection instead of the 524 // ConnectionManager directly. This lets the connection control how 525 // it is released. 526 try { 527 managedConn.releaseConnection(); 528 } catch(IOException ignored) { 529 this.log.debug("IOException releasing connection", ignored); 530 } 531 managedConn = null; 532 } 533 534 /** 535 * Determines the route for a request. 536 * Called by {@link #execute} 537 * to determine the route for either the original or a followup request. 538 * 539 * @param target the target host for the request. 540 * Implementations may accept <code>null</code> 541 * if they can still determine a route, for example 542 * to a default target or by inspecting the request. 543 * @param request the request to execute 544 * @param context the context to use for the execution, 545 * never <code>null</code> 546 * 547 * @return the route the request should take 548 * 549 * @throws HttpException in case of a problem 550 */ 551 protected HttpRoute determineRoute(HttpHost target, 552 HttpRequest request, 553 HttpContext context) 554 throws HttpException { 555 556 if (target == null) { 557 target = (HttpHost) request.getParams().getParameter( 558 ClientPNames.DEFAULT_HOST); 559 } 560 if (target == null) { 561 throw new IllegalStateException 562 ("Target host must not be null, or set in parameters."); 563 } 564 565 return this.routePlanner.determineRoute(target, request, context); 566 } 567 568 569 /** 570 * Establishes the target route. 571 * 572 * @param route the route to establish 573 * @param context the context for the request execution 574 * 575 * @throws HttpException in case of a problem 576 * @throws IOException in case of an IO problem 577 */ 578 protected void establishRoute(HttpRoute route, HttpContext context) 579 throws HttpException, IOException { 580 581 //@@@ how to handle CONNECT requests for tunnelling? 582 //@@@ refuse to send external CONNECT via director? special handling? 583 584 //@@@ should the request parameters already be used below? 585 //@@@ probably yes, but they're not linked yet 586 //@@@ will linking above cause problems with linking in reqExec? 587 //@@@ probably not, because the parent is replaced 588 //@@@ just make sure we don't link parameters to themselves 589 590 HttpRouteDirector rowdy = new BasicRouteDirector(); 591 int step; 592 do { 593 HttpRoute fact = managedConn.getRoute(); 594 step = rowdy.nextStep(route, fact); 595 596 switch (step) { 597 598 case HttpRouteDirector.CONNECT_TARGET: 599 case HttpRouteDirector.CONNECT_PROXY: 600 managedConn.open(route, context, this.params); 601 break; 602 603 case HttpRouteDirector.TUNNEL_TARGET: { 604 boolean secure = createTunnelToTarget(route, context); 605 this.log.debug("Tunnel to target created."); 606 managedConn.tunnelTarget(secure, this.params); 607 } break; 608 609 case HttpRouteDirector.TUNNEL_PROXY: { 610 // The most simple example for this case is a proxy chain 611 // of two proxies, where P1 must be tunnelled to P2. 612 // route: Source -> P1 -> P2 -> Target (3 hops) 613 // fact: Source -> P1 -> Target (2 hops) 614 final int hop = fact.getHopCount()-1; // the hop to establish 615 boolean secure = createTunnelToProxy(route, hop, context); 616 this.log.debug("Tunnel to proxy created."); 617 managedConn.tunnelProxy(route.getHopTarget(hop), 618 secure, this.params); 619 } break; 620 621 622 case HttpRouteDirector.LAYER_PROTOCOL: 623 managedConn.layerProtocol(context, this.params); 624 break; 625 626 case HttpRouteDirector.UNREACHABLE: 627 throw new IllegalStateException 628 ("Unable to establish route." + 629 "\nplanned = " + route + 630 "\ncurrent = " + fact); 631 632 case HttpRouteDirector.COMPLETE: 633 // do nothing 634 break; 635 636 default: 637 throw new IllegalStateException 638 ("Unknown step indicator "+step+" from RouteDirector."); 639 } // switch 640 641 } while (step > HttpRouteDirector.COMPLETE); 642 643 } // establishConnection 644 645 646 /** 647 * Creates a tunnel to the target server. 648 * The connection must be established to the (last) proxy. 649 * A CONNECT request for tunnelling through the proxy will 650 * be created and sent, the response received and checked. 651 * This method does <i>not</i> update the connection with 652 * information about the tunnel, that is left to the caller. 653 * 654 * @param route the route to establish 655 * @param context the context for request execution 656 * 657 * @return <code>true</code> if the tunnelled route is secure, 658 * <code>false</code> otherwise. 659 * The implementation here always returns <code>false</code>, 660 * but derived classes may override. 661 * 662 * @throws HttpException in case of a problem 663 * @throws IOException in case of an IO problem 664 */ 665 protected boolean createTunnelToTarget(HttpRoute route, 666 HttpContext context) 667 throws HttpException, IOException { 668 669 HttpHost proxy = route.getProxyHost(); 670 HttpHost target = route.getTargetHost(); 671 HttpResponse response = null; 672 673 boolean done = false; 674 while (!done) { 675 676 done = true; 677 678 if (!this.managedConn.isOpen()) { 679 this.managedConn.open(route, context, this.params); 680 } 681 682 HttpRequest connect = createConnectRequest(route, context); 683 684 String agent = HttpProtocolParams.getUserAgent(params); 685 if (agent != null) { 686 connect.addHeader(HTTP.USER_AGENT, agent); 687 } 688 connect.addHeader(HTTP.TARGET_HOST, target.toHostString()); 689 690 AuthScheme authScheme = this.proxyAuthState.getAuthScheme(); 691 AuthScope authScope = this.proxyAuthState.getAuthScope(); 692 Credentials creds = this.proxyAuthState.getCredentials(); 693 if (creds != null) { 694 if (authScope != null || !authScheme.isConnectionBased()) { 695 try { 696 connect.addHeader(authScheme.authenticate(creds, connect)); 697 } catch (AuthenticationException ex) { 698 if (this.log.isErrorEnabled()) { 699 this.log.error("Proxy authentication error: " + ex.getMessage()); 700 } 701 } 702 } 703 } 704 705 response = requestExec.execute(connect, this.managedConn, context); 706 707 int status = response.getStatusLine().getStatusCode(); 708 if (status < 200) { 709 throw new HttpException("Unexpected response to CONNECT request: " + 710 response.getStatusLine()); 711 } 712 713 CredentialsProvider credsProvider = (CredentialsProvider) 714 context.getAttribute(ClientContext.CREDS_PROVIDER); 715 716 if (credsProvider != null && HttpClientParams.isAuthenticating(params)) { 717 if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) { 718 719 this.log.debug("Proxy requested authentication"); 720 Map<String, Header> challenges = this.proxyAuthHandler.getChallenges( 721 response, context); 722 try { 723 processChallenges( 724 challenges, this.proxyAuthState, this.proxyAuthHandler, 725 response, context); 726 } catch (AuthenticationException ex) { 727 if (this.log.isWarnEnabled()) { 728 this.log.warn("Authentication error: " + ex.getMessage()); 729 break; 730 } 731 } 732 updateAuthState(this.proxyAuthState, proxy, credsProvider); 733 734 if (this.proxyAuthState.getCredentials() != null) { 735 done = false; 736 737 // Retry request 738 if (this.reuseStrategy.keepAlive(response, context)) { 739 this.log.debug("Connection kept alive"); 740 // Consume response content 741 HttpEntity entity = response.getEntity(); 742 if (entity != null) { 743 entity.consumeContent(); 744 } 745 } else { 746 this.managedConn.close(); 747 } 748 749 } 750 751 } else { 752 // Reset proxy auth scope 753 this.proxyAuthState.setAuthScope(null); 754 } 755 } 756 } 757 758 int status = response.getStatusLine().getStatusCode(); 759 760 if (status > 299) { 761 762 // Buffer response content 763 HttpEntity entity = response.getEntity(); 764 if (entity != null) { 765 response.setEntity(new BufferedHttpEntity(entity)); 766 } 767 768 this.managedConn.close(); 769 throw new TunnelRefusedException("CONNECT refused by proxy: " + 770 response.getStatusLine(), response); 771 } 772 773 this.managedConn.markReusable(); 774 775 // How to decide on security of the tunnelled connection? 776 // The socket factory knows only about the segment to the proxy. 777 // Even if that is secure, the hop to the target may be insecure. 778 // Leave it to derived classes, consider insecure by default here. 779 return false; 780 781 } // createTunnelToTarget 782 783 784 785 /** 786 * Creates a tunnel to an intermediate proxy. 787 * This method is <i>not</i> implemented in this class. 788 * It just throws an exception here. 789 * 790 * @param route the route to establish 791 * @param hop the hop in the route to establish now. 792 * <code>route.getHopTarget(hop)</code> 793 * will return the proxy to tunnel to. 794 * @param context the context for request execution 795 * 796 * @return <code>true</code> if the partially tunnelled connection 797 * is secure, <code>false</code> otherwise. 798 * 799 * @throws HttpException in case of a problem 800 * @throws IOException in case of an IO problem 801 */ 802 protected boolean createTunnelToProxy(HttpRoute route, int hop, 803 HttpContext context) 804 throws HttpException, IOException { 805 806 // Have a look at createTunnelToTarget and replicate the parts 807 // you need in a custom derived class. If your proxies don't require 808 // authentication, it is not too hard. But for the stock version of 809 // HttpClient, we cannot make such simplifying assumptions and would 810 // have to include proxy authentication code. The HttpComponents team 811 // is currently not in a position to support rarely used code of this 812 // complexity. Feel free to submit patches that refactor the code in 813 // createTunnelToTarget to facilitate re-use for proxy tunnelling. 814 815 throw new UnsupportedOperationException 816 ("Proxy chains are not supported."); 817 } 818 819 820 821 /** 822 * Creates the CONNECT request for tunnelling. 823 * Called by {@link #createTunnelToTarget createTunnelToTarget}. 824 * 825 * @param route the route to establish 826 * @param context the context for request execution 827 * 828 * @return the CONNECT request for tunnelling 829 */ 830 protected HttpRequest createConnectRequest(HttpRoute route, 831 HttpContext context) { 832 // see RFC 2817, section 5.2 and 833 // INTERNET-DRAFT: Tunneling TCP based protocols through 834 // Web proxy servers 835 836 HttpHost target = route.getTargetHost(); 837 838 String host = target.getHostName(); 839 int port = target.getPort(); 840 if (port < 0) { 841 Scheme scheme = connManager.getSchemeRegistry(). 842 getScheme(target.getSchemeName()); 843 port = scheme.getDefaultPort(); 844 } 845 846 StringBuilder buffer = new StringBuilder(host.length() + 6); 847 buffer.append(host); 848 buffer.append(':'); 849 buffer.append(Integer.toString(port)); 850 851 String authority = buffer.toString(); 852 ProtocolVersion ver = HttpProtocolParams.getVersion(params); 853 HttpRequest req = new BasicHttpRequest 854 ("CONNECT", authority, ver); 855 856 return req; 857 } 858 859 860 /** 861 * Analyzes a response to check need for a followup. 862 * 863 * @param roureq the request and route. 864 * @param response the response to analayze 865 * @param context the context used for the current request execution 866 * 867 * @return the followup request and route if there is a followup, or 868 * <code>null</code> if the response should be returned as is 869 * 870 * @throws HttpException in case of a problem 871 * @throws IOException in case of an IO problem 872 */ 873 protected RoutedRequest handleResponse(RoutedRequest roureq, 874 HttpResponse response, 875 HttpContext context) 876 throws HttpException, IOException { 877 878 HttpRoute route = roureq.getRoute(); 879 HttpHost proxy = route.getProxyHost(); 880 RequestWrapper request = roureq.getRequest(); 881 882 HttpParams params = request.getParams(); 883 if (HttpClientParams.isRedirecting(params) && 884 this.redirectHandler.isRedirectRequested(response, context)) { 885 886 if (redirectCount >= maxRedirects) { 887 throw new RedirectException("Maximum redirects (" 888 + maxRedirects + ") exceeded"); 889 } 890 redirectCount++; 891 892 URI uri = this.redirectHandler.getLocationURI(response, context); 893 894 HttpHost newTarget = new HttpHost( 895 uri.getHost(), 896 uri.getPort(), 897 uri.getScheme()); 898 899 HttpGet redirect = new HttpGet(uri); 900 901 HttpRequest orig = request.getOriginal(); 902 redirect.setHeaders(orig.getAllHeaders()); 903 904 RequestWrapper wrapper = new RequestWrapper(redirect); 905 wrapper.setParams(params); 906 907 HttpRoute newRoute = determineRoute(newTarget, wrapper, context); 908 RoutedRequest newRequest = new RoutedRequest(wrapper, newRoute); 909 910 if (this.log.isDebugEnabled()) { 911 this.log.debug("Redirecting to '" + uri + "' via " + newRoute); 912 } 913 914 return newRequest; 915 } 916 917 CredentialsProvider credsProvider = (CredentialsProvider) 918 context.getAttribute(ClientContext.CREDS_PROVIDER); 919 920 if (credsProvider != null && HttpClientParams.isAuthenticating(params)) { 921 922 if (this.targetAuthHandler.isAuthenticationRequested(response, context)) { 923 924 HttpHost target = (HttpHost) 925 context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); 926 if (target == null) { 927 target = route.getTargetHost(); 928 } 929 930 this.log.debug("Target requested authentication"); 931 Map<String, Header> challenges = this.targetAuthHandler.getChallenges( 932 response, context); 933 try { 934 processChallenges(challenges, 935 this.targetAuthState, this.targetAuthHandler, 936 response, context); 937 } catch (AuthenticationException ex) { 938 if (this.log.isWarnEnabled()) { 939 this.log.warn("Authentication error: " + ex.getMessage()); 940 return null; 941 } 942 } 943 updateAuthState(this.targetAuthState, target, credsProvider); 944 945 if (this.targetAuthState.getCredentials() != null) { 946 // Re-try the same request via the same route 947 return roureq; 948 } else { 949 return null; 950 } 951 } else { 952 // Reset target auth scope 953 this.targetAuthState.setAuthScope(null); 954 } 955 956 if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) { 957 958 this.log.debug("Proxy requested authentication"); 959 Map<String, Header> challenges = this.proxyAuthHandler.getChallenges( 960 response, context); 961 try { 962 processChallenges(challenges, 963 this.proxyAuthState, this.proxyAuthHandler, 964 response, context); 965 } catch (AuthenticationException ex) { 966 if (this.log.isWarnEnabled()) { 967 this.log.warn("Authentication error: " + ex.getMessage()); 968 return null; 969 } 970 } 971 updateAuthState(this.proxyAuthState, proxy, credsProvider); 972 973 if (this.proxyAuthState.getCredentials() != null) { 974 // Re-try the same request via the same route 975 return roureq; 976 } else { 977 return null; 978 } 979 } else { 980 // Reset proxy auth scope 981 this.proxyAuthState.setAuthScope(null); 982 } 983 } 984 return null; 985 } // handleResponse 986 987 988 /** 989 * Shuts down the connection. 990 * This method is called from a <code>catch</code> block in 991 * {@link #execute execute} during exception handling. 992 */ 993 private void abortConnection() { 994 ManagedClientConnection mcc = managedConn; 995 if (mcc != null) { 996 // we got here as the result of an exception 997 // no response will be returned, release the connection 998 managedConn = null; 999 try { 1000 mcc.abortConnection(); 1001 } catch (IOException ex) { 1002 if (this.log.isDebugEnabled()) { 1003 this.log.debug(ex.getMessage(), ex); 1004 } 1005 } 1006 // ensure the connection manager properly releases this connection 1007 try { 1008 mcc.releaseConnection(); 1009 } catch(IOException ignored) { 1010 this.log.debug("Error releasing connection", ignored); 1011 } 1012 } 1013 } // abortConnection 1014 1015 1016 private void processChallenges( 1017 final Map<String, Header> challenges, 1018 final AuthState authState, 1019 final AuthenticationHandler authHandler, 1020 final HttpResponse response, 1021 final HttpContext context) 1022 throws MalformedChallengeException, AuthenticationException { 1023 1024 AuthScheme authScheme = authState.getAuthScheme(); 1025 if (authScheme == null) { 1026 // Authentication not attempted before 1027 authScheme = authHandler.selectScheme(challenges, response, context); 1028 authState.setAuthScheme(authScheme); 1029 } 1030 String id = authScheme.getSchemeName(); 1031 1032 Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH)); 1033 if (challenge == null) { 1034 throw new AuthenticationException(id + 1035 " authorization challenge expected, but not found"); 1036 } 1037 authScheme.processChallenge(challenge); 1038 this.log.debug("Authorization challenge processed"); 1039 } 1040 1041 1042 private void updateAuthState( 1043 final AuthState authState, 1044 final HttpHost host, 1045 final CredentialsProvider credsProvider) { 1046 1047 if (!authState.isValid()) { 1048 return; 1049 } 1050 1051 String hostname = host.getHostName(); 1052 int port = host.getPort(); 1053 if (port < 0) { 1054 Scheme scheme = connManager.getSchemeRegistry().getScheme(host); 1055 port = scheme.getDefaultPort(); 1056 } 1057 1058 AuthScheme authScheme = authState.getAuthScheme(); 1059 AuthScope authScope = new AuthScope( 1060 hostname, 1061 port, 1062 authScheme.getRealm(), 1063 authScheme.getSchemeName()); 1064 1065 if (this.log.isDebugEnabled()) { 1066 this.log.debug("Authentication scope: " + authScope); 1067 } 1068 Credentials creds = authState.getCredentials(); 1069 if (creds == null) { 1070 creds = credsProvider.getCredentials(authScope); 1071 if (this.log.isDebugEnabled()) { 1072 if (creds != null) { 1073 this.log.debug("Found credentials"); 1074 } else { 1075 this.log.debug("Credentials not found"); 1076 } 1077 } 1078 } else { 1079 if (authScheme.isComplete()) { 1080 this.log.debug("Authentication failed"); 1081 creds = null; 1082 } 1083 } 1084 authState.setAuthScope(authScope); 1085 authState.setCredentials(creds); 1086 } 1087 1088} // class DefaultClientRequestDirector 1089