001package org.eclipse.aether.repository;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 * 
012 *  http://www.apache.org/licenses/LICENSE-2.0
013 * 
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.io.Closeable;
023import java.io.File;
024import java.util.Arrays;
025import java.util.HashMap;
026import java.util.Map;
027import static java.util.Objects.requireNonNull;
028
029import org.eclipse.aether.RepositorySystemSession;
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
061    implements Closeable
062{
063
064    /**
065     * The key used to store the username. The corresponding authentication data should be of type {@link String}.
066     */
067    public static final String USERNAME = "username";
068
069    /**
070     * The key used to store the password. The corresponding authentication data should be of type {@code char[]} or
071     * {@link String}.
072     */
073    public static final String PASSWORD = "password";
074
075    /**
076     * The key used to store the NTLM domain. The corresponding authentication data should be of type {@link String}.
077     */
078    public static final String NTLM_DOMAIN = "ntlm.domain";
079
080    /**
081     * The key used to store the NTML workstation. The corresponding authentication data should be of type
082     * {@link String}.
083     */
084    public static final String NTLM_WORKSTATION = "ntlm.workstation";
085
086    /**
087     * The key used to store the pathname to a private key file. The corresponding authentication data should be of type
088     * {@link String} or {@link File}.
089     */
090    public static final String PRIVATE_KEY_PATH = "privateKey.path";
091
092    /**
093     * The key used to store the passphrase protecting the private key. The corresponding authentication data should be
094     * of type {@code char[]} or {@link String}.
095     */
096    public static final String PRIVATE_KEY_PASSPHRASE = "privateKey.passphrase";
097
098    /**
099     * The key used to store the acceptance policy for unknown host keys. The corresponding authentication data should
100     * be of type {@link Boolean}. When querying this authentication data, the extra data should provide
101     * {@link #HOST_KEY_REMOTE} and {@link #HOST_KEY_LOCAL}, e.g. to enable a well-founded decision of the user during
102     * an interactive prompt.
103     */
104    public static final String HOST_KEY_ACCEPTANCE = "hostKey.acceptance";
105
106    /**
107     * The key used to store the fingerprint of the public key advertised by remote host. Note that this key is used to
108     * query the extra data passed to {@link #get(String, Map, Class)} when getting {@link #HOST_KEY_ACCEPTANCE}, not
109     * the authentication data in a context.
110     */
111    public static final String HOST_KEY_REMOTE = "hostKey.remote";
112
113    /**
114     * The key used to store the fingerprint of the public key expected from remote host as recorded in a known hosts
115     * database. Note that this key is used to query the extra data passed to {@link #get(String, Map, Class)} when
116     * getting {@link #HOST_KEY_ACCEPTANCE}, not the authentication data in a context.
117     */
118    public static final String HOST_KEY_LOCAL = "hostKey.local";
119
120    /**
121     * The key used to store the SSL context. The corresponding authentication data should be of type
122     * {@link javax.net.ssl.SSLContext}.
123     */
124    public static final String SSL_CONTEXT = "ssl.context";
125
126    /**
127     * The key used to store the SSL hostname verifier. The corresponding authentication data should be of type
128     * {@link javax.net.ssl.HostnameVerifier}.
129     */
130    public static final String SSL_HOSTNAME_VERIFIER = "ssl.hostnameVerifier";
131
132    private final RepositorySystemSession session;
133
134    private final RemoteRepository repository;
135
136    private final Proxy proxy;
137
138    private final Authentication auth;
139
140    private final Map<String, Object> authData;
141
142    private boolean fillingAuthData;
143
144    /**
145     * Gets an authentication context for the specified repository.
146     * 
147     * @param session The repository system session during which the repository is accessed, must not be {@code null}.
148     * @param repository The repository for which to create an authentication context, must not be {@code null}.
149     * @return An authentication context for the repository or {@code null} if no authentication is configured for it.
150     */
151    public static AuthenticationContext forRepository( RepositorySystemSession session, RemoteRepository repository )
152    {
153        return newInstance( session, repository, null, repository.getAuthentication() );
154    }
155
156    /**
157     * Gets an authentication context for the proxy of the specified repository.
158     * 
159     * @param session The repository system session during which the repository is accessed, must not be {@code null}.
160     * @param repository The repository for whose proxy to create an authentication context, must not be {@code null}.
161     * @return An authentication context for the proxy or {@code null} if no proxy is set or no authentication is
162     *         configured for it.
163     */
164    public static AuthenticationContext forProxy( RepositorySystemSession session, RemoteRepository repository )
165    {
166        Proxy proxy = repository.getProxy();
167        return newInstance( session, repository, proxy, ( proxy != null ) ? proxy.getAuthentication() : null );
168    }
169
170    private static AuthenticationContext newInstance( RepositorySystemSession session, RemoteRepository repository,
171                                                      Proxy proxy, Authentication auth )
172    {
173        if ( auth == null )
174        {
175            return null;
176        }
177        return new AuthenticationContext( session, repository, proxy, auth );
178    }
179
180    private AuthenticationContext( RepositorySystemSession session, RemoteRepository repository, Proxy proxy,
181                                   Authentication auth )
182    {
183        this.session = requireNonNull( session, "repository system session cannot be null" );
184        this.repository = repository;
185        this.proxy = proxy;
186        this.auth = auth;
187        authData = new HashMap<String, Object>();
188    }
189
190    /**
191     * Gets the repository system session during which the authentication happens.
192     * 
193     * @return The repository system session, never {@code null}.
194     */
195    public RepositorySystemSession getSession()
196    {
197        return session;
198    }
199
200    /**
201     * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
202     * this authentication context does not apply to the repository's host but rather the proxy.
203     * 
204     * @return The repository to be contacted, never {@code null}.
205     */
206    public RemoteRepository getRepository()
207    {
208        return repository;
209    }
210
211    /**
212     * Gets the proxy (if any) to be authenticated with.
213     * 
214     * @return The proxy or {@code null} if authenticating directly with the repository's host.
215     */
216    public Proxy getProxy()
217    {
218        return proxy;
219    }
220
221    /**
222     * Gets the authentication data for the specified key.
223     * 
224     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
225     * @return The requested authentication data or {@code null} if none.
226     */
227    public String get( String key )
228    {
229        return get( key, null, String.class );
230    }
231
232    /**
233     * Gets the authentication data for the specified key.
234     * 
235     * @param <T> The data type of the authentication data.
236     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
237     * @param type The expected type of the authentication data, must not be {@code null}.
238     * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
239     */
240    public <T> T get( String key, Class<T> type )
241    {
242        return get( key, null, type );
243    }
244
245    /**
246     * Gets the authentication data for the specified key.
247     * 
248     * @param <T> The data type of the authentication data.
249     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
250     * @param data Any (read-only) extra data in form of key value pairs that might be useful when getting the
251     *            authentication data, may be {@code null}.
252     * @param type The expected type of the authentication data, must not be {@code null}.
253     * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
254     */
255    public <T> T get( String key, Map<String, String> data, Class<T> type )
256    {
257        requireNonNull( key, "authentication key cannot be null" );
258        if ( key.length() == 0 )
259        {
260            throw new IllegalArgumentException( "authentication key cannot be empty" );
261        }
262
263        Object value;
264        synchronized ( authData )
265        {
266            value = authData.get( key );
267            if ( value == null && !authData.containsKey( key ) && !fillingAuthData )
268            {
269                if ( auth != null )
270                {
271                    try
272                    {
273                        fillingAuthData = true;
274                        auth.fill( this, key, data );
275                    }
276                    finally
277                    {
278                        fillingAuthData = false;
279                    }
280                    value = authData.get( key );
281                }
282                if ( value == null )
283                {
284                    authData.put( key, value );
285                }
286            }
287        }
288
289        return convert( value, type );
290    }
291
292    private <T> T convert( Object value, Class<T> type )
293    {
294        if ( !type.isInstance( value ) )
295        {
296            if ( String.class.equals( type ) )
297            {
298                if ( value instanceof File )
299                {
300                    value = ( (File) value ).getPath();
301                }
302                else if ( value instanceof char[] )
303                {
304                    value = new String( (char[]) value );
305                }
306            }
307            else if ( File.class.equals( type ) )
308            {
309                if ( value instanceof String )
310                {
311                    value = new File( (String) value );
312                }
313            }
314            else if ( char[].class.equals( type ) )
315            {
316                if ( value instanceof String )
317                {
318                    value = ( (String) value ).toCharArray();
319                }
320            }
321        }
322
323        if ( type.isInstance( value ) )
324        {
325            return type.cast( value );
326        }
327
328        return null;
329    }
330
331    /**
332     * Puts the specified authentication data into this context. This method should only be called from implementors of
333     * {@link Authentication#fill(AuthenticationContext, String, Map)}. Passed in character arrays are not cloned and
334     * become owned by this context, i.e. get erased when the context gets closed.
335     *
336     * @param key The key to associate the authentication data with, must not be {@code null}.
337     * @param value The (cleartext) authentication data to store, may be {@code null}.
338     */
339    public void put( String key, Object value )
340    {
341        requireNonNull( key, "authentication key cannot be null" );
342        if ( key.length() == 0 )
343        {
344            throw new IllegalArgumentException( "authentication key cannot be empty" );
345        }
346
347        synchronized ( authData )
348        {
349            Object oldValue = authData.put( key, value );
350            if ( oldValue instanceof char[] )
351            {
352                Arrays.fill( (char[]) oldValue, '\0' );
353            }
354        }
355    }
356
357    /**
358     * Closes this authentication context and erases sensitive authentication data from heap memory. Closing an already
359     * closed context has no effect.
360     */
361    public void close()
362    {
363        synchronized ( authData )
364        {
365            for ( Object value : authData.values() )
366            {
367                if ( value instanceof char[] )
368                {
369                    Arrays.fill( (char[]) value, '\0' );
370                }
371            }
372            authData.clear();
373        }
374    }
375
376    /**
377     * Closes the specified authentication context. This is a convenience method doing a {@code null} check before
378     * calling {@link #close()} on the given context.
379     * 
380     * @param context The authentication context to close, may be {@code null}.
381     */
382    public static void close( AuthenticationContext context )
383    {
384        if ( context != null )
385        {
386            context.close();
387        }
388    }
389
390}