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: 413281 $
4    * $Date: 2006-06-10 12:36:20 +0200 (Sat, 10 Jun 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(this.hostConfiguration);
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             this.conn.close();
519             return false;
520         }
521     }
522 
523     /***
524      * Fake response
525      * @param method
526      * @return
527      */
528     
529     private void fakeResponse(final HttpMethod method)
530         throws IOException, HttpException {
531         // What is to follow is an ugly hack.
532         // I REALLY hate having to resort to such
533         // an appalling trick
534         // The only feasible solution is to split monolithic
535         // HttpMethod into HttpRequest/HttpResponse pair.
536         // That would allow to execute CONNECT method 
537         // behind the scene and return CONNECT HttpResponse 
538         // object in response to the original request that 
539         // contains the correct status line, headers & 
540         // response body.
541         LOG.debug("CONNECT failed, fake the response for the original method");
542         // Pass the status, headers and response stream to the wrapped
543         // method.
544         // To ensure that the connection is not released more than once
545         // this method is still responsible for releasing the connection. 
546         // This will happen when the response body is consumed, or when
547         // the wrapped method closes the response connection in 
548         // releaseConnection().
549         if (method instanceof HttpMethodBase) {
550             ((HttpMethodBase) method).fakeResponse(
551                 this.connectMethod.getStatusLine(),
552                 this.connectMethod.getResponseHeaderGroup(),
553                 this.connectMethod.getResponseBodyAsStream()
554             );
555             method.getProxyAuthState().setAuthScheme(
556                 this.connectMethod.getProxyAuthState().getAuthScheme());
557             this.connectMethod = null;
558         } else {
559             releaseConnection = true;
560             LOG.warn(
561                 "Unable to fake response on method as it is not derived from HttpMethodBase.");
562         }
563     }
564     
565 	/***
566 	 * Process the redirect response.
567      * 
568 	 * @return <code>true</code> if the redirect was successful
569 	 */
570 	private boolean processRedirectResponse(final HttpMethod method)
571      throws RedirectException {
572 		//get the location header to find out where to redirect to
573 		Header locationHeader = method.getResponseHeader("location");
574 		if (locationHeader == null) {
575 			// got a redirect response, but no location header
576 			LOG.error("Received redirect response " + method.getStatusCode()
577 					+ " but no location header");
578 			return false;
579 		}
580 		String location = locationHeader.getValue();
581 		if (LOG.isDebugEnabled()) {
582 			LOG.debug("Redirect requested to location '" + location + "'");
583 		}
584         
585 		//rfc2616 demands the location value be a complete URI
586 		//Location       = "Location" ":" absoluteURI
587 		URI redirectUri = null;
588 		URI currentUri = null;
589 
590 		try {
591 			currentUri = new URI(
592 				this.conn.getProtocol().getScheme(),
593 				null,
594                 this.conn.getHost(), 
595                 this.conn.getPort(), 
596 				method.getPath()
597 			);
598 			redirectUri = new URI(location, true);
599 			if (redirectUri.isRelativeURI()) {
600 				if (this.params.isParameterTrue(HttpClientParams.REJECT_RELATIVE_REDIRECT)) {
601 					LOG.warn("Relative redirect location '" + location + "' not allowed");
602 					return false;
603 				} else { 
604 					//location is incomplete, use current values for defaults
605 					LOG.debug("Redirect URI is not absolute - parsing as relative");
606 					redirectUri = new URI(currentUri, redirectUri);
607 				}
608 			} else {
609                 // Reset the default params
610                 method.getParams().setDefaults(this.params);
611             }
612             method.setURI(redirectUri);
613             hostConfiguration.setHost(redirectUri);
614 		} catch (URIException ex) {
615             throw new InvalidRedirectLocationException(
616                     "Invalid redirect location: " + location, location, ex);
617 		}
618 
619         if (this.params.isParameterFalse(HttpClientParams.ALLOW_CIRCULAR_REDIRECTS)) {
620             if (this.redirectLocations == null) {
621                 this.redirectLocations = new HashSet();
622             }
623             this.redirectLocations.add(currentUri);
624             try {
625                 if(redirectUri.hasQuery()) {
626                     redirectUri.setQuery(null);
627                 }
628             } catch (URIException e) {
629                 // Should never happen
630                 return false;
631             }
632 
633             if (this.redirectLocations.contains(redirectUri)) {
634                 throw new CircularRedirectException("Circular redirect to '" +
635                     redirectUri + "'");
636             }
637         }
638 
639 		if (LOG.isDebugEnabled()) {
640 			LOG.debug("Redirecting from '" + currentUri.getEscapedURI()
641 				+ "' to '" + redirectUri.getEscapedURI());
642 		}
643         //And finally invalidate the actual authentication scheme
644         method.getHostAuthState().invalidate(); 
645 		return true;
646 	}
647 
648 	/***
649 	 * Processes a response that requires authentication
650 	 *
651 	 * @param method the current {@link HttpMethod HTTP method}
652 	 *
653 	 * @return <tt>true</tt> if the authentication challenge can be responsed to,
654      *   (that is, at least one of the requested authentication scheme is supported, 
655      *   and matching credentials have been found), <tt>false</tt> otherwise.
656 	 */
657 	private boolean processAuthenticationResponse(final HttpMethod method) {
658 		LOG.trace("enter HttpMethodBase.processAuthenticationResponse("
659 			+ "HttpState, HttpConnection)");
660 
661 		try {
662             switch (method.getStatusCode()) {
663                 case HttpStatus.SC_UNAUTHORIZED:
664                     return processWWWAuthChallenge(method);
665                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
666                     return processProxyAuthChallenge(method);
667                 default:
668                     return false;
669             }
670         } catch (Exception e) {
671             if (LOG.isErrorEnabled()) {
672                 LOG.error(e.getMessage(), e);
673             }
674             return false;
675         }
676 	}
677 
678     private boolean processWWWAuthChallenge(final HttpMethod method)
679         throws MalformedChallengeException, AuthenticationException  
680     {
681         AuthState authstate = method.getHostAuthState();
682         Map challenges = AuthChallengeParser.parseChallenges(
683             method.getResponseHeaders(WWW_AUTH_CHALLENGE));
684         if (challenges.isEmpty()) {
685             LOG.debug("Authentication challenge(s) not found");
686             return false; 
687         }
688         AuthScheme authscheme = null;
689         try {
690             authscheme = this.authProcessor.processChallenge(authstate, challenges);
691         } catch (AuthChallengeException e) {
692             if (LOG.isWarnEnabled()) {
693                 LOG.warn(e.getMessage());
694             }
695         }
696         if (authscheme == null) {
697             return false;
698         }
699         String host = method.getParams().getVirtualHost();
700         if (host == null) {
701             host = conn.getHost();
702         }
703         int port = conn.getPort();
704         AuthScope authscope = new AuthScope(
705             host, port, 
706             authscheme.getRealm(), 
707             authscheme.getSchemeName());
708         
709         if (LOG.isDebugEnabled()) {
710             LOG.debug("Authentication scope: " + authscope);
711         }
712         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
713             // Already tried and failed
714             Credentials credentials = promptForCredentials(
715                 authscheme, method.getParams(), authscope);
716             if (credentials == null) {
717                 if (LOG.isInfoEnabled()) {
718                     LOG.info("Failure authenticating with " + authscope);
719                 }
720                 return false;
721             } else {
722                 return true;
723             }
724         } else {
725             authstate.setAuthAttempted(true);
726             Credentials credentials = this.state.getCredentials(authscope);
727             if (credentials == null) {
728                 credentials = promptForCredentials(
729                     authscheme, method.getParams(), authscope);
730             }
731             if (credentials == null) {
732                 if (LOG.isInfoEnabled()) {
733                     LOG.info("No credentials available for " + authscope); 
734                 }
735                 return false;
736             } else {
737                 return true;
738             }
739         }
740     }
741 
742     private boolean processProxyAuthChallenge(final HttpMethod method)
743         throws MalformedChallengeException, AuthenticationException
744     {  
745         AuthState authstate = method.getProxyAuthState();
746         Map proxyChallenges = AuthChallengeParser.parseChallenges(
747             method.getResponseHeaders(PROXY_AUTH_CHALLENGE));
748         if (proxyChallenges.isEmpty()) {
749             LOG.debug("Proxy authentication challenge(s) not found");
750             return false; 
751         }
752         AuthScheme authscheme = null;
753         try {
754             authscheme = this.authProcessor.processChallenge(authstate, proxyChallenges);
755         } catch (AuthChallengeException e) {
756             if (LOG.isWarnEnabled()) {
757                 LOG.warn(e.getMessage());
758             }
759         }
760         if (authscheme == null) {
761             return false;
762         }
763         AuthScope authscope = new AuthScope(
764             conn.getProxyHost(), conn.getProxyPort(), 
765             authscheme.getRealm(), 
766             authscheme.getSchemeName());  
767 
768         if (LOG.isDebugEnabled()) {
769             LOG.debug("Proxy authentication scope: " + authscope);
770         }
771         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
772             // Already tried and failed
773             Credentials credentials = promptForProxyCredentials(
774                 authscheme, method.getParams(), authscope);
775             if (credentials == null) {
776                 if (LOG.isInfoEnabled()) {
777                     LOG.info("Failure authenticating with " + authscope);
778                 }
779                 return false;
780             } else {
781                 return true;
782             }
783         } else {
784             authstate.setAuthAttempted(true);
785             Credentials credentials = this.state.getProxyCredentials(authscope);
786             if (credentials == null) {
787                 credentials = promptForProxyCredentials(
788                     authscheme, method.getParams(), authscope);
789             }
790             if (credentials == null) {
791                 if (LOG.isInfoEnabled()) {
792                     LOG.info("No credentials available for " + authscope); 
793                 }
794                 return false;
795             } else {
796                 return true;
797             }
798         }
799     }
800 
801     /***
802      * Tests if the {@link HttpMethod method} requires a redirect to another location.
803      * 
804      * @param method HTTP method
805      * 
806      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
807      */
808 	private boolean isRedirectNeeded(final HttpMethod method) {
809 		switch (method.getStatusCode()) {
810 			case HttpStatus.SC_MOVED_TEMPORARILY:
811 			case HttpStatus.SC_MOVED_PERMANENTLY:
812 			case HttpStatus.SC_SEE_OTHER:
813 			case HttpStatus.SC_TEMPORARY_REDIRECT:
814 				LOG.debug("Redirect required");
815                 if (method.getFollowRedirects()) {
816                     return true;
817                 } else {
818                     return false;
819                 }
820 			default:
821 				return false;
822 		} //end of switch
823 	}
824 
825     /***
826      * Tests if the {@link HttpMethod method} requires authentication.
827      * 
828      * @param method HTTP method
829      * 
830      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
831      */
832     private boolean isAuthenticationNeeded(final HttpMethod method) {
833         method.getHostAuthState().setAuthRequested(
834                 method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED);
835         method.getProxyAuthState().setAuthRequested(
836                 method.getStatusCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
837         if (method.getHostAuthState().isAuthRequested() || 
838             method.getProxyAuthState().isAuthRequested()) {
839             LOG.debug("Authorization required");
840             if (method.getDoAuthentication()) { //process authentication response
841                 return true;
842             } else { //let the client handle the authenticaiton
843                 LOG.info("Authentication requested but doAuthentication is "
844                         + "disabled");
845                 return false;
846             }
847         } else {
848             return false;
849         }
850     }
851 
852     private Credentials promptForCredentials(
853         final AuthScheme authScheme,
854         final HttpParams params, 
855         final AuthScope authscope)
856     {
857         LOG.debug("Credentials required");
858         Credentials creds = null;
859         CredentialsProvider credProvider = 
860             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
861         if (credProvider != null) {
862             try {
863                 creds = credProvider.getCredentials(
864                     authScheme, authscope.getHost(), authscope.getPort(), false);
865             } catch (CredentialsNotAvailableException e) {
866                 LOG.warn(e.getMessage());
867             }
868             if (creds != null) {
869                 this.state.setCredentials(authscope, creds);
870                 if (LOG.isDebugEnabled()) {
871                     LOG.debug(authscope + " new credentials given");
872                 }
873             }
874         } else {
875             LOG.debug("Credentials provider not available");
876         }
877         return creds;
878     }
879 
880     private Credentials promptForProxyCredentials(
881         final AuthScheme authScheme,
882         final HttpParams params,
883         final AuthScope authscope) 
884     {
885         LOG.debug("Proxy credentials required");
886         Credentials creds = null;
887         CredentialsProvider credProvider = 
888             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
889         if (credProvider != null) {
890             try {
891                 creds = credProvider.getCredentials(
892                     authScheme, authscope.getHost(), authscope.getPort(), true);
893             } catch (CredentialsNotAvailableException e) {
894                 LOG.warn(e.getMessage());
895             }
896             if (creds != null) {
897                 this.state.setProxyCredentials(authscope, creds);
898                 if (LOG.isDebugEnabled()) {
899                     LOG.debug(authscope + " new credentials given");
900                 }
901             }
902         } else {
903             LOG.debug("Proxy credentials provider not available");
904         }
905         return creds;
906     }
907 
908     /***
909      * @return
910      */
911     public HostConfiguration getHostConfiguration() {
912         return hostConfiguration;
913     }
914 
915     /***
916      * @return
917      */
918     public HttpState getState() {
919         return state;
920     }
921 
922     /***
923      * @return
924      */
925     public HttpConnectionManager getConnectionManager() {
926         return connectionManager;
927     }
928 
929     /***
930      * @return
931      */
932     public HttpParams getParams() {
933         return this.params;
934     }
935 }