~testplan-team/testplan/source-collection

« back to all changes in this revision

Viewing changes to commons-httpclient-3.1/src/java/org/apache/commons/httpclient/HttpMethodDirector.java

  • Committer: edA-qa mort-ora-y
  • Date: 2009-01-28 15:36:51 UTC
  • Revision ID: eda-qa@disemia.com-20090128153651-u4uzd035zyjea16j
adding httpclient

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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: 486658 $
 
4
 * $Date: 2006-12-13 15:05:50 +0100 (Wed, 13 Dec 2006) $
 
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
}