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: 1.34 $
4    * $Date: 2005-01-14 14:40:39 -0500 (Fri, 14 Jan 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                         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 = method.getParams().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             try {
597                 if(redirectUri.hasQuery()) {
598                     redirectUri.setQuery(null);
599                 }
600             } catch (URIException e) {
601                 // Should never happen
602                 return false;
603             }
604 
605             if (this.redirectLocations.contains(redirectUri)) {
606                 throw new CircularRedirectException("Circular redirect to '" +
607                     redirectUri + "'");
608             }
609         }
610 
611 		if (LOG.isDebugEnabled()) {
612 			LOG.debug("Redirecting from '" + currentUri.getEscapedURI()
613 				+ "' to '" + redirectUri.getEscapedURI());
614 		}
615         //And finally invalidate the actual authentication scheme
616         method.getHostAuthState().invalidate(); 
617 		return true;
618 	}
619 
620 	/***
621 	 * Processes a response that requires authentication
622 	 *
623 	 * @param method the current {@link HttpMethod HTTP method}
624 	 *
625 	 * @return <tt>true</tt> if the authentication challenge can be responsed to,
626      *   (that is, at least one of the requested authentication scheme is supported, 
627      *   and matching credentials have been found), <tt>false</tt> otherwise.
628 	 */
629 	private boolean processAuthenticationResponse(final HttpMethod method) {
630 		LOG.trace("enter HttpMethodBase.processAuthenticationResponse("
631 			+ "HttpState, HttpConnection)");
632 
633 		try {
634             switch (method.getStatusCode()) {
635                 case HttpStatus.SC_UNAUTHORIZED:
636                     return processWWWAuthChallenge(method);
637                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
638                     return processProxyAuthChallenge(method);
639                 default:
640                     return false;
641             }
642         } catch (Exception e) {
643             if (LOG.isErrorEnabled()) {
644                 LOG.error(e.getMessage(), e);
645             }
646             return false;
647         }
648 	}
649 
650     private boolean processWWWAuthChallenge(final HttpMethod method)
651         throws MalformedChallengeException, AuthenticationException  
652     {
653         AuthState authstate = method.getHostAuthState();
654         if (authstate.isPreemptive()) {
655             authstate.invalidate();
656         }
657         Map challenges = AuthChallengeParser.parseChallenges(
658             method.getResponseHeaders(WWW_AUTH_CHALLENGE));
659         if (challenges.isEmpty()) {
660             return false; 
661         }
662         AuthScheme authscheme = null;
663         try {
664             authscheme = this.authProcessor.processChallenge(authstate, challenges);
665         } catch (AuthChallengeException e) {
666             if (LOG.isWarnEnabled()) {
667                 LOG.warn(e.getMessage());
668             }
669         }
670         if (authscheme == null) {
671             return false;
672         }
673         String host = method.getParams().getVirtualHost();
674         if (host == null) {
675             host = conn.getHost();
676         }
677         int port = conn.getPort();
678         AuthScope authscope = new AuthScope(
679             host, port, 
680             authscheme.getRealm(), 
681             authscheme.getSchemeName());  
682 
683         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
684             // Already tried and failed
685             Credentials credentials = promptForCredentials(
686                 authscheme, method.getParams(), authscope);
687             if (credentials == null) {
688                 if (LOG.isInfoEnabled()) {
689                     LOG.info("Failure authenticating with " + authscope);
690                 }
691                 return false;
692             } else {
693                 return true;
694             }
695         } else {
696             authstate.setAuthAttempted(true);
697             Credentials credentials = this.state.getCredentials(authscope);
698             if (credentials == null) {
699                 credentials = promptForCredentials(
700                     authscheme, method.getParams(), authscope);
701             }
702             if (credentials == null) {
703                 if (LOG.isInfoEnabled()) {
704                     LOG.info("No credentials available for " + authscope); 
705                 }
706                 return false;
707             } else {
708                 return true;
709             }
710         }
711     }
712 
713     private boolean processProxyAuthChallenge(final HttpMethod method)
714         throws MalformedChallengeException, AuthenticationException
715     {  
716         AuthState authstate = method.getProxyAuthState();
717         if (authstate.isPreemptive()) {
718             authstate.invalidate();
719         }
720         Map proxyChallenges = AuthChallengeParser.parseChallenges(
721             method.getResponseHeaders(PROXY_AUTH_CHALLENGE));
722         if (proxyChallenges.isEmpty()) {
723             return false; 
724         }
725         AuthScheme authscheme = null;
726         try {
727             authscheme = this.authProcessor.processChallenge(authstate, proxyChallenges);
728         } catch (AuthChallengeException e) {
729             if (LOG.isWarnEnabled()) {
730                 LOG.warn(e.getMessage());
731             }
732         }
733         if (authscheme == null) {
734             return false;
735         }
736         AuthScope authscope = new AuthScope(
737             conn.getProxyHost(), conn.getProxyPort(), 
738             authscheme.getRealm(), 
739             authscheme.getSchemeName());  
740 
741         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
742             // Already tried and failed
743             Credentials credentials = promptForProxyCredentials(
744                 authscheme, method.getParams(), authscope);
745             if (credentials == null) {
746                 if (LOG.isInfoEnabled()) {
747                     LOG.info("Failure authenticating with " + authscope);
748                 }
749                 return false;
750             } else {
751                 return true;
752             }
753         } else {
754             authstate.setAuthAttempted(true);
755             Credentials credentials = this.state.getProxyCredentials(authscope);
756             if (credentials == null) {
757                 credentials = promptForProxyCredentials(
758                     authscheme, method.getParams(), authscope);
759             }
760             if (credentials == null) {
761                 if (LOG.isInfoEnabled()) {
762                     LOG.info("No credentials available for " + authscope); 
763                 }
764                 return false;
765             } else {
766                 return true;
767             }
768         }
769     }
770 
771     /***
772      * Tests if the {@link HttpMethod method} requires a redirect to another location.
773      * 
774      * @param method HTTP method
775      * 
776      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
777      */
778 	private boolean isRedirectNeeded(final HttpMethod method) {
779 		switch (method.getStatusCode()) {
780 			case HttpStatus.SC_MOVED_TEMPORARILY:
781 			case HttpStatus.SC_MOVED_PERMANENTLY:
782 			case HttpStatus.SC_SEE_OTHER:
783 			case HttpStatus.SC_TEMPORARY_REDIRECT:
784 				LOG.debug("Redirect required");
785                 if (method.getFollowRedirects()) {
786                     return true;
787                 } else {
788                     LOG.info("Redirect requested but followRedirects is "
789                             + "disabled");
790                     return false;
791                 }
792 			default:
793 				return false;
794 		} //end of switch
795 	}
796 
797     /***
798      * Tests if the {@link HttpMethod method} requires authentication.
799      * 
800      * @param method HTTP method
801      * 
802      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
803      */
804     private boolean isAuthenticationNeeded(final HttpMethod method) {
805         method.getHostAuthState().setAuthRequested(
806                 method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED);
807         method.getProxyAuthState().setAuthRequested(
808                 method.getStatusCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
809         if (method.getHostAuthState().isAuthRequested() || 
810             method.getProxyAuthState().isAuthRequested()) {
811             LOG.debug("Authorization required");
812             if (method.getDoAuthentication()) { //process authentication response
813                 return true;
814             } else { //let the client handle the authenticaiton
815                 LOG.info("Authentication requested but doAuthentication is "
816                         + "disabled");
817                 return false;
818             }
819         } else {
820             return false;
821         }
822     }
823 
824     private Credentials promptForCredentials(
825         final AuthScheme authScheme,
826         final HttpParams params, 
827         final AuthScope authscope)
828     {
829         Credentials creds = null;
830         CredentialsProvider credProvider = 
831             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
832         if (credProvider != null) {
833             try {
834                 creds = credProvider.getCredentials(
835                     authScheme, authscope.getHost(), authscope.getPort(), false);
836             } catch (CredentialsNotAvailableException e) {
837                 LOG.warn(e.getMessage());
838             }
839             if (creds != null) {
840                 this.state.setCredentials(authscope, creds);
841                 if (LOG.isDebugEnabled()) {
842                     LOG.debug(authscope + " new credentials given");
843                 }
844             }
845         }
846         return creds;
847     }
848 
849     private Credentials promptForProxyCredentials(
850         final AuthScheme authScheme,
851         final HttpParams params,
852         final AuthScope authscope) 
853     {
854         Credentials creds = null;
855         CredentialsProvider credProvider = 
856             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
857         if (credProvider != null) {
858             try {
859                 creds = credProvider.getCredentials(
860                     authScheme, authscope.getHost(), authscope.getPort(), true);
861             } catch (CredentialsNotAvailableException e) {
862                 LOG.warn(e.getMessage());
863             }
864             if (creds != null) {
865                 this.state.setProxyCredentials(authscope, creds);
866                 if (LOG.isDebugEnabled()) {
867                     LOG.debug(authscope + " new credentials given");
868                 }
869             }
870         }
871         return creds;
872     }
873 
874     /***
875      * @return
876      */
877     public HostConfiguration getHostConfiguration() {
878         return hostConfiguration;
879     }
880 
881     /***
882      * @return
883      */
884     public HttpState getState() {
885         return state;
886     }
887 
888     /***
889      * @return
890      */
891     public HttpConnectionManager getConnectionManager() {
892         return connectionManager;
893     }
894 
895     /***
896      * @return
897      */
898     public HttpParams getParams() {
899         return this.params;
900     }
901 }