View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.repository;
20  
21  import java.io.Closeable;
22  import java.io.File;
23  import java.util.Arrays;
24  import java.util.HashMap;
25  import java.util.Map;
26  
27  import org.eclipse.aether.RepositorySystemSession;
28  
29  import static java.util.Objects.requireNonNull;
30  
31  /**
32   * A glorified map of key value pairs holding (cleartext) authentication data. Authentication contexts are used
33   * internally when network operations need to access secured repositories or proxies. Each authentication context
34   * manages the credentials required to access a single host. Unlike {@link Authentication} callbacks which exist for a
35   * potentially long time like the duration of a repository system session, an authentication context has a supposedly
36   * short lifetime and should be {@link #close() closed} as soon as the corresponding network operation has finished:
37   *
38   * <pre>
39   * AuthenticationContext context = AuthenticationContext.forRepository( session, repository );
40   * try {
41   *     // get credentials
42   *     char[] password = context.get( AuthenticationContext.PASSWORD, char[].class );
43   *     // perform network operation using retrieved credentials
44   *     ...
45   * } finally {
46   *     // erase confidential authentication data from heap memory
47   *     AuthenticationContext.close( context );
48   * }
49   * </pre>
50   *
51   * The same authentication data can often be presented using different data types, e.g. a password can be presented
52   * using a character array or (less securely) using a string. For ease of use, an authentication context treats the
53   * following groups of data types as equivalent and converts values automatically during retrieval:
54   * <ul>
55   * <li>{@code String}, {@code char[]}</li>
56   * <li>{@code String}, {@code File}</li>
57   * </ul>
58   * An authentication context is thread-safe.
59   */
60  public final class AuthenticationContext implements Closeable {
61  
62      /**
63       * The key used to store the username. The corresponding authentication data should be of type {@link String}.
64       */
65      public static final String USERNAME = "username";
66  
67      /**
68       * The key used to store the password. The corresponding authentication data should be of type {@code char[]} or
69       * {@link String}.
70       */
71      public static final String PASSWORD = "password";
72  
73      /**
74       * The key used to store the NTLM domain. The corresponding authentication data should be of type {@link String}.
75       */
76      public static final String NTLM_DOMAIN = "ntlm.domain";
77  
78      /**
79       * The key used to store the NTML workstation. The corresponding authentication data should be of type
80       * {@link String}.
81       */
82      public static final String NTLM_WORKSTATION = "ntlm.workstation";
83  
84      /**
85       * The key used to store the pathname to a private key file. The corresponding authentication data should be of type
86       * {@link String} or {@link File}.
87       */
88      public static final String PRIVATE_KEY_PATH = "privateKey.path";
89  
90      /**
91       * The key used to store the passphrase protecting the private key. The corresponding authentication data should be
92       * of type {@code char[]} or {@link String}.
93       */
94      public static final String PRIVATE_KEY_PASSPHRASE = "privateKey.passphrase";
95  
96      /**
97       * The key used to store the acceptance policy for unknown host keys. The corresponding authentication data should
98       * be of type {@link Boolean}. When querying this authentication data, the extra data should provide
99       * {@link #HOST_KEY_REMOTE} and {@link #HOST_KEY_LOCAL}, e.g. to enable a well-founded decision of the user during
100      * an interactive prompt.
101      */
102     public static final String HOST_KEY_ACCEPTANCE = "hostKey.acceptance";
103 
104     /**
105      * The key used to store the fingerprint of the public key advertised by remote host. Note that this key is used to
106      * query the extra data passed to {@link #get(String, Map, Class)} when getting {@link #HOST_KEY_ACCEPTANCE}, not
107      * the authentication data in a context.
108      */
109     public static final String HOST_KEY_REMOTE = "hostKey.remote";
110 
111     /**
112      * The key used to store the fingerprint of the public key expected from remote host as recorded in a known hosts
113      * database. Note that this key is used to query the extra data passed to {@link #get(String, Map, Class)} when
114      * getting {@link #HOST_KEY_ACCEPTANCE}, not the authentication data in a context.
115      */
116     public static final String HOST_KEY_LOCAL = "hostKey.local";
117 
118     /**
119      * The key used to store the SSL context. The corresponding authentication data should be of type
120      * {@link javax.net.ssl.SSLContext}.
121      */
122     public static final String SSL_CONTEXT = "ssl.context";
123 
124     /**
125      * The key used to store the SSL hostname verifier. The corresponding authentication data should be of type
126      * {@link javax.net.ssl.HostnameVerifier}.
127      */
128     public static final String SSL_HOSTNAME_VERIFIER = "ssl.hostnameVerifier";
129 
130     private final RepositorySystemSession session;
131 
132     private final RemoteRepository repository;
133 
134     private final Proxy proxy;
135 
136     private final Authentication auth;
137 
138     private final Map<String, Object> authData;
139 
140     private boolean fillingAuthData;
141 
142     /**
143      * Gets an authentication context for the specified repository.
144      *
145      * @param session The repository system session during which the repository is accessed, may be {@code null}.
146      * @param repository The repository for which to create an authentication context, must not be {@code null}.
147      * @return An authentication context for the repository or {@code null} if no authentication is configured for it.
148      */
149     public static AuthenticationContext forRepository(RepositorySystemSession session, RemoteRepository repository) {
150         return newInstance(session, repository, null, repository.getAuthentication());
151     }
152 
153     /**
154      * Gets an authentication context for the proxy of the specified repository.
155      *
156      * @param session The repository system session during which the repository is accessed, may be {@code null}.
157      * @param repository The repository for whose proxy to create an authentication context, must not be {@code null}.
158      * @return An authentication context for the proxy or {@code null} if no proxy is set or no authentication is
159      *         configured for it.
160      */
161     public static AuthenticationContext forProxy(RepositorySystemSession session, RemoteRepository repository) {
162         Proxy proxy = repository.getProxy();
163         return newInstance(session, repository, proxy, (proxy != null) ? proxy.getAuthentication() : null);
164     }
165 
166     private static AuthenticationContext newInstance(
167             RepositorySystemSession session, RemoteRepository repository, Proxy proxy, Authentication auth) {
168         if (auth == null) {
169             return null;
170         }
171         return new AuthenticationContext(session, repository, proxy, auth);
172     }
173 
174     private AuthenticationContext(
175             RepositorySystemSession session, RemoteRepository repository, Proxy proxy, Authentication auth) {
176         this.session = session;
177         this.repository = requireNonNull(repository, "null repository");
178         this.proxy = proxy;
179         this.auth = auth;
180         authData = new HashMap<>();
181     }
182 
183     /**
184      * Gets the repository system session during which the authentication happens (if within session).
185      *
186      * @return The repository system session, may be {@code null} if context is created outside of session.
187      */
188     public RepositorySystemSession getSession() {
189         return session;
190     }
191 
192     /**
193      * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
194      * this authentication context does not apply to the repository's host but rather the proxy.
195      *
196      * @return The repository to be contacted, never {@code null}.
197      */
198     public RemoteRepository getRepository() {
199         return repository;
200     }
201 
202     /**
203      * Gets the proxy (if any) to be authenticated with.
204      *
205      * @return The proxy or {@code null} if authenticating directly with the repository's host.
206      */
207     public Proxy getProxy() {
208         return proxy;
209     }
210 
211     /**
212      * Gets the authentication data for the specified key.
213      *
214      * @param key The key whose authentication data should be retrieved, must not be {@code null}.
215      * @return The requested authentication data or {@code null} if none.
216      */
217     public String get(String key) {
218         return get(key, null, String.class);
219     }
220 
221     /**
222      * Gets the authentication data for the specified key.
223      *
224      * @param <T> The data type of the authentication data.
225      * @param key The key whose authentication data should be retrieved, must not be {@code null}.
226      * @param type The expected type of the authentication data, must not be {@code null}.
227      * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
228      */
229     public <T> T get(String key, Class<T> type) {
230         return get(key, null, type);
231     }
232 
233     /**
234      * Gets the authentication data for the specified key.
235      *
236      * @param <T> The data type of the authentication data.
237      * @param key The key whose authentication data should be retrieved, must not be {@code null}.
238      * @param data Any (read-only) extra data in form of key value pairs that might be useful when getting the
239      *            authentication data, may be {@code null}.
240      * @param type The expected type of the authentication data, must not be {@code null}.
241      * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
242      */
243     public <T> T get(String key, Map<String, String> data, Class<T> type) {
244         requireNonNull(key, "authentication key cannot be null");
245         if (key.isEmpty()) {
246             throw new IllegalArgumentException("authentication key cannot be empty");
247         }
248 
249         Object value;
250         synchronized (authData) {
251             value = authData.get(key);
252             if (value == null && !authData.containsKey(key) && !fillingAuthData) {
253                 if (auth != null) {
254                     try {
255                         fillingAuthData = true;
256                         auth.fill(this, key, data);
257                     } finally {
258                         fillingAuthData = false;
259                     }
260                     value = authData.get(key);
261                 }
262                 if (value == null) {
263                     authData.put(key, null);
264                 }
265             }
266         }
267 
268         return convert(value, type);
269     }
270 
271     private <T> T convert(Object value, Class<T> type) {
272         if (!type.isInstance(value)) {
273             if (String.class.equals(type)) {
274                 if (value instanceof File) {
275                     value = ((File) value).getPath();
276                 } else if (value instanceof char[]) {
277                     value = new String((char[]) value);
278                 }
279             } else if (File.class.equals(type)) {
280                 if (value instanceof String) {
281                     value = new File((String) value);
282                 }
283             } else if (char[].class.equals(type)) {
284                 if (value instanceof String) {
285                     value = ((String) value).toCharArray();
286                 }
287             }
288         }
289 
290         if (type.isInstance(value)) {
291             return type.cast(value);
292         }
293 
294         return null;
295     }
296 
297     /**
298      * Puts the specified authentication data into this context. This method should only be called from implementors of
299      * {@link Authentication#fill(AuthenticationContext, String, Map)}. Passed in character arrays are not cloned and
300      * become owned by this context, i.e. get erased when the context gets closed.
301      *
302      * @param key The key to associate the authentication data with, must not be {@code null}.
303      * @param value The (cleartext) authentication data to store, may be {@code null}.
304      */
305     public void put(String key, Object value) {
306         requireNonNull(key, "authentication key cannot be null");
307         if (key.isEmpty()) {
308             throw new IllegalArgumentException("authentication key cannot be empty");
309         }
310 
311         synchronized (authData) {
312             Object oldValue = authData.put(key, value);
313             if (oldValue instanceof char[]) {
314                 Arrays.fill((char[]) oldValue, '\0');
315             }
316         }
317     }
318 
319     /**
320      * Closes this authentication context and erases sensitive authentication data from heap memory. Closing an already
321      * closed context has no effect.
322      */
323     public void close() {
324         synchronized (authData) {
325             for (Object value : authData.values()) {
326                 if (value instanceof char[]) {
327                     Arrays.fill((char[]) value, '\0');
328                 }
329             }
330             authData.clear();
331         }
332     }
333 
334     /**
335      * Closes the specified authentication context. This is a convenience method doing a {@code null} check before
336      * calling {@link #close()} on the given context.
337      *
338      * @param context The authentication context to close, may be {@code null}.
339      */
340     public static void close(AuthenticationContext context) {
341         if (context != null) {
342             context.close();
343         }
344     }
345 }