View Javadoc

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