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