001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.repository;
020
021import java.io.Closeable;
022import java.io.File;
023import java.util.Arrays;
024import java.util.HashMap;
025import java.util.Map;
026
027import org.eclipse.aether.RepositorySystemSession;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * A glorified map of key value pairs holding (cleartext) authentication data. Authentication contexts are used
033 * internally when network operations need to access secured repositories or proxies. Each authentication context
034 * manages the credentials required to access a single host. Unlike {@link Authentication} callbacks which exist for a
035 * potentially long time like the duration of a repository system session, an authentication context has a supposedly
036 * short lifetime and should be {@link #close() closed} as soon as the corresponding network operation has finished:
037 *
038 * <pre>
039 * AuthenticationContext context = AuthenticationContext.forRepository( session, repository );
040 * try {
041 *     // get credentials
042 *     char[] password = context.get( AuthenticationContext.PASSWORD, char[].class );
043 *     // perform network operation using retrieved credentials
044 *     ...
045 * } finally {
046 *     // erase confidential authentication data from heap memory
047 *     AuthenticationContext.close( context );
048 * }
049 * </pre>
050 *
051 * The same authentication data can often be presented using different data types, e.g. a password can be presented
052 * using a character array or (less securely) using a string. For ease of use, an authentication context treats the
053 * following groups of data types as equivalent and converts values automatically during retrieval:
054 * <ul>
055 * <li>{@code String}, {@code char[]}</li>
056 * <li>{@code String}, {@code File}</li>
057 * </ul>
058 * An authentication context is thread-safe.
059 */
060public final class AuthenticationContext implements Closeable {
061
062    /**
063     * The key used to store the username. The corresponding authentication data should be of type {@link String}.
064     */
065    public static final String USERNAME = "username";
066
067    /**
068     * The key used to store the password. The corresponding authentication data should be of type {@code char[]} or
069     * {@link String}.
070     */
071    public static final String PASSWORD = "password";
072
073    /**
074     * The key used to store the NTLM domain. The corresponding authentication data should be of type {@link String}.
075     */
076    public static final String NTLM_DOMAIN = "ntlm.domain";
077
078    /**
079     * The key used to store the NTML workstation. The corresponding authentication data should be of type
080     * {@link String}.
081     */
082    public static final String NTLM_WORKSTATION = "ntlm.workstation";
083
084    /**
085     * The key used to store the pathname to a private key file. The corresponding authentication data should be of type
086     * {@link String} or {@link File}.
087     */
088    public static final String PRIVATE_KEY_PATH = "privateKey.path";
089
090    /**
091     * The key used to store the passphrase protecting the private key. The corresponding authentication data should be
092     * of type {@code char[]} or {@link String}.
093     */
094    public static final String PRIVATE_KEY_PASSPHRASE = "privateKey.passphrase";
095
096    /**
097     * The key used to store the acceptance policy for unknown host keys. The corresponding authentication data should
098     * be of type {@link Boolean}. When querying this authentication data, the extra data should provide
099     * {@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}