001/* 002 * ==================================================================== 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, 014 * software distributed under the License is distributed on an 015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 016 * KIND, either express or implied. See the License for the 017 * specific language governing permissions and limitations 018 * under the License. 019 * ==================================================================== 020 * 021 * This software consists of voluntary contributions made by many 022 * individuals on behalf of the Apache Software Foundation. For more 023 * information on the Apache Software Foundation, please see 024 * <http://www.apache.org/>. 025 * 026 */ 027 028package org.apache.http.impl.execchain; 029 030import java.io.IOException; 031import java.io.InterruptedIOException; 032import java.util.concurrent.ExecutionException; 033import java.util.concurrent.TimeUnit; 034 035import org.apache.commons.logging.Log; 036import org.apache.commons.logging.LogFactory; 037import org.apache.http.ConnectionReuseStrategy; 038import org.apache.http.HttpClientConnection; 039import org.apache.http.HttpEntity; 040import org.apache.http.HttpEntityEnclosingRequest; 041import org.apache.http.HttpException; 042import org.apache.http.HttpHost; 043import org.apache.http.HttpRequest; 044import org.apache.http.HttpResponse; 045import org.apache.http.annotation.Contract; 046import org.apache.http.annotation.ThreadingBehavior; 047import org.apache.http.auth.AUTH; 048import org.apache.http.auth.AuthProtocolState; 049import org.apache.http.auth.AuthState; 050import org.apache.http.client.AuthenticationStrategy; 051import org.apache.http.client.NonRepeatableRequestException; 052import org.apache.http.client.UserTokenHandler; 053import org.apache.http.client.config.RequestConfig; 054import org.apache.http.client.methods.CloseableHttpResponse; 055import org.apache.http.client.methods.HttpExecutionAware; 056import org.apache.http.client.methods.HttpRequestWrapper; 057import org.apache.http.client.protocol.HttpClientContext; 058import org.apache.http.conn.ConnectionKeepAliveStrategy; 059import org.apache.http.conn.ConnectionRequest; 060import org.apache.http.conn.HttpClientConnectionManager; 061import org.apache.http.conn.routing.BasicRouteDirector; 062import org.apache.http.conn.routing.HttpRoute; 063import org.apache.http.conn.routing.HttpRouteDirector; 064import org.apache.http.conn.routing.RouteTracker; 065import org.apache.http.entity.BufferedHttpEntity; 066import org.apache.http.impl.auth.HttpAuthenticator; 067import org.apache.http.impl.conn.ConnectionShutdownException; 068import org.apache.http.message.BasicHttpRequest; 069import org.apache.http.protocol.HttpCoreContext; 070import org.apache.http.protocol.HttpProcessor; 071import org.apache.http.protocol.HttpRequestExecutor; 072import org.apache.http.protocol.ImmutableHttpProcessor; 073import org.apache.http.protocol.RequestTargetHost; 074import org.apache.http.util.Args; 075import org.apache.http.util.EntityUtils; 076 077/** 078 * The last request executor in the HTTP request execution chain 079 * that is responsible for execution of request / response 080 * exchanges with the opposite endpoint. 081 * This executor will automatically retry the request in case 082 * of an authentication challenge by an intermediate proxy or 083 * by the target server. 084 * 085 * @since 4.3 086 */ 087@SuppressWarnings("deprecation") 088@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL) 089public class MainClientExec implements ClientExecChain { 090 091 private final Log log = LogFactory.getLog(getClass()); 092 093 private final HttpRequestExecutor requestExecutor; 094 private final HttpClientConnectionManager connManager; 095 private final ConnectionReuseStrategy reuseStrategy; 096 private final ConnectionKeepAliveStrategy keepAliveStrategy; 097 private final HttpProcessor proxyHttpProcessor; 098 private final AuthenticationStrategy targetAuthStrategy; 099 private final AuthenticationStrategy proxyAuthStrategy; 100 private final HttpAuthenticator authenticator; 101 private final UserTokenHandler userTokenHandler; 102 private final HttpRouteDirector routeDirector; 103 104 /** 105 * @since 4.4 106 */ 107 public MainClientExec( 108 final HttpRequestExecutor requestExecutor, 109 final HttpClientConnectionManager connManager, 110 final ConnectionReuseStrategy reuseStrategy, 111 final ConnectionKeepAliveStrategy keepAliveStrategy, 112 final HttpProcessor proxyHttpProcessor, 113 final AuthenticationStrategy targetAuthStrategy, 114 final AuthenticationStrategy proxyAuthStrategy, 115 final UserTokenHandler userTokenHandler) { 116 Args.notNull(requestExecutor, "HTTP request executor"); 117 Args.notNull(connManager, "Client connection manager"); 118 Args.notNull(reuseStrategy, "Connection reuse strategy"); 119 Args.notNull(keepAliveStrategy, "Connection keep alive strategy"); 120 Args.notNull(proxyHttpProcessor, "Proxy HTTP processor"); 121 Args.notNull(targetAuthStrategy, "Target authentication strategy"); 122 Args.notNull(proxyAuthStrategy, "Proxy authentication strategy"); 123 Args.notNull(userTokenHandler, "User token handler"); 124 this.authenticator = new HttpAuthenticator(); 125 this.routeDirector = new BasicRouteDirector(); 126 this.requestExecutor = requestExecutor; 127 this.connManager = connManager; 128 this.reuseStrategy = reuseStrategy; 129 this.keepAliveStrategy = keepAliveStrategy; 130 this.proxyHttpProcessor = proxyHttpProcessor; 131 this.targetAuthStrategy = targetAuthStrategy; 132 this.proxyAuthStrategy = proxyAuthStrategy; 133 this.userTokenHandler = userTokenHandler; 134 } 135 136 public MainClientExec( 137 final HttpRequestExecutor requestExecutor, 138 final HttpClientConnectionManager connManager, 139 final ConnectionReuseStrategy reuseStrategy, 140 final ConnectionKeepAliveStrategy keepAliveStrategy, 141 final AuthenticationStrategy targetAuthStrategy, 142 final AuthenticationStrategy proxyAuthStrategy, 143 final UserTokenHandler userTokenHandler) { 144 this(requestExecutor, connManager, reuseStrategy, keepAliveStrategy, 145 new ImmutableHttpProcessor(new RequestTargetHost()), 146 targetAuthStrategy, proxyAuthStrategy, userTokenHandler); 147 } 148 149 @Override 150 public CloseableHttpResponse execute( 151 final HttpRoute route, 152 final HttpRequestWrapper request, 153 final HttpClientContext context, 154 final HttpExecutionAware execAware) throws IOException, HttpException { 155 Args.notNull(route, "HTTP route"); 156 Args.notNull(request, "HTTP request"); 157 Args.notNull(context, "HTTP context"); 158 159 AuthState targetAuthState = context.getTargetAuthState(); 160 if (targetAuthState == null) { 161 targetAuthState = new AuthState(); 162 context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, targetAuthState); 163 } 164 AuthState proxyAuthState = context.getProxyAuthState(); 165 if (proxyAuthState == null) { 166 proxyAuthState = new AuthState(); 167 context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, proxyAuthState); 168 } 169 170 if (request instanceof HttpEntityEnclosingRequest) { 171 RequestEntityProxy.enhance((HttpEntityEnclosingRequest) request); 172 } 173 174 Object userToken = context.getUserToken(); 175 176 final ConnectionRequest connRequest = connManager.requestConnection(route, userToken); 177 if (execAware != null) { 178 if (execAware.isAborted()) { 179 connRequest.cancel(); 180 throw new RequestAbortedException("Request aborted"); 181 } else { 182 execAware.setCancellable(connRequest); 183 } 184 } 185 186 final RequestConfig config = context.getRequestConfig(); 187 188 final HttpClientConnection managedConn; 189 try { 190 final int timeout = config.getConnectionRequestTimeout(); 191 managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS); 192 } catch(final InterruptedException interrupted) { 193 Thread.currentThread().interrupt(); 194 throw new RequestAbortedException("Request aborted", interrupted); 195 } catch(final ExecutionException ex) { 196 Throwable cause = ex.getCause(); 197 if (cause == null) { 198 cause = ex; 199 } 200 throw new RequestAbortedException("Request execution failed", cause); 201 } 202 203 context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn); 204 205 if (config.isStaleConnectionCheckEnabled()) { 206 // validate connection 207 if (managedConn.isOpen()) { 208 this.log.debug("Stale connection check"); 209 if (managedConn.isStale()) { 210 this.log.debug("Stale connection detected"); 211 managedConn.close(); 212 } 213 } 214 } 215 216 final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn); 217 try { 218 if (execAware != null) { 219 execAware.setCancellable(connHolder); 220 } 221 222 HttpResponse response; 223 for (int execCount = 1;; execCount++) { 224 225 if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) { 226 throw new NonRepeatableRequestException("Cannot retry request " + 227 "with a non-repeatable request entity."); 228 } 229 230 if (execAware != null && execAware.isAborted()) { 231 throw new RequestAbortedException("Request aborted"); 232 } 233 234 if (!managedConn.isOpen()) { 235 this.log.debug("Opening connection " + route); 236 try { 237 establishRoute(proxyAuthState, managedConn, route, request, context); 238 } catch (final TunnelRefusedException ex) { 239 if (this.log.isDebugEnabled()) { 240 this.log.debug(ex.getMessage()); 241 } 242 response = ex.getResponse(); 243 break; 244 } 245 } 246 final int timeout = config.getSocketTimeout(); 247 if (timeout >= 0) { 248 managedConn.setSocketTimeout(timeout); 249 } 250 251 if (execAware != null && execAware.isAborted()) { 252 throw new RequestAbortedException("Request aborted"); 253 } 254 255 if (this.log.isDebugEnabled()) { 256 this.log.debug("Executing request " + request.getRequestLine()); 257 } 258 259 if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) { 260 if (this.log.isDebugEnabled()) { 261 this.log.debug("Target auth state: " + targetAuthState.getState()); 262 } 263 this.authenticator.generateAuthResponse(request, targetAuthState, context); 264 } 265 if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) { 266 if (this.log.isDebugEnabled()) { 267 this.log.debug("Proxy auth state: " + proxyAuthState.getState()); 268 } 269 this.authenticator.generateAuthResponse(request, proxyAuthState, context); 270 } 271 272 response = requestExecutor.execute(request, managedConn, context); 273 274 // The connection is in or can be brought to a re-usable state. 275 if (reuseStrategy.keepAlive(response, context)) { 276 // Set the idle duration of this connection 277 final long duration = keepAliveStrategy.getKeepAliveDuration(response, context); 278 if (this.log.isDebugEnabled()) { 279 final String s; 280 if (duration > 0) { 281 s = "for " + duration + " " + TimeUnit.MILLISECONDS; 282 } else { 283 s = "indefinitely"; 284 } 285 this.log.debug("Connection can be kept alive " + s); 286 } 287 connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); 288 connHolder.markReusable(); 289 } else { 290 connHolder.markNonReusable(); 291 } 292 293 if (needAuthentication( 294 targetAuthState, proxyAuthState, route, response, context)) { 295 // Make sure the response body is fully consumed, if present 296 final HttpEntity entity = response.getEntity(); 297 if (connHolder.isReusable()) { 298 EntityUtils.consume(entity); 299 } else { 300 managedConn.close(); 301 if (proxyAuthState.getState() == AuthProtocolState.SUCCESS 302 && proxyAuthState.getAuthScheme() != null 303 && proxyAuthState.getAuthScheme().isConnectionBased()) { 304 this.log.debug("Resetting proxy auth state"); 305 proxyAuthState.reset(); 306 } 307 if (targetAuthState.getState() == AuthProtocolState.SUCCESS 308 && targetAuthState.getAuthScheme() != null 309 && targetAuthState.getAuthScheme().isConnectionBased()) { 310 this.log.debug("Resetting target auth state"); 311 targetAuthState.reset(); 312 } 313 } 314 // discard previous auth headers 315 final HttpRequest original = request.getOriginal(); 316 if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) { 317 request.removeHeaders(AUTH.WWW_AUTH_RESP); 318 } 319 if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) { 320 request.removeHeaders(AUTH.PROXY_AUTH_RESP); 321 } 322 } else { 323 break; 324 } 325 } 326 327 if (userToken == null) { 328 userToken = userTokenHandler.getUserToken(context); 329 context.setAttribute(HttpClientContext.USER_TOKEN, userToken); 330 } 331 if (userToken != null) { 332 connHolder.setState(userToken); 333 } 334 335 // check for entity, release connection if possible 336 final HttpEntity entity = response.getEntity(); 337 if (entity == null || !entity.isStreaming()) { 338 // connection not needed and (assumed to be) in re-usable state 339 connHolder.releaseConnection(); 340 return new HttpResponseProxy(response, null); 341 } else { 342 return new HttpResponseProxy(response, connHolder); 343 } 344 } catch (final ConnectionShutdownException ex) { 345 final InterruptedIOException ioex = new InterruptedIOException( 346 "Connection has been shut down"); 347 ioex.initCause(ex); 348 throw ioex; 349 } catch (final HttpException ex) { 350 connHolder.abortConnection(); 351 throw ex; 352 } catch (final IOException ex) { 353 connHolder.abortConnection(); 354 throw ex; 355 } catch (final RuntimeException ex) { 356 connHolder.abortConnection(); 357 throw ex; 358 } 359 } 360 361 /** 362 * Establishes the target route. 363 */ 364 void establishRoute( 365 final AuthState proxyAuthState, 366 final HttpClientConnection managedConn, 367 final HttpRoute route, 368 final HttpRequest request, 369 final HttpClientContext context) throws HttpException, IOException { 370 final RequestConfig config = context.getRequestConfig(); 371 final int timeout = config.getConnectTimeout(); 372 final RouteTracker tracker = new RouteTracker(route); 373 int step; 374 do { 375 final HttpRoute fact = tracker.toRoute(); 376 step = this.routeDirector.nextStep(route, fact); 377 378 switch (step) { 379 380 case HttpRouteDirector.CONNECT_TARGET: 381 this.connManager.connect( 382 managedConn, 383 route, 384 timeout > 0 ? timeout : 0, 385 context); 386 tracker.connectTarget(route.isSecure()); 387 break; 388 case HttpRouteDirector.CONNECT_PROXY: 389 this.connManager.connect( 390 managedConn, 391 route, 392 timeout > 0 ? timeout : 0, 393 context); 394 final HttpHost proxy = route.getProxyHost(); 395 tracker.connectProxy(proxy, false); 396 break; 397 case HttpRouteDirector.TUNNEL_TARGET: { 398 final boolean secure = createTunnelToTarget( 399 proxyAuthState, managedConn, route, request, context); 400 this.log.debug("Tunnel to target created."); 401 tracker.tunnelTarget(secure); 402 } break; 403 404 case HttpRouteDirector.TUNNEL_PROXY: { 405 // The most simple example for this case is a proxy chain 406 // of two proxies, where P1 must be tunnelled to P2. 407 // route: Source -> P1 -> P2 -> Target (3 hops) 408 // fact: Source -> P1 -> Target (2 hops) 409 final int hop = fact.getHopCount()-1; // the hop to establish 410 final boolean secure = createTunnelToProxy(route, hop, context); 411 this.log.debug("Tunnel to proxy created."); 412 tracker.tunnelProxy(route.getHopTarget(hop), secure); 413 } break; 414 415 case HttpRouteDirector.LAYER_PROTOCOL: 416 this.connManager.upgrade(managedConn, route, context); 417 tracker.layerProtocol(route.isSecure()); 418 break; 419 420 case HttpRouteDirector.UNREACHABLE: 421 throw new HttpException("Unable to establish route: " + 422 "planned = " + route + "; current = " + fact); 423 case HttpRouteDirector.COMPLETE: 424 this.connManager.routeComplete(managedConn, route, context); 425 break; 426 default: 427 throw new IllegalStateException("Unknown step indicator " 428 + step + " from RouteDirector."); 429 } 430 431 } while (step > HttpRouteDirector.COMPLETE); 432 } 433 434 /** 435 * Creates a tunnel to the target server. 436 * The connection must be established to the (last) proxy. 437 * A CONNECT request for tunnelling through the proxy will 438 * be created and sent, the response received and checked. 439 * This method does <i>not</i> update the connection with 440 * information about the tunnel, that is left to the caller. 441 */ 442 private boolean createTunnelToTarget( 443 final AuthState proxyAuthState, 444 final HttpClientConnection managedConn, 445 final HttpRoute route, 446 final HttpRequest request, 447 final HttpClientContext context) throws HttpException, IOException { 448 449 final RequestConfig config = context.getRequestConfig(); 450 final int timeout = config.getConnectTimeout(); 451 452 final HttpHost target = route.getTargetHost(); 453 final HttpHost proxy = route.getProxyHost(); 454 HttpResponse response = null; 455 456 final String authority = target.toHostString(); 457 final HttpRequest connect = new BasicHttpRequest("CONNECT", authority, request.getProtocolVersion()); 458 459 this.requestExecutor.preProcess(connect, this.proxyHttpProcessor, context); 460 461 while (response == null) { 462 if (!managedConn.isOpen()) { 463 this.connManager.connect( 464 managedConn, 465 route, 466 timeout > 0 ? timeout : 0, 467 context); 468 } 469 470 connect.removeHeaders(AUTH.PROXY_AUTH_RESP); 471 this.authenticator.generateAuthResponse(connect, proxyAuthState, context); 472 473 response = this.requestExecutor.execute(connect, managedConn, context); 474 475 final int status = response.getStatusLine().getStatusCode(); 476 if (status < 200) { 477 throw new HttpException("Unexpected response to CONNECT request: " + 478 response.getStatusLine()); 479 } 480 481 if (config.isAuthenticationEnabled()) { 482 if (this.authenticator.isAuthenticationRequested(proxy, response, 483 this.proxyAuthStrategy, proxyAuthState, context)) { 484 if (this.authenticator.handleAuthChallenge(proxy, response, 485 this.proxyAuthStrategy, proxyAuthState, context)) { 486 // Retry request 487 if (this.reuseStrategy.keepAlive(response, context)) { 488 this.log.debug("Connection kept alive"); 489 // Consume response content 490 final HttpEntity entity = response.getEntity(); 491 EntityUtils.consume(entity); 492 } else { 493 managedConn.close(); 494 } 495 response = null; 496 } 497 } 498 } 499 } 500 501 final int status = response.getStatusLine().getStatusCode(); 502 503 if (status > 299) { 504 505 // Buffer response content 506 final HttpEntity entity = response.getEntity(); 507 if (entity != null) { 508 response.setEntity(new BufferedHttpEntity(entity)); 509 } 510 511 managedConn.close(); 512 throw new TunnelRefusedException("CONNECT refused by proxy: " + 513 response.getStatusLine(), response); 514 } 515 516 // How to decide on security of the tunnelled connection? 517 // The socket factory knows only about the segment to the proxy. 518 // Even if that is secure, the hop to the target may be insecure. 519 // Leave it to derived classes, consider insecure by default here. 520 return false; 521 } 522 523 /** 524 * Creates a tunnel to an intermediate proxy. 525 * This method is <i>not</i> implemented in this class. 526 * It just throws an exception here. 527 */ 528 private boolean createTunnelToProxy( 529 final HttpRoute route, 530 final int hop, 531 final HttpClientContext context) throws HttpException { 532 533 // Have a look at createTunnelToTarget and replicate the parts 534 // you need in a custom derived class. If your proxies don't require 535 // authentication, it is not too hard. But for the stock version of 536 // HttpClient, we cannot make such simplifying assumptions and would 537 // have to include proxy authentication code. The HttpComponents team 538 // is currently not in a position to support rarely used code of this 539 // complexity. Feel free to submit patches that refactor the code in 540 // createTunnelToTarget to facilitate re-use for proxy tunnelling. 541 542 throw new HttpException("Proxy chains are not supported."); 543 } 544 545 private boolean needAuthentication( 546 final AuthState targetAuthState, 547 final AuthState proxyAuthState, 548 final HttpRoute route, 549 final HttpResponse response, 550 final HttpClientContext context) { 551 final RequestConfig config = context.getRequestConfig(); 552 if (config.isAuthenticationEnabled()) { 553 HttpHost target = context.getTargetHost(); 554 if (target == null) { 555 target = route.getTargetHost(); 556 } 557 if (target.getPort() < 0) { 558 target = new HttpHost( 559 target.getHostName(), 560 route.getTargetHost().getPort(), 561 target.getSchemeName()); 562 } 563 final boolean targetAuthRequested = this.authenticator.isAuthenticationRequested( 564 target, response, this.targetAuthStrategy, targetAuthState, context); 565 566 HttpHost proxy = route.getProxyHost(); 567 // if proxy is not set use target host instead 568 if (proxy == null) { 569 proxy = route.getTargetHost(); 570 } 571 final boolean proxyAuthRequested = this.authenticator.isAuthenticationRequested( 572 proxy, response, this.proxyAuthStrategy, proxyAuthState, context); 573 574 if (targetAuthRequested) { 575 return this.authenticator.handleAuthChallenge(target, response, 576 this.targetAuthStrategy, targetAuthState, context); 577 } 578 if (proxyAuthRequested) { 579 return this.authenticator.handleAuthChallenge(proxy, response, 580 this.proxyAuthStrategy, proxyAuthState, context); 581 } 582 } 583 return false; 584 } 585 586}