View Javadoc
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 }