michael@0: /* michael@0: * ==================================================================== michael@0: * Licensed to the Apache Software Foundation (ASF) under one michael@0: * or more contributor license agreements. See the NOTICE file michael@0: * distributed with this work for additional information michael@0: * regarding copyright ownership. The ASF licenses this file michael@0: * to you under the Apache License, Version 2.0 (the michael@0: * "License"); you may not use this file except in compliance michael@0: * with the License. You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, michael@0: * software distributed under the License is distributed on an michael@0: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY michael@0: * KIND, either express or implied. See the License for the michael@0: * specific language governing permissions and limitations michael@0: * under the License. michael@0: * ==================================================================== michael@0: * michael@0: * This software consists of voluntary contributions made by many michael@0: * individuals on behalf of the Apache Software Foundation. For more michael@0: * information on the Apache Software Foundation, please see michael@0: * . michael@0: * michael@0: */ michael@0: michael@0: package ch.boye.httpclientandroidlib.impl.client; michael@0: michael@0: import java.io.IOException; michael@0: import java.io.InterruptedIOException; michael@0: import java.net.URI; michael@0: import java.net.URISyntaxException; michael@0: import java.util.Locale; michael@0: import java.util.Map; michael@0: import java.util.concurrent.TimeUnit; michael@0: michael@0: import ch.boye.httpclientandroidlib.annotation.NotThreadSafe; michael@0: michael@0: import ch.boye.httpclientandroidlib.androidextra.HttpClientAndroidLog; michael@0: /* LogFactory removed by HttpClient for Android script. */ michael@0: import ch.boye.httpclientandroidlib.ConnectionReuseStrategy; michael@0: import ch.boye.httpclientandroidlib.Header; michael@0: import ch.boye.httpclientandroidlib.HttpEntity; michael@0: import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest; michael@0: import ch.boye.httpclientandroidlib.HttpException; michael@0: import ch.boye.httpclientandroidlib.HttpHost; michael@0: import ch.boye.httpclientandroidlib.HttpRequest; michael@0: import ch.boye.httpclientandroidlib.HttpResponse; michael@0: import ch.boye.httpclientandroidlib.ProtocolException; michael@0: import ch.boye.httpclientandroidlib.ProtocolVersion; michael@0: import ch.boye.httpclientandroidlib.auth.AuthScheme; michael@0: import ch.boye.httpclientandroidlib.auth.AuthScope; michael@0: import ch.boye.httpclientandroidlib.auth.AuthState; michael@0: import ch.boye.httpclientandroidlib.auth.AuthenticationException; michael@0: import ch.boye.httpclientandroidlib.auth.Credentials; michael@0: import ch.boye.httpclientandroidlib.auth.MalformedChallengeException; michael@0: import ch.boye.httpclientandroidlib.client.AuthenticationHandler; michael@0: import ch.boye.httpclientandroidlib.client.RedirectStrategy; michael@0: import ch.boye.httpclientandroidlib.client.RequestDirector; michael@0: import ch.boye.httpclientandroidlib.client.CredentialsProvider; michael@0: import ch.boye.httpclientandroidlib.client.HttpRequestRetryHandler; michael@0: import ch.boye.httpclientandroidlib.client.NonRepeatableRequestException; michael@0: import ch.boye.httpclientandroidlib.client.RedirectException; michael@0: import ch.boye.httpclientandroidlib.client.UserTokenHandler; michael@0: import ch.boye.httpclientandroidlib.client.methods.AbortableHttpRequest; michael@0: import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest; michael@0: import ch.boye.httpclientandroidlib.client.params.ClientPNames; michael@0: import ch.boye.httpclientandroidlib.client.params.HttpClientParams; michael@0: import ch.boye.httpclientandroidlib.client.protocol.ClientContext; michael@0: import ch.boye.httpclientandroidlib.client.utils.URIUtils; michael@0: import ch.boye.httpclientandroidlib.conn.BasicManagedEntity; michael@0: import ch.boye.httpclientandroidlib.conn.ClientConnectionManager; michael@0: import ch.boye.httpclientandroidlib.conn.ClientConnectionRequest; michael@0: import ch.boye.httpclientandroidlib.conn.ConnectionKeepAliveStrategy; michael@0: import ch.boye.httpclientandroidlib.conn.ManagedClientConnection; michael@0: import ch.boye.httpclientandroidlib.conn.params.ConnManagerParams; michael@0: import ch.boye.httpclientandroidlib.conn.routing.BasicRouteDirector; michael@0: import ch.boye.httpclientandroidlib.conn.routing.HttpRoute; michael@0: import ch.boye.httpclientandroidlib.conn.routing.HttpRouteDirector; michael@0: import ch.boye.httpclientandroidlib.conn.routing.HttpRoutePlanner; michael@0: import ch.boye.httpclientandroidlib.conn.scheme.Scheme; michael@0: import ch.boye.httpclientandroidlib.entity.BufferedHttpEntity; michael@0: import ch.boye.httpclientandroidlib.impl.conn.ConnectionShutdownException; michael@0: import ch.boye.httpclientandroidlib.message.BasicHttpRequest; michael@0: import ch.boye.httpclientandroidlib.params.HttpConnectionParams; michael@0: import ch.boye.httpclientandroidlib.params.HttpParams; michael@0: import ch.boye.httpclientandroidlib.params.HttpProtocolParams; michael@0: import ch.boye.httpclientandroidlib.protocol.ExecutionContext; michael@0: import ch.boye.httpclientandroidlib.protocol.HttpContext; michael@0: import ch.boye.httpclientandroidlib.protocol.HttpProcessor; michael@0: import ch.boye.httpclientandroidlib.protocol.HttpRequestExecutor; michael@0: import ch.boye.httpclientandroidlib.util.EntityUtils; michael@0: michael@0: /** michael@0: * Default implementation of {@link RequestDirector}. michael@0: *

michael@0: * The following parameters can be used to customize the behavior of this michael@0: * class: michael@0: *

michael@0: * michael@0: * @since 4.0 michael@0: */ michael@0: @SuppressWarnings("deprecation") michael@0: @NotThreadSafe // e.g. managedConn michael@0: public class DefaultRequestDirector implements RequestDirector { michael@0: michael@0: public HttpClientAndroidLog log; michael@0: michael@0: /** The connection manager. */ michael@0: protected final ClientConnectionManager connManager; michael@0: michael@0: /** The route planner. */ michael@0: protected final HttpRoutePlanner routePlanner; michael@0: michael@0: /** The connection re-use strategy. */ michael@0: protected final ConnectionReuseStrategy reuseStrategy; michael@0: michael@0: /** The keep-alive duration strategy. */ michael@0: protected final ConnectionKeepAliveStrategy keepAliveStrategy; michael@0: michael@0: /** The request executor. */ michael@0: protected final HttpRequestExecutor requestExec; michael@0: michael@0: /** The HTTP protocol processor. */ michael@0: protected final HttpProcessor httpProcessor; michael@0: michael@0: /** The request retry handler. */ michael@0: protected final HttpRequestRetryHandler retryHandler; michael@0: michael@0: /** The redirect handler. */ michael@0: @Deprecated michael@0: protected final ch.boye.httpclientandroidlib.client.RedirectHandler redirectHandler = null; michael@0: michael@0: /** The redirect strategy. */ michael@0: protected final RedirectStrategy redirectStrategy; michael@0: michael@0: /** The target authentication handler. */ michael@0: protected final AuthenticationHandler targetAuthHandler; michael@0: michael@0: /** The proxy authentication handler. */ michael@0: protected final AuthenticationHandler proxyAuthHandler; michael@0: michael@0: /** The user token handler. */ michael@0: protected final UserTokenHandler userTokenHandler; michael@0: michael@0: /** The HTTP parameters. */ michael@0: protected final HttpParams params; michael@0: michael@0: /** The currently allocated connection. */ michael@0: protected ManagedClientConnection managedConn; michael@0: michael@0: protected final AuthState targetAuthState; michael@0: michael@0: protected final AuthState proxyAuthState; michael@0: michael@0: private int execCount; michael@0: michael@0: private int redirectCount; michael@0: michael@0: private int maxRedirects; michael@0: michael@0: private HttpHost virtualHost; michael@0: michael@0: @Deprecated michael@0: public DefaultRequestDirector( michael@0: final HttpRequestExecutor requestExec, michael@0: final ClientConnectionManager conman, michael@0: final ConnectionReuseStrategy reustrat, michael@0: final ConnectionKeepAliveStrategy kastrat, michael@0: final HttpRoutePlanner rouplan, michael@0: final HttpProcessor httpProcessor, michael@0: final HttpRequestRetryHandler retryHandler, michael@0: final ch.boye.httpclientandroidlib.client.RedirectHandler redirectHandler, michael@0: final AuthenticationHandler targetAuthHandler, michael@0: final AuthenticationHandler proxyAuthHandler, michael@0: final UserTokenHandler userTokenHandler, michael@0: final HttpParams params) { michael@0: this(new HttpClientAndroidLog(DefaultRequestDirector.class), michael@0: requestExec, conman, reustrat, kastrat, rouplan, httpProcessor, retryHandler, michael@0: new DefaultRedirectStrategyAdaptor(redirectHandler), michael@0: targetAuthHandler, proxyAuthHandler, userTokenHandler, params); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * @since 4.1 michael@0: */ michael@0: public DefaultRequestDirector( michael@0: final HttpClientAndroidLog log, michael@0: final HttpRequestExecutor requestExec, michael@0: final ClientConnectionManager conman, michael@0: final ConnectionReuseStrategy reustrat, michael@0: final ConnectionKeepAliveStrategy kastrat, michael@0: final HttpRoutePlanner rouplan, michael@0: final HttpProcessor httpProcessor, michael@0: final HttpRequestRetryHandler retryHandler, michael@0: final RedirectStrategy redirectStrategy, michael@0: final AuthenticationHandler targetAuthHandler, michael@0: final AuthenticationHandler proxyAuthHandler, michael@0: final UserTokenHandler userTokenHandler, michael@0: final HttpParams params) { michael@0: michael@0: if (log == null) { michael@0: throw new IllegalArgumentException michael@0: ("Log may not be null."); michael@0: } michael@0: if (requestExec == null) { michael@0: throw new IllegalArgumentException michael@0: ("Request executor may not be null."); michael@0: } michael@0: if (conman == null) { michael@0: throw new IllegalArgumentException michael@0: ("Client connection manager may not be null."); michael@0: } michael@0: if (reustrat == null) { michael@0: throw new IllegalArgumentException michael@0: ("Connection reuse strategy may not be null."); michael@0: } michael@0: if (kastrat == null) { michael@0: throw new IllegalArgumentException michael@0: ("Connection keep alive strategy may not be null."); michael@0: } michael@0: if (rouplan == null) { michael@0: throw new IllegalArgumentException michael@0: ("Route planner may not be null."); michael@0: } michael@0: if (httpProcessor == null) { michael@0: throw new IllegalArgumentException michael@0: ("HTTP protocol processor may not be null."); michael@0: } michael@0: if (retryHandler == null) { michael@0: throw new IllegalArgumentException michael@0: ("HTTP request retry handler may not be null."); michael@0: } michael@0: if (redirectStrategy == null) { michael@0: throw new IllegalArgumentException michael@0: ("Redirect strategy may not be null."); michael@0: } michael@0: if (targetAuthHandler == null) { michael@0: throw new IllegalArgumentException michael@0: ("Target authentication handler may not be null."); michael@0: } michael@0: if (proxyAuthHandler == null) { michael@0: throw new IllegalArgumentException michael@0: ("Proxy authentication handler may not be null."); michael@0: } michael@0: if (userTokenHandler == null) { michael@0: throw new IllegalArgumentException michael@0: ("User token handler may not be null."); michael@0: } michael@0: if (params == null) { michael@0: throw new IllegalArgumentException michael@0: ("HTTP parameters may not be null"); michael@0: } michael@0: this.log = log; michael@0: this.requestExec = requestExec; michael@0: this.connManager = conman; michael@0: this.reuseStrategy = reustrat; michael@0: this.keepAliveStrategy = kastrat; michael@0: this.routePlanner = rouplan; michael@0: this.httpProcessor = httpProcessor; michael@0: this.retryHandler = retryHandler; michael@0: this.redirectStrategy = redirectStrategy; michael@0: this.targetAuthHandler = targetAuthHandler; michael@0: this.proxyAuthHandler = proxyAuthHandler; michael@0: this.userTokenHandler = userTokenHandler; michael@0: this.params = params; michael@0: michael@0: this.managedConn = null; michael@0: michael@0: this.execCount = 0; michael@0: this.redirectCount = 0; michael@0: this.maxRedirects = this.params.getIntParameter(ClientPNames.MAX_REDIRECTS, 100); michael@0: this.targetAuthState = new AuthState(); michael@0: this.proxyAuthState = new AuthState(); michael@0: } // constructor michael@0: michael@0: michael@0: private RequestWrapper wrapRequest( michael@0: final HttpRequest request) throws ProtocolException { michael@0: if (request instanceof HttpEntityEnclosingRequest) { michael@0: return new EntityEnclosingRequestWrapper( michael@0: (HttpEntityEnclosingRequest) request); michael@0: } else { michael@0: return new RequestWrapper( michael@0: request); michael@0: } michael@0: } michael@0: michael@0: michael@0: protected void rewriteRequestURI( michael@0: final RequestWrapper request, michael@0: final HttpRoute route) throws ProtocolException { michael@0: try { michael@0: michael@0: URI uri = request.getURI(); michael@0: if (route.getProxyHost() != null && !route.isTunnelled()) { michael@0: // Make sure the request URI is absolute michael@0: if (!uri.isAbsolute()) { michael@0: HttpHost target = route.getTargetHost(); michael@0: uri = URIUtils.rewriteURI(uri, target); michael@0: request.setURI(uri); michael@0: } michael@0: } else { michael@0: // Make sure the request URI is relative michael@0: if (uri.isAbsolute()) { michael@0: uri = URIUtils.rewriteURI(uri, null); michael@0: request.setURI(uri); michael@0: } michael@0: } michael@0: michael@0: } catch (URISyntaxException ex) { michael@0: throw new ProtocolException("Invalid URI: " + michael@0: request.getRequestLine().getUri(), ex); michael@0: } michael@0: } michael@0: michael@0: michael@0: // non-javadoc, see interface ClientRequestDirector michael@0: public HttpResponse execute(HttpHost target, HttpRequest request, michael@0: HttpContext context) michael@0: throws HttpException, IOException { michael@0: michael@0: HttpRequest orig = request; michael@0: RequestWrapper origWrapper = wrapRequest(orig); michael@0: origWrapper.setParams(params); michael@0: HttpRoute origRoute = determineRoute(target, origWrapper, context); michael@0: michael@0: virtualHost = (HttpHost) orig.getParams().getParameter( michael@0: ClientPNames.VIRTUAL_HOST); michael@0: michael@0: // HTTPCLIENT-1092 - add the port if necessary michael@0: if (virtualHost != null && virtualHost.getPort() == -1) michael@0: { michael@0: int port = target.getPort(); michael@0: if (port != -1){ michael@0: virtualHost = new HttpHost(virtualHost.getHostName(), port, virtualHost.getSchemeName()); michael@0: } michael@0: } michael@0: michael@0: RoutedRequest roureq = new RoutedRequest(origWrapper, origRoute); michael@0: michael@0: boolean reuse = false; michael@0: boolean done = false; michael@0: try { michael@0: HttpResponse response = null; michael@0: while (!done) { michael@0: // In this loop, the RoutedRequest may be replaced by a michael@0: // followup request and route. The request and route passed michael@0: // in the method arguments will be replaced. The original michael@0: // request is still available in 'orig'. michael@0: michael@0: RequestWrapper wrapper = roureq.getRequest(); michael@0: HttpRoute route = roureq.getRoute(); michael@0: response = null; michael@0: michael@0: // See if we have a user token bound to the execution context michael@0: Object userToken = context.getAttribute(ClientContext.USER_TOKEN); michael@0: michael@0: // Allocate connection if needed michael@0: if (managedConn == null) { michael@0: ClientConnectionRequest connRequest = connManager.requestConnection( michael@0: route, userToken); michael@0: if (orig instanceof AbortableHttpRequest) { michael@0: ((AbortableHttpRequest) orig).setConnectionRequest(connRequest); michael@0: } michael@0: michael@0: long timeout = ConnManagerParams.getTimeout(params); michael@0: try { michael@0: managedConn = connRequest.getConnection(timeout, TimeUnit.MILLISECONDS); michael@0: } catch(InterruptedException interrupted) { michael@0: InterruptedIOException iox = new InterruptedIOException(); michael@0: iox.initCause(interrupted); michael@0: throw iox; michael@0: } michael@0: michael@0: if (HttpConnectionParams.isStaleCheckingEnabled(params)) { michael@0: // validate connection michael@0: if (managedConn.isOpen()) { michael@0: this.log.debug("Stale connection check"); michael@0: if (managedConn.isStale()) { michael@0: this.log.debug("Stale connection detected"); michael@0: managedConn.close(); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (orig instanceof AbortableHttpRequest) { michael@0: ((AbortableHttpRequest) orig).setReleaseTrigger(managedConn); michael@0: } michael@0: michael@0: try { michael@0: tryConnect(roureq, context); michael@0: } catch (TunnelRefusedException ex) { michael@0: if (this.log.isDebugEnabled()) { michael@0: this.log.debug(ex.getMessage()); michael@0: } michael@0: response = ex.getResponse(); michael@0: break; michael@0: } michael@0: michael@0: // Reset headers on the request wrapper michael@0: wrapper.resetHeaders(); michael@0: michael@0: // Re-write request URI if needed michael@0: rewriteRequestURI(wrapper, route); michael@0: michael@0: // Use virtual host if set michael@0: target = virtualHost; michael@0: michael@0: if (target == null) { michael@0: target = route.getTargetHost(); michael@0: } michael@0: michael@0: HttpHost proxy = route.getProxyHost(); michael@0: michael@0: // Populate the execution context michael@0: context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, michael@0: target); michael@0: context.setAttribute(ExecutionContext.HTTP_PROXY_HOST, michael@0: proxy); michael@0: context.setAttribute(ExecutionContext.HTTP_CONNECTION, michael@0: managedConn); michael@0: context.setAttribute(ClientContext.TARGET_AUTH_STATE, michael@0: targetAuthState); michael@0: context.setAttribute(ClientContext.PROXY_AUTH_STATE, michael@0: proxyAuthState); michael@0: michael@0: // Run request protocol interceptors michael@0: requestExec.preProcess(wrapper, httpProcessor, context); michael@0: michael@0: response = tryExecute(roureq, context); michael@0: if (response == null) { michael@0: // Need to start over michael@0: continue; michael@0: } michael@0: michael@0: // Run response protocol interceptors michael@0: response.setParams(params); michael@0: requestExec.postProcess(response, httpProcessor, context); michael@0: michael@0: michael@0: // The connection is in or can be brought to a re-usable state. michael@0: reuse = reuseStrategy.keepAlive(response, context); michael@0: if (reuse) { michael@0: // Set the idle duration of this connection michael@0: long duration = keepAliveStrategy.getKeepAliveDuration(response, context); michael@0: if (this.log.isDebugEnabled()) { michael@0: String s; michael@0: if (duration > 0) { michael@0: s = "for " + duration + " " + TimeUnit.MILLISECONDS; michael@0: } else { michael@0: s = "indefinitely"; michael@0: } michael@0: this.log.debug("Connection can be kept alive " + s); michael@0: } michael@0: managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS); michael@0: } michael@0: michael@0: RoutedRequest followup = handleResponse(roureq, response, context); michael@0: if (followup == null) { michael@0: done = true; michael@0: } else { michael@0: if (reuse) { michael@0: // Make sure the response body is fully consumed, if present michael@0: HttpEntity entity = response.getEntity(); michael@0: EntityUtils.consume(entity); michael@0: // entity consumed above is not an auto-release entity, michael@0: // need to mark the connection re-usable explicitly michael@0: managedConn.markReusable(); michael@0: } else { michael@0: managedConn.close(); michael@0: invalidateAuthIfSuccessful(this.proxyAuthState); michael@0: invalidateAuthIfSuccessful(this.targetAuthState); michael@0: } michael@0: // check if we can use the same connection for the followup michael@0: if (!followup.getRoute().equals(roureq.getRoute())) { michael@0: releaseConnection(); michael@0: } michael@0: roureq = followup; michael@0: } michael@0: michael@0: if (managedConn != null && userToken == null) { michael@0: userToken = userTokenHandler.getUserToken(context); michael@0: context.setAttribute(ClientContext.USER_TOKEN, userToken); michael@0: if (userToken != null) { michael@0: managedConn.setState(userToken); michael@0: } michael@0: } michael@0: michael@0: } // while not done michael@0: michael@0: michael@0: // check for entity, release connection if possible michael@0: if ((response == null) || (response.getEntity() == null) || michael@0: !response.getEntity().isStreaming()) { michael@0: // connection not needed and (assumed to be) in re-usable state michael@0: if (reuse) michael@0: managedConn.markReusable(); michael@0: releaseConnection(); michael@0: } else { michael@0: // install an auto-release entity michael@0: HttpEntity entity = response.getEntity(); michael@0: entity = new BasicManagedEntity(entity, managedConn, reuse); michael@0: response.setEntity(entity); michael@0: } michael@0: michael@0: return response; michael@0: michael@0: } catch (ConnectionShutdownException ex) { michael@0: InterruptedIOException ioex = new InterruptedIOException( michael@0: "Connection has been shut down"); michael@0: ioex.initCause(ex); michael@0: throw ioex; michael@0: } catch (HttpException ex) { michael@0: abortConnection(); michael@0: throw ex; michael@0: } catch (IOException ex) { michael@0: abortConnection(); michael@0: throw ex; michael@0: } catch (RuntimeException ex) { michael@0: abortConnection(); michael@0: throw ex; michael@0: } michael@0: } // execute michael@0: michael@0: /** michael@0: * Establish connection either directly or through a tunnel and retry in case of michael@0: * a recoverable I/O failure michael@0: */ michael@0: private void tryConnect( michael@0: final RoutedRequest req, final HttpContext context) throws HttpException, IOException { michael@0: HttpRoute route = req.getRoute(); michael@0: michael@0: int connectCount = 0; michael@0: for (;;) { michael@0: // Increment connect count michael@0: connectCount++; michael@0: try { michael@0: if (!managedConn.isOpen()) { michael@0: managedConn.open(route, context, params); michael@0: } else { michael@0: managedConn.setSocketTimeout(HttpConnectionParams.getSoTimeout(params)); michael@0: } michael@0: establishRoute(route, context); michael@0: break; michael@0: } catch (IOException ex) { michael@0: try { michael@0: managedConn.close(); michael@0: } catch (IOException ignore) { michael@0: } michael@0: if (retryHandler.retryRequest(ex, connectCount, context)) { michael@0: if (this.log.isInfoEnabled()) { michael@0: this.log.info("I/O exception ("+ ex.getClass().getName() + michael@0: ") caught when connecting to the target host: " michael@0: + ex.getMessage()); michael@0: } michael@0: if (this.log.isDebugEnabled()) { michael@0: this.log.debug(ex.getMessage(), ex); michael@0: } michael@0: this.log.info("Retrying connect"); michael@0: } else { michael@0: throw ex; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Execute request and retry in case of a recoverable I/O failure michael@0: */ michael@0: private HttpResponse tryExecute( michael@0: final RoutedRequest req, final HttpContext context) throws HttpException, IOException { michael@0: RequestWrapper wrapper = req.getRequest(); michael@0: HttpRoute route = req.getRoute(); michael@0: HttpResponse response = null; michael@0: michael@0: Exception retryReason = null; michael@0: for (;;) { michael@0: // Increment total exec count (with redirects) michael@0: execCount++; michael@0: // Increment exec count for this particular request michael@0: wrapper.incrementExecCount(); michael@0: if (!wrapper.isRepeatable()) { michael@0: this.log.debug("Cannot retry non-repeatable request"); michael@0: if (retryReason != null) { michael@0: throw new NonRepeatableRequestException("Cannot retry request " + michael@0: "with a non-repeatable request entity. The cause lists the " + michael@0: "reason the original request failed.", retryReason); michael@0: } else { michael@0: throw new NonRepeatableRequestException("Cannot retry request " + michael@0: "with a non-repeatable request entity."); michael@0: } michael@0: } michael@0: michael@0: try { michael@0: if (!managedConn.isOpen()) { michael@0: // If we have a direct route to the target host michael@0: // just re-open connection and re-try the request michael@0: if (!route.isTunnelled()) { michael@0: this.log.debug("Reopening the direct connection."); michael@0: managedConn.open(route, context, params); michael@0: } else { michael@0: // otherwise give up michael@0: this.log.debug("Proxied connection. Need to start over."); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (this.log.isDebugEnabled()) { michael@0: this.log.debug("Attempt " + execCount + " to execute request"); michael@0: } michael@0: response = requestExec.execute(wrapper, managedConn, context); michael@0: break; michael@0: michael@0: } catch (IOException ex) { michael@0: this.log.debug("Closing the connection."); michael@0: try { michael@0: managedConn.close(); michael@0: } catch (IOException ignore) { michael@0: } michael@0: if (retryHandler.retryRequest(ex, wrapper.getExecCount(), context)) { michael@0: if (this.log.isInfoEnabled()) { michael@0: this.log.info("I/O exception ("+ ex.getClass().getName() + michael@0: ") caught when processing request: " michael@0: + ex.getMessage()); michael@0: } michael@0: if (this.log.isDebugEnabled()) { michael@0: this.log.debug(ex.getMessage(), ex); michael@0: } michael@0: this.log.info("Retrying request"); michael@0: retryReason = ex; michael@0: } else { michael@0: throw ex; michael@0: } michael@0: } michael@0: } michael@0: return response; michael@0: } michael@0: michael@0: /** michael@0: * Returns the connection back to the connection manager michael@0: * and prepares for retrieving a new connection during michael@0: * the next request. michael@0: */ michael@0: protected void releaseConnection() { michael@0: // Release the connection through the ManagedConnection instead of the michael@0: // ConnectionManager directly. This lets the connection control how michael@0: // it is released. michael@0: try { michael@0: managedConn.releaseConnection(); michael@0: } catch(IOException ignored) { michael@0: this.log.debug("IOException releasing connection", ignored); michael@0: } michael@0: managedConn = null; michael@0: } michael@0: michael@0: /** michael@0: * Determines the route for a request. michael@0: * Called by {@link #execute} michael@0: * to determine the route for either the original or a followup request. michael@0: * michael@0: * @param target the target host for the request. michael@0: * Implementations may accept null michael@0: * if they can still determine a route, for example michael@0: * to a default target or by inspecting the request. michael@0: * @param request the request to execute michael@0: * @param context the context to use for the execution, michael@0: * never null michael@0: * michael@0: * @return the route the request should take michael@0: * michael@0: * @throws HttpException in case of a problem michael@0: */ michael@0: protected HttpRoute determineRoute(HttpHost target, michael@0: HttpRequest request, michael@0: HttpContext context) michael@0: throws HttpException { michael@0: michael@0: if (target == null) { michael@0: target = (HttpHost) request.getParams().getParameter( michael@0: ClientPNames.DEFAULT_HOST); michael@0: } michael@0: if (target == null) { michael@0: throw new IllegalStateException michael@0: ("Target host must not be null, or set in parameters."); michael@0: } michael@0: michael@0: return this.routePlanner.determineRoute(target, request, context); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Establishes the target route. michael@0: * michael@0: * @param route the route to establish michael@0: * @param context the context for the request execution michael@0: * michael@0: * @throws HttpException in case of a problem michael@0: * @throws IOException in case of an IO problem michael@0: */ michael@0: protected void establishRoute(HttpRoute route, HttpContext context) michael@0: throws HttpException, IOException { michael@0: michael@0: HttpRouteDirector rowdy = new BasicRouteDirector(); michael@0: int step; michael@0: do { michael@0: HttpRoute fact = managedConn.getRoute(); michael@0: step = rowdy.nextStep(route, fact); michael@0: michael@0: switch (step) { michael@0: michael@0: case HttpRouteDirector.CONNECT_TARGET: michael@0: case HttpRouteDirector.CONNECT_PROXY: michael@0: managedConn.open(route, context, this.params); michael@0: break; michael@0: michael@0: case HttpRouteDirector.TUNNEL_TARGET: { michael@0: boolean secure = createTunnelToTarget(route, context); michael@0: this.log.debug("Tunnel to target created."); michael@0: managedConn.tunnelTarget(secure, this.params); michael@0: } break; michael@0: michael@0: case HttpRouteDirector.TUNNEL_PROXY: { michael@0: // The most simple example for this case is a proxy chain michael@0: // of two proxies, where P1 must be tunnelled to P2. michael@0: // route: Source -> P1 -> P2 -> Target (3 hops) michael@0: // fact: Source -> P1 -> Target (2 hops) michael@0: final int hop = fact.getHopCount()-1; // the hop to establish michael@0: boolean secure = createTunnelToProxy(route, hop, context); michael@0: this.log.debug("Tunnel to proxy created."); michael@0: managedConn.tunnelProxy(route.getHopTarget(hop), michael@0: secure, this.params); michael@0: } break; michael@0: michael@0: michael@0: case HttpRouteDirector.LAYER_PROTOCOL: michael@0: managedConn.layerProtocol(context, this.params); michael@0: break; michael@0: michael@0: case HttpRouteDirector.UNREACHABLE: michael@0: throw new HttpException("Unable to establish route: " + michael@0: "planned = " + route + "; current = " + fact); michael@0: case HttpRouteDirector.COMPLETE: michael@0: // do nothing michael@0: break; michael@0: default: michael@0: throw new IllegalStateException("Unknown step indicator " michael@0: + step + " from RouteDirector."); michael@0: } michael@0: michael@0: } while (step > HttpRouteDirector.COMPLETE); michael@0: michael@0: } // establishConnection michael@0: michael@0: michael@0: /** michael@0: * Creates a tunnel to the target server. michael@0: * The connection must be established to the (last) proxy. michael@0: * A CONNECT request for tunnelling through the proxy will michael@0: * be created and sent, the response received and checked. michael@0: * This method does not update the connection with michael@0: * information about the tunnel, that is left to the caller. michael@0: * michael@0: * @param route the route to establish michael@0: * @param context the context for request execution michael@0: * michael@0: * @return true if the tunnelled route is secure, michael@0: * false otherwise. michael@0: * The implementation here always returns false, michael@0: * but derived classes may override. michael@0: * michael@0: * @throws HttpException in case of a problem michael@0: * @throws IOException in case of an IO problem michael@0: */ michael@0: protected boolean createTunnelToTarget(HttpRoute route, michael@0: HttpContext context) michael@0: throws HttpException, IOException { michael@0: michael@0: HttpHost proxy = route.getProxyHost(); michael@0: HttpHost target = route.getTargetHost(); michael@0: HttpResponse response = null; michael@0: michael@0: boolean done = false; michael@0: while (!done) { michael@0: michael@0: done = true; michael@0: michael@0: if (!this.managedConn.isOpen()) { michael@0: this.managedConn.open(route, context, this.params); michael@0: } michael@0: michael@0: HttpRequest connect = createConnectRequest(route, context); michael@0: connect.setParams(this.params); michael@0: michael@0: // Populate the execution context michael@0: context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, michael@0: target); michael@0: context.setAttribute(ExecutionContext.HTTP_PROXY_HOST, michael@0: proxy); michael@0: context.setAttribute(ExecutionContext.HTTP_CONNECTION, michael@0: managedConn); michael@0: context.setAttribute(ClientContext.TARGET_AUTH_STATE, michael@0: targetAuthState); michael@0: context.setAttribute(ClientContext.PROXY_AUTH_STATE, michael@0: proxyAuthState); michael@0: context.setAttribute(ExecutionContext.HTTP_REQUEST, michael@0: connect); michael@0: michael@0: this.requestExec.preProcess(connect, this.httpProcessor, context); michael@0: michael@0: response = this.requestExec.execute(connect, this.managedConn, context); michael@0: michael@0: response.setParams(this.params); michael@0: this.requestExec.postProcess(response, this.httpProcessor, context); michael@0: michael@0: int status = response.getStatusLine().getStatusCode(); michael@0: if (status < 200) { michael@0: throw new HttpException("Unexpected response to CONNECT request: " + michael@0: response.getStatusLine()); michael@0: } michael@0: michael@0: CredentialsProvider credsProvider = (CredentialsProvider) michael@0: context.getAttribute(ClientContext.CREDS_PROVIDER); michael@0: michael@0: if (credsProvider != null && HttpClientParams.isAuthenticating(params)) { michael@0: if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) { michael@0: michael@0: this.log.debug("Proxy requested authentication"); michael@0: Map challenges = this.proxyAuthHandler.getChallenges( michael@0: response, context); michael@0: try { michael@0: processChallenges( michael@0: challenges, this.proxyAuthState, this.proxyAuthHandler, michael@0: response, context); michael@0: } catch (AuthenticationException ex) { michael@0: if (this.log.isWarnEnabled()) { michael@0: this.log.warn("Authentication error: " + ex.getMessage()); michael@0: break; michael@0: } michael@0: } michael@0: updateAuthState(this.proxyAuthState, proxy, credsProvider); michael@0: michael@0: if (this.proxyAuthState.getCredentials() != null) { michael@0: done = false; michael@0: michael@0: // Retry request michael@0: if (this.reuseStrategy.keepAlive(response, context)) { michael@0: this.log.debug("Connection kept alive"); michael@0: // Consume response content michael@0: HttpEntity entity = response.getEntity(); michael@0: EntityUtils.consume(entity); michael@0: } else { michael@0: this.managedConn.close(); michael@0: } michael@0: michael@0: } michael@0: michael@0: } else { michael@0: // Reset proxy auth scope michael@0: this.proxyAuthState.setAuthScope(null); michael@0: } michael@0: } michael@0: } michael@0: michael@0: int status = response.getStatusLine().getStatusCode(); // can't be null michael@0: michael@0: if (status > 299) { michael@0: michael@0: // Buffer response content michael@0: HttpEntity entity = response.getEntity(); michael@0: if (entity != null) { michael@0: response.setEntity(new BufferedHttpEntity(entity)); michael@0: } michael@0: michael@0: this.managedConn.close(); michael@0: throw new TunnelRefusedException("CONNECT refused by proxy: " + michael@0: response.getStatusLine(), response); michael@0: } michael@0: michael@0: this.managedConn.markReusable(); michael@0: michael@0: // How to decide on security of the tunnelled connection? michael@0: // The socket factory knows only about the segment to the proxy. michael@0: // Even if that is secure, the hop to the target may be insecure. michael@0: // Leave it to derived classes, consider insecure by default here. michael@0: return false; michael@0: michael@0: } // createTunnelToTarget michael@0: michael@0: michael@0: michael@0: /** michael@0: * Creates a tunnel to an intermediate proxy. michael@0: * This method is not implemented in this class. michael@0: * It just throws an exception here. michael@0: * michael@0: * @param route the route to establish michael@0: * @param hop the hop in the route to establish now. michael@0: * route.getHopTarget(hop) michael@0: * will return the proxy to tunnel to. michael@0: * @param context the context for request execution michael@0: * michael@0: * @return true if the partially tunnelled connection michael@0: * is secure, false otherwise. michael@0: * michael@0: * @throws HttpException in case of a problem michael@0: * @throws IOException in case of an IO problem michael@0: */ michael@0: protected boolean createTunnelToProxy(HttpRoute route, int hop, michael@0: HttpContext context) michael@0: throws HttpException, IOException { michael@0: michael@0: // Have a look at createTunnelToTarget and replicate the parts michael@0: // you need in a custom derived class. If your proxies don't require michael@0: // authentication, it is not too hard. But for the stock version of michael@0: // HttpClient, we cannot make such simplifying assumptions and would michael@0: // have to include proxy authentication code. The HttpComponents team michael@0: // is currently not in a position to support rarely used code of this michael@0: // complexity. Feel free to submit patches that refactor the code in michael@0: // createTunnelToTarget to facilitate re-use for proxy tunnelling. michael@0: michael@0: throw new HttpException("Proxy chains are not supported."); michael@0: } michael@0: michael@0: michael@0: michael@0: /** michael@0: * Creates the CONNECT request for tunnelling. michael@0: * Called by {@link #createTunnelToTarget createTunnelToTarget}. michael@0: * michael@0: * @param route the route to establish michael@0: * @param context the context for request execution michael@0: * michael@0: * @return the CONNECT request for tunnelling michael@0: */ michael@0: protected HttpRequest createConnectRequest(HttpRoute route, michael@0: HttpContext context) { michael@0: // see RFC 2817, section 5.2 and michael@0: // INTERNET-DRAFT: Tunneling TCP based protocols through michael@0: // Web proxy servers michael@0: michael@0: HttpHost target = route.getTargetHost(); michael@0: michael@0: String host = target.getHostName(); michael@0: int port = target.getPort(); michael@0: if (port < 0) { michael@0: Scheme scheme = connManager.getSchemeRegistry(). michael@0: getScheme(target.getSchemeName()); michael@0: port = scheme.getDefaultPort(); michael@0: } michael@0: michael@0: StringBuilder buffer = new StringBuilder(host.length() + 6); michael@0: buffer.append(host); michael@0: buffer.append(':'); michael@0: buffer.append(Integer.toString(port)); michael@0: michael@0: String authority = buffer.toString(); michael@0: ProtocolVersion ver = HttpProtocolParams.getVersion(params); michael@0: HttpRequest req = new BasicHttpRequest michael@0: ("CONNECT", authority, ver); michael@0: michael@0: return req; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Analyzes a response to check need for a followup. michael@0: * michael@0: * @param roureq the request and route. michael@0: * @param response the response to analayze michael@0: * @param context the context used for the current request execution michael@0: * michael@0: * @return the followup request and route if there is a followup, or michael@0: * null if the response should be returned as is michael@0: * michael@0: * @throws HttpException in case of a problem michael@0: * @throws IOException in case of an IO problem michael@0: */ michael@0: protected RoutedRequest handleResponse(RoutedRequest roureq, michael@0: HttpResponse response, michael@0: HttpContext context) michael@0: throws HttpException, IOException { michael@0: michael@0: HttpRoute route = roureq.getRoute(); michael@0: RequestWrapper request = roureq.getRequest(); michael@0: michael@0: HttpParams params = request.getParams(); michael@0: if (HttpClientParams.isRedirecting(params) && michael@0: this.redirectStrategy.isRedirected(request, response, context)) { michael@0: michael@0: if (redirectCount >= maxRedirects) { michael@0: throw new RedirectException("Maximum redirects (" michael@0: + maxRedirects + ") exceeded"); michael@0: } michael@0: redirectCount++; michael@0: michael@0: // Virtual host cannot be used any longer michael@0: virtualHost = null; michael@0: michael@0: HttpUriRequest redirect = redirectStrategy.getRedirect(request, response, context); michael@0: HttpRequest orig = request.getOriginal(); michael@0: redirect.setHeaders(orig.getAllHeaders()); michael@0: michael@0: URI uri = redirect.getURI(); michael@0: if (uri.getHost() == null) { michael@0: throw new ProtocolException("Redirect URI does not specify a valid host name: " + uri); michael@0: } michael@0: michael@0: HttpHost newTarget = new HttpHost( michael@0: uri.getHost(), michael@0: uri.getPort(), michael@0: uri.getScheme()); michael@0: michael@0: // Unset auth scope michael@0: targetAuthState.setAuthScope(null); michael@0: proxyAuthState.setAuthScope(null); michael@0: michael@0: // Invalidate auth states if redirecting to another host michael@0: if (!route.getTargetHost().equals(newTarget)) { michael@0: targetAuthState.invalidate(); michael@0: AuthScheme authScheme = proxyAuthState.getAuthScheme(); michael@0: if (authScheme != null && authScheme.isConnectionBased()) { michael@0: proxyAuthState.invalidate(); michael@0: } michael@0: } michael@0: michael@0: RequestWrapper wrapper = wrapRequest(redirect); michael@0: wrapper.setParams(params); michael@0: michael@0: HttpRoute newRoute = determineRoute(newTarget, wrapper, context); michael@0: RoutedRequest newRequest = new RoutedRequest(wrapper, newRoute); michael@0: michael@0: if (this.log.isDebugEnabled()) { michael@0: this.log.debug("Redirecting to '" + uri + "' via " + newRoute); michael@0: } michael@0: michael@0: return newRequest; michael@0: } michael@0: michael@0: CredentialsProvider credsProvider = (CredentialsProvider) michael@0: context.getAttribute(ClientContext.CREDS_PROVIDER); michael@0: michael@0: if (credsProvider != null && HttpClientParams.isAuthenticating(params)) { michael@0: michael@0: if (this.targetAuthHandler.isAuthenticationRequested(response, context)) { michael@0: michael@0: HttpHost target = (HttpHost) michael@0: context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); michael@0: if (target == null) { michael@0: target = route.getTargetHost(); michael@0: } michael@0: michael@0: this.log.debug("Target requested authentication"); michael@0: Map challenges = this.targetAuthHandler.getChallenges( michael@0: response, context); michael@0: try { michael@0: processChallenges(challenges, michael@0: this.targetAuthState, this.targetAuthHandler, michael@0: response, context); michael@0: } catch (AuthenticationException ex) { michael@0: if (this.log.isWarnEnabled()) { michael@0: this.log.warn("Authentication error: " + ex.getMessage()); michael@0: return null; michael@0: } michael@0: } michael@0: updateAuthState(this.targetAuthState, target, credsProvider); michael@0: michael@0: if (this.targetAuthState.getCredentials() != null) { michael@0: // Re-try the same request via the same route michael@0: return roureq; michael@0: } else { michael@0: return null; michael@0: } michael@0: } else { michael@0: // Reset target auth scope michael@0: this.targetAuthState.setAuthScope(null); michael@0: } michael@0: michael@0: if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) { michael@0: michael@0: HttpHost proxy = route.getProxyHost(); michael@0: michael@0: this.log.debug("Proxy requested authentication"); michael@0: Map challenges = this.proxyAuthHandler.getChallenges( michael@0: response, context); michael@0: try { michael@0: processChallenges(challenges, michael@0: this.proxyAuthState, this.proxyAuthHandler, michael@0: response, context); michael@0: } catch (AuthenticationException ex) { michael@0: if (this.log.isWarnEnabled()) { michael@0: this.log.warn("Authentication error: " + ex.getMessage()); michael@0: return null; michael@0: } michael@0: } michael@0: updateAuthState(this.proxyAuthState, proxy, credsProvider); michael@0: michael@0: if (this.proxyAuthState.getCredentials() != null) { michael@0: // Re-try the same request via the same route michael@0: return roureq; michael@0: } else { michael@0: return null; michael@0: } michael@0: } else { michael@0: // Reset proxy auth scope michael@0: this.proxyAuthState.setAuthScope(null); michael@0: } michael@0: } michael@0: return null; michael@0: } // handleResponse michael@0: michael@0: michael@0: /** michael@0: * Shuts down the connection. michael@0: * This method is called from a catch block in michael@0: * {@link #execute execute} during exception handling. michael@0: */ michael@0: private void abortConnection() { michael@0: ManagedClientConnection mcc = managedConn; michael@0: if (mcc != null) { michael@0: // we got here as the result of an exception michael@0: // no response will be returned, release the connection michael@0: managedConn = null; michael@0: try { michael@0: mcc.abortConnection(); michael@0: } catch (IOException ex) { michael@0: if (this.log.isDebugEnabled()) { michael@0: this.log.debug(ex.getMessage(), ex); michael@0: } michael@0: } michael@0: // ensure the connection manager properly releases this connection michael@0: try { michael@0: mcc.releaseConnection(); michael@0: } catch(IOException ignored) { michael@0: this.log.debug("Error releasing connection", ignored); michael@0: } michael@0: } michael@0: } // abortConnection michael@0: michael@0: michael@0: private void processChallenges( michael@0: final Map challenges, michael@0: final AuthState authState, michael@0: final AuthenticationHandler authHandler, michael@0: final HttpResponse response, michael@0: final HttpContext context) michael@0: throws MalformedChallengeException, AuthenticationException { michael@0: michael@0: AuthScheme authScheme = authState.getAuthScheme(); michael@0: if (authScheme == null) { michael@0: // Authentication not attempted before michael@0: authScheme = authHandler.selectScheme(challenges, response, context); michael@0: authState.setAuthScheme(authScheme); michael@0: } michael@0: String id = authScheme.getSchemeName(); michael@0: michael@0: Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH)); michael@0: if (challenge == null) { michael@0: throw new AuthenticationException(id + michael@0: " authorization challenge expected, but not found"); michael@0: } michael@0: authScheme.processChallenge(challenge); michael@0: this.log.debug("Authorization challenge processed"); michael@0: } michael@0: michael@0: michael@0: private void updateAuthState( michael@0: final AuthState authState, michael@0: final HttpHost host, michael@0: final CredentialsProvider credsProvider) { michael@0: michael@0: if (!authState.isValid()) { michael@0: return; michael@0: } michael@0: michael@0: String hostname = host.getHostName(); michael@0: int port = host.getPort(); michael@0: if (port < 0) { michael@0: Scheme scheme = connManager.getSchemeRegistry().getScheme(host); michael@0: port = scheme.getDefaultPort(); michael@0: } michael@0: michael@0: AuthScheme authScheme = authState.getAuthScheme(); michael@0: AuthScope authScope = new AuthScope( michael@0: hostname, michael@0: port, michael@0: authScheme.getRealm(), michael@0: authScheme.getSchemeName()); michael@0: michael@0: if (this.log.isDebugEnabled()) { michael@0: this.log.debug("Authentication scope: " + authScope); michael@0: } michael@0: Credentials creds = authState.getCredentials(); michael@0: if (creds == null) { michael@0: creds = credsProvider.getCredentials(authScope); michael@0: if (this.log.isDebugEnabled()) { michael@0: if (creds != null) { michael@0: this.log.debug("Found credentials"); michael@0: } else { michael@0: this.log.debug("Credentials not found"); michael@0: } michael@0: } michael@0: } else { michael@0: if (authScheme.isComplete()) { michael@0: this.log.debug("Authentication failed"); michael@0: creds = null; michael@0: } michael@0: } michael@0: authState.setAuthScope(authScope); michael@0: authState.setCredentials(creds); michael@0: } michael@0: michael@0: private void invalidateAuthIfSuccessful(final AuthState authState) { michael@0: AuthScheme authscheme = authState.getAuthScheme(); michael@0: if (authscheme != null michael@0: && authscheme.isConnectionBased() michael@0: && authscheme.isComplete() michael@0: && authState.getCredentials() != null) { michael@0: authState.invalidate(); michael@0: } michael@0: } michael@0: michael@0: } // class DefaultClientRequestDirector