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: 290260 $
4    * $Date: 2005-09-19 16:37:48 -0400 (Mon, 19 Sep 2005) $
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()) {
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             authenticateProxy(method);
231             authenticateHost(method);
232         } catch (AuthenticationException e) {
233             LOG.error(e.getMessage(), e);
234         }
235     }
236 
237 
238     private boolean cleanAuthHeaders(final HttpMethod method, final String name) {
239         Header[] authheaders = method.getRequestHeaders(name);
240         boolean clean = true;
241         for (int i = 0; i < authheaders.length; i++) {
242             Header authheader = authheaders[i];
243             if (authheader.isAutogenerated()) {
244                 method.removeRequestHeader(authheader);
245             } else {
246                 clean = false;
247             }
248         }
249         return clean;
250     }
251     
252 
253     private void authenticateHost(final HttpMethod method) throws AuthenticationException {
254         // Clean up existing authentication headers
255         if (!cleanAuthHeaders(method, WWW_AUTH_RESP)) {
256             // User defined authentication header(s) present
257             return;
258         }
259         AuthState authstate = method.getHostAuthState();
260         AuthScheme authscheme = authstate.getAuthScheme();
261         if (authscheme == null) {
262             return;
263         }
264         if (authstate.isAuthRequested() || !authscheme.isConnectionBased()) {
265             String host = method.getParams().getVirtualHost();
266             if (host == null) {
267                 host = conn.getHost();
268             }
269             int port = conn.getPort();
270             AuthScope authscope = new AuthScope(
271                 host, port, 
272                 authscheme.getRealm(), 
273                 authscheme.getSchemeName());  
274             if (LOG.isDebugEnabled()) {
275                 LOG.debug("Authenticating with " + authscope);
276             }
277             Credentials credentials = this.state.getCredentials(authscope);
278             if (credentials != null) {
279                 String authstring = authscheme.authenticate(credentials, method);
280                 if (authstring != null) {
281                     method.addRequestHeader(new Header(WWW_AUTH_RESP, authstring, true));
282                 }
283             } else {
284                 if (LOG.isWarnEnabled()) {
285                     LOG.warn("Required credentials not available for " + authscope);
286                     if (method.getHostAuthState().isPreemptive()) {
287                         LOG.warn("Preemptive authentication requested but no default " +
288                             "credentials available"); 
289                     }
290                 }
291             }
292         }
293     }
294 
295 
296     private void authenticateProxy(final HttpMethod method) throws AuthenticationException {
297         // Clean up existing authentication headers
298         if (!cleanAuthHeaders(method, PROXY_AUTH_RESP)) {
299             // User defined authentication header(s) present
300             return;
301         }
302         AuthState authstate = method.getProxyAuthState();
303         AuthScheme authscheme = authstate.getAuthScheme();
304         if (authscheme == null) {
305             return;
306         }
307         if (authstate.isAuthRequested() || !authscheme.isConnectionBased()) {
308             AuthScope authscope = new AuthScope(
309                 conn.getProxyHost(), conn.getProxyPort(), 
310                 authscheme.getRealm(), 
311                 authscheme.getSchemeName());  
312             if (LOG.isDebugEnabled()) {
313                 LOG.debug("Authenticating with " + authscope);
314             }
315             Credentials credentials = this.state.getProxyCredentials(authscope);
316             if (credentials != null) {
317                 String authstring = authscheme.authenticate(credentials, method);
318                 if (authstring != null) {
319                     method.addRequestHeader(new Header(PROXY_AUTH_RESP, authstring, true));
320                 }
321             } else {
322                 if (LOG.isWarnEnabled()) {
323                     LOG.warn("Required proxy credentials not available for " + authscope);
324                     if (method.getProxyAuthState().isPreemptive()) {
325                         LOG.warn("Preemptive authentication requested but no default " +
326                             "proxy credentials available"); 
327                     }
328                 }
329             }
330         }
331     }
332     
333     
334     /***
335      * Applies connection parameters specified for a given method
336      * 
337      * @param method HTTP method
338      * 
339      * @throws IOException if an I/O occurs setting connection parameters 
340      */
341     private void applyConnectionParams(final HttpMethod method) throws IOException {
342         int timeout = 0;
343         // see if a timeout is given for this method
344         Object param = method.getParams().getParameter(HttpMethodParams.SO_TIMEOUT);
345         if (param == null) {
346             // if not, use the default value
347             param = this.conn.getParams().getParameter(HttpConnectionParams.SO_TIMEOUT);
348         }
349         if (param != null) {
350             timeout = ((Integer)param).intValue();
351         }
352         this.conn.setSocketTimeout(timeout);                    
353     }
354     
355     /***
356      * Executes a method with the current hostConfiguration.
357      *
358      * @throws IOException if an I/O (transport) error occurs. Some transport exceptions 
359      * can be recovered from.
360      * @throws HttpException  if a protocol exception occurs. Usually protocol exceptions 
361      * cannot be recovered from.
362      */
363     private void executeWithRetry(final HttpMethod method) 
364         throws IOException, HttpException {
365         
366         /*** How many times did this transparently handle a recoverable exception? */
367         int execCount = 0;
368         // loop until the method is successfully processed, the retryHandler 
369         // returns false or a non-recoverable exception is thrown
370         try {
371             while (true) {
372                 execCount++;
373                 try {
374 
375                     if (LOG.isTraceEnabled()) {
376                         LOG.trace("Attempt number " + execCount + " to process request");
377                     }
378                     if (this.conn.getParams().isStaleCheckingEnabled()) {
379                         this.conn.closeIfStale();
380                     }
381                     if (!this.conn.isOpen()) {
382                         // this connection must be opened before it can be used
383                         // This has nothing to do with opening a secure tunnel
384                         this.conn.open();
385                         if (this.conn.isProxied() && this.conn.isSecure() 
386                         && !(method instanceof ConnectMethod)) {
387                             // we need to create a secure tunnel before we can execute the real method
388                             if (!executeConnect()) {
389                                 // abort, the connect method failed
390                                 return;
391                             }
392                         }
393                     }
394                     applyConnectionParams(method);                    
395                     method.execute(state, this.conn);
396                     break;
397                 } catch (HttpException e) {
398                     // filter out protocol exceptions which cannot be recovered from
399                     throw e;
400                 } catch (IOException e) {
401                     LOG.debug("Closing the connection.");
402                     this.conn.close();
403                     // test if this method should be retried
404                     // ========================================
405                     // this code is provided for backward compatibility with 2.0
406                     // will be removed in the next major release
407                     if (method instanceof HttpMethodBase) {
408                         MethodRetryHandler handler = 
409                             ((HttpMethodBase)method).getMethodRetryHandler();
410                         if (handler != null) {
411                             if (!handler.retryMethod(
412                                     method,
413                                     this.conn, 
414                                     new HttpRecoverableException(e.getMessage()),
415                                     execCount, 
416                                     method.isRequestSent())) {
417                                 LOG.debug("Method retry handler returned false. "
418                                         + "Automatic recovery will not be attempted");
419                                 throw e;
420                             }
421                         }
422                     }
423                     // ========================================
424                     HttpMethodRetryHandler handler = 
425                         (HttpMethodRetryHandler)method.getParams().getParameter(
426                                 HttpMethodParams.RETRY_HANDLER);
427                     if (handler == null) {
428                         handler = new DefaultHttpMethodRetryHandler();
429                     }
430                     if (!handler.retryMethod(method, e, execCount)) {
431                         LOG.debug("Method retry handler returned false. "
432                                 + "Automatic recovery will not be attempted");
433                         throw e;
434                     }
435                     if (LOG.isInfoEnabled()) {
436                         LOG.info("I/O exception ("+ e.getClass().getName() +") caught when processing request: "
437                                 + e.getMessage());
438                     }
439                     if (LOG.isDebugEnabled()) {
440                         LOG.debug(e.getMessage(), e);
441                     }
442                     LOG.info("Retrying request");
443                 }
444             }
445         } catch (IOException e) {
446             if (this.conn.isOpen()) {
447                 LOG.debug("Closing the connection.");
448                 this.conn.close();
449             }
450             releaseConnection = true;
451             throw e;
452         } catch (RuntimeException e) {
453             if (this.conn.isOpen) {
454                 LOG.debug("Closing the connection.");
455                 this.conn.close();
456             }
457             releaseConnection = true;
458             throw e;
459         }
460     }
461     
462     /***
463      * Executes a ConnectMethod to establish a tunneled connection.
464      * 
465      * @return <code>true</code> if the connect was successful
466      * 
467      * @throws IOException
468      * @throws HttpException
469      */
470     private boolean executeConnect() 
471         throws IOException, HttpException {
472 
473         this.connectMethod = new ConnectMethod();
474         this.connectMethod.getParams().setDefaults(this.hostConfiguration.getParams());
475         
476         int code;
477         for (;;) {
478             if (!this.conn.isOpen()) {
479                 this.conn.open();
480             }
481             try {
482                 authenticateProxy(this.connectMethod);
483             } catch (AuthenticationException e) {
484                 LOG.error(e.getMessage(), e);
485             }
486             applyConnectionParams(this.connectMethod);                    
487             this.connectMethod.execute(state, this.conn);
488             code = this.connectMethod.getStatusCode();
489             boolean retry = false;
490             AuthState authstate = this.connectMethod.getProxyAuthState(); 
491             authstate.setAuthRequested(code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
492             if (authstate.isAuthRequested()) {
493                 if (processAuthenticationResponse(this.connectMethod)) {
494                     retry = true;
495                 }
496             }
497             if (!retry) {
498                 break;
499             }
500             if (this.connectMethod.getResponseBodyAsStream() != null) {
501                 this.connectMethod.getResponseBodyAsStream().close();
502             }
503         }
504         if ((code >= 200) && (code < 300)) {
505             this.conn.tunnelCreated();
506             // Drop the connect method, as it is no longer needed
507             this.connectMethod = null;
508             return true;
509         } else {
510             return false;
511         }
512     }
513 
514     /***
515      * Fake response
516      * @param method
517      * @return
518      */
519     
520     private void fakeResponse(final HttpMethod method)
521         throws IOException, HttpException {
522         // What is to follow is an ugly hack.
523         // I REALLY hate having to resort to such
524         // an appalling trick
525         // The only feasible solution is to split monolithic
526         // HttpMethod into HttpRequest/HttpResponse pair.
527         // That would allow to execute CONNECT method 
528         // behind the scene and return CONNECT HttpResponse 
529         // object in response to the original request that 
530         // contains the correct status line, headers & 
531         // response body.
532         LOG.debug("CONNECT failed, fake the response for the original method");
533         // Pass the status, headers and response stream to the wrapped
534         // method.
535         // To ensure that the connection is not released more than once
536         // this method is still responsible for releasing the connection. 
537         // This will happen when the response body is consumed, or when
538         // the wrapped method closes the response connection in 
539         // releaseConnection().
540         if (method instanceof HttpMethodBase) {
541             ((HttpMethodBase) method).fakeResponse(
542                 this.connectMethod.getStatusLine(),
543                 this.connectMethod.getResponseHeaderGroup(),
544                 this.connectMethod.getResponseBodyAsStream()
545             );
546             method.getProxyAuthState().setAuthScheme(
547                 this.connectMethod.getProxyAuthState().getAuthScheme());
548             this.connectMethod = null;
549         } else {
550             releaseConnection = true;
551             LOG.warn(
552                 "Unable to fake response on method as it is not derived from HttpMethodBase.");
553         }
554     }
555     
556 	/***
557 	 * Process the redirect response.
558      * 
559 	 * @return <code>true</code> if the redirect was successful
560 	 */
561 	private boolean processRedirectResponse(final HttpMethod method)
562      throws RedirectException {
563 		//get the location header to find out where to redirect to
564 		Header locationHeader = method.getResponseHeader("location");
565 		if (locationHeader == null) {
566 			// got a redirect response, but no location header
567 			LOG.error("Received redirect response " + method.getStatusCode()
568 					+ " but no location header");
569 			return false;
570 		}
571 		String location = locationHeader.getValue();
572 		if (LOG.isDebugEnabled()) {
573 			LOG.debug("Redirect requested to location '" + location + "'");
574 		}
575         
576 		//rfc2616 demands the location value be a complete URI
577 		//Location       = "Location" ":" absoluteURI
578 		URI redirectUri = null;
579 		URI currentUri = null;
580 
581 		try {
582 			currentUri = new URI(
583 				this.conn.getProtocol().getScheme(),
584 				null,
585                 this.conn.getHost(), 
586                 this.conn.getPort(), 
587 				method.getPath()
588 			);
589 			redirectUri = new URI(location, true);
590 			if (redirectUri.isRelativeURI()) {
591 				if (this.params.isParameterTrue(HttpClientParams.REJECT_RELATIVE_REDIRECT)) {
592 					LOG.warn("Relative redirect location '" + location + "' not allowed");
593 					return false;
594 				} else { 
595 					//location is incomplete, use current values for defaults
596 					LOG.debug("Redirect URI is not absolute - parsing as relative");
597 					redirectUri = new URI(currentUri, redirectUri);
598 				}
599 			}
600             method.setURI(redirectUri);
601             hostConfiguration.setHost(redirectUri);
602 		} catch (URIException e) {
603 			LOG.warn("Redirected location '" + location + "' is malformed");
604 			return false;
605 		}
606 
607         if (this.params.isParameterFalse(HttpClientParams.ALLOW_CIRCULAR_REDIRECTS)) {
608             if (this.redirectLocations == null) {
609                 this.redirectLocations = new HashSet();
610             }
611             this.redirectLocations.add(currentUri);
612             try {
613                 if(redirectUri.hasQuery()) {
614                     redirectUri.setQuery(null);
615                 }
616             } catch (URIException e) {
617                 // Should never happen
618                 return false;
619             }
620 
621             if (this.redirectLocations.contains(redirectUri)) {
622                 throw new CircularRedirectException("Circular redirect to '" +
623                     redirectUri + "'");
624             }
625         }
626 
627 		if (LOG.isDebugEnabled()) {
628 			LOG.debug("Redirecting from '" + currentUri.getEscapedURI()
629 				+ "' to '" + redirectUri.getEscapedURI());
630 		}
631         //And finally invalidate the actual authentication scheme
632         method.getHostAuthState().invalidate(); 
633 		return true;
634 	}
635 
636 	/***
637 	 * Processes a response that requires authentication
638 	 *
639 	 * @param method the current {@link HttpMethod HTTP method}
640 	 *
641 	 * @return <tt>true</tt> if the authentication challenge can be responsed to,
642      *   (that is, at least one of the requested authentication scheme is supported, 
643      *   and matching credentials have been found), <tt>false</tt> otherwise.
644 	 */
645 	private boolean processAuthenticationResponse(final HttpMethod method) {
646 		LOG.trace("enter HttpMethodBase.processAuthenticationResponse("
647 			+ "HttpState, HttpConnection)");
648 
649 		try {
650             switch (method.getStatusCode()) {
651                 case HttpStatus.SC_UNAUTHORIZED:
652                     return processWWWAuthChallenge(method);
653                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
654                     return processProxyAuthChallenge(method);
655                 default:
656                     return false;
657             }
658         } catch (Exception e) {
659             if (LOG.isErrorEnabled()) {
660                 LOG.error(e.getMessage(), e);
661             }
662             return false;
663         }
664 	}
665 
666     private boolean processWWWAuthChallenge(final HttpMethod method)
667         throws MalformedChallengeException, AuthenticationException  
668     {
669         AuthState authstate = method.getHostAuthState();
670         Map challenges = AuthChallengeParser.parseChallenges(
671             method.getResponseHeaders(WWW_AUTH_CHALLENGE));
672         if (challenges.isEmpty()) {
673             LOG.debug("Authentication challenge(s) not found");
674             return false; 
675         }
676         AuthScheme authscheme = null;
677         try {
678             authscheme = this.authProcessor.processChallenge(authstate, challenges);
679         } catch (AuthChallengeException e) {
680             if (LOG.isWarnEnabled()) {
681                 LOG.warn(e.getMessage());
682             }
683         }
684         if (authscheme == null) {
685             return false;
686         }
687         String host = method.getParams().getVirtualHost();
688         if (host == null) {
689             host = conn.getHost();
690         }
691         int port = conn.getPort();
692         AuthScope authscope = new AuthScope(
693             host, port, 
694             authscheme.getRealm(), 
695             authscheme.getSchemeName());
696         
697         if (LOG.isDebugEnabled()) {
698             LOG.debug("Authentication scope: " + authscope);
699         }
700         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
701             // Already tried and failed
702             Credentials credentials = promptForCredentials(
703                 authscheme, method.getParams(), authscope);
704             if (credentials == null) {
705                 if (LOG.isInfoEnabled()) {
706                     LOG.info("Failure authenticating with " + authscope);
707                 }
708                 return false;
709             } else {
710                 return true;
711             }
712         } else {
713             authstate.setAuthAttempted(true);
714             Credentials credentials = this.state.getCredentials(authscope);
715             if (credentials == null) {
716                 credentials = promptForCredentials(
717                     authscheme, method.getParams(), authscope);
718             }
719             if (credentials == null) {
720                 if (LOG.isInfoEnabled()) {
721                     LOG.info("No credentials available for " + authscope); 
722                 }
723                 return false;
724             } else {
725                 return true;
726             }
727         }
728     }
729 
730     private boolean processProxyAuthChallenge(final HttpMethod method)
731         throws MalformedChallengeException, AuthenticationException
732     {  
733         AuthState authstate = method.getProxyAuthState();
734         Map proxyChallenges = AuthChallengeParser.parseChallenges(
735             method.getResponseHeaders(PROXY_AUTH_CHALLENGE));
736         if (proxyChallenges.isEmpty()) {
737             LOG.debug("Proxy authentication challenge(s) not found");
738             return false; 
739         }
740         AuthScheme authscheme = null;
741         try {
742             authscheme = this.authProcessor.processChallenge(authstate, proxyChallenges);
743         } catch (AuthChallengeException e) {
744             if (LOG.isWarnEnabled()) {
745                 LOG.warn(e.getMessage());
746             }
747         }
748         if (authscheme == null) {
749             return false;
750         }
751         AuthScope authscope = new AuthScope(
752             conn.getProxyHost(), conn.getProxyPort(), 
753             authscheme.getRealm(), 
754             authscheme.getSchemeName());  
755 
756         if (LOG.isDebugEnabled()) {
757             LOG.debug("Proxy authentication scope: " + authscope);
758         }
759         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
760             // Already tried and failed
761             Credentials credentials = promptForProxyCredentials(
762                 authscheme, method.getParams(), authscope);
763             if (credentials == null) {
764                 if (LOG.isInfoEnabled()) {
765                     LOG.info("Failure authenticating with " + authscope);
766                 }
767                 return false;
768             } else {
769                 return true;
770             }
771         } else {
772             authstate.setAuthAttempted(true);
773             Credentials credentials = this.state.getProxyCredentials(authscope);
774             if (credentials == null) {
775                 credentials = promptForProxyCredentials(
776                     authscheme, method.getParams(), authscope);
777             }
778             if (credentials == null) {
779                 if (LOG.isInfoEnabled()) {
780                     LOG.info("No credentials available for " + authscope); 
781                 }
782                 return false;
783             } else {
784                 return true;
785             }
786         }
787     }
788 
789     /***
790      * Tests if the {@link HttpMethod method} requires a redirect to another location.
791      * 
792      * @param method HTTP method
793      * 
794      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
795      */
796 	private boolean isRedirectNeeded(final HttpMethod method) {
797 		switch (method.getStatusCode()) {
798 			case HttpStatus.SC_MOVED_TEMPORARILY:
799 			case HttpStatus.SC_MOVED_PERMANENTLY:
800 			case HttpStatus.SC_SEE_OTHER:
801 			case HttpStatus.SC_TEMPORARY_REDIRECT:
802 				LOG.debug("Redirect required");
803                 if (method.getFollowRedirects()) {
804                     return true;
805                 } else {
806                     LOG.info("Redirect requested but followRedirects is "
807                             + "disabled");
808                     return false;
809                 }
810 			default:
811 				return false;
812 		} //end of switch
813 	}
814 
815     /***
816      * Tests if the {@link HttpMethod method} requires authentication.
817      * 
818      * @param method HTTP method
819      * 
820      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
821      */
822     private boolean isAuthenticationNeeded(final HttpMethod method) {
823         method.getHostAuthState().setAuthRequested(
824                 method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED);
825         method.getProxyAuthState().setAuthRequested(
826                 method.getStatusCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
827         if (method.getHostAuthState().isAuthRequested() || 
828             method.getProxyAuthState().isAuthRequested()) {
829             LOG.debug("Authorization required");
830             if (method.getDoAuthentication()) { //process authentication response
831                 return true;
832             } else { //let the client handle the authenticaiton
833                 LOG.info("Authentication requested but doAuthentication is "
834                         + "disabled");
835                 return false;
836             }
837         } else {
838             return false;
839         }
840     }
841 
842     private Credentials promptForCredentials(
843         final AuthScheme authScheme,
844         final HttpParams params, 
845         final AuthScope authscope)
846     {
847         LOG.debug("Credentials required");
848         Credentials creds = null;
849         CredentialsProvider credProvider = 
850             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
851         if (credProvider != null) {
852             try {
853                 creds = credProvider.getCredentials(
854                     authScheme, authscope.getHost(), authscope.getPort(), false);
855             } catch (CredentialsNotAvailableException e) {
856                 LOG.warn(e.getMessage());
857             }
858             if (creds != null) {
859                 this.state.setCredentials(authscope, creds);
860                 if (LOG.isDebugEnabled()) {
861                     LOG.debug(authscope + " new credentials given");
862                 }
863             }
864         } else {
865             LOG.debug("Credentials provider not available");
866         }
867         return creds;
868     }
869 
870     private Credentials promptForProxyCredentials(
871         final AuthScheme authScheme,
872         final HttpParams params,
873         final AuthScope authscope) 
874     {
875         LOG.debug("Proxy credentials required");
876         Credentials creds = null;
877         CredentialsProvider credProvider = 
878             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
879         if (credProvider != null) {
880             try {
881                 creds = credProvider.getCredentials(
882                     authScheme, authscope.getHost(), authscope.getPort(), true);
883             } catch (CredentialsNotAvailableException e) {
884                 LOG.warn(e.getMessage());
885             }
886             if (creds != null) {
887                 this.state.setProxyCredentials(authscope, creds);
888                 if (LOG.isDebugEnabled()) {
889                     LOG.debug(authscope + " new credentials given");
890                 }
891             }
892         } else {
893             LOG.debug("Proxy credentials provider not available");
894         }
895         return creds;
896     }
897 
898     /***
899      * @return
900      */
901     public HostConfiguration getHostConfiguration() {
902         return hostConfiguration;
903     }
904 
905     /***
906      * @return
907      */
908     public HttpState getState() {
909         return state;
910     }
911 
912     /***
913      * @return
914      */
915     public HttpConnectionManager getConnectionManager() {
916         return connectionManager;
917     }
918 
919     /***
920      * @return
921      */
922     public HttpParams getParams() {
923         return this.params;
924     }
925 }