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