001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.client;
029
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.Locale;
037import java.util.Map;
038import java.util.Queue;
039
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042import org.apache.http.FormattedHeader;
043import org.apache.http.Header;
044import org.apache.http.HttpHost;
045import org.apache.http.HttpResponse;
046import org.apache.http.annotation.Contract;
047import org.apache.http.annotation.ThreadingBehavior;
048import org.apache.http.auth.AuthOption;
049import org.apache.http.auth.AuthScheme;
050import org.apache.http.auth.AuthSchemeProvider;
051import org.apache.http.auth.AuthScope;
052import org.apache.http.auth.Credentials;
053import org.apache.http.auth.MalformedChallengeException;
054import org.apache.http.client.AuthCache;
055import org.apache.http.client.AuthenticationStrategy;
056import org.apache.http.client.CredentialsProvider;
057import org.apache.http.client.config.AuthSchemes;
058import org.apache.http.client.config.RequestConfig;
059import org.apache.http.client.protocol.HttpClientContext;
060import org.apache.http.config.Lookup;
061import org.apache.http.protocol.HTTP;
062import org.apache.http.protocol.HttpContext;
063import org.apache.http.util.Args;
064import org.apache.http.util.CharArrayBuffer;
065
066@Contract(threading = ThreadingBehavior.IMMUTABLE)
067abstract class AuthenticationStrategyImpl implements AuthenticationStrategy {
068
069    private final Log log = LogFactory.getLog(getClass());
070
071    private static final List<String> DEFAULT_SCHEME_PRIORITY =
072        Collections.unmodifiableList(Arrays.asList(
073                AuthSchemes.SPNEGO,
074                AuthSchemes.KERBEROS,
075                AuthSchemes.NTLM,
076                AuthSchemes.DIGEST,
077                AuthSchemes.BASIC));
078
079    private final int challengeCode;
080    private final String headerName;
081
082    /**
083     * @param challengeCode for example SC_PROXY_AUTHENTICATION_REQUIRED or SC_UNAUTHORIZED
084     * @param headerName for example "Proxy-Authenticate" or "WWW-Authenticate"
085     */
086    AuthenticationStrategyImpl(final int challengeCode, final String headerName) {
087        super();
088        this.challengeCode = challengeCode;
089        this.headerName = headerName;
090    }
091
092    @Override
093    public boolean isAuthenticationRequested(
094            final HttpHost authhost,
095            final HttpResponse response,
096            final HttpContext context) {
097        Args.notNull(response, "HTTP response");
098        final int status = response.getStatusLine().getStatusCode();
099        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}