| /* |
| * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/client/DefaultRequestDirector.java $ |
| * $Revision: 676023 $ |
| * $Date: 2008-07-11 09:40:56 -0700 (Fri, 11 Jul 2008) $ |
| * |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| * |
| * This software consists of voluntary contributions made by many |
| * individuals on behalf of the Apache Software Foundation. For more |
| * information on the Apache Software Foundation, please see |
| * <http://www.apache.org/>. |
| * |
| */ |
| |
| package org.apache.http.impl.client; |
| |
| import java.io.IOException; |
| import java.io.InterruptedIOException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.concurrent.TimeUnit; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.http.ConnectionReuseStrategy; |
| import org.apache.http.Header; |
| import org.apache.http.HttpEntity; |
| import org.apache.http.HttpEntityEnclosingRequest; |
| import org.apache.http.HttpException; |
| import org.apache.http.HttpHost; |
| import org.apache.http.HttpRequest; |
| import org.apache.http.HttpResponse; |
| import org.apache.http.ProtocolException; |
| import org.apache.http.ProtocolVersion; |
| import org.apache.http.auth.AuthScheme; |
| import org.apache.http.auth.AuthScope; |
| import org.apache.http.auth.AuthState; |
| import org.apache.http.auth.AuthenticationException; |
| import org.apache.http.auth.Credentials; |
| import org.apache.http.auth.MalformedChallengeException; |
| import org.apache.http.client.AuthenticationHandler; |
| import org.apache.http.client.RequestDirector; |
| import org.apache.http.client.CredentialsProvider; |
| import org.apache.http.client.HttpRequestRetryHandler; |
| import org.apache.http.client.NonRepeatableRequestException; |
| import org.apache.http.client.RedirectException; |
| import org.apache.http.client.RedirectHandler; |
| import org.apache.http.client.UserTokenHandler; |
| import org.apache.http.client.methods.AbortableHttpRequest; |
| import org.apache.http.client.methods.HttpGet; |
| import org.apache.http.client.params.ClientPNames; |
| import org.apache.http.client.params.HttpClientParams; |
| import org.apache.http.client.protocol.ClientContext; |
| import org.apache.http.client.utils.URIUtils; |
| import org.apache.http.conn.BasicManagedEntity; |
| import org.apache.http.conn.ClientConnectionManager; |
| import org.apache.http.conn.ClientConnectionRequest; |
| import org.apache.http.conn.ConnectionKeepAliveStrategy; |
| import org.apache.http.conn.ManagedClientConnection; |
| import org.apache.http.conn.params.ConnManagerParams; |
| import org.apache.http.conn.routing.BasicRouteDirector; |
| import org.apache.http.conn.routing.HttpRoute; |
| import org.apache.http.conn.routing.HttpRouteDirector; |
| import org.apache.http.conn.routing.HttpRoutePlanner; |
| import org.apache.http.conn.scheme.Scheme; |
| import org.apache.http.entity.BufferedHttpEntity; |
| import org.apache.http.message.BasicHttpRequest; |
| import org.apache.http.params.HttpConnectionParams; |
| import org.apache.http.params.HttpParams; |
| import org.apache.http.params.HttpProtocolParams; |
| import org.apache.http.protocol.ExecutionContext; |
| import org.apache.http.protocol.HTTP; |
| import org.apache.http.protocol.HttpContext; |
| import org.apache.http.protocol.HttpProcessor; |
| import org.apache.http.protocol.HttpRequestExecutor; |
| |
| /** |
| * Default implementation of {@link RequestDirector}. |
| * <br/> |
| * This class replaces the <code>HttpMethodDirector</code> in HttpClient 3. |
| * |
| * @author <a href="mailto:rolandw at apache.org">Roland Weber</a> |
| * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> |
| * |
| * <!-- empty lines to avoid svn diff problems --> |
| * @version $Revision: 676023 $ |
| * |
| * @since 4.0 |
| */ |
| public class DefaultRequestDirector implements RequestDirector { |
| |
| private final Log log = LogFactory.getLog(getClass()); |
| |
| /** The connection manager. */ |
| protected final ClientConnectionManager connManager; |
| |
| /** The route planner. */ |
| protected final HttpRoutePlanner routePlanner; |
| |
| /** The connection re-use strategy. */ |
| protected final ConnectionReuseStrategy reuseStrategy; |
| |
| /** The keep-alive duration strategy. */ |
| protected final ConnectionKeepAliveStrategy keepAliveStrategy; |
| |
| /** The request executor. */ |
| protected final HttpRequestExecutor requestExec; |
| |
| /** The HTTP protocol processor. */ |
| protected final HttpProcessor httpProcessor; |
| |
| /** The request retry handler. */ |
| protected final HttpRequestRetryHandler retryHandler; |
| |
| /** The redirect handler. */ |
| protected final RedirectHandler redirectHandler; |
| |
| /** The target authentication handler. */ |
| private final AuthenticationHandler targetAuthHandler; |
| |
| /** The proxy authentication handler. */ |
| private final AuthenticationHandler proxyAuthHandler; |
| |
| /** The user token handler. */ |
| private final UserTokenHandler userTokenHandler; |
| |
| /** The HTTP parameters. */ |
| protected final HttpParams params; |
| |
| /** The currently allocated connection. */ |
| protected ManagedClientConnection managedConn; |
| |
| private int redirectCount; |
| |
| private int maxRedirects; |
| |
| private final AuthState targetAuthState; |
| |
| private final AuthState proxyAuthState; |
| |
| public DefaultRequestDirector( |
| final HttpRequestExecutor requestExec, |
| final ClientConnectionManager conman, |
| final ConnectionReuseStrategy reustrat, |
| final ConnectionKeepAliveStrategy kastrat, |
| final HttpRoutePlanner rouplan, |
| final HttpProcessor httpProcessor, |
| final HttpRequestRetryHandler retryHandler, |
| final RedirectHandler redirectHandler, |
| final AuthenticationHandler targetAuthHandler, |
| final AuthenticationHandler proxyAuthHandler, |
| final UserTokenHandler userTokenHandler, |
| final HttpParams params) { |
| |
| if (requestExec == null) { |
| throw new IllegalArgumentException |
| ("Request executor may not be null."); |
| } |
| if (conman == null) { |
| throw new IllegalArgumentException |
| ("Client connection manager may not be null."); |
| } |
| if (reustrat == null) { |
| throw new IllegalArgumentException |
| ("Connection reuse strategy may not be null."); |
| } |
| if (kastrat == null) { |
| throw new IllegalArgumentException |
| ("Connection keep alive strategy may not be null."); |
| } |
| if (rouplan == null) { |
| throw new IllegalArgumentException |
| ("Route planner may not be null."); |
| } |
| if (httpProcessor == null) { |
| throw new IllegalArgumentException |
| ("HTTP protocol processor may not be null."); |
| } |
| if (retryHandler == null) { |
| throw new IllegalArgumentException |
| ("HTTP request retry handler may not be null."); |
| } |
| if (redirectHandler == null) { |
| throw new IllegalArgumentException |
| ("Redirect handler may not be null."); |
| } |
| if (targetAuthHandler == null) { |
| throw new IllegalArgumentException |
| ("Target authentication handler may not be null."); |
| } |
| if (proxyAuthHandler == null) { |
| throw new IllegalArgumentException |
| ("Proxy authentication handler may not be null."); |
| } |
| if (userTokenHandler == null) { |
| throw new IllegalArgumentException |
| ("User token handler may not be null."); |
| } |
| if (params == null) { |
| throw new IllegalArgumentException |
| ("HTTP parameters may not be null"); |
| } |
| this.requestExec = requestExec; |
| this.connManager = conman; |
| this.reuseStrategy = reustrat; |
| this.keepAliveStrategy = kastrat; |
| this.routePlanner = rouplan; |
| this.httpProcessor = httpProcessor; |
| this.retryHandler = retryHandler; |
| this.redirectHandler = redirectHandler; |
| this.targetAuthHandler = targetAuthHandler; |
| this.proxyAuthHandler = proxyAuthHandler; |
| this.userTokenHandler = userTokenHandler; |
| this.params = params; |
| |
| this.managedConn = null; |
| |
| this.redirectCount = 0; |
| this.maxRedirects = this.params.getIntParameter(ClientPNames.MAX_REDIRECTS, 100); |
| this.targetAuthState = new AuthState(); |
| this.proxyAuthState = new AuthState(); |
| } // constructor |
| |
| |
| private RequestWrapper wrapRequest( |
| final HttpRequest request) throws ProtocolException { |
| if (request instanceof HttpEntityEnclosingRequest) { |
| return new EntityEnclosingRequestWrapper( |
| (HttpEntityEnclosingRequest) request); |
| } else { |
| return new RequestWrapper( |
| request); |
| } |
| } |
| |
| |
| protected void rewriteRequestURI( |
| final RequestWrapper request, |
| final HttpRoute route) throws ProtocolException { |
| try { |
| |
| URI uri = request.getURI(); |
| if (route.getProxyHost() != null && !route.isTunnelled()) { |
| // Make sure the request URI is absolute |
| if (!uri.isAbsolute()) { |
| HttpHost target = route.getTargetHost(); |
| uri = URIUtils.rewriteURI(uri, target); |
| request.setURI(uri); |
| } |
| } else { |
| // Make sure the request URI is relative |
| if (uri.isAbsolute()) { |
| uri = URIUtils.rewriteURI(uri, null); |
| request.setURI(uri); |
| } |
| } |
| |
| } catch (URISyntaxException ex) { |
| throw new ProtocolException("Invalid URI: " + |
| request.getRequestLine().getUri(), ex); |
| } |
| } |
| |
| |
| // non-javadoc, see interface ClientRequestDirector |
| public HttpResponse execute(HttpHost target, HttpRequest request, |
| HttpContext context) |
| throws HttpException, IOException { |
| |
| HttpRequest orig = request; |
| RequestWrapper origWrapper = wrapRequest(orig); |
| origWrapper.setParams(params); |
| HttpRoute origRoute = determineRoute(target, origWrapper, context); |
| |
| RoutedRequest roureq = new RoutedRequest(origWrapper, origRoute); |
| |
| long timeout = ConnManagerParams.getTimeout(params); |
| |
| int execCount = 0; |
| |
| boolean reuse = false; |
| HttpResponse response = null; |
| boolean done = false; |
| try { |
| while (!done) { |
| // In this loop, the RoutedRequest may be replaced by a |
| // followup request and route. The request and route passed |
| // in the method arguments will be replaced. The original |
| // request is still available in 'orig'. |
| |
| RequestWrapper wrapper = roureq.getRequest(); |
| HttpRoute route = roureq.getRoute(); |
| |
| // See if we have a user token bound to the execution context |
| Object userToken = context.getAttribute(ClientContext.USER_TOKEN); |
| |
| // Allocate connection if needed |
| if (managedConn == null) { |
| ClientConnectionRequest connRequest = connManager.requestConnection( |
| route, userToken); |
| if (orig instanceof AbortableHttpRequest) { |
| ((AbortableHttpRequest) orig).setConnectionRequest(connRequest); |
| } |
| |
| try { |
| managedConn = connRequest.getConnection(timeout, TimeUnit.MILLISECONDS); |
| } catch(InterruptedException interrupted) { |
| InterruptedIOException iox = new InterruptedIOException(); |
| iox.initCause(interrupted); |
| throw iox; |
| } |
| |
| if (HttpConnectionParams.isStaleCheckingEnabled(params)) { |
| // validate connection |
| this.log.debug("Stale connection check"); |
| if (managedConn.isStale()) { |
| this.log.debug("Stale connection detected"); |
| // BEGIN android-changed |
| try { |
| managedConn.close(); |
| } catch (IOException ignored) { |
| // SSLSocket's will throw IOException |
| // because they can't send a "close |
| // notify" protocol message to the |
| // server. Just supresss any |
| // exceptions related to closing the |
| // stale connection. |
| } |
| // END android-changed |
| } |
| } |
| } |
| |
| if (orig instanceof AbortableHttpRequest) { |
| ((AbortableHttpRequest) orig).setReleaseTrigger(managedConn); |
| } |
| |
| // Reopen connection if needed |
| if (!managedConn.isOpen()) { |
| managedConn.open(route, context, params); |
| } |
| |
| try { |
| establishRoute(route, context); |
| } catch (TunnelRefusedException ex) { |
| if (this.log.isDebugEnabled()) { |
| this.log.debug(ex.getMessage()); |
| } |
| response = ex.getResponse(); |
| break; |
| } |
| |
| // Reset headers on the request wrapper |
| wrapper.resetHeaders(); |
| |
| // Re-write request URI if needed |
| rewriteRequestURI(wrapper, route); |
| |
| // Use virtual host if set |
| target = (HttpHost) wrapper.getParams().getParameter( |
| ClientPNames.VIRTUAL_HOST); |
| |
| if (target == null) { |
| target = route.getTargetHost(); |
| } |
| |
| HttpHost proxy = route.getProxyHost(); |
| |
| // Populate the execution context |
| context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, |
| target); |
| context.setAttribute(ExecutionContext.HTTP_PROXY_HOST, |
| proxy); |
| context.setAttribute(ExecutionContext.HTTP_CONNECTION, |
| managedConn); |
| context.setAttribute(ClientContext.TARGET_AUTH_STATE, |
| targetAuthState); |
| context.setAttribute(ClientContext.PROXY_AUTH_STATE, |
| proxyAuthState); |
| |
| // Run request protocol interceptors |
| requestExec.preProcess(wrapper, httpProcessor, context); |
| |
| context.setAttribute(ExecutionContext.HTTP_REQUEST, |
| wrapper); |
| |
| boolean retrying = true; |
| while (retrying) { |
| // Increment total exec count (with redirects) |
| execCount++; |
| // Increment exec count for this particular request |
| wrapper.incrementExecCount(); |
| if (wrapper.getExecCount() > 1 && !wrapper.isRepeatable()) { |
| throw new NonRepeatableRequestException("Cannot retry request " + |
| "with a non-repeatable request entity"); |
| } |
| |
| try { |
| if (this.log.isDebugEnabled()) { |
| this.log.debug("Attempt " + execCount + " to execute request"); |
| } |
| response = requestExec.execute(wrapper, managedConn, context); |
| retrying = false; |
| |
| } catch (IOException ex) { |
| this.log.debug("Closing the connection."); |
| managedConn.close(); |
| if (retryHandler.retryRequest(ex, execCount, context)) { |
| if (this.log.isInfoEnabled()) { |
| this.log.info("I/O exception ("+ ex.getClass().getName() + |
| ") caught when processing request: " |
| + ex.getMessage()); |
| } |
| if (this.log.isDebugEnabled()) { |
| this.log.debug(ex.getMessage(), ex); |
| } |
| this.log.info("Retrying request"); |
| } else { |
| throw ex; |
| } |
| |
| // If we have a direct route to the target host |
| // just re-open connection and re-try the request |
| if (route.getHopCount() == 1) { |
| this.log.debug("Reopening the direct connection."); |
| managedConn.open(route, context, params); |
| } else { |
| // otherwise give up |
| retrying = false; |
| } |
| |
| } |
| |
| } |
| |
| // Run response protocol interceptors |
| response.setParams(params); |
| requestExec.postProcess(response, httpProcessor, context); |
| |
| |
| // The connection is in or can be brought to a re-usable state. |
| reuse = reuseStrategy.keepAlive(response, context); |
| if(reuse) { |
| // Set the idle duration of this connection |
| long duration = keepAliveStrategy.getKeepAliveDuration(response, context); |
| managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS); |
| } |
| |
| RoutedRequest followup = handleResponse(roureq, response, context); |
| if (followup == null) { |
| done = true; |
| } else { |
| if (reuse) { |
| this.log.debug("Connection kept alive"); |
| // Make sure the response body is fully consumed, if present |
| HttpEntity entity = response.getEntity(); |
| if (entity != null) { |
| entity.consumeContent(); |
| } |
| // entity consumed above is not an auto-release entity, |
| // need to mark the connection re-usable explicitly |
| managedConn.markReusable(); |
| } else { |
| managedConn.close(); |
| } |
| // check if we can use the same connection for the followup |
| if (!followup.getRoute().equals(roureq.getRoute())) { |
| releaseConnection(); |
| } |
| roureq = followup; |
| } |
| |
| userToken = this.userTokenHandler.getUserToken(context); |
| context.setAttribute(ClientContext.USER_TOKEN, userToken); |
| if (managedConn != null) { |
| managedConn.setState(userToken); |
| } |
| } // while not done |
| |
| |
| // check for entity, release connection if possible |
| if ((response == null) || (response.getEntity() == null) || |
| !response.getEntity().isStreaming()) { |
| // connection not needed and (assumed to be) in re-usable state |
| if (reuse) |
| managedConn.markReusable(); |
| releaseConnection(); |
| } else { |
| // install an auto-release entity |
| HttpEntity entity = response.getEntity(); |
| entity = new BasicManagedEntity(entity, managedConn, reuse); |
| response.setEntity(entity); |
| } |
| |
| return response; |
| |
| } catch (HttpException ex) { |
| abortConnection(); |
| throw ex; |
| } catch (IOException ex) { |
| abortConnection(); |
| throw ex; |
| } catch (RuntimeException ex) { |
| abortConnection(); |
| throw ex; |
| } |
| } // execute |
| |
| /** |
| * Returns the connection back to the connection manager |
| * and prepares for retrieving a new connection during |
| * the next request. |
| */ |
| protected void releaseConnection() { |
| // Release the connection through the ManagedConnection instead of the |
| // ConnectionManager directly. This lets the connection control how |
| // it is released. |
| try { |
| managedConn.releaseConnection(); |
| } catch(IOException ignored) { |
| this.log.debug("IOException releasing connection", ignored); |
| } |
| managedConn = null; |
| } |
| |
| /** |
| * Determines the route for a request. |
| * Called by {@link #execute} |
| * to determine the route for either the original or a followup request. |
| * |
| * @param target the target host for the request. |
| * Implementations may accept <code>null</code> |
| * if they can still determine a route, for example |
| * to a default target or by inspecting the request. |
| * @param request the request to execute |
| * @param context the context to use for the execution, |
| * never <code>null</code> |
| * |
| * @return the route the request should take |
| * |
| * @throws HttpException in case of a problem |
| */ |
| protected HttpRoute determineRoute(HttpHost target, |
| HttpRequest request, |
| HttpContext context) |
| throws HttpException { |
| |
| if (target == null) { |
| target = (HttpHost) request.getParams().getParameter( |
| ClientPNames.DEFAULT_HOST); |
| } |
| if (target == null) { |
| throw new IllegalStateException |
| ("Target host must not be null, or set in parameters."); |
| } |
| |
| return this.routePlanner.determineRoute(target, request, context); |
| } |
| |
| |
| /** |
| * Establishes the target route. |
| * |
| * @param route the route to establish |
| * @param context the context for the request execution |
| * |
| * @throws HttpException in case of a problem |
| * @throws IOException in case of an IO problem |
| */ |
| protected void establishRoute(HttpRoute route, HttpContext context) |
| throws HttpException, IOException { |
| |
| //@@@ how to handle CONNECT requests for tunnelling? |
| //@@@ refuse to send external CONNECT via director? special handling? |
| |
| //@@@ should the request parameters already be used below? |
| //@@@ probably yes, but they're not linked yet |
| //@@@ will linking above cause problems with linking in reqExec? |
| //@@@ probably not, because the parent is replaced |
| //@@@ just make sure we don't link parameters to themselves |
| |
| HttpRouteDirector rowdy = new BasicRouteDirector(); |
| int step; |
| do { |
| HttpRoute fact = managedConn.getRoute(); |
| step = rowdy.nextStep(route, fact); |
| |
| switch (step) { |
| |
| case HttpRouteDirector.CONNECT_TARGET: |
| case HttpRouteDirector.CONNECT_PROXY: |
| managedConn.open(route, context, this.params); |
| break; |
| |
| case HttpRouteDirector.TUNNEL_TARGET: { |
| boolean secure = createTunnelToTarget(route, context); |
| this.log.debug("Tunnel to target created."); |
| managedConn.tunnelTarget(secure, this.params); |
| } break; |
| |
| case HttpRouteDirector.TUNNEL_PROXY: { |
| // The most simple example for this case is a proxy chain |
| // of two proxies, where P1 must be tunnelled to P2. |
| // route: Source -> P1 -> P2 -> Target (3 hops) |
| // fact: Source -> P1 -> Target (2 hops) |
| final int hop = fact.getHopCount()-1; // the hop to establish |
| boolean secure = createTunnelToProxy(route, hop, context); |
| this.log.debug("Tunnel to proxy created."); |
| managedConn.tunnelProxy(route.getHopTarget(hop), |
| secure, this.params); |
| } break; |
| |
| |
| case HttpRouteDirector.LAYER_PROTOCOL: |
| managedConn.layerProtocol(context, this.params); |
| break; |
| |
| case HttpRouteDirector.UNREACHABLE: |
| throw new IllegalStateException |
| ("Unable to establish route." + |
| "\nplanned = " + route + |
| "\ncurrent = " + fact); |
| |
| case HttpRouteDirector.COMPLETE: |
| // do nothing |
| break; |
| |
| default: |
| throw new IllegalStateException |
| ("Unknown step indicator "+step+" from RouteDirector."); |
| } // switch |
| |
| } while (step > HttpRouteDirector.COMPLETE); |
| |
| } // establishConnection |
| |
| |
| /** |
| * Creates a tunnel to the target server. |
| * The connection must be established to the (last) proxy. |
| * A CONNECT request for tunnelling through the proxy will |
| * be created and sent, the response received and checked. |
| * This method does <i>not</i> update the connection with |
| * information about the tunnel, that is left to the caller. |
| * |
| * @param route the route to establish |
| * @param context the context for request execution |
| * |
| * @return <code>true</code> if the tunnelled route is secure, |
| * <code>false</code> otherwise. |
| * The implementation here always returns <code>false</code>, |
| * but derived classes may override. |
| * |
| * @throws HttpException in case of a problem |
| * @throws IOException in case of an IO problem |
| */ |
| protected boolean createTunnelToTarget(HttpRoute route, |
| HttpContext context) |
| throws HttpException, IOException { |
| |
| HttpHost proxy = route.getProxyHost(); |
| HttpHost target = route.getTargetHost(); |
| HttpResponse response = null; |
| |
| boolean done = false; |
| while (!done) { |
| |
| done = true; |
| |
| if (!this.managedConn.isOpen()) { |
| this.managedConn.open(route, context, this.params); |
| } |
| |
| HttpRequest connect = createConnectRequest(route, context); |
| |
| String agent = HttpProtocolParams.getUserAgent(params); |
| if (agent != null) { |
| connect.addHeader(HTTP.USER_AGENT, agent); |
| } |
| connect.addHeader(HTTP.TARGET_HOST, target.toHostString()); |
| |
| AuthScheme authScheme = this.proxyAuthState.getAuthScheme(); |
| AuthScope authScope = this.proxyAuthState.getAuthScope(); |
| Credentials creds = this.proxyAuthState.getCredentials(); |
| if (creds != null) { |
| if (authScope != null || !authScheme.isConnectionBased()) { |
| try { |
| connect.addHeader(authScheme.authenticate(creds, connect)); |
| } catch (AuthenticationException ex) { |
| if (this.log.isErrorEnabled()) { |
| this.log.error("Proxy authentication error: " + ex.getMessage()); |
| } |
| } |
| } |
| } |
| |
| response = requestExec.execute(connect, this.managedConn, context); |
| |
| int status = response.getStatusLine().getStatusCode(); |
| if (status < 200) { |
| throw new HttpException("Unexpected response to CONNECT request: " + |
| response.getStatusLine()); |
| } |
| |
| CredentialsProvider credsProvider = (CredentialsProvider) |
| context.getAttribute(ClientContext.CREDS_PROVIDER); |
| |
| if (credsProvider != null && HttpClientParams.isAuthenticating(params)) { |
| if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) { |
| |
| this.log.debug("Proxy requested authentication"); |
| Map<String, Header> challenges = this.proxyAuthHandler.getChallenges( |
| response, context); |
| try { |
| processChallenges( |
| challenges, this.proxyAuthState, this.proxyAuthHandler, |
| response, context); |
| } catch (AuthenticationException ex) { |
| if (this.log.isWarnEnabled()) { |
| this.log.warn("Authentication error: " + ex.getMessage()); |
| break; |
| } |
| } |
| updateAuthState(this.proxyAuthState, proxy, credsProvider); |
| |
| if (this.proxyAuthState.getCredentials() != null) { |
| done = false; |
| |
| // Retry request |
| if (this.reuseStrategy.keepAlive(response, context)) { |
| this.log.debug("Connection kept alive"); |
| // Consume response content |
| HttpEntity entity = response.getEntity(); |
| if (entity != null) { |
| entity.consumeContent(); |
| } |
| } else { |
| this.managedConn.close(); |
| } |
| |
| } |
| |
| } else { |
| // Reset proxy auth scope |
| this.proxyAuthState.setAuthScope(null); |
| } |
| } |
| } |
| |
| int status = response.getStatusLine().getStatusCode(); |
| |
| if (status > 299) { |
| |
| // Buffer response content |
| HttpEntity entity = response.getEntity(); |
| if (entity != null) { |
| response.setEntity(new BufferedHttpEntity(entity)); |
| } |
| |
| this.managedConn.close(); |
| throw new TunnelRefusedException("CONNECT refused by proxy: " + |
| response.getStatusLine(), response); |
| } |
| |
| this.managedConn.markReusable(); |
| |
| // How to decide on security of the tunnelled connection? |
| // The socket factory knows only about the segment to the proxy. |
| // Even if that is secure, the hop to the target may be insecure. |
| // Leave it to derived classes, consider insecure by default here. |
| return false; |
| |
| } // createTunnelToTarget |
| |
| |
| |
| /** |
| * Creates a tunnel to an intermediate proxy. |
| * This method is <i>not</i> implemented in this class. |
| * It just throws an exception here. |
| * |
| * @param route the route to establish |
| * @param hop the hop in the route to establish now. |
| * <code>route.getHopTarget(hop)</code> |
| * will return the proxy to tunnel to. |
| * @param context the context for request execution |
| * |
| * @return <code>true</code> if the partially tunnelled connection |
| * is secure, <code>false</code> otherwise. |
| * |
| * @throws HttpException in case of a problem |
| * @throws IOException in case of an IO problem |
| */ |
| protected boolean createTunnelToProxy(HttpRoute route, int hop, |
| HttpContext context) |
| throws HttpException, IOException { |
| |
| // Have a look at createTunnelToTarget and replicate the parts |
| // you need in a custom derived class. If your proxies don't require |
| // authentication, it is not too hard. But for the stock version of |
| // HttpClient, we cannot make such simplifying assumptions and would |
| // have to include proxy authentication code. The HttpComponents team |
| // is currently not in a position to support rarely used code of this |
| // complexity. Feel free to submit patches that refactor the code in |
| // createTunnelToTarget to facilitate re-use for proxy tunnelling. |
| |
| throw new UnsupportedOperationException |
| ("Proxy chains are not supported."); |
| } |
| |
| |
| |
| /** |
| * Creates the CONNECT request for tunnelling. |
| * Called by {@link #createTunnelToTarget createTunnelToTarget}. |
| * |
| * @param route the route to establish |
| * @param context the context for request execution |
| * |
| * @return the CONNECT request for tunnelling |
| */ |
| protected HttpRequest createConnectRequest(HttpRoute route, |
| HttpContext context) { |
| // see RFC 2817, section 5.2 and |
| // INTERNET-DRAFT: Tunneling TCP based protocols through |
| // Web proxy servers |
| |
| HttpHost target = route.getTargetHost(); |
| |
| String host = target.getHostName(); |
| int port = target.getPort(); |
| if (port < 0) { |
| Scheme scheme = connManager.getSchemeRegistry(). |
| getScheme(target.getSchemeName()); |
| port = scheme.getDefaultPort(); |
| } |
| |
| StringBuilder buffer = new StringBuilder(host.length() + 6); |
| buffer.append(host); |
| buffer.append(':'); |
| buffer.append(Integer.toString(port)); |
| |
| String authority = buffer.toString(); |
| ProtocolVersion ver = HttpProtocolParams.getVersion(params); |
| HttpRequest req = new BasicHttpRequest |
| ("CONNECT", authority, ver); |
| |
| return req; |
| } |
| |
| |
| /** |
| * Analyzes a response to check need for a followup. |
| * |
| * @param roureq the request and route. |
| * @param response the response to analayze |
| * @param context the context used for the current request execution |
| * |
| * @return the followup request and route if there is a followup, or |
| * <code>null</code> if the response should be returned as is |
| * |
| * @throws HttpException in case of a problem |
| * @throws IOException in case of an IO problem |
| */ |
| protected RoutedRequest handleResponse(RoutedRequest roureq, |
| HttpResponse response, |
| HttpContext context) |
| throws HttpException, IOException { |
| |
| HttpRoute route = roureq.getRoute(); |
| HttpHost proxy = route.getProxyHost(); |
| RequestWrapper request = roureq.getRequest(); |
| |
| HttpParams params = request.getParams(); |
| if (HttpClientParams.isRedirecting(params) && |
| this.redirectHandler.isRedirectRequested(response, context)) { |
| |
| if (redirectCount >= maxRedirects) { |
| throw new RedirectException("Maximum redirects (" |
| + maxRedirects + ") exceeded"); |
| } |
| redirectCount++; |
| |
| URI uri = this.redirectHandler.getLocationURI(response, context); |
| |
| HttpHost newTarget = new HttpHost( |
| uri.getHost(), |
| uri.getPort(), |
| uri.getScheme()); |
| |
| HttpGet redirect = new HttpGet(uri); |
| |
| HttpRequest orig = request.getOriginal(); |
| redirect.setHeaders(orig.getAllHeaders()); |
| |
| RequestWrapper wrapper = new RequestWrapper(redirect); |
| wrapper.setParams(params); |
| |
| HttpRoute newRoute = determineRoute(newTarget, wrapper, context); |
| RoutedRequest newRequest = new RoutedRequest(wrapper, newRoute); |
| |
| if (this.log.isDebugEnabled()) { |
| this.log.debug("Redirecting to '" + uri + "' via " + newRoute); |
| } |
| |
| return newRequest; |
| } |
| |
| CredentialsProvider credsProvider = (CredentialsProvider) |
| context.getAttribute(ClientContext.CREDS_PROVIDER); |
| |
| if (credsProvider != null && HttpClientParams.isAuthenticating(params)) { |
| |
| if (this.targetAuthHandler.isAuthenticationRequested(response, context)) { |
| |
| HttpHost target = (HttpHost) |
| context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); |
| if (target == null) { |
| target = route.getTargetHost(); |
| } |
| |
| this.log.debug("Target requested authentication"); |
| Map<String, Header> challenges = this.targetAuthHandler.getChallenges( |
| response, context); |
| try { |
| processChallenges(challenges, |
| this.targetAuthState, this.targetAuthHandler, |
| response, context); |
| } catch (AuthenticationException ex) { |
| if (this.log.isWarnEnabled()) { |
| this.log.warn("Authentication error: " + ex.getMessage()); |
| return null; |
| } |
| } |
| updateAuthState(this.targetAuthState, target, credsProvider); |
| |
| if (this.targetAuthState.getCredentials() != null) { |
| // Re-try the same request via the same route |
| return roureq; |
| } else { |
| return null; |
| } |
| } else { |
| // Reset target auth scope |
| this.targetAuthState.setAuthScope(null); |
| } |
| |
| if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) { |
| |
| this.log.debug("Proxy requested authentication"); |
| Map<String, Header> challenges = this.proxyAuthHandler.getChallenges( |
| response, context); |
| try { |
| processChallenges(challenges, |
| this.proxyAuthState, this.proxyAuthHandler, |
| response, context); |
| } catch (AuthenticationException ex) { |
| if (this.log.isWarnEnabled()) { |
| this.log.warn("Authentication error: " + ex.getMessage()); |
| return null; |
| } |
| } |
| updateAuthState(this.proxyAuthState, proxy, credsProvider); |
| |
| if (this.proxyAuthState.getCredentials() != null) { |
| // Re-try the same request via the same route |
| return roureq; |
| } else { |
| return null; |
| } |
| } else { |
| // Reset proxy auth scope |
| this.proxyAuthState.setAuthScope(null); |
| } |
| } |
| return null; |
| } // handleResponse |
| |
| |
| /** |
| * Shuts down the connection. |
| * This method is called from a <code>catch</code> block in |
| * {@link #execute execute} during exception handling. |
| */ |
| private void abortConnection() { |
| ManagedClientConnection mcc = managedConn; |
| if (mcc != null) { |
| // we got here as the result of an exception |
| // no response will be returned, release the connection |
| managedConn = null; |
| try { |
| mcc.abortConnection(); |
| } catch (IOException ex) { |
| if (this.log.isDebugEnabled()) { |
| this.log.debug(ex.getMessage(), ex); |
| } |
| } |
| // ensure the connection manager properly releases this connection |
| try { |
| mcc.releaseConnection(); |
| } catch(IOException ignored) { |
| this.log.debug("Error releasing connection", ignored); |
| } |
| } |
| } // abortConnection |
| |
| |
| private void processChallenges( |
| final Map<String, Header> challenges, |
| final AuthState authState, |
| final AuthenticationHandler authHandler, |
| final HttpResponse response, |
| final HttpContext context) |
| throws MalformedChallengeException, AuthenticationException { |
| |
| AuthScheme authScheme = authState.getAuthScheme(); |
| if (authScheme == null) { |
| // Authentication not attempted before |
| authScheme = authHandler.selectScheme(challenges, response, context); |
| authState.setAuthScheme(authScheme); |
| } |
| String id = authScheme.getSchemeName(); |
| |
| Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH)); |
| if (challenge == null) { |
| throw new AuthenticationException(id + |
| " authorization challenge expected, but not found"); |
| } |
| authScheme.processChallenge(challenge); |
| this.log.debug("Authorization challenge processed"); |
| } |
| |
| |
| private void updateAuthState( |
| final AuthState authState, |
| final HttpHost host, |
| final CredentialsProvider credsProvider) { |
| |
| if (!authState.isValid()) { |
| return; |
| } |
| |
| String hostname = host.getHostName(); |
| int port = host.getPort(); |
| if (port < 0) { |
| Scheme scheme = connManager.getSchemeRegistry().getScheme(host); |
| port = scheme.getDefaultPort(); |
| } |
| |
| AuthScheme authScheme = authState.getAuthScheme(); |
| AuthScope authScope = new AuthScope( |
| hostname, |
| port, |
| authScheme.getRealm(), |
| authScheme.getSchemeName()); |
| |
| if (this.log.isDebugEnabled()) { |
| this.log.debug("Authentication scope: " + authScope); |
| } |
| Credentials creds = authState.getCredentials(); |
| if (creds == null) { |
| creds = credsProvider.getCredentials(authScope); |
| if (this.log.isDebugEnabled()) { |
| if (creds != null) { |
| this.log.debug("Found credentials"); |
| } else { |
| this.log.debug("Credentials not found"); |
| } |
| } |
| } else { |
| if (authScheme.isComplete()) { |
| this.log.debug("Authentication failed"); |
| creds = null; |
| } |
| } |
| authState.setAuthScope(authScope); |
| authState.setCredentials(creds); |
| } |
| |
| } // class DefaultClientRequestDirector |