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 }