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 => 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}