View Javadoc

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