View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.http.impl.client;
29  
30  import java.util.Arrays;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.HashMap;
34  import java.util.LinkedList;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.Queue;
39  
40  import org.apache.commons.logging.Log;
41  import org.apache.commons.logging.LogFactory;
42  import org.apache.http.FormattedHeader;
43  import org.apache.http.Header;
44  import org.apache.http.HttpHost;
45  import org.apache.http.HttpResponse;
46  import org.apache.http.annotation.Contract;
47  import org.apache.http.annotation.ThreadingBehavior;
48  import org.apache.http.auth.AuthOption;
49  import org.apache.http.auth.AuthScheme;
50  import org.apache.http.auth.AuthSchemeProvider;
51  import org.apache.http.auth.AuthScope;
52  import org.apache.http.auth.Credentials;
53  import org.apache.http.auth.MalformedChallengeException;
54  import org.apache.http.client.AuthCache;
55  import org.apache.http.client.AuthenticationStrategy;
56  import org.apache.http.client.CredentialsProvider;
57  import org.apache.http.client.config.AuthSchemes;
58  import org.apache.http.client.config.RequestConfig;
59  import org.apache.http.client.protocol.HttpClientContext;
60  import org.apache.http.config.Lookup;
61  import org.apache.http.protocol.HTTP;
62  import org.apache.http.protocol.HttpContext;
63  import org.apache.http.util.Args;
64  import org.apache.http.util.CharArrayBuffer;
65  
66  @Contract(threading = ThreadingBehavior.IMMUTABLE)
67  abstract class AuthenticationStrategyImpl implements AuthenticationStrategy {
68  
69      private final Log log = LogFactory.getLog(getClass());
70  
71      private static final List<String> DEFAULT_SCHEME_PRIORITY =
72          Collections.unmodifiableList(Arrays.asList(
73                  AuthSchemes.SPNEGO,
74                  AuthSchemes.KERBEROS,
75                  AuthSchemes.NTLM,
76                  AuthSchemes.DIGEST,
77                  AuthSchemes.BASIC));
78  
79      private final int challengeCode;
80      private final String headerName;
81  
82      /**
83       * @param challengeCode for example SC_PROXY_AUTHENTICATION_REQUIRED or SC_UNAUTHORIZED
84       * @param headerName for example "Proxy-Authenticate" or "WWW-Authenticate"
85       */
86      AuthenticationStrategyImpl(final int challengeCode, final String headerName) {
87          super();
88          this.challengeCode = challengeCode;
89          this.headerName = headerName;
90      }
91  
92      @Override
93      public boolean isAuthenticationRequested(
94              final HttpHost authhost,
95              final HttpResponse response,
96              final HttpContext context) {
97          Args.notNull(response, "HTTP response");
98          final int status = response.getStatusLine().getStatusCode();
99          return status == this.challengeCode;
100     }
101 
102     /**
103      * Generates a map of challenge auth-scheme =&gt; Header entries.
104      *
105      * @return map: key=lower-cased auth-scheme name, value=Header that contains the challenge
106      */
107     @Override
108     public Map<String, Header> getChallenges(
109             final HttpHost authhost,
110             final HttpResponse response,
111             final HttpContext context) throws MalformedChallengeException {
112         Args.notNull(response, "HTTP response");
113         final Header[] headers = response.getHeaders(this.headerName);
114         final Map<String, Header> map = new HashMap<String, Header>(headers.length);
115         for (final Header header : headers) {
116             final CharArrayBuffer buffer;
117             int pos;
118             if (header instanceof FormattedHeader) {
119                 buffer = ((FormattedHeader) header).getBuffer();
120                 pos = ((FormattedHeader) header).getValuePos();
121             } else {
122                 final String s = header.getValue();
123                 if (s == null) {
124                     throw new MalformedChallengeException("Header value is null");
125                 }
126                 buffer = new CharArrayBuffer(s.length());
127                 buffer.append(s);
128                 pos = 0;
129             }
130             while (pos < buffer.length() && HTTP.isWhitespace(buffer.charAt(pos))) {
131                 pos++;
132             }
133             final int beginIndex = pos;
134             while (pos < buffer.length() && !HTTP.isWhitespace(buffer.charAt(pos))) {
135                 pos++;
136             }
137             final int endIndex = pos;
138             final String s = buffer.substring(beginIndex, endIndex);
139             map.put(s.toLowerCase(Locale.ROOT), header);
140         }
141         return map;
142     }
143 
144     abstract Collection<String> getPreferredAuthSchemes(RequestConfig config);
145 
146     @Override
147     public Queue<AuthOption> select(
148             final Map<String, Header> challenges,
149             final HttpHost authhost,
150             final HttpResponse response,
151             final HttpContext context) throws MalformedChallengeException {
152         Args.notNull(challenges, "Map of auth challenges");
153         Args.notNull(authhost, "Host");
154         Args.notNull(response, "HTTP response");
155         Args.notNull(context, "HTTP context");
156         final HttpClientContext clientContext = HttpClientContext.adapt(context);
157 
158         final Queue<AuthOption> options = new LinkedList<AuthOption>();
159         final Lookup<AuthSchemeProvider> registry = clientContext.getAuthSchemeRegistry();
160         if (registry == null) {
161             this.log.debug("Auth scheme registry not set in the context");
162             return options;
163         }
164         final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
165         if (credsProvider == null) {
166             this.log.debug("Credentials provider not set in the context");
167             return options;
168         }
169         final RequestConfig config = clientContext.getRequestConfig();
170         Collection<String> authPrefs = getPreferredAuthSchemes(config);
171         if (authPrefs == null) {
172             authPrefs = DEFAULT_SCHEME_PRIORITY;
173         }
174         if (this.log.isDebugEnabled()) {
175             this.log.debug("Authentication schemes in the order of preference: " + authPrefs);
176         }
177 
178         for (final String id: authPrefs) {
179             final Header challenge = challenges.get(id.toLowerCase(Locale.ROOT));
180             if (challenge != null) {
181                 final AuthSchemeProvider authSchemeProvider = registry.lookup(id);
182                 if (authSchemeProvider == null) {
183                     if (this.log.isWarnEnabled()) {
184                         this.log.warn("Authentication scheme " + id + " not supported");
185                         // Try again
186                     }
187                     continue;
188                 }
189                 final AuthScheme authScheme = authSchemeProvider.create(context);
190                 authScheme.processChallenge(challenge);
191 
192                 final AuthScope authScope = new AuthScope(
193                         authhost.getHostName(),
194                         authhost.getPort(),
195                         authScheme.getRealm(),
196                         authScheme.getSchemeName());
197 
198                 final Credentials credentials = credsProvider.getCredentials(authScope);
199                 if (credentials != null) {
200                     options.add(new AuthOption(authScheme, credentials));
201                 }
202             } else {
203                 if (this.log.isDebugEnabled()) {
204                     this.log.debug("Challenge for " + id + " authentication scheme not available");
205                     // Try again
206                 }
207             }
208         }
209         return options;
210     }
211 
212     @Override
213     public void authSucceeded(
214             final HttpHost authhost, final AuthScheme authScheme, final HttpContext context) {
215         Args.notNull(authhost, "Host");
216         Args.notNull(authScheme, "Auth scheme");
217         Args.notNull(context, "HTTP context");
218 
219         final HttpClientContext clientContext = HttpClientContext.adapt(context);
220 
221         if (isCachable(authScheme)) {
222             AuthCache authCache = clientContext.getAuthCache();
223             if (authCache == null) {
224                 authCache = new BasicAuthCache();
225                 clientContext.setAuthCache(authCache);
226             }
227             if (this.log.isDebugEnabled()) {
228                 this.log.debug("Caching '" + authScheme.getSchemeName() +
229                         "' auth scheme for " + authhost);
230             }
231             authCache.put(authhost, authScheme);
232         }
233     }
234 
235     protected boolean isCachable(final AuthScheme authScheme) {
236         if (authScheme == null || !authScheme.isComplete()) {
237             return false;
238         }
239         final String schemeName = authScheme.getSchemeName();
240         return schemeName.equalsIgnoreCase(AuthSchemes.BASIC) ||
241                 schemeName.equalsIgnoreCase(AuthSchemes.DIGEST);
242     }
243 
244     @Override
245     public void authFailed(
246             final HttpHost authhost, final AuthScheme authScheme, final HttpContext context) {
247         Args.notNull(authhost, "Host");
248         Args.notNull(context, "HTTP context");
249 
250         final HttpClientContext clientContext = HttpClientContext.adapt(context);
251 
252         final AuthCache authCache = clientContext.getAuthCache();
253         if (authCache != null) {
254             if (this.log.isDebugEnabled()) {
255                 this.log.debug("Clearing cached auth scheme for " + authhost);
256             }
257             authCache.remove(authhost);
258         }
259     }
260 
261 }