/ org.apache.commons.httpclient / src / org / apache / commons / httpclient / HttpMethodDirector.java
HttpMethodDirector.java
  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  }