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