View Javadoc

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