blob: 0330d2010429fe729839d4bc204ded50c2cab059 [file] [log] [blame]
/*******************************************************************************
* 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));
}
}