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;
027
028import org.eclipse.aether.RepositorySystemSession;
029
030/**
031 * A glorified map of key value pairs holding (cleartext) authentication data. Authentication contexts are used
032 * internally when network operations need to access secured repositories or proxies. Each authentication context
033 * manages the credentials required to access a single host. Unlike {@link Authentication} callbacks which exist for a
034 * potentially long time like the duration of a repository system session, an authentication context has a supposedly
035 * short lifetime and should be {@link #close() closed} as soon as the corresponding network operation has finished:
036 * 
037 * <pre>
038 * AuthenticationContext context = AuthenticationContext.forRepository( session, repository );
039 * try {
040 *     // get credentials
041 *     char[] password = context.get( AuthenticationContext.PASSWORD, char[].class );
042 *     // perform network operation using retrieved credentials
043 *     ...
044 * } finally {
045 *     // erase confidential authentication data from heap memory
046 *     AuthenticationContext.close( context );
047 * }
048 * </pre>
049 * 
050 * The same authentication data can often be presented using different data types, e.g. a password can be presented
051 * using a character array or (less securely) using a string. For ease of use, an authentication context treats the
052 * following groups of data types as equivalent and converts values automatically during retrieval:
053 * <ul>
054 * <li>{@code String}, {@code char[]}</li>
055 * <li>{@code String}, {@code File}</li>
056 * </ul>
057 * An authentication context is thread-safe.
058 */
059public final class AuthenticationContext
060    implements Closeable
061{
062
063    /**
064     * The key used to store the username. The corresponding authentication data should be of type {@link String}.
065     */
066    public static final String USERNAME = "username";
067
068    /**
069     * The key used to store the password. The corresponding authentication data should be of type {@code char[]} or
070     * {@link String}.
071     */
072    public static final String PASSWORD = "password";
073
074    /**
075     * The key used to store the NTLM domain. The corresponding authentication data should be of type {@link String}.
076     */
077    public static final String NTLM_DOMAIN = "ntlm.domain";
078
079    /**
080     * The key used to store the NTML workstation. The corresponding authentication data should be of type
081     * {@link String}.
082     */
083    public static final String NTLM_WORKSTATION = "ntlm.workstation";
084
085    /**
086     * The key used to store the pathname to a private key file. The corresponding authentication data should be of type
087     * {@link String} or {@link File}.
088     */
089    public static final String PRIVATE_KEY_PATH = "privateKey.path";
090
091    /**
092     * The key used to store the passphrase protecting the private key. The corresponding authentication data should be
093     * of type {@code char[]} or {@link String}.
094     */
095    public static final String PRIVATE_KEY_PASSPHRASE = "privateKey.passphrase";
096
097    /**
098     * The key used to store the acceptance policy for unknown host keys. The corresponding authentication data should
099     * be of type {@link Boolean}. When querying this authentication data, the extra data should provide
100     * {@link #HOST_KEY_REMOTE} and {@link #HOST_KEY_LOCAL}, e.g. to enable a well-founded decision of the user during
101     * an interactive prompt.
102     */
103    public static final String HOST_KEY_ACCEPTANCE = "hostKey.acceptance";
104
105    /**
106     * The key used to store the fingerprint of the public key advertised by remote host. Note that this key is used to
107     * query the extra data passed to {@link #get(String, Map, Class)} when getting {@link #HOST_KEY_ACCEPTANCE}, not
108     * the authentication data in a context.
109     */
110    public static final String HOST_KEY_REMOTE = "hostKey.remote";
111
112    /**
113     * The key used to store the fingerprint of the public key expected from remote host as recorded in a known hosts
114     * database. Note that this key is used to query the extra data passed to {@link #get(String, Map, Class)} when
115     * getting {@link #HOST_KEY_ACCEPTANCE}, not the authentication data in a context.
116     */
117    public static final String HOST_KEY_LOCAL = "hostKey.local";
118
119    /**
120     * The key used to store the SSL context. The corresponding authentication data should be of type
121     * {@link javax.net.ssl.SSLContext}.
122     */
123    public static final String SSL_CONTEXT = "ssl.context";
124
125    /**
126     * The key used to store the SSL hostname verifier. The corresponding authentication data should be of type
127     * {@link javax.net.ssl.HostnameVerifier}.
128     */
129    public static final String SSL_HOSTNAME_VERIFIER = "ssl.hostnameVerifier";
130
131    private final RepositorySystemSession session;
132
133    private final RemoteRepository repository;
134
135    private final Proxy proxy;
136
137    private final Authentication auth;
138
139    private final Map<String, Object> authData;
140
141    private boolean fillingAuthData;
142
143    /**
144     * Gets an authentication context for the specified repository.
145     * 
146     * @param session The repository system session during which the repository is accessed, must not be {@code null}.
147     * @param repository The repository for which to create an authentication context, must not be {@code null}.
148     * @return An authentication context for the repository or {@code null} if no authentication is configured for it.
149     */
150    public static AuthenticationContext forRepository( RepositorySystemSession session, RemoteRepository repository )
151    {
152        return newInstance( session, repository, null, repository.getAuthentication() );
153    }
154
155    /**
156     * Gets an authentication context for the proxy of the specified repository.
157     * 
158     * @param session The repository system session during which the repository is accessed, must not be {@code null}.
159     * @param repository The repository for whose proxy to create an authentication context, must not be {@code null}.
160     * @return An authentication context for the proxy or {@code null} if no proxy is set or no authentication is
161     *         configured for it.
162     */
163    public static AuthenticationContext forProxy( RepositorySystemSession session, RemoteRepository repository )
164    {
165        Proxy proxy = repository.getProxy();
166        return newInstance( session, repository, proxy, ( proxy != null ) ? proxy.getAuthentication() : null );
167    }
168
169    private static AuthenticationContext newInstance( RepositorySystemSession session, RemoteRepository repository,
170                                                      Proxy proxy, Authentication auth )
171    {
172        if ( auth == null )
173        {
174            return null;
175        }
176        return new AuthenticationContext( session, repository, proxy, auth );
177    }
178
179    private AuthenticationContext( RepositorySystemSession session, RemoteRepository repository, Proxy proxy,
180                                   Authentication auth )
181    {
182        if ( session == null )
183        {
184            throw new IllegalArgumentException( "repository system session missing" );
185        }
186        this.session = session;
187        this.repository = repository;
188        this.proxy = proxy;
189        this.auth = auth;
190        authData = new HashMap<String, Object>();
191    }
192
193    /**
194     * Gets the repository system session during which the authentication happens.
195     * 
196     * @return The repository system session, never {@code null}.
197     */
198    public RepositorySystemSession getSession()
199    {
200        return session;
201    }
202
203    /**
204     * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
205     * this authentication context does not apply to the repository's host but rather the proxy.
206     * 
207     * @return The repository to be contacted, never {@code null}.
208     */
209    public RemoteRepository getRepository()
210    {
211        return repository;
212    }
213
214    /**
215     * Gets the proxy (if any) to be authenticated with.
216     * 
217     * @return The proxy or {@code null} if authenticating directly with the repository's host.
218     */
219    public Proxy getProxy()
220    {
221        return proxy;
222    }
223
224    /**
225     * Gets the authentication data for the specified key.
226     * 
227     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
228     * @return The requested authentication data or {@code null} if none.
229     */
230    public String get( String key )
231    {
232        return get( key, null, String.class );
233    }
234
235    /**
236     * Gets the authentication data for the specified key.
237     * 
238     * @param <T> The data type of the authentication data.
239     * @param key The key whose authentication data should be retrieved, must not 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, Class<T> type )
244    {
245        return get( key, null, type );
246    }
247
248    /**
249     * Gets the authentication data for the specified key.
250     * 
251     * @param <T> The data type of the authentication data.
252     * @param key The key whose authentication data should be retrieved, must not be {@code null}.
253     * @param data Any (read-only) extra data in form of key value pairs that might be useful when getting the
254     *            authentication data, may be {@code null}.
255     * @param type The expected type of the authentication data, must not be {@code null}.
256     * @return The requested authentication data or {@code null} if none or if the data doesn't match the expected type.
257     */
258    public <T> T get( String key, Map<String, String> data, Class<T> type )
259    {
260        if ( key == null )
261        {
262            throw new IllegalArgumentException( "authentication data key missing" );
263        }
264        Object value;
265        synchronized ( authData )
266        {
267            value = authData.get( key );
268            if ( value == null && !authData.containsKey( key ) && !fillingAuthData )
269            {
270                if ( auth != null )
271                {
272                    try
273                    {
274                        fillingAuthData = true;
275                        auth.fill( this, key, data );
276                    }
277                    finally
278                    {
279                        fillingAuthData = false;
280                    }
281                    value = authData.get( key );
282                }
283                if ( value == null )
284                {
285                    authData.put( key, value );
286                }
287            }
288        }
289
290        return convert( value, type );
291    }
292
293    private <T> T convert( Object value, Class<T> type )
294    {
295        if ( !type.isInstance( value ) )
296        {
297            if ( String.class.equals( type ) )
298            {
299                if ( value instanceof File )
300                {
301                    value = ( (File) value ).getPath();
302                }
303                else if ( value instanceof char[] )
304                {
305                    value = new String( (char[]) value );
306                }
307            }
308            else if ( File.class.equals( type ) )
309            {
310                if ( value instanceof String )
311                {
312                    value = new File( (String) value );
313                }
314            }
315            else if ( char[].class.equals( type ) )
316            {
317                if ( value instanceof String )
318                {
319                    value = ( (String) value ).toCharArray();
320                }
321            }
322        }
323
324        if ( type.isInstance( value ) )
325        {
326            return type.cast( value );
327        }
328
329        return null;
330    }
331
332    /**
333     * Puts the specified authentication data into this context. This method should only be called from implementors of
334     * {@link Authentication#fill(AuthenticationContext, String, Map)}. Passed in character arrays are not cloned and
335     * become owned by this context, i.e. get erased when the context gets closed.
336     * 
337     * @param key The key to associate the authentication data with, must not be {@code null}.
338     * @param value The (cleartext) authentication data to store, may be {@code null}.
339     */
340    public void put( String key, Object value )
341    {
342        if ( key == null )
343        {
344            throw new IllegalArgumentException( "authentication data key missing" );
345        }
346        synchronized ( authData )
347        {
348            Object oldValue = authData.put( key, value );
349            if ( oldValue instanceof char[] )
350            {
351                Arrays.fill( (char[]) oldValue, '\0' );
352            }
353        }
354    }
355
356    /**
357     * Closes this authentication context and erases sensitive authentication data from heap memory. Closing an already
358     * closed context has no effect.
359     */
360    public void close()
361    {
362        synchronized ( authData )
363        {
364            for ( Object value : authData.values() )
365            {
366                if ( value instanceof char[] )
367                {
368                    Arrays.fill( (char[]) value, '\0' );
369                }
370            }
371            authData.clear();
372        }
373    }
374
375    /**
376     * Closes the specified authentication context. This is a convenience method doing a {@code null} check before
377     * calling {@link #close()} on the given context.
378     * 
379     * @param context The authentication context to close, may be {@code null}.
380     */
381    public static void close( AuthenticationContext context )
382    {
383        if ( context != null )
384        {
385            context.close();
386        }
387    }
388
389}