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: 169676 $
4    * $Date: 2005-05-11 15:13:29 -0400 (Wed, 11 May 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             if (!this.conn.isOpen()) {
477                 this.conn.open();
478             }
479             try {
480                 authenticateProxy(this.connectMethod);
481             } catch (AuthenticationException e) {
482                 LOG.error(e.getMessage(), e);
483             }
484             applyConnectionParams(this.connectMethod);                    
485             this.connectMethod.execute(state, this.conn);
486             code = this.connectMethod.getStatusCode();
487             boolean retry = false;
488             AuthState authstate = this.connectMethod.getProxyAuthState(); 
489             authstate.setAuthRequested(code == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
490             if (authstate.isAuthRequested()) {
491                 if (processAuthenticationResponse(this.connectMethod)) {
492                     retry = true;
493                 }
494             }
495             if (!retry) {
496                 break;
497             }
498             if (this.connectMethod.getResponseBodyAsStream() != null) {
499                 this.connectMethod.getResponseBodyAsStream().close();
500             }
501         }
502         if ((code >= 200) && (code < 300)) {
503             this.conn.tunnelCreated();
504             // Drop the connect method, as it is no longer needed
505             this.connectMethod = null;
506             return true;
507         } else {
508             return false;
509         }
510     }
511 
512     /***
513      * Fake response
514      * @param method
515      * @return
516      */
517     
518     private void fakeResponse(final HttpMethod method)
519         throws IOException, HttpException {
520         // What is to follow is an ugly hack.
521         // I REALLY hate having to resort to such
522         // an appalling trick
523         // The only feasible solution is to split monolithic
524         // HttpMethod into HttpRequest/HttpResponse pair.
525         // That would allow to execute CONNECT method 
526         // behind the scene and return CONNECT HttpResponse 
527         // object in response to the original request that 
528         // contains the correct status line, headers & 
529         // response body.
530         LOG.debug("CONNECT failed, fake the response for the original method");
531         // Pass the status, headers and response stream to the wrapped
532         // method.
533         // To ensure that the connection is not released more than once
534         // this method is still responsible for releasing the connection. 
535         // This will happen when the response body is consumed, or when
536         // the wrapped method closes the response connection in 
537         // releaseConnection().
538         if (method instanceof HttpMethodBase) {
539             ((HttpMethodBase) method).fakeResponse(
540                 this.connectMethod.getStatusLine(),
541                 this.connectMethod.getResponseHeaderGroup(),
542                 this.connectMethod.getResponseBodyAsStream()
543             );
544             method.getProxyAuthState().setAuthScheme(
545                 this.connectMethod.getProxyAuthState().getAuthScheme());
546             this.connectMethod = null;
547         } else {
548             releaseConnection = true;
549             LOG.warn(
550                 "Unable to fake response on method as it is not derived from HttpMethodBase.");
551         }
552     }
553     
554 	/***
555 	 * Process the redirect response.
556      * 
557 	 * @return <code>true</code> if the redirect was successful
558 	 */
559 	private boolean processRedirectResponse(final HttpMethod method)
560      throws RedirectException  
561     {
562 		//get the location header to find out where to redirect to
563 		Header locationHeader = method.getResponseHeader("location");
564 		if (locationHeader == null) {
565 			// got a redirect response, but no location header
566 			LOG.error("Received redirect response " + method.getStatusCode()
567 					+ " but no location header");
568 			return false;
569 		}
570 		String location = locationHeader.getValue();
571 		if (LOG.isDebugEnabled()) {
572 			LOG.debug("Redirect requested to location '" + location + "'");
573 		}
574         
575 		//rfc2616 demands the location value be a complete URI
576 		//Location       = "Location" ":" absoluteURI
577 		URI redirectUri = null;
578 		URI currentUri = null;
579 
580 		try {
581 			currentUri = new URI(
582 				this.conn.getProtocol().getScheme(),
583 				null,
584                 this.conn.getHost(), 
585                 this.conn.getPort(), 
586 				method.getPath()
587 			);
588 			redirectUri = new URI(location, true);
589 			if (redirectUri.isRelativeURI()) {
590 				if (this.params.isParameterTrue(HttpClientParams.REJECT_RELATIVE_REDIRECT)) {
591 					LOG.warn("Relative redirect location '" + location + "' not allowed");
592 					return false;
593 				} else { 
594 					//location is incomplete, use current values for defaults
595 					LOG.debug("Redirect URI is not absolute - parsing as relative");
596 					redirectUri = new URI(currentUri, redirectUri);
597 				}
598 			}
599             method.setURI(redirectUri);
600             hostConfiguration.setHost(redirectUri);
601 		} catch (URIException e) {
602 			LOG.warn("Redirected location '" + location + "' is malformed");
603 			return false;
604 		}
605 
606         if (this.params.isParameterFalse(HttpClientParams.ALLOW_CIRCULAR_REDIRECTS)) {
607             if (this.redirectLocations == null) {
608                 this.redirectLocations = new HashSet();
609             }
610             this.redirectLocations.add(currentUri);
611             try {
612                 if(redirectUri.hasQuery()) {
613                     redirectUri.setQuery(null);
614                 }
615             } catch (URIException e) {
616                 // Should never happen
617                 return false;
618             }
619 
620             if (this.redirectLocations.contains(redirectUri)) {
621                 throw new CircularRedirectException("Circular redirect to '" +
622                     redirectUri + "'");
623             }
624         }
625 
626 		if (LOG.isDebugEnabled()) {
627 			LOG.debug("Redirecting from '" + currentUri.getEscapedURI()
628 				+ "' to '" + redirectUri.getEscapedURI());
629 		}
630         //And finally invalidate the actual authentication scheme
631         method.getHostAuthState().invalidate(); 
632 		return true;
633 	}
634 
635 	/***
636 	 * Processes a response that requires authentication
637 	 *
638 	 * @param method the current {@link HttpMethod HTTP method}
639 	 *
640 	 * @return <tt>true</tt> if the authentication challenge can be responsed to,
641      *   (that is, at least one of the requested authentication scheme is supported, 
642      *   and matching credentials have been found), <tt>false</tt> otherwise.
643 	 */
644 	private boolean processAuthenticationResponse(final HttpMethod method) {
645 		LOG.trace("enter HttpMethodBase.processAuthenticationResponse("
646 			+ "HttpState, HttpConnection)");
647 
648 		try {
649             switch (method.getStatusCode()) {
650                 case HttpStatus.SC_UNAUTHORIZED:
651                     return processWWWAuthChallenge(method);
652                 case HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED:
653                     return processProxyAuthChallenge(method);
654                 default:
655                     return false;
656             }
657         } catch (Exception e) {
658             if (LOG.isErrorEnabled()) {
659                 LOG.error(e.getMessage(), e);
660             }
661             return false;
662         }
663 	}
664 
665     private boolean processWWWAuthChallenge(final HttpMethod method)
666         throws MalformedChallengeException, AuthenticationException  
667     {
668         AuthState authstate = method.getHostAuthState();
669         if (authstate.isPreemptive()) {
670             authstate.invalidate();
671             authstate.setAuthRequested(true);
672         }
673         Map challenges = AuthChallengeParser.parseChallenges(
674             method.getResponseHeaders(WWW_AUTH_CHALLENGE));
675         if (challenges.isEmpty()) {
676             LOG.debug("Authentication challenge(s) not found");
677             return false; 
678         }
679         AuthScheme authscheme = null;
680         try {
681             authscheme = this.authProcessor.processChallenge(authstate, challenges);
682         } catch (AuthChallengeException e) {
683             if (LOG.isWarnEnabled()) {
684                 LOG.warn(e.getMessage());
685             }
686         }
687         if (authscheme == null) {
688             return false;
689         }
690         String host = method.getParams().getVirtualHost();
691         if (host == null) {
692             host = conn.getHost();
693         }
694         int port = conn.getPort();
695         AuthScope authscope = new AuthScope(
696             host, port, 
697             authscheme.getRealm(), 
698             authscheme.getSchemeName());
699         
700         if (LOG.isDebugEnabled()) {
701             LOG.debug("Authentication scope: " + authscope);
702         }
703         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
704             // Already tried and failed
705             Credentials credentials = promptForCredentials(
706                 authscheme, method.getParams(), authscope);
707             if (credentials == null) {
708                 if (LOG.isInfoEnabled()) {
709                     LOG.info("Failure authenticating with " + authscope);
710                 }
711                 return false;
712             } else {
713                 return true;
714             }
715         } else {
716             authstate.setAuthAttempted(true);
717             Credentials credentials = this.state.getCredentials(authscope);
718             if (credentials == null) {
719                 credentials = promptForCredentials(
720                     authscheme, method.getParams(), authscope);
721             }
722             if (credentials == null) {
723                 if (LOG.isInfoEnabled()) {
724                     LOG.info("No credentials available for " + authscope); 
725                 }
726                 return false;
727             } else {
728                 return true;
729             }
730         }
731     }
732 
733     private boolean processProxyAuthChallenge(final HttpMethod method)
734         throws MalformedChallengeException, AuthenticationException
735     {  
736         AuthState authstate = method.getProxyAuthState();
737         if (authstate.isPreemptive()) {
738             authstate.invalidate();
739             authstate.setAuthRequested(true);
740         }
741         Map proxyChallenges = AuthChallengeParser.parseChallenges(
742             method.getResponseHeaders(PROXY_AUTH_CHALLENGE));
743         if (proxyChallenges.isEmpty()) {
744             LOG.debug("Proxy authentication challenge(s) not found");
745             return false; 
746         }
747         AuthScheme authscheme = null;
748         try {
749             authscheme = this.authProcessor.processChallenge(authstate, proxyChallenges);
750         } catch (AuthChallengeException e) {
751             if (LOG.isWarnEnabled()) {
752                 LOG.warn(e.getMessage());
753             }
754         }
755         if (authscheme == null) {
756             return false;
757         }
758         AuthScope authscope = new AuthScope(
759             conn.getProxyHost(), conn.getProxyPort(), 
760             authscheme.getRealm(), 
761             authscheme.getSchemeName());  
762 
763         if (LOG.isDebugEnabled()) {
764             LOG.debug("Proxy authentication scope: " + authscope);
765         }
766         if (authstate.isAuthAttempted() && authscheme.isComplete()) {
767             // Already tried and failed
768             Credentials credentials = promptForProxyCredentials(
769                 authscheme, method.getParams(), authscope);
770             if (credentials == null) {
771                 if (LOG.isInfoEnabled()) {
772                     LOG.info("Failure authenticating with " + authscope);
773                 }
774                 return false;
775             } else {
776                 return true;
777             }
778         } else {
779             authstate.setAuthAttempted(true);
780             Credentials credentials = this.state.getProxyCredentials(authscope);
781             if (credentials == null) {
782                 credentials = promptForProxyCredentials(
783                     authscheme, method.getParams(), authscope);
784             }
785             if (credentials == null) {
786                 if (LOG.isInfoEnabled()) {
787                     LOG.info("No credentials available for " + authscope); 
788                 }
789                 return false;
790             } else {
791                 return true;
792             }
793         }
794     }
795 
796     /***
797      * Tests if the {@link HttpMethod method} requires a redirect to another location.
798      * 
799      * @param method HTTP method
800      * 
801      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
802      */
803 	private boolean isRedirectNeeded(final HttpMethod method) {
804 		switch (method.getStatusCode()) {
805 			case HttpStatus.SC_MOVED_TEMPORARILY:
806 			case HttpStatus.SC_MOVED_PERMANENTLY:
807 			case HttpStatus.SC_SEE_OTHER:
808 			case HttpStatus.SC_TEMPORARY_REDIRECT:
809 				LOG.debug("Redirect required");
810                 if (method.getFollowRedirects()) {
811                     return true;
812                 } else {
813                     LOG.info("Redirect requested but followRedirects is "
814                             + "disabled");
815                     return false;
816                 }
817 			default:
818 				return false;
819 		} //end of switch
820 	}
821 
822     /***
823      * Tests if the {@link HttpMethod method} requires authentication.
824      * 
825      * @param method HTTP method
826      * 
827      * @return boolean <tt>true</tt> if a retry is needed, <tt>false</tt> otherwise.
828      */
829     private boolean isAuthenticationNeeded(final HttpMethod method) {
830         method.getHostAuthState().setAuthRequested(
831                 method.getStatusCode() == HttpStatus.SC_UNAUTHORIZED);
832         method.getProxyAuthState().setAuthRequested(
833                 method.getStatusCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED);
834         if (method.getHostAuthState().isAuthRequested() || 
835             method.getProxyAuthState().isAuthRequested()) {
836             LOG.debug("Authorization required");
837             if (method.getDoAuthentication()) { //process authentication response
838                 return true;
839             } else { //let the client handle the authenticaiton
840                 LOG.info("Authentication requested but doAuthentication is "
841                         + "disabled");
842                 return false;
843             }
844         } else {
845             return false;
846         }
847     }
848 
849     private Credentials promptForCredentials(
850         final AuthScheme authScheme,
851         final HttpParams params, 
852         final AuthScope authscope)
853     {
854         LOG.debug("Credentials required");
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(), false);
862             } catch (CredentialsNotAvailableException e) {
863                 LOG.warn(e.getMessage());
864             }
865             if (creds != null) {
866                 this.state.setCredentials(authscope, creds);
867                 if (LOG.isDebugEnabled()) {
868                     LOG.debug(authscope + " new credentials given");
869                 }
870             }
871         } else {
872             LOG.debug("Credentials provider not available");
873         }
874         return creds;
875     }
876 
877     private Credentials promptForProxyCredentials(
878         final AuthScheme authScheme,
879         final HttpParams params,
880         final AuthScope authscope) 
881     {
882         LOG.debug("Proxy credentials required");
883         Credentials creds = null;
884         CredentialsProvider credProvider = 
885             (CredentialsProvider)params.getParameter(CredentialsProvider.PROVIDER);
886         if (credProvider != null) {
887             try {
888                 creds = credProvider.getCredentials(
889                     authScheme, authscope.getHost(), authscope.getPort(), true);
890             } catch (CredentialsNotAvailableException e) {
891                 LOG.warn(e.getMessage());
892             }
893             if (creds != null) {
894                 this.state.setProxyCredentials(authscope, creds);
895                 if (LOG.isDebugEnabled()) {
896                     LOG.debug(authscope + " new credentials given");
897                 }
898             }
899         } else {
900             LOG.debug("Proxy credentials provider not available");
901         }
902         return creds;
903     }
904 
905     /***
906      * @return
907      */
908     public HostConfiguration getHostConfiguration() {
909         return hostConfiguration;
910     }
911 
912     /***
913      * @return
914      */
915     public HttpState getState() {
916         return state;
917     }
918 
919     /***
920      * @return
921      */
922     public HttpConnectionManager getConnectionManager() {
923         return connectionManager;
924     }
925 
926     /***
927      * @return
928      */
929     public HttpParams getParams() {
930         return this.params;
931     }
932 }