View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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</code> 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      private static SocksProxyAuthenticator SocksAuthenticator_;
60      private final Map<AuthScopeProxy, Credentials> credentialsMap_ = new HashMap<>();
61  
62      // Because this is used for the whole JVM i try to make it as less invasive as possible.
63      // But in general this might disturb other application running on the same JVM.
64      private static final class SocksProxyAuthenticator extends Authenticator {
65          private CredentialsProvider credentialsProvider_;
66  
67          @Override
68          protected PasswordAuthentication getPasswordAuthentication() {
69              // java.base/java/net/SocksSocketImpl.java line 154 ff
70              // no RequestorType set from java - we have to check the requesting prompt string
71              final boolean isProxy = Authenticator.RequestorType.PROXY.equals(getRequestorType())
72                      || "SOCKS authentication".equals(getRequestingPrompt());
73              if (!isProxy) {
74                  return null;
75              }
76  
77              final AuthScope authScope = new AuthScope(getRequestingHost(), getRequestingPort(), getRequestingScheme());
78              final Credentials credentials = credentialsProvider_.getCredentials(authScope);
79              if (credentials == null) {
80                  return null;
81              }
82  
83              return new PasswordAuthentication(credentials.getUserPrincipal().getName(),
84                      credentials.getPassword().toCharArray());
85          }
86      }
87  
88      /**
89       * Adds credentials for the specified username/password for any host/port/realm combination.
90       * The credentials may be for any authentication scheme, including NTLM, digest and basic
91       * HTTP authentication. If you are using sensitive username/password information, please do
92       * NOT use this method. If you add credentials using this method, any server that requires
93       * authentication may receive the specified username and password.
94       * @param username the username for the new credentials
95       * @param password the password for the new credentials
96       */
97      public void addCredentials(final String username, final char[] password) {
98          addCredentials(username, password, ANY_HOST, ANY_PORT, ANY_REALM);
99      }
100 
101     /**
102      * Adds credentials for the specified username/password on the specified host/port for the
103      * specified realm. The credentials may be for any authentication scheme, including NTLM,
104      * digest and basic HTTP authentication.
105      * @param username the username for the new credentials
106      * @param password the password for the new credentials
107      * @param host the host to which to the new credentials apply ({@code null} if applicable to any host)
108      * @param port the port to which to the new credentials apply (negative if applicable to any port)
109      * @param realm the realm to which to the new credentials apply ({@code null} if applicable to any realm)
110      */
111     public void addCredentials(final String username, final char[] password, final String host,
112             final int port, final String realm) {
113         final AuthScope authscope = new AuthScope(host, port, realm, ANY_SCHEME);
114         final HtmlUnitUsernamePasswordCredentials credentials =
115                     new HtmlUnitUsernamePasswordCredentials(username, password);
116         setCredentials(authscope, credentials);
117     }
118 
119     /**
120      * Adds NTLM credentials for the specified username/password on the specified host/port.
121      * @param username the username for the new credentials; should not include the domain to authenticate with;
122      *        for example: <code>"user"</code> is correct whereas <code>"DOMAIN\\user"</code> is not
123      * @param password the password for the new credentials
124      * @param host the host to which to the new credentials apply ({@code null} if applicable to any host)
125      * @param port the port to which to the new credentials apply (negative if applicable to any port)
126      * @param workstation The workstation the authentication request is originating from.
127      *        Essentially, the computer name for this machine.
128      * @param domain the domain to authenticate within
129      */
130     public void addNTLMCredentials(final String username, final char[] password, final String host,
131             final int port, final String workstation, final String domain) {
132         final AuthScope authscope = new AuthScope(host, port, ANY_REALM, ANY_SCHEME);
133         final NTCredentials credentials = new NTCredentials(username, String.valueOf(password), workstation, domain);
134         setCredentials(authscope, credentials);
135     }
136 
137     /**
138      * Adds Socks credentials for the specified username/password on the specified host/port.
139      * @param username the username for the new credentials
140      * @param password the password for the new credentials
141      * @param host the host to which to the new credentials apply ({@code null} if applicable to any host)
142      * @param port the port to which to the new credentials apply (negative if applicable to any port)
143      */
144     public void addSocksCredentials(final String username, final char[] password, final String host,
145             final int port) {
146         final AuthScope authscope = new AuthScope(host, port, ANY_REALM, ANY_SCHEME);
147         final HtmlUnitUsernamePasswordCredentials credentials =
148                     new HtmlUnitUsernamePasswordCredentials(username, password);
149         setCredentials(authscope, credentials);
150 
151         initSocksAuthenticatorIfNeeded(this);
152     }
153 
154     private static synchronized void initSocksAuthenticatorIfNeeded(final CredentialsProvider provider) {
155         if (SocksAuthenticator_ == null) {
156             SocksAuthenticator_ = new SocksProxyAuthenticator();
157             SocksAuthenticator_.credentialsProvider_ = provider;
158 
159             Authenticator.setDefault(SocksAuthenticator_);
160         }
161     }
162 
163     /**
164      * {@inheritDoc}
165      */
166     @Override
167     public synchronized void setCredentials(final AuthScope authscope, final Credentials credentials) {
168         if (authscope == null) {
169             throw new IllegalArgumentException("Authentication scope may not be null");
170         }
171 
172         if (credentials instanceof UsernamePasswordCredentials
173                 || credentials instanceof HtmlUnitUsernamePasswordCredentials
174                 || credentials instanceof NTCredentials) {
175             credentialsMap_.put(new AuthScopeProxy(authscope), credentials);
176             return;
177         }
178 
179         throw new IllegalArgumentException("Unsupported Credential type: " + credentials.getClass().getName());
180     }
181 
182     /**
183      * Find matching {@link Credentials credentials} for the given authentication scope.
184      *
185      * @param map the credentials hash map
186      * @param authscope the {@link AuthScope authentication scope}
187      * @return the credentials
188      */
189     private static Credentials matchCredentials(final Map<AuthScopeProxy, Credentials> map, final AuthScope authscope) {
190         Credentials creds = map.get(new AuthScopeProxy(authscope));
191         if (creds == null) {
192             int bestMatchFactor = -1;
193             AuthScopeProxy bestMatch = null;
194             for (final AuthScopeProxy proxy : map.keySet()) {
195                 final AuthScope current = proxy.getAuthScope();
196                 final int factor = authscope.match(current);
197                 if (factor > bestMatchFactor) {
198                     bestMatchFactor = factor;
199                     bestMatch = proxy;
200                 }
201             }
202             if (bestMatch != null) {
203                 creds = map.get(bestMatch);
204             }
205         }
206         return creds;
207     }
208 
209     /**
210      * {@inheritDoc}
211      */
212     @Override
213     public synchronized Credentials getCredentials(final AuthScope authscope) {
214         if (authscope == null) {
215             throw new IllegalArgumentException("Authentication scope may not be null");
216         }
217         return matchCredentials(credentialsMap_, authscope);
218     }
219 
220     /**
221      * Removes the credentials from the AuthScope.
222      * @param authscope the AuthScope to remove the credentials of
223      * @return whether it was removed or not
224      */
225     public synchronized boolean removeCredentials(final AuthScope authscope) {
226         if (authscope == null) {
227             throw new IllegalArgumentException("Authentication scope may not be null");
228         }
229         int bestMatchFactor = -1;
230         AuthScopeProxy bestMatch = null;
231         for (final AuthScopeProxy proxy : credentialsMap_.keySet()) {
232             final AuthScope current = proxy.getAuthScope();
233             final int factor = authscope.match(current);
234             if (factor > bestMatchFactor) {
235                 bestMatchFactor = factor;
236                 bestMatch = proxy;
237             }
238         }
239         return credentialsMap_.remove(bestMatch) != null;
240     }
241 
242     /**
243      * {@inheritDoc}
244      */
245     @Override
246     public String toString() {
247         return credentialsMap_.toString();
248     }
249 
250     /**
251      * {@inheritDoc}
252      */
253     @Override
254     public synchronized void clear() {
255         credentialsMap_.clear();
256     }
257 
258     /**
259      * We have to wrap {@link AuthScope} instances in a serializable proxy so that the
260      * {@link DefaultCredentialsProvider} class can be serialized correctly.
261      */
262     private static class AuthScopeProxy implements Serializable {
263         private AuthScope authScope_;
264 
265         AuthScopeProxy(final AuthScope authScope) {
266             authScope_ = authScope;
267         }
268 
269         public AuthScope getAuthScope() {
270             return authScope_;
271         }
272 
273         private void writeObject(final ObjectOutputStream stream) throws IOException {
274             stream.writeObject(authScope_.getHost());
275             stream.writeInt(authScope_.getPort());
276             stream.writeObject(authScope_.getRealm());
277             stream.writeObject(authScope_.getScheme());
278         }
279 
280         private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
281             final String host = (String) stream.readObject();
282             final int port = stream.readInt();
283             final String realm = (String) stream.readObject();
284             final String scheme = (String) stream.readObject();
285             authScope_ = new AuthScope(host, port, realm, scheme);
286         }
287 
288         @Override
289         public int hashCode() {
290             return authScope_.hashCode();
291         }
292 
293         @Override
294         public boolean equals(final Object obj) {
295             return obj instanceof AuthScopeProxy && authScope_.equals(((AuthScopeProxy) obj).getAuthScope());
296         }
297     }
298 }