| /******************************************************************************* |
| * Copyright (c) 2009, 2012 IBM Corporation and others. |
| * The code, documentation and other materials contained herein have been |
| * licensed under the Eclipse Public License - v 1.0 by the copyright holder |
| * listed above, as the Initial Contributor under such license. The text of |
| * such license is available at www.eclipse.org. |
| * Contributors: |
| * IBM Corporation - Initial API and implementation |
| * Cloudsmith Inc - Implementation |
| * Sonatype Inc - Ongoing development |
| ******************************************************************************/ |
| |
| package org.eclipse.equinox.internal.p2.repository; |
| |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URI; |
| import java.net.URLEncoder; |
| import java.util.*; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.equinox.internal.p2.core.helpers.ServiceHelper; |
| import org.eclipse.equinox.internal.p2.core.helpers.UIServicesHelper_R37x; |
| import org.eclipse.equinox.internal.p2.repository.helpers.DebugHelper; |
| import org.eclipse.equinox.p2.core.*; |
| import org.eclipse.equinox.p2.repository.IRepository; |
| import org.eclipse.equinox.security.storage.*; |
| |
| /** |
| * Credentials handles AuthenticationInfo that can be used to established an |
| * ECF connection context. An AuthenticationInfo is obtained for a URI buy looking |
| * in a store, if none is provided the user is optionally prompted for the information. |
| */ |
| public class Credentials { |
| public static class LoginCanceledException extends Exception { |
| private static final long serialVersionUID = 1L; |
| |
| } |
| |
| /** |
| * Cache of auth information that is not persisted, and modified auth info. |
| */ |
| private static final Map<String, UIServices.AuthenticationInfo> savedAuthInfo = Collections.synchronizedMap(new HashMap<String, UIServices.AuthenticationInfo>()); |
| |
| /** |
| * Information about retry counts, and prompts canceled by user. The SoftReference is |
| * a Map if not null. The keys are also used as serialization per host. |
| */ |
| private static Map<String, HostEntry> remembered; |
| |
| /** |
| * Serializes pop up of login/password prompt |
| */ |
| private static final Object promptLock = new Object(); |
| |
| /** |
| * Returns the AuthenticationInfo for the given URI. This may prompt the |
| * user for user name and password as required. |
| * |
| * If the URI is opaque, the entire URI is used as the key. For non opaque URIs, |
| * the key is based on the host name, using a host name of "localhost" if host name is |
| * missing. |
| * |
| * @param location - the file location requiring login details |
| * @param prompt - use <code>true</code> to prompt the user instead of |
| * looking at the secure preference store for login, use <code>false</code> |
| * to only try the secure preference store |
| * @throws LoginCanceledException when the user cancels the login prompt |
| * @throws CoreException if the password cannot be read or saved |
| * @return The authentication info. |
| */ |
| public static UIServices.AuthenticationInfo forLocation(URI location, boolean prompt) throws LoginCanceledException, CoreException { |
| return forLocation(location, prompt, null); |
| } |
| |
| /** |
| * Returns the AuthenticationInfo for the given URI. This may prompt the |
| * user for user name and password as required. |
| * |
| * If the URI is opaque, the entire URI is used as the key. For non opaque URIs, |
| * the key is based on the host name, using a host name of "localhost" if host name is |
| * missing. |
| * |
| * This method allows passing a previously used AuthenticationInfo. If set, the user interface |
| * may present the information "on file" to the user for editing. |
| * |
| * @param location - the location for which to obtain authentication information |
| * @param prompt - if true, user will be prompted for information |
| * @param lastUsed - optional information used in an previous attempt to login |
| * @return AuthenticationInfo, or null if there was no information available |
| * @throws LoginCanceledException - user canceled the prompt for name/password |
| * @throws CoreException if there is an error |
| */ |
| public static UIServices.AuthenticationInfo forLocation(URI location, boolean prompt, UIServices.AuthenticationInfo lastUsed) throws LoginCanceledException, CoreException { |
| String host = uriToHost(location); |
| String nodeKey; |
| try { |
| nodeKey = URLEncoder.encode(host, "UTF-8"); //$NON-NLS-1$ |
| } catch (UnsupportedEncodingException e2) { |
| // fall back to default platform encoding |
| try { |
| // Uses getProperty "file.encoding" instead of using deprecated URLEncoder.encode(String location) |
| // which does the same, but throws NPE on missing property. |
| String enc = System.getProperty("file.encoding");//$NON-NLS-1$ |
| if (enc == null) |
| throw new UnsupportedEncodingException("No UTF-8 encoding and missing system property: file.encoding"); //$NON-NLS-1$ |
| nodeKey = URLEncoder.encode(host, enc); |
| } catch (UnsupportedEncodingException e) { |
| throw internalError(e); |
| } |
| } |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| DebugHelper.debug("Credentials", "forLocation:ENTER", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location, "prompt", Boolean.toString(prompt)}); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| // Must serialize getting stored permissions per host as the location may |
| // be prompted right now |
| // Start by getting a key to lock on |
| HostEntry hostLock = null; |
| synchronized (Credentials.class) { |
| Map<String, HostEntry> r = getRemembered(); |
| hostLock = r.get(host); |
| if (hostLock == null) { |
| hostLock = new HostEntry(0); |
| r.put(host, hostLock); |
| } |
| } |
| UIServices.AuthenticationInfo loginDetails = null; |
| ISecurePreferences securePreferences = null; |
| // synchronize getting secure store with prompting user, as it may prompt. |
| synchronized (promptLock) { |
| securePreferences = SecurePreferencesFactory.getDefault(); |
| } |
| |
| // serialize the prompting per host |
| synchronized (hostLock) { |
| try { |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| DebugHelper.debug("Credentials", "forLocation:HOSTLOCK OBTAINED", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location, "prompt", Boolean.toString(prompt)}); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| String nodeName = IRepository.PREFERENCE_NODE + '/' + nodeKey; |
| ISecurePreferences prefNode = null; |
| try { |
| if (securePreferences.nodeExists(nodeName)) |
| prefNode = securePreferences.node(nodeName); |
| } catch (IllegalArgumentException e) { |
| // if the node name is illegal/malformed (should not happen). |
| throw internalError(e); |
| } catch (IllegalStateException e) { |
| // thrown if preference store has been tampered with |
| throw internalError(e); |
| } |
| if (!prompt) { |
| try { |
| if (prefNode != null) { |
| String username = prefNode.get(IRepository.PROP_USERNAME, null); |
| String password = prefNode.get(IRepository.PROP_PASSWORD, null); |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| if (username != null && password != null) { |
| DebugHelper.debug("Credentials", "forLocation:PREFNODE FOUND - USING STORED INFO", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location, "prompt", Boolean.toString(prompt)}); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| |
| // if we don't have stored connection data just return a null auth info |
| if (username != null && password != null) |
| return new UIServices.AuthenticationInfo(username, password, true); |
| } |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| DebugHelper.debug("Credentials", "forLocation:PREFNODE NOT FOUND - RETURN FROM MEMORY", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location, "prompt", Boolean.toString(prompt)}); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| return restoreFromMemory(nodeName); |
| } catch (StorageException e) { |
| throw internalError(e); |
| } |
| } |
| // need to prompt user for user name and password |
| // first check (throw exception) if having a remembered cancel |
| checkRememberedCancel(host); |
| |
| // check if another thread has modified the credentials since last attempt |
| // made by current thread - if so, try with latest without prompting |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| UIServices.AuthenticationInfo latest = restoreFromMemory(nodeName); |
| boolean useLatest = false; |
| if (latest != null && lastUsed != null) |
| if (!(latest.getUserName().equals(lastUsed.getUserName()) && latest.getPassword().equals(lastUsed.getPassword()))) |
| useLatest = true; |
| if (useLatest) |
| DebugHelper.debug("Credentials", "forLocation:LATER INFO AVAILABLE - RETURNING IT", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location, "prompt", Boolean.toString(prompt)}); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| UIServices.AuthenticationInfo latest = restoreFromMemory(nodeName); |
| if (latest != null) |
| if (lastUsed == null || !(latest.getUserName().equals(lastUsed.getUserName()) && latest.getPassword().equals(lastUsed.getPassword()))) |
| return latest; |
| |
| // check if number of prompts have been exceeded for the host - if so |
| // do a synthetic Login canceled by user |
| // (The alternative is to return "latest" until retry login gives up with |
| // authentication failed - but that would waste time). |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| if (getPromptCount(host) >= RepositoryPreferences.getLoginRetryCount()) { |
| if (lastUsed == null && latest == null) |
| DebugHelper.debug("Credentials", "forLocation:NO INFO - SYNTHETIC CANCEL", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location}); //$NON-NLS-1$ |
| return latest == null ? lastUsed : latest; // keep client failing on the latest known |
| } |
| DebugHelper.debug("Credentials", "forLocation:LATER INFO AVAILABLE - RETURNING IT", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location, "prompt", Boolean.toString(prompt)}); //$NON-NLS-1$ //$NON-NLS-2$ |
| |
| } |
| if (getPromptCount(host) >= RepositoryPreferences.getLoginRetryCount()) { |
| if (lastUsed == null && latest == null) |
| throw new LoginCanceledException(); |
| return latest == null ? lastUsed : latest; // keep client failing on the latest known |
| } |
| IProvisioningAgent agent = (IProvisioningAgent) ServiceHelper.getService(Activator.getContext(), IProvisioningAgent.SERVICE_NAME); |
| UIServices adminUIService = (UIServices) agent.getService(UIServices.SERVICE_NAME); |
| |
| if (adminUIService != null) |
| synchronized (promptLock) { |
| try { |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| DebugHelper.debug("Credentials", "forLocation:PROMPTLOCK OBTAINED", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location}); //$NON-NLS-1$ |
| } |
| |
| // serialize the popping of the dialog itself |
| loginDetails = lastUsed != null ? adminUIService.getUsernamePassword(host, lastUsed) : adminUIService.getUsernamePassword(host); |
| //null result means user canceled password dialog |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| if (loginDetails == UIServicesHelper_R37x.AUTHENTICATION_PROMPT_CANCELED) |
| DebugHelper.debug("Credentials", "forLocation:PROMPTED - USER CANCELED (PROMPT LOCK RELEASED)", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location}); //$NON-NLS-1$ |
| } |
| if (loginDetails == UIServicesHelper_R37x.AUTHENTICATION_PROMPT_CANCELED) { |
| rememberCancel(host); |
| throw new LoginCanceledException(); |
| } else if (loginDetails == null) { |
| throw new LoginCanceledException(); |
| } |
| //save user name and password if requested by user |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| if (loginDetails.saveResult()) |
| DebugHelper.debug("Credentials", "forLocation:SAVING RESULT", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location}); //$NON-NLS-1$ |
| } |
| |
| if (loginDetails.saveResult()) { |
| if (prefNode == null) |
| prefNode = securePreferences.node(nodeName); |
| try { |
| prefNode.put(IRepository.PROP_USERNAME, loginDetails.getUserName(), true); |
| prefNode.put(IRepository.PROP_PASSWORD, loginDetails.getPassword(), true); |
| prefNode.flush(); |
| } catch (StorageException e1) { |
| throw internalError(e1); |
| } catch (IOException e) { |
| throw internalError(e); |
| } |
| } else { |
| // if persisted earlier - the preference should be removed |
| if (securePreferences.nodeExists(nodeName)) { |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| DebugHelper.debug("Credentials", "forLocation:REMOVING PREVIOUSLY SAVED INFO", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location}); //$NON-NLS-1$ |
| } |
| |
| prefNode = securePreferences.node(nodeName); |
| prefNode.removeNode(); |
| try { |
| prefNode.flush(); |
| } catch (IOException e) { |
| throw internalError(e); |
| } |
| } |
| } |
| saveInMemory(nodeName, loginDetails); |
| } finally { |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| DebugHelper.debug("Credentials", "forLocation:PROMPTLOCK RELEASED", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location}); //$NON-NLS-1$ |
| } |
| } |
| } |
| incrementPromptCount(host); |
| } finally { |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| DebugHelper.debug("Credentials", "forLocation:HOSTLOCK RELEASED", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", location}); //$NON-NLS-1$ |
| } |
| } |
| |
| } |
| |
| return loginDetails; |
| } |
| |
| private static String uriToHost(URI location) { |
| // if URI is not opaque, just getting the host may be enough |
| String host = location.getHost(); |
| if (host == null) { |
| String scheme = location.getScheme(); |
| if (URIUtil.isFileURI(location) || scheme == null) |
| // If the URI references a file, a password could possibly be needed for the directory |
| // (it could be a protected zip file representing a compressed directory) - in this |
| // case the key is the path without the last segment. |
| // Using "Path" this way may result in an empty string - which later will result in |
| // an invalid key. |
| host = new Path(location.toString()).removeLastSegments(1).toString(); |
| else |
| // it is an opaque URI - details are unknown - can only use entire string. |
| host = location.toString(); |
| } |
| return host; |
| } |
| |
| /** |
| * Returns authentication details stored in memory for the given node name, |
| * or <code>null</code> if no information is stored. |
| */ |
| private static UIServices.AuthenticationInfo restoreFromMemory(String nodeName) { |
| return savedAuthInfo.get(nodeName); |
| } |
| |
| /** |
| * Saves authentication details in memory so user is only prompted once per (SDK) session |
| */ |
| private static void saveInMemory(String nodeName, UIServices.AuthenticationInfo loginDetails) { |
| savedAuthInfo.put(nodeName, loginDetails); |
| } |
| |
| /** |
| * Remember the fact that the host was canceled. |
| * @param host |
| */ |
| private static void rememberCancel(String host) { |
| Map<String, HostEntry> r = getRemembered(); |
| if (r != null) |
| r.put(host, new HostEntry(-1)); |
| } |
| |
| /** |
| * Throws LoginCancledException if the host was previously canceled, and the information |
| * is not stale. |
| * @param host |
| * @throws LoginCanceledException |
| */ |
| private static void checkRememberedCancel(String host) throws LoginCanceledException { |
| Map<String, HostEntry> r = getRemembered(); |
| if (r != null) { |
| Object x = r.get(host); |
| if (x != null && x instanceof HostEntry) |
| if (((HostEntry) x).isCanceled()) { |
| if (DebugHelper.DEBUG_REPOSITORY_CREDENTIALS) { |
| DebugHelper.debug("Credentials", "checkRememberCancel:PREVIOUSLY CANCELED", // //$NON-NLS-1$ //$NON-NLS-2$ |
| new Object[] {"host", host}); //$NON-NLS-1$ |
| } |
| |
| throw new LoginCanceledException(); |
| } |
| } |
| |
| } |
| |
| /** |
| * Increments the prompt count for host. If information is stale, the count is restarted |
| * at 1. |
| * @param host |
| */ |
| private static void incrementPromptCount(String host) { |
| Map<String, HostEntry> r = getRemembered(); |
| if (r != null) { |
| HostEntry value = r.get(host); |
| if (value == null) |
| r.put(host, value = new HostEntry(1)); |
| else { |
| if (value.isStale()) |
| value.reset(); |
| value.increment(); |
| } |
| } |
| } |
| |
| /** |
| * Returns prompt count for host, except if information is stale in which case 0 is returned. |
| * @param host |
| * @return number of time prompt has been performed for a host (or 0 if information is stale) |
| */ |
| private static int getPromptCount(String host) { |
| Map<String, HostEntry> r = getRemembered(); |
| if (r != null) { |
| HostEntry value = r.get(host); |
| if (value != null && !value.isStale()) |
| return value.getCount(); |
| } |
| return 0; |
| |
| } |
| |
| /** |
| * Clears the cached information about prompts for all login/password and |
| * canceled logins. |
| */ |
| public static synchronized void clearPromptCache() { |
| if (remembered == null) |
| return; |
| Map<String, HostEntry> r = remembered; |
| if (r == null || r.isEmpty()) |
| return; |
| // reset entries rather than creating a new empty map since the entries |
| // are also used as locks |
| for (HostEntry entry : r.values()) |
| entry.reset(); |
| } |
| |
| /** |
| * Clears the cached information for location about prompts for login/password and |
| * canceled logins. |
| * @param location the repository location |
| */ |
| public static synchronized void clearPromptCache(URI location) { |
| clearPromptCache(uriToHost(location)); |
| } |
| |
| /** |
| * Clears the cached information for host about prompts for login/password and |
| * canceled logins. |
| * @param host a host as returned from uriToHost for a location |
| */ |
| public static synchronized void clearPromptCache(String host) { |
| if (remembered == null) |
| return; |
| Map<String, HostEntry> r = remembered; |
| if (r == null) |
| return; |
| HostEntry value = r.get(host); |
| if (value != null) |
| value.reset(); |
| } |
| |
| private static synchronized Map<String, HostEntry> getRemembered() { |
| if (remembered == null) |
| remembered = Collections.synchronizedMap(new HashMap<String, HostEntry>()); |
| return remembered; |
| } |
| |
| private static class HostEntry { |
| long timestamp; |
| int count; |
| |
| public HostEntry(int count) { |
| this.count = count; |
| this.timestamp = System.currentTimeMillis(); |
| } |
| |
| public boolean isCanceled() { |
| return count == -1 && !isStale(); |
| } |
| |
| public boolean isStale() { |
| // a record is stale if older than 3 minutes |
| return System.currentTimeMillis() - timestamp > 1000 * 60 * 3; |
| } |
| |
| public int getCount() { |
| return count; |
| } |
| |
| public void increment() { |
| if (count != -1) |
| count++; |
| } |
| |
| public void reset() { |
| count = 0; |
| timestamp = System.currentTimeMillis(); |
| } |
| } |
| |
| /** |
| * Get default "InternalError" ProvisionException. |
| * @param t |
| * @return a default "InternalError" |
| */ |
| public static ProvisionException internalError(Throwable t) { |
| return new ProvisionException(new Status(IStatus.ERROR, Activator.ID, // |
| ProvisionException.INTERNAL_ERROR, Messages.repoMan_internalError, t)); |
| } |
| } |