1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit;
16
17 import java.io.IOException;
18 import java.io.ObjectInputStream;
19 import java.io.ObjectOutputStream;
20 import java.io.Serializable;
21 import java.net.Authenticator;
22 import java.net.PasswordAuthentication;
23 import java.util.HashMap;
24 import java.util.Map;
25
26 import org.apache.http.auth.AuthScope;
27 import org.apache.http.auth.Credentials;
28 import org.apache.http.auth.NTCredentials;
29 import org.apache.http.auth.UsernamePasswordCredentials;
30 import org.apache.http.client.CredentialsProvider;
31 import org.htmlunit.httpclient.HtmlUnitUsernamePasswordCredentials;
32
33 /**
34 * Default HtmlUnit implementation of the {@code CredentialsProvider} interface. Provides
35 * credentials for both web servers and proxies. Supports Digest
36 * authentication, Socks authentication and Basic HTTP authentication.
37 *
38 * @author Daniel Gredler
39 * @author Vikram Shitole
40 * @author Marc Guillemot
41 * @author Ahmed Ashour
42 * @author Nicolas Belisle
43 * @author Ronald Brill
44 */
45 public class DefaultCredentialsProvider implements CredentialsProvider, Serializable {
46
47 /** The {@code null} value represents any host. */
48 public static final String ANY_HOST = null;
49
50 /** The {@code -1} value represents any port. */
51 public static final int ANY_PORT = -1;
52
53 /** The {@code null} value represents any realm. */
54 public static final String ANY_REALM = null;
55
56 /** The {@code null} value represents any authentication scheme. */
57 public static final String ANY_SCHEME = null;
58
59 // volatile: written only inside the synchronized initSocksAuthenticatorIfNeeded,
60 // but declared volatile to guard against unsynchronized reads in future refactors.
61 private static volatile SocksProxyAuthenticator SocksAuthenticator_;
62 private final Map<AuthScopeProxy, Credentials> credentialsMap_ = new HashMap<>();
63
64 // Because this is used for the whole JVM I try to make it as less invasive as possible.
65 // But in general this might disturb other application running on the same JVM.
66 private static final class SocksProxyAuthenticator extends Authenticator {
67 private CredentialsProvider credentialsProvider_;
68
69 @Override
70 protected PasswordAuthentication getPasswordAuthentication() {
71 // java.base/java/net/SocksSocketImpl.java line 154 ff
72 // no RequestorType set from java - we have to check the requesting prompt string
73 final boolean isProxy = Authenticator.RequestorType.PROXY.equals(getRequestorType())
74 || "SOCKS authentication".equals(getRequestingPrompt());
75 if (!isProxy) {
76 return null;
77 }
78
79 final AuthScope authScope = new AuthScope(getRequestingHost(), getRequestingPort(), getRequestingScheme());
80 final Credentials credentials = credentialsProvider_.getCredentials(authScope);
81 if (credentials == null) {
82 return null;
83 }
84
85 return new PasswordAuthentication(credentials.getUserPrincipal().getName(),
86 credentials.getPassword().toCharArray());
87 }
88 }
89
90 /**
91 * Adds credentials for the specified username/password for any host/port/realm combination.
92 * The credentials may be for any authentication scheme, including NTLM, digest and basic
93 * HTTP authentication. If you are using sensitive username/password information, please do
94 * NOT use this method. If you add credentials using this method, any server that requires
95 * authentication may receive the specified username and password.
96 *
97 * @param username the username for the new credentials
98 * @param password the password for the new credentials
99 * @see #addCredentials(String, char[], String, int, String)
100 */
101 public void addCredentials(final String username, final char[] password) {
102 addCredentials(username, password, ANY_HOST, ANY_PORT, ANY_REALM);
103 }
104
105 /**
106 * Adds credentials for the specified username/password on the specified host/port for the
107 * specified realm. The credentials may be for any authentication scheme, including NTLM,
108 * digest and basic HTTP authentication.
109 *
110 * @param username the username for the new credentials
111 * @param password the password for the new credentials
112 * @param host the host to which to the new credentials apply ({@code null} if applicable to any host)
113 * @param port the port to which to the new credentials apply (negative if applicable to any port)
114 * @param realm the realm to which to the new credentials apply ({@code null} if applicable to any realm)
115 */
116 public void addCredentials(final String username, final char[] password, final String host,
117 final int port, final String realm) {
118 final AuthScope authscope = new AuthScope(host, port, realm, ANY_SCHEME);
119 final HtmlUnitUsernamePasswordCredentials credentials =
120 new HtmlUnitUsernamePasswordCredentials(username, password);
121 setCredentials(authscope, credentials);
122 }
123
124 /**
125 * Adds NTLM credentials for the specified username/password on the specified host/port.
126 *
127 * <p>Note: due to a limitation of the underlying {@link NTCredentials} class, the password
128 * is converted to a {@link String} internally and cannot be zeroed from memory after use.
129 * </p>
130 *
131 * @param username the username for the new credentials; should not include the domain to authenticate with;
132 * for example: {@code "user"} is correct whereas {@code "DOMAIN\\user"} is not
133 * @param password the password for the new credentials
134 * @param host the host to which to the new credentials apply ({@code null} if applicable to any host)
135 * @param port the port to which to the new credentials apply (negative if applicable to any port)
136 * @param workstation the workstation the authentication request is originating from;
137 * essentially, the computer name for this machine
138 * @param domain the domain to authenticate within
139 */
140 public void addNTLMCredentials(final String username, final char[] password, final String host,
141 final int port, final String workstation, final String domain) {
142 final AuthScope authscope = new AuthScope(host, port, ANY_REALM, ANY_SCHEME);
143 final NTCredentials credentials = new NTCredentials(username, String.valueOf(password), workstation, domain);
144 setCredentials(authscope, credentials);
145 }
146
147 /**
148 * Adds Socks credentials for the specified username/password on the specified host/port.
149 *
150 * <p>Note: the SOCKS authenticator is a JVM-wide singleton. If multiple
151 * {@link DefaultCredentialsProvider} instances add SOCKS credentials, only the first
152 * instance's credentials will be used for SOCKS authentication.
153 * </p>
154 *
155 * @param username the username for the new credentials
156 * @param password the password for the new credentials
157 * @param host the host to which to the new credentials apply ({@code null} if applicable to any host)
158 * @param port the port to which to the new credentials apply (negative if applicable to any port)
159 */
160 public void addSocksCredentials(final String username, final char[] password, final String host,
161 final int port) {
162 final AuthScope authscope = new AuthScope(host, port, ANY_REALM, ANY_SCHEME);
163 final HtmlUnitUsernamePasswordCredentials credentials =
164 new HtmlUnitUsernamePasswordCredentials(username, password);
165 setCredentials(authscope, credentials);
166
167 initSocksAuthenticatorIfNeeded(this);
168 }
169
170 private static synchronized void initSocksAuthenticatorIfNeeded(final CredentialsProvider provider) {
171 if (SocksAuthenticator_ == null) {
172 SocksAuthenticator_ = new SocksProxyAuthenticator();
173 SocksAuthenticator_.credentialsProvider_ = provider;
174
175 Authenticator.setDefault(SocksAuthenticator_);
176 }
177 }
178
179 /**
180 * {@inheritDoc}
181 *
182 * @throws IllegalArgumentException if {@code authscope} is {@code null}, if {@code credentials}
183 * is {@code null}, or if the credentials type is not one of
184 * {@link UsernamePasswordCredentials}, {@link HtmlUnitUsernamePasswordCredentials},
185 * or {@link NTCredentials}
186 */
187 @Override
188 public synchronized void setCredentials(final AuthScope authscope, final Credentials credentials) {
189 if (authscope == null) {
190 throw new IllegalArgumentException("Authentication scope may not be null");
191 }
192 if (credentials == null) {
193 throw new IllegalArgumentException("Credentials may not be null");
194 }
195
196 if (credentials instanceof UsernamePasswordCredentials
197 || credentials instanceof HtmlUnitUsernamePasswordCredentials
198 || credentials instanceof NTCredentials) {
199 credentialsMap_.put(new AuthScopeProxy(authscope), credentials);
200 return;
201 }
202
203 throw new IllegalArgumentException("Unsupported Credential type: " + credentials.getClass().getName());
204 }
205
206 // Callers must hold the monitor on the enclosing DefaultCredentialsProvider instance.
207 private static Credentials matchCredentials(final Map<AuthScopeProxy, Credentials> map, final AuthScope authscope) {
208 Credentials creds = map.get(new AuthScopeProxy(authscope));
209 if (creds == null) {
210 int bestMatchFactor = -1;
211 AuthScopeProxy bestMatch = null;
212 for (final AuthScopeProxy proxy : map.keySet()) {
213 final AuthScope current = proxy.getAuthScope();
214 final int factor = authscope.match(current);
215 if (factor > bestMatchFactor) {
216 bestMatchFactor = factor;
217 bestMatch = proxy;
218 }
219 }
220 if (bestMatch != null) {
221 creds = map.get(bestMatch);
222 }
223 }
224 return creds;
225 }
226
227 /**
228 * {@inheritDoc}
229 */
230 @Override
231 public synchronized Credentials getCredentials(final AuthScope authscope) {
232 if (authscope == null) {
233 throw new IllegalArgumentException("Authentication scope may not be null");
234 }
235 return matchCredentials(credentialsMap_, authscope);
236 }
237
238 /**
239 * Removes the credentials that best match the given {@link AuthScope}.
240 * An exact scope match is preferred over a fuzzy best-match, consistent
241 * with the resolution strategy used by {@link #getCredentials(AuthScope)}.
242 *
243 * @param authscope the {@link AuthScope} whose credentials should be removed;
244 * must not be {@code null}
245 * @return {@code true} if credentials were removed; {@code false} if no match was found
246 */
247 public synchronized boolean removeCredentials(final AuthScope authscope) {
248 if (authscope == null) {
249 throw new IllegalArgumentException("Authentication scope may not be null");
250 }
251
252 // try exact match first, consistent with matchCredentials
253 final AuthScopeProxy exactKey = new AuthScopeProxy(authscope);
254 if (credentialsMap_.remove(exactKey) != null) {
255 return true;
256 }
257
258 // fall back to best-match scoring
259 int bestMatchFactor = -1;
260 AuthScopeProxy bestMatch = null;
261 for (final AuthScopeProxy proxy : credentialsMap_.keySet()) {
262 final AuthScope current = proxy.getAuthScope();
263 final int factor = authscope.match(current);
264 if (factor > bestMatchFactor) {
265 bestMatchFactor = factor;
266 bestMatch = proxy;
267 }
268 }
269 return credentialsMap_.remove(bestMatch) != null;
270 }
271
272 /**
273 * {@inheritDoc}
274 */
275 @Override
276 public String toString() {
277 return credentialsMap_.toString();
278 }
279
280 /**
281 * {@inheritDoc}
282 */
283 @Override
284 public synchronized void clear() {
285 credentialsMap_.clear();
286 }
287
288 /**
289 * Serialization proxy for {@link AuthScope}, which is not itself {@link Serializable}.
290 * Required to allow {@link DefaultCredentialsProvider} to be serialized correctly.
291 */
292 private static class AuthScopeProxy implements Serializable {
293 private AuthScope authScope_;
294
295 AuthScopeProxy(final AuthScope authScope) {
296 authScope_ = authScope;
297 }
298
299 public AuthScope getAuthScope() {
300 return authScope_;
301 }
302
303 private void writeObject(final ObjectOutputStream stream) throws IOException {
304 stream.writeObject(authScope_.getHost());
305 stream.writeInt(authScope_.getPort());
306 stream.writeObject(authScope_.getRealm());
307 stream.writeObject(authScope_.getScheme());
308 }
309
310 private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
311 final String host = (String) stream.readObject();
312 final int port = stream.readInt();
313 final String realm = (String) stream.readObject();
314 final String scheme = (String) stream.readObject();
315 authScope_ = new AuthScope(host, port, realm, scheme);
316 }
317
318 @Override
319 public int hashCode() {
320 return authScope_.hashCode();
321 }
322
323 @Override
324 public boolean equals(final Object obj) {
325 return obj instanceof AuthScopeProxy asp && authScope_.equals(asp.getAuthScope());
326 }
327 }
328 }