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