View Javadoc

    1   /*
    2    * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/HttpMethodDirector.java,v 1.34 2005/01/14 19:40:39 olegk Exp $
    3    * $Revision: 486658 $
    4    * $Date: 2006-12-13 15:05:50 +0100 (Wed, 13 Dec 2006) $
    5    *
    6    * ====================================================================
    7    *
    8    *  Licensed to the Apache Software Foundation (ASF) under one or more
    9    *  contributor license agreements.  See the NOTICE file distributed with
    10   *  this work for additional information regarding copyright ownership.
    11   *  The ASF licenses this file to You under the Apache License, Version 2.0
    12   *  (the "License"); you may not use this file except in compliance with
    13   *  the License.  You may obtain a copy of the License at
    14   *
    15   *      http://www.apache.org/licenses/LICENSE-2.0
    16   *
    17   *  Unless required by applicable law or agreed to in writing, software
    18   *  distributed under the License is distributed on an "AS IS" BASIS,
    19   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    20   *  See the License for the specific language governing permissions and
    21   *  limitations under the License.
    22   * ====================================================================
    23   *
    24   * This software consists of voluntary contributions made by many
    25   * individuals on behalf of the Apache Software Foundation.  For more
    26   * information on the Apache Software Foundation, please see
    27   * <http://www.apache.org/>.
    28   *
    29   */
    30
    31  package org.apache.commons.httpclient;
    32
    33  import java.io.IOException;
    34  import java.util.Collection;
    35  import java.util.HashSet;
    36  import java.util.Iterator;
    37  import java.util.Map;
    38  import java.util.Set;
    39
    40  import org.apache.commons.httpclient.auth.AuthChallengeException;
    41  import org.apache.commons.httpclient.auth.AuthChallengeParser;
    42  import org.apache.commons.httpclient.auth.AuthChallengeProcessor;
    43  import org.apache.commons.httpclient.auth.AuthScheme;
    44  import org.apache.commons.httpclient.auth.AuthState;
    45  import org.apache.commons.httpclient.auth.AuthenticationException;
    46  import org.apache.commons.httpclient.auth.CredentialsProvider;
    47  import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;
    48  import org.apache.commons.httpclient.auth.AuthScope;
    49  import org.apache.commons.httpclient.auth.MalformedChallengeException;
    50  import org.apache.commons.httpclient.params.HostParams;
    51  import org.apache.commons.httpclient.params.HttpClientParams;
    52  import org.apache.commons.httpclient.params.HttpConnectionParams;
    53  import org.apache.commons.httpclient.params.HttpMethodParams;
    54  import org.apache.commons.httpclient.params.HttpParams;
    55  import org.apache.commons.logging.Log;
    56  import org.apache.commons.logging.LogFactory;
    57
    58  /**
    59   * Handles the process of executing a method including authentication, redirection and retries.
    60   *
    61   * @since 3.0
    62   */
    63  class HttpMethodDirector {
    64
    65      /** The www authenticate challange header. */
    66      public static final String WWW_AUTH_CHALLENGE = "WWW-Authenticate";
    67
    68      /** The www authenticate response header. */
    69      public static final String WWW_AUTH_RESP = "Authorization";
    70
    71      /** The proxy authenticate challange header. */
    72      public static final String PROXY_AUTH_CHALLENGE = "Proxy-Authenticate";
    73
    74      /** The proxy authenticate response header. */
    75      public static final String PROXY_AUTH_RESP = "Proxy-Authorization";
    76
    77      private static final Log LOG = LogFactory.getLog(HttpMethodDirector.class);
    78
    79      private ConnectMethod connectMethod;
    80
    81      private HttpState state;
    82
    83      private HostConfiguration hostConfiguration;
    84
    85      private HttpConnectionManager connectionManager;
    86
    87      private HttpClientParams params;
    88
    89      private HttpConnection conn;
    90
    91      /** A flag to indicate if the connection should be released after the method is executed. */
    92      private boolean releaseConnection = false;
    93
    94      /** Authentication processor */
    95      private AuthChallengeProcessor authProcessor = null;
    96
    97      private Set redirectLocations = null;
    98
    99      public HttpMethodDirector(
    100         final HttpConnectionManager connectionManager,
    101         final HostConfiguration hostConfiguration,
    102         final HttpClientParams params,
    103         final HttpState state
    104     ) {
    105         super();
    106         this.connectionManager = connectionManager;
    107         this.hostConfiguration = hostConfiguration;
    108         this.params = params;
    109         this.state = state;
    110         this.authProcessor = new AuthChallengeProcessor(this.params);
    111     }
    112
    113
    114     /**
    115      * Executes the method associated with this method director.
    116      *
    117      * @throws IOException
    118      * @throws HttpException
    119      */
    120     public void executeMethod(final HttpMethod method) throws IOException, HttpException {
    121         if (method == null) {
    122             throw new IllegalArgumentException("Method may not be null");
    123         }
    124         // Link all parameter collections to form the hierarchy:
    125         // Global -> HttpClient -> HostConfiguration -> HttpMethod
    126         this.hostConfiguration.getParams().setDefaults(this.params);
    127         method.getParams().setDefaults(this.hostConfiguration.getParams());
    128
    129         // Generate default request headers
    130         Collection defaults = (Collection)this.hostConfiguration.getParams().
    131 			getParameter(HostParams.DEFAULT_HEADERS);
    132         if (defaults != null) {
    133         	Iterator i = defaults.iterator();
    134         	while (i.hasNext()) {
    135         		method.addRequestHeader((Header)i.next());
    136         	}
    137         }
    138
    139         try {
    140             int maxRedirects = this.params.getIntParameter(HttpClientParams.MAX_REDIRECTS, 100);
    141
    142             for (int redirectCount = 0;;) {
    143
    144                 // make sure the connection we have is appropriate
    145                 if (this.conn != null && !hostConfiguration.hostEquals(this.conn)) {
    146                     this.conn.setLocked(false);
    147                     this.conn.releaseConnection();
    148                     this.conn = null;
    149                 }
    150
    151                 // get a connection, if we need one
    152                 if (this.conn == null) {
    153                     this.conn = connectionManager.getConnectionWithTimeout(
    154                         hostConfiguration,
    155                         this.params.getConnectionManagerTimeout()
    156                     );
    157                     this.conn.setLocked(true);
    158                     if (this.params.isAuthenticationPreemptive()
    159                      || this.state.isAuthenticationPreemptive())
    160                     {
    161                         LOG.debug("Preemptively sending default basic credentials");
    162                         method.getHostAuthState().setPreemptive();
    163                         method.getHostAuthState().setAuthAttempted(true);
    164                         if (this.conn.isProxied() && !this.conn.isSecure()) {
    165                             method.getProxyAuthState().setPreemptive();
    166                             method.getProxyAuthState().setAuthAttempted(true);
    167                         }
    168                     }
    169                 }
    170                 authenticate(method);
    171                 executeWithRetry(method);
    172                 if (this.connectMethod != null) {
    173                     fakeResponse(method);
    174                     break;
    175                 }
    176
    177                 boolean retry = false;
    178                 if (isRedirectNeeded(method)) {
    179                     if (processRedirectResponse(method)) {
    180                         retry = true;
    181                         ++redirectCount;
    182                         if (redirectCount >= maxRedirects) {
    183                             LOG.error("Narrowly avoided an infinite loop in execute");
    184                             throw new RedirectException("Maximum redirects ("
    185                                 + maxRedirects + ") exceeded");
    186                         }
    187                         if (LOG.isDebugEnabled()) {
    188                             LOG.debug("Execute redirect " + redirectCount + " of " + maxRedirects);
    189                         }
    190                     }
    191                 }
    192                 if (isAuthenticationNeeded(method)) {
    193                     if (processAuthenticationResponse(method)) {
    194                         LOG.debug("Retry authentication");
    195                         retry = true;
    196                     }
    197                 }
    198                 if (!retry) {
    199                     break;
    200                 }
    201                 // retry - close previous stream.  Caution - this causes
    202                 // responseBodyConsumed to be called, which may also close the
    203                 // connection.
    204                 if (method.getResponseBodyAsStream() != null) {
    205                     method.getResponseBodyAsStream().close();
    206                 }
    207
    208             } //end of retry loop
    209         } finally {
    210             if (this.conn != null) {
    211                 this.conn.setLocked(false);
    212             }
    213             // If the response has been fully processed, return the connection
    214             // to the pool.  Use this flag, rather than other tests (like
    215             // responseStream == null), as subclasses, might reset the stream,
    216             // for example, reading the entire response into a file and then
    217             // setting the file as the stream.
    218             if (
    219                 (releaseConnection || method.getResponseBodyAsStream() == null)
    220                 && this.conn != null
    221             ) {
    222                 this.conn.releaseConnection();
    223             }
    224         }
    225
    226     }
    227
    228
    229     private void authenticate(final HttpMethod method) {
    230         try {
    231             if (this.conn.isProxied() && !this.conn.isSecure()) {
    232                 authenticateProxy(method);
    233             }
    234             authenticateHost(method);
    235         } catch (AuthenticationException e) {
    236             LOG.error(e.getMessage(), e);
    237         }
    238     }
    239
    240
    241     private boolean cleanAuthHeaders(final HttpMethod method, final String name) {
    242         Header[] authheaders = method.getRequestHeaders(name);
    243         boolean clean = true;
    244         for (int i = 0; i < authheaders.length; i++) {
    245             Header authheader = authheaders[i];
    246             if (authheader.isAutogenerated()) {
    247                 method.removeRequestHeader(authheader);
    248             } else {
    249                 clean = false;
    250             }
    251         }
    252         return clean;
    253     }
    254
    255
    256     private void authenticateHost(final HttpMethod method) throws AuthenticationException {
    257         // Clean up existing authentication headers
    258         if (!cleanAuthHeaders(method, WWW_AUTH_RESP)) {
    259             // User defined authentication header(s) present
    260             return;
    261         }
    262         AuthState authstate = method.getHostAuthState();
    263         AuthScheme authscheme = authstate.getAuthScheme();
    264         if (authscheme == null) {
    265             return;
    266         }
    267         if (authstate.isAuthRequested() || !authscheme.isConnectionBased()) {
    268             String host = method.getParams().getVirtualHost();
    269             if (host == null) {
    270                 host = conn.getHost();
    271             }
    272             int port = conn.getPort();
    273             AuthScope authscope = new AuthScope(
    274                 host, port,
    275                 authscheme.getRealm(),
    276                 authscheme.getSchemeName());
    277             if (LOG.isDebugEnabled()) {
    278                 LOG.debug("Authenticating with " + authscope);
    279             }
    280             Credentials credentials = this.state.getCredentials(authscope);
    281             if (credentials != null) {
    282                 String authstring = authscheme.authenticate(credentials, method);
    283                 if (authstring != null) {
    284                     method.addRequestHeader(new Header(WWW_AUTH_RESP, authstring, true));
    285                 }
    286             } else {
    287                 if (LOG.isWarnEnabled()) {
    288                     LOG.warn("Required credentials not available for " + authscope);
    289                     if (method.getHostAuthState().isPreemptive()) {
    290                         LOG.warn("Preemptive authentication requested but no default " +
    291                             "credentials available");
    292                     }
    293                 }
    294             }
    295         }
    296     }
    297
    298
    299     private void authenticateProxy(final HttpMethod method) throws AuthenticationException {
    300         // Clean up existing authentication headers
    301         if (!cleanAuthHeaders(method, PROXY_AUTH_RESP)) {
    302             // User defined authentication header(s) present
    303             return;
    304         }
    305         AuthState authstate = method.getProxyAuthState();
    306         AuthScheme authscheme = authstate.getAuthScheme();
    307         if (authscheme == null) {
    308             return;
    309         }
    310         if (authstate.isAuthRequested() || !authscheme.isConnectionBased()) {
    311             AuthScope authscope = new AuthScope(
    312                 conn.getProxyHost(), conn.getProxyPort(),
    313                 authscheme.getRealm(),
    314                 authscheme.getSchemeName());
    315             if (LOG.isDebugEnabled()) {
    316                 LOG.debug("Authenticating with " + authscope);
    317             }
    318             Credentials credentials = this.state.getProxyCredentials(authscope);
    319             if (credentials != null) {
    320                 String authstring = authscheme.authenticate(credentials, method);
    321                 if (authstring != null) {
    322                     method.addRequestHeader(new Header(PROXY_AUTH_RESP, authstring, true));
    323                 }
    324             } else {
    325                 if (LOG.isWarnEnabled()) {
    326                     LOG.warn("Required proxy credentials not available for " + authscope);
    327                     if (method.getProxyAuthState().isPreemptive()) {
    328                         LOG.warn("Preemptive authentication requested but no default " +
    329                             "proxy credentials available");
    330                     }
    331                 }
    332             }
    333         }
    334     }
    335
    336
    337     /**
    338      * Applies connection parameters specified for a given method
    339      *
    340      * @param method HTTP method
    341      *
    342      * @throws IOException if an I/O occurs setting connection parameters
    343      */
    344     private void applyConnectionParams(final HttpMethod method) throws IOException {
    345         int timeout = 0;
    346         // see if a timeout is given for this method
    347         Object param = method.getParams().getParameter(HttpMethodParams.SO_TIMEOUT);
    348         if (param == null) {
    349             // if not, use the default value
    350             param = this.conn.getParams().getParameter(HttpConnectionParams.SO_TIMEOUT);
    351         }
    352         if (param != null) {
    353             timeout = ((Integer)param).intValue();
    354         }
    355         this.conn.setSocketTimeout(timeout);
    356     }
    357
    358     /**
    359      * Executes a method with the current hostConfiguration.
    360      *
    361      * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
    362      * can be recovered from.
    363      * @throws HttpException  if a protocol exception occurs. Usually protocol exceptions
    364      * cannot be recovered from.
    365      */
    366     private void executeWithRetry(final HttpMethod method)
    367         throws IOException, HttpException {
    368
    369         /** How many times did this transparently handle a recoverable exception? */
    370         int execCount = 0;
    371         // loop until the method is successfully processed, the retryHandler
    372         // returns false or a non-recoverable exception is thrown
    373         try {
    374             while (true) {
    375                 execCount++;
    376                 try {
    377
    378                     if (LOG.isTraceEnabled()) {
    379                         LOG.trace("Attempt number " + execCount + " to process request");
    380                     }
    381                     if (this.conn.getParams().isStaleCheckingEnabled()) {
    382                         this.conn.closeIfStale();
    383                     }
    384                     if (!this.conn.isOpen()) {
    385                         // this connection must be opened before it can be used
    386                         // This has nothing to do with opening a secure tunnel
    387                         this.conn.open();
    388                         if (this.conn.isProxied() && this.conn.isSecure()
    389                         && !(method instanceof ConnectMethod)) {
    390                             // we need to create a secure tunnel before we can execute the real method
    391                             if (!executeConnect()) {
    392                                 // abort, the connect method failed
    393                                 return;
    394                             }
    395                         }
    396                     }
    397                     applyConnectionParams(method);
    398                     method.execute(state, this.conn);
    399                     break;
    400                 } catch (HttpException e) {
    401                     // filter out protocol exceptions which cannot be recovered from
    402                     throw e;
    403                 } catch (IOException e) {
    404                     LOG.debug("Closing the connection.");
    405                     this.conn.close();
    406                     // test if this method should be retried
    407                     // ========================================
    408                     // this code is provided for backward compatibility with 2.0
    409                     // will be removed in the next major release
    410                     if (method instanceof HttpMethodBase) {
    411                         MethodRetryHandler handler =
    412                             ((HttpMethodBase)method).getMethodRetryHandler();
    413                         if (handler != null) {
    414                             if (!handler.retryMethod(
    415                                     method,
    416                                     this.conn,
    417                                     new HttpRecoverableException(e.getMessage()),
    418                                     execCount,
    419                                     method.isRequestSent())) {
    420                                 LOG.debug("Method retry handler returned false. "
    421                                         + "Automatic recovery will not be attempted");
    422                                 throw e;
    423                             }
    424                         }
    425                     }
    426                     // ========================================
    427                     HttpMethodRetryHandler handler =
    428                         (HttpMethodRetryHandler)method.getParams().getParameter(
    429                                 HttpMethodParams.RETRY_HANDLER);
    430                     if (handler == null) {
    431                         handler = new DefaultHttpMethodRetryHandler();
    432                     }
    433                     if (!handler.retryMethod(method, e, execCount)) {
    434                         LOG.debug("Method retry handler returned false. "
    435                                 + "Automatic recovery will not be attempted");
    436                         throw e;
    437                     }
    438                     if (LOG.isInfoEnabled()) {
    439                         LOG.info("I/O exception ("+ e.getClass().getName() +") caught when processing request: "
    440                                 + e.getMessage());
    441                     }
    442                     if (LOG.isDebugEnabled()) {
    443                         LOG.debug(e.getMessage(), e);
    444                     }
    445                     LOG.info("Retrying request");
    446                 }
    447             }
    448         } catch (IOException e) {
    449             if (this.conn.isOpen()) {
    450                 LOG.debug("Closing the connection.");
    451                 this.conn.close();
    452             }
    453             releaseConnection = true;
    454             throw e;
    455         } catch (RuntimeException e) {
    456             if (this.conn.isOpen()) {
    457                 LOG.debug("Closing the connection.");
    458                 this.conn.close();
    459             }
    460             releaseConnection = true;
    461             throw e;
    462         }
    463     }
    464
    465     /**
    466      * Executes a ConnectMethod to establish a tunneled connection.
    467      *
    468      * @return <code>true</code> if the connect was successful
    469      *
    470      * @throws IOException
    471      * @throws HttpException
    472      */
    473     private boolean executeConnect()
    474         throws IOException, HttpException {
    475
    476         this.connectMethod = new ConnectMethod(this.hostConfiguration);
    477         this.connectMethod.getParams().setDefaults(this.hostConfiguration.getParams());
    478
    479         int code;
    480         for (;;) {
    481             if (!this.conn.isOpen()) {
    482                 this.conn.open();
    483             }
    484             if (this.params.isAuthenticationPreemptive()
    485                     || this.state.isAuthenticationPreemptive()) {
    486                 LOG.debug("Preemptively sending default basic credentials");
    487                 this.connectMethod.getProxyAuthState().setPreemptive();
    488                 this.connectMethod.getProxyAuthState().setAuthAttempted(true);
    489             }
    490             try {
    491                 authenticateProxy(this.connectMethod);
    492             } catch (AuthenticationException e) {
    493                 LOG.error(e.getMessage(), e);
    494             }
    495             applyConnectionParams(this.connectMethod);
    496             this.connectMethod.execute(state, this.conn);
    497             code = this.connectMethod.getStatusCode();
    498             boolean retry = false;
    499             AuthState authstate = this.connectMethod.getProxyAuthState();
    500             authstate.setAuthRequested(code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
    501             if (authstate.isAuthRequested()) {
    502                 if (processAuthenticationResponse(this.connectMethod)) {
    503                     retry = true;
    504                 }
    505             }
    506             if (!retry) {
    507                 break;
    508             }
    509             if (this.connectMethod.getResponseBodyAsStream() != null) {
    510                 this.connectMethod.getResponseBodyAsStream().close();
    511             }
    512         }
    513         if ((code >= 200) && (code < 300)) {
    514             this.conn.tunnelCreated();
    515             // Drop the connect method, as it is no longer needed
    516             this.connectMethod = null;
    517             return true;
    518         } else {
    519             this.conn.close();
    520             return false;
    521         }
    522     }
    523
    524     /**
    525      * Fake response
    526      * @param method
    527      * @return
    528      */
    529
    530     private void fakeResponse(final HttpMethod method)
    531         throws IOException, HttpException {
    532         // What is to follow is an ugly hack.
    533         // I REALLY hate having to resort to such
    534         // an appalling trick
    535         // The only feasible solution is to split monolithic
    536         // HttpMethod into HttpRequest/HttpResponse pair.
    537         // That would allow to execute CONNECT method
    538         // behind the scene and return CONNECT HttpResponse
    539         // object in response to the original request that
    540         // contains the correct status line, headers &
    541         // response body.
    542         LOG.debug("CONNECT failed, fake the response for the original method");
    543         // Pass the status, headers and response stream to the wrapped
    544         // method.
    545         // To ensure that the connection is not released more than once
    546         // this method is still responsible for releasing the connection.
    547         // This will happen when the response body is consumed, or when
    548         // the wrapped method closes the response connection in
    549         // releaseConnection().
    550         if (method instanceof HttpMethodBase) {
    551             ((HttpMethodBase) method).fakeResponse(
    552                 this.connectMethod.getStatusLine(),
    553                 this.connectMethod.getResponseHeaderGroup(),
    554                 this.connectMethod.getResponseBodyAsStream()
    555             );
    556             method.getProxyAuthState().setAuthScheme(
    557                 this.connectMethod.getProxyAuthState().getAuthScheme());
    558             this.connectMethod = null;
    559         } else {
    560             releaseConnection = true;
    561             LOG.warn(
    562                 "Unable to fake response on method as it is not derived from HttpMethodBase.");
    563         }
    564     }
    565
    566 	/**
    567 	 * Process the redirect response.
    568      *
    569 	 * @return <code>true</code> if the redirect was successful
    570 	 */
    571 	private boolean processRedirectResponse(final HttpMethod method)
    572      throws RedirectException {
    573 		//get the location header to find out where to redirect to
    574 		Header locationHeader = method.getResponseHeader("location");
    575 		if (locationHeader == null) {
    576 			// got a redirect response, but no location header
    577 			LOG.error("Received redirect response " + method.getStatusCode()
    578 					+ " but no location header");
    579 			return false;
    580 		}
    581 		String location = locationHeader.getValue();
    582 		if (LOG.isDebugEnabled()) {
    583 			LOG.debug("Redirect requested to location '" + location + "'");
    584 		}
    585
    586 		//rfc2616 demands the location value be a complete URI
    587 		//Location       = "Location" ":" absoluteURI
    588 		URI redirectUri = null;
    589 		URI currentUri = null;
    590
    591 		try {
    592 			currentUri = new URI(
    593 				this.conn.getProtocol().getScheme(),
    594 				null,
    595                 this.conn.getHost(),
    596                 this.conn.getPort(),
    597 				method.getPath()
    598 			);
    599
    600             String charset = method.getParams().getUriCharset();
    601             redirectUri = new URI(location, true, charset);
    602
    603             if (redirectUri.isRelativeURI()) {
    604 				if (this.params.isParameterTrue(HttpClientParams.REJECT_RELATIVE_REDIRECT)) {
    605 					LOG.warn("Relative redirect location '" + location + "' not allowed");
    606 					return false;
    607 				} else {
    608 					//location is incomplete, use current values for defaults
    609 					LOG.debug("Redirect URI is not absolute - parsing as relative");
    610 					redirectUri = new URI(currentUri, redirectUri);
    611 				}
    612 			} else {
    613                 // Reset the default params
    614                 method.getParams().setDefaults(this.params);
    615             }
    616             method.setURI(redirectUri);
    617             hostConfiguration.setHost(redirectUri);
    618 		} catch (URIException ex) {
    619             throw new InvalidRedirectLocationException(
    620                     "Invalid redirect location: " + location, location, ex);
    621 		}
    622
    623         if (this.params.isParameterFalse(HttpClientParams.ALLOW_CIRCULAR_REDIRECTS)) {
    624             if (this.redirectLocations == null) {
    625                 this.redirectLocations = new HashSet();
    626             }
    627             this.redirectLocations.add(currentUri);
    628             try {
    629                 if(redirectUri.hasQuery()) {
    630                     redirectUri.setQuery(null);
    631                 }
    632             } catch (URIException e) {
    633                 // Should never happen
    634                 return false;
    635             }
    636
    637             if (this.redirectLocations.contains(redirectUri)) {
    638                 throw new CircularRedirectException("Circular redirect to '" +
    639                     redirectUri + "'");
    640             }
    641         }
    642
    643 		if (LOG.isDebugEnabled()) {
    644 			LOG.debug("Redirecting from '" + currentUri.getEscapedURI()
    645 				+ "' to '" + redirectUri.getEscapedURI());
    646 		}
    647         //And finally invalidate the actual authentication scheme
    648         method.getHostAuthState().invalidate();
    649 		return true;
    650 	}
    651
    652 	/**
    653 	 * Processes a response that requires authentication
    654 	 *
    655 	 * @param method the current {@link HttpMethod HTTP method}
    656 	 *
    657 	 * @return <tt>true</tt> if the authentication challenge can be responsed to,
    658      *   (that is, at least one of the requested authentication scheme is supported,
    659      *   and matching credentials have been found), <tt>false</tt> otherwise.
    660 	 */
    661 	private boolean processAuthenticationResponse(final HttpMethod method) {
    662 		LOG.trace("enter HttpMethodBase.processAuthenticationResponse("
    663 			+ "HttpState, HttpConnection)");
    664
    665 		try {
    666             switch (method.getStatusCode()) {
    667                 case HttpStatus.SC_UNAUTHORIZED:
    668                     return processWWWAuthChallenge(method);
    669                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
    670                     return processProxyAuthChallenge(method);
    671                 default:
    672                     return false;
    673             }
    674         } catch (Exception e) {
    675             if (LOG.isErrorEnabled()) {
    676                 LOG.error(e.getMessage(), e);
    677             }
    678             return false;
    679         }
    680 	}
    681
    682     private boolean processWWWAuthChallenge(final HttpMethod method)
    683         throws MalformedChallengeException, AuthenticationException
    684     {
    685         AuthState authstate = method.getHostAuthState();
    686         Map challenges = AuthChallengeParser.parseChallenges(
    687             method.getResponseHeaders(WWW_AUTH_CHALLENGE));
    688         if (challenges.isEmpty()) {
    689             LOG.debug("Authentication challenge(s) not found");
    690             return false;
    691         }
    692         AuthScheme authscheme = null;
    693         try {
    694             authscheme = this.authProcessor.processChallenge(authstate, challenges);
    695         } catch (AuthChallengeException e) {
    696             if (LOG.isWarnEnabled()) {
    697                 LOG.warn(e.getMessage());
    698             }
    699         }
    700         if (authscheme == null) {
    701             return false;
    702         }
    703         String host = method.getParams().getVirtualHost();
    704         if (host == null) {
    705             host = conn.getHost();
    706         }
    707         int port = conn.getPort();
    708         AuthScope authscope = new AuthScope(
    709             host, port,
    710             authscheme.getRealm(),
    711             authscheme.getSchemeName());
    712
    713         if (LOG.isDebugEnabled()) {
    714             LOG.debug("Authentication scope: " + authscope);
    715         }
    716         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
    717             // Already tried and failed
    718             Credentials credentials = promptForCredentials(
    719                 authscheme, method.getParams(), authscope);
    720             if (credentials == null) {
    721                 if (LOG.isInfoEnabled()) {
    722                     LOG.info("Failure authenticating with " + authscope);
    723                 }
    724                 return false;
    725             } else {
    726                 return true;
    727             }
    728         } else {
    729             authstate.setAuthAttempted(true);
    730             Credentials credentials = this.state.getCredentials(authscope);
    731             if (credentials == null) {
    732                 credentials = promptForCredentials(
    733                     authscheme, method.getParams(), authscope);
    734             }
    735             if (credentials == null) {
    736                 if (LOG.isInfoEnabled()) {
    737                     LOG.info("No credentials available for " + authscope);
    738                 }
    739                 return false;
    740             } else {
    741                 return true;
    742             }
    743         }
    744     }
    745
    746     private boolean processProxyAuthChallenge(final HttpMethod method)
    747         throws MalformedChallengeException, AuthenticationException
    748     {
    749         AuthState authstate = method.getProxyAuthState();
    750         Map proxyChallenges = AuthChallengeParser.parseChallenges(
    751             method.getResponseHeaders(PROXY_AUTH_CHALLENGE));
    752         if (proxyChallenges.isEmpty()) {
    753             LOG.debug("Proxy authentication challenge(s) not found");
    754             return false;
    755         }
    756         AuthScheme authscheme = null;
    757         try {
    758             authscheme = this.authProcessor.processChallenge(authstate, proxyChallenges);
    759         } catch (AuthChallengeException e) {
    760             if (LOG.isWarnEnabled()) {
    761                 LOG.warn(e.getMessage());
    762             }
    763         }
    764         if (authscheme == null) {
    765             return false;
    766         }
    767         AuthScope authscope = new AuthScope(
    768             conn.getProxyHost(), conn.getProxyPort(),
    769             authscheme.getRealm(),
    770             authscheme.getSchemeName());
    771
    772         if (LOG.isDebugEnabled()) {
    773             LOG.debug("Proxy authentication scope: " + authscope);
    774         }
    775         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
    776             // Already tried and failed
    777             Credentials credentials = promptForProxyCredentials(
    778                 authscheme, method.getParams(), authscope);
    779             if (credentials == null) {
    780                 if (LOG.isInfoEnabled()) {
    781                     LOG.info("Failure authenticating with " + authscope);
    782                 }
    783                 return false;
    784             } else {
    785                 return true;
    786             }
    787         } else {
    788             authstate.setAuthAttempted(true);
    789             Credentials credentials = this.state.getProxyCredentials(authscope);
    790             if (credentials == null) {
    791                 credentials = promptForProxyCredentials(
    792                     authscheme, method.getParams(), authscope);
    793             }
    794             if (credentials == null) {
    795                 if (LOG.isInfoEnabled()) {
    796                     LOG.info("No credentials available for " + authscope);
    797                 }
    798                 return false;
    799             } else {
    800                 return true;
    801             }
    802         }
    803     }
    804
    805     /**
    806      * Tests if the {@link HttpMethod method} requires a redirect to another location.
    807      *
    808      * @param method HTTP method
    809      *
    810      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
    811      */
    812 	private boolean isRedirectNeeded(final HttpMethod method) {
    813 		switch (method.getStatusCode()) {
    814 			case HttpStatus.SC_MOVED_TEMPORARILY:
    815 			case HttpStatus.SC_MOVED_PERMANENTLY:
    816 			case HttpStatus.SC_SEE_OTHER:
    817 			case HttpStatus.SC_TEMPORARY_REDIRECT:
    818 				LOG.debug("Redirect required");
    819                 if (method.getFollowRedirects()) {
    820                     return true;
    821                 } else {
    822                     return false;
    823                 }
    824 			default:
    825 				return false;
    826 		} //end of switch
    827 	}
    828
    829     /**
    830      * Tests if the {@link HttpMethod method} requires authentication.
    831      *
    832      * @param method HTTP method
    833      *
    834      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
    835      */
    836     private boolean isAuthenticationNeeded(final HttpMethod method) {
    837         method.getHostAuthState().setAuthRequested(
    838                 method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED);
    839         method.getProxyAuthState().setAuthRequested(
    840                 method.getStatusCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
    841         if (method.getHostAuthState().isAuthRequested() ||
    842             method.getProxyAuthState().isAuthRequested()) {
    843             LOG.debug("Authorization required");
    844             if (method.getDoAuthentication()) { //process authentication response
    845                 return true;
    846             } else { //let the client handle the authenticaiton
    847                 LOG.info("Authentication requested but doAuthentication is "
    848                         + "disabled");
    849                 return false;
    850             }
    851         } else {
    852             return false;
    853         }
    854     }
    855
    856     private Credentials promptForCredentials(
    857         final AuthScheme authScheme,
    858         final HttpParams params,
    859         final AuthScope authscope)
    860     {
    861         LOG.debug("Credentials required");
    862         Credentials creds = null;
    863         CredentialsProvider credProvider =
    864             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
    865         if (credProvider != null) {
    866             try {
    867                 creds = credProvider.getCredentials(
    868                     authScheme, authscope.getHost(), authscope.getPort(), false);
    869             } catch (CredentialsNotAvailableException e) {
    870                 LOG.warn(e.getMessage());
    871             }
    872             if (creds != null) {
    873                 this.state.setCredentials(authscope, creds);
    874                 if (LOG.isDebugEnabled()) {
    875                     LOG.debug(authscope + " new credentials given");
    876                 }
    877             }
    878         } else {
    879             LOG.debug("Credentials provider not available");
    880         }
    881         return creds;
    882     }
    883
    884     private Credentials promptForProxyCredentials(
    885         final AuthScheme authScheme,
    886         final HttpParams params,
    887         final AuthScope authscope)
    888     {
    889         LOG.debug("Proxy credentials required");
    890         Credentials creds = null;
    891         CredentialsProvider credProvider =
    892             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
    893         if (credProvider != null) {
    894             try {
    895                 creds = credProvider.getCredentials(
    896                     authScheme, authscope.getHost(), authscope.getPort(), true);
    897             } catch (CredentialsNotAvailableException e) {
    898                 LOG.warn(e.getMessage());
    899             }
    900             if (creds != null) {
    901                 this.state.setProxyCredentials(authscope, creds);
    902                 if (LOG.isDebugEnabled()) {
    903                     LOG.debug(authscope + " new credentials given");
    904                 }
    905             }
    906         } else {
    907             LOG.debug("Proxy credentials provider not available");
    908         }
    909         return creds;
    910     }
    911
    912     /**
    913      * @return
    914      */
    915     public HostConfiguration getHostConfiguration() {
    916         return hostConfiguration;
    917     }
    918
    919     /**
    920      * @return
    921      */
    922     public HttpState getState() {
    923         return state;
    924     }
    925
    926     /**
    927      * @return
    928      */
    929     public HttpConnectionManager getConnectionManager() {
    930         return connectionManager;
    931     }
    932
    933     /**
    934      * @return
    935      */
    936     public HttpParams getParams() {
    937         return this.params;
    938     }
    939 }