blob: 51486a6adc5c5138bd2c1d49e7f7d661ee41901d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2011 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.team.internal.ccvs.core.connection;
import java.io.IOException;
import java.util.*;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.ILock;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.equinox.security.storage.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.internal.ccvs.core.*;
import org.eclipse.team.internal.ccvs.core.client.*;
import org.eclipse.team.internal.ccvs.core.resources.*;
import org.eclipse.team.internal.ccvs.core.util.KnownRepositories;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
/**
* This class manages a CVS repository location.
*
* It provides the mapping between connection method name and the
* plugged in ICunnectionMethod.
*
* It parses location strings into instances.
*
* It provides a method to open a connection to the server along
* with a method to validate that connections can be made.
*
* It manages its user info using the plugged in IUserAuthenticator
* (unless a username and password are provided as part of the creation
* string, in which case, no authenticator is used).
*
* Instances must be disposed of when no longer needed in order to
* notify the authenticator so cached properties can be cleared
*/
public class CVSRepositoryLocation extends PlatformObject implements ICVSRepositoryLocation, IUserInfo {
/**
* Top secure preferences node to cache CVS information
*/
static final private String cvsNameSegment = "/CVS/"; //$NON-NLS-1$
/**
* Keys determining connection information for a given server
*/
static final private String PASSWORD_KEY = "password"; //$NON-NLS-1$
static final private String USERNAME_KEY = "login"; //$NON-NLS-1$
/**
* The name of the preferences node in the CVS preferences that contains
* the known repositories as its children.
*/
public static final String PREF_REPOSITORIES_NODE = "repositories"; //$NON-NLS-1$
/*
* The name of the node in the default scope that has the default settings
* for a repository.
*/
private static final String DEFAULT_REPOSITORY_SETTINGS_NODE = "default_repository_settings"; //$NON-NLS-1$
// Preference keys used to persist the state of the location
public static final String PREF_LOCATION = "location"; //$NON-NLS-1$
public static final String PREF_SERVER_ENCODING = "encoding"; //$NON-NLS-1$
// server platform constants
public static final int UNDETERMINED_PLATFORM = 0;
public static final int CVS_SERVER = 1;
public static final int CVSNT_SERVER = 2;
public static final int UNSUPPORTED_SERVER = 3;
public static final int UNKNOWN_SERVER = 4;
// static variables for extension points
private static IUserAuthenticator authenticator;
private static IConnectionMethod[] pluggedInConnectionMethods = null;
// Locks for ensuring that authentication to a host is serialized
// so that invalid passwords do not result in account lockout
private static Map<String, ILock> hostLocks = new HashMap<>();
private IConnectionMethod method;
private String user;
private String password;
private String host;
private int port;
private String root;
private boolean userFixed;
private boolean passwordFixed;
private boolean allowCaching;
private int serverPlatform = UNDETERMINED_PLATFORM;
public static final char COLON = ':';
public static final char SEMICOLON = ';';
public static final char HOST_SEPARATOR = '@';
public static final char PORT_SEPARATOR = '#';
public static final boolean STANDALONE_MODE = (System.getProperty("eclipse.cvs.standalone")==null) ? //$NON-NLS-1$
false :(Boolean.valueOf(System.getProperty("eclipse.cvs.standalone")).booleanValue()); //$NON-NLS-1$
// command to start remote cvs in server mode
private static final String INVOKE_SVR_CMD = "server"; //$NON-NLS-1$
// fields needed for caching the password
public static final String INFO_PASSWORD = "org.eclipse.team.cvs.core.password";//$NON-NLS-1$
public static final String INFO_USERNAME = "org.eclipse.team.cvs.core.username";//$NON-NLS-1$
public static final String AUTH_SCHEME = "";//$NON-NLS-1$
/*
* Fields used to create the EXT command invocation
*/
public static final String USER_VARIABLE = "{user}"; //$NON-NLS-1$
public static final String PASSWORD_VARIABLE = "{password}"; //$NON-NLS-1$
public static final String HOST_VARIABLE = "{host}"; //$NON-NLS-1$
public static final String PORT_VARIABLE = "{port}"; //$NON-NLS-1$
/*
* Field that indicates which connection method is to be used for
* locations that use the EXT connection method.
*/
private static String extProxy;
/*
* Field that indicates that the last connection attempt made to
* this repository location failed due to an authentication failure.
* When this is set, subsequent attempts should prompt before attempting to connect
*/
private boolean previousAuthenticationFailed = false;
/**
* Return the preferences node whose child nodes are the know repositories
* @return a preferences node
*/
public static Preferences getParentPreferences() {
return CVSProviderPlugin.getPlugin().getInstancePreferences().node(PREF_REPOSITORIES_NODE);
}
/**
* Return a preferences node that contains suitable defaults for a
* repository location.
* @return a preferences node
*/
public static Preferences getDefaultPreferences() {
Preferences defaults = DefaultScope.INSTANCE.getNode(CVSProviderPlugin.ID).node(DEFAULT_REPOSITORY_SETTINGS_NODE);
defaults.put(PREF_SERVER_ENCODING, getDefaultEncoding());
return defaults;
}
private static String getDefaultEncoding() {
return System.getProperty("file.encoding", "UTF-8"); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Set the proxy connection method that is to be used when a
* repository location has the ext connection method. This is
* usefull with the extssh connection method as it can be used to
* keep the sandbox compatible with the command line client.
* @param string
*/
public static void setExtConnectionMethodProxy(String string) {
extProxy = string;
}
/**
* Create a repository location instance from the given properties.
* The supported properties are:
*
* connection The connection method to be used
* user The username for the connection (optional)
* password The password used for the connection (optional)
* host The host where the repository resides
* port The port to connect to (optional)
* root The server directory where the repository is located
* encoding The file system encoding of the server
*/
public static CVSRepositoryLocation fromProperties(Properties configuration) throws CVSException {
// We build a string to allow validation of the components that are provided to us
String connection = configuration.getProperty("connection");//$NON-NLS-1$
if (connection == null)
connection = "pserver";//$NON-NLS-1$
IConnectionMethod method = getPluggedInConnectionMethod(connection);
if (method == null)
throw new CVSException(new Status(IStatus.ERROR, CVSProviderPlugin.ID, TeamException.UNABLE, NLS.bind(CVSMessages.CVSRepositoryLocation_methods, (new Object[] {getPluggedInConnectionMethodNames()})), null));//
String user = configuration.getProperty("user");//$NON-NLS-1$
if (user.length() == 0)
user = null;
String password = configuration.getProperty("password");//$NON-NLS-1$
if (user == null)
password = null;
String host = configuration.getProperty("host");//$NON-NLS-1$
if (host == null)
throw new CVSException(new Status(IStatus.ERROR, CVSProviderPlugin.ID, TeamException.UNABLE, CVSMessages.CVSRepositoryLocation_hostRequired, null));//
String portString = configuration.getProperty("port");//$NON-NLS-1$
int port;
if (portString == null)
port = ICVSRepositoryLocation.USE_DEFAULT_PORT;
else
port = Integer.parseInt(portString);
String root = configuration.getProperty("root");//$NON-NLS-1$
if (root == null)
throw new CVSException(new Status(IStatus.ERROR, CVSProviderPlugin.ID, TeamException.UNABLE, CVSMessages.CVSRepositoryLocation_rootRequired, null));//
String encoding = configuration.getProperty("encoding"); //$NON-NLS-1$
return new CVSRepositoryLocation(method, user, password, host, port, root, encoding, user != null, false);
}
/**
* Parse a location string and return a CVSRepositoryLocation.
*
* On failure, the status of the exception will be a MultiStatus
* that includes the original parsing error and a general status
* displaying the passed location and proper form. This form is
* better for logging, etc.
*/
public static CVSRepositoryLocation fromString(String location) throws CVSException {
try {
return fromString(location, false);
} catch (CVSException e) {
// Parsing failed. Include a status that
// shows the passed location and the proper form
MultiStatus error = new MultiStatus(CVSProviderPlugin.ID, IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_invalidFormat, (new Object[] {location})), null);//
error.merge(new CVSStatus(IStatus.ERROR, CVSMessages.CVSRepositoryLocation_locationForm));//
error.merge(e.getStatus());
throw new CVSException(error);
}
}
/**
* Parse a location string and return a CVSRepositoryLocation.
*
* The valid format (from the cederqvist) is:
*
* :method:[[user][:password]@]hostname[:[port]]/path/to/repository
*
* However, this does not work with CVS on NT so we use the format
*
* :method:[user[:password]@]hostname[#port]:/path/to/repository
*
* Some differences to note:
* The : after the host/port is not optional because of NT naming including device
* e.g. :pserver:username:password@hostname#port:D:\cvsroot
*
* Also parse alternative format from WinCVS, which stores connection
* parameters such as username and hostname in method options:
*
* :method[;option=arg...]:other_connection_data
*
* e.g. :pserver;username=anonymous;hostname=localhost:/path/to/repository
*
* If validateOnly is true, this method will always throw an exception.
* The status of the exception indicates success or failure. The status
* of the exception contains a specific message suitable for displaying
* to a user who has knowledge of the provided location string.
* @see CVSRepositoryLocation.fromString(String)
*/
public static CVSRepositoryLocation fromString(String location, boolean validateOnly) throws CVSException {
String errorMessage = null;
try {
// Get the connection method
errorMessage = CVSMessages.CVSRepositoryLocation_parsingMethod;
int start = location.indexOf(COLON);
String methodName;
int end;
// For parsing alternative location format
int optionStart = location.indexOf(SEMICOLON);
HashMap<String, String> hmOptions = new HashMap<>();
if (start == 0) {
end = location.indexOf(COLON, start + 1);
// Check for alternative location syntax
if (optionStart != -1) {
// errorMessage = CVSMessages.CVSRepositoryLocation_parsingMethodOptions;
methodName = location.substring(start + 1, optionStart);
// Save options in hash table
StringTokenizer stOpt = new StringTokenizer(
location.substring(optionStart+1, end),
"=;" //$NON-NLS-1$
);
while (stOpt.hasMoreTokens()) {
hmOptions.put(stOpt.nextToken(), stOpt.nextToken());
}
start = end + 1;
} else {
methodName = location.substring(start + 1, end);
start = end + 1;
}
} else {
// this could be an alternate format for ext: username:password@host:path
methodName = "ext"; //$NON-NLS-1$
start = 0;
}
IConnectionMethod method = getPluggedInConnectionMethod(methodName);
if (method == null)
throw new CVSException(new CVSStatus(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_methods, (new Object[] {getPluggedInConnectionMethodNames()}))));//
// Get the user name and password (if provided)
errorMessage = CVSMessages.CVSRepositoryLocation_parsingUser;
//Since there is a @ sign in the user name so use lastIndexOf to get to the host separator @
end = location.lastIndexOf(HOST_SEPARATOR, location.length());
String user = null;
String password = null;
// if end is -1 then there is no host separator meaning that the username is not present
// or set in options of alternative-style location string
if (end != -1) {
// Get the optional user and password
user = location.substring(start, end);
// Separate the user and password (if there is a password)
start = user.indexOf(COLON);
if (start != -1) {
errorMessage = CVSMessages.CVSRepositoryLocation_parsingPassword;
password = user.substring(start+1);
user = user.substring(0, start);
}
// Set start to point after the host separator
start = end + 1;
} else if (optionStart != -1) {
// alternative location string data
// errorMessage = CVSMessages.CVSRepositoryLocation_parsingOptionsUsername;
if (hmOptions.containsKey("username")) user = hmOptions.get("username").toString(); //$NON-NLS-1$ //$NON-NLS-2$
// errorMessage = CVSMessages.CVSRepositoryLocation_parsingOptionsPassword;
if (hmOptions.containsKey("password")) password = hmOptions.get("password").toString(); //$NON-NLS-1$ //$NON-NLS-2$
}
// Get the host (and port)
errorMessage = CVSMessages.CVSRepositoryLocation_parsingHost;
end= location.indexOf(COLON, start);
int hostEnd = end;
if (end == -1) {
// The last colon is optional so look for the slash that starts the path
end = location.indexOf('/', start);
hostEnd = end;
// Decrement the end since the slash is part of the path
if (end != -1) end--;
}
String host = (hmOptions.containsKey("hostname")) ? hmOptions.get("hostname").toString() : location.substring(start, hostEnd); //$NON-NLS-1$ //$NON-NLS-2$
int port = USE_DEFAULT_PORT;
boolean havePort = false;
if (hmOptions.containsKey("port")) { //$NON-NLS-1$
port = Integer.parseInt(hmOptions.get("port").toString()); //$NON-NLS-1$
havePort = true;
}
// Separate the port and host if there is a port
start = host.indexOf(PORT_SEPARATOR);
if (start != -1) {
try {
// Initially, we used a # between the host and port
errorMessage = CVSMessages.CVSRepositoryLocation_parsingPort;
port = Integer.parseInt(host.substring(start+1));
host = host.substring(0, start);
havePort = true;
} catch (NumberFormatException e) {
// Ignore this as the #1234 port could be part of a proxy host string
}
}
if (!havePort) {
// In the correct CVS format, the port follows the COLON
errorMessage = CVSMessages.CVSRepositoryLocation_parsingPort;
int index = end;
char c = location.charAt(++index);
String portString = new String();
while (Character.isDigit(c)) {
portString += c;
c = location.charAt(++index);
}
if (portString.length() > 0) {
end = index - 1;
port = Integer.parseInt(portString);
}
}
// Get the repository path (translating backslashes to slashes)
errorMessage = CVSMessages.CVSRepositoryLocation_parsingRoot;
start = end + 1;
String root = location.substring(start);
if (validateOnly)
throw new CVSException(new CVSStatus(IStatus.OK, CVSMessages.ok));//
return new CVSRepositoryLocation(method, user, password, host, port, root, null /* encoding */, (user != null), (password != null));
} catch (IndexOutOfBoundsException e) {
// We'll get here if anything funny happened while extracting substrings
IStatus status = new CVSStatus(IStatus.ERROR, errorMessage);
throw new CVSException(status);
} catch (NumberFormatException e) {
IStatus status = new CVSStatus(IStatus.ERROR, errorMessage);
// We'll get here if we couldn't parse a number
throw new CVSException(status);
}
}
/**
* Get the plugged-in user authenticator if there is one.
* @return the plugged-in user authenticator or <code>null</code>
*/
public static IUserAuthenticator getAuthenticator() {
if (authenticator == null) {
authenticator = getPluggedInAuthenticator();
}
return authenticator;
}
/**
* Return the sorted array of plugged-in connection methods.
* @return the sorted array of plugged-in connection methods
*/
public static IConnectionMethod[] getPluggedInConnectionMethods() {
if(pluggedInConnectionMethods==null) {
List<Object> connectionMethods = new ArrayList<Object>();
if (STANDALONE_MODE) {
connectionMethods.add(new PServerConnectionMethod());
} else {
IExtension[] extensions = Platform.getExtensionRegistry().getExtensionPoint(CVSProviderPlugin.ID, CVSProviderPlugin.PT_CONNECTIONMETHODS).getExtensions();
for(int i=0; i<extensions.length; i++) {
IExtension extension = extensions[i];
IConfigurationElement[] configs = extension.getConfigurationElements();
if (configs.length == 0) {
CVSProviderPlugin.log(IStatus.ERROR, NLS.bind("Connection method {0} is missing required fields", new Object[] {extension.getUniqueIdentifier()}), null);//$NON-NLS-1$
continue;
}
try {
IConfigurationElement config = configs[0];
connectionMethods.add(config.createExecutableExtension("run"));//$NON-NLS-1$
} catch (CoreException ex) {
CVSProviderPlugin.log(IStatus.ERROR, NLS.bind("Could not instantiate connection method for {0}", new Object[] {extension.getUniqueIdentifier()}), ex);//$NON-NLS-1$
}
}
}
IConnectionMethod[] methods = connectionMethods.toArray(new IConnectionMethod[0]);
Arrays.sort(methods, (cm1, cm2) -> cm1.getName().compareTo(cm2.getName()));
pluggedInConnectionMethods = methods;
}
return pluggedInConnectionMethods;
}
/*
* Return the connection method registered for the given name
* or <code>null</code> if none is registered with the given name.
*/
private static IConnectionMethod getPluggedInConnectionMethod(String methodName) {
Assert.isNotNull(methodName);
IConnectionMethod[] methods = getPluggedInConnectionMethods();
for(int i=0; i<methods.length; i++) {
if(methodName.equals(methods[i].getName()))
return methods[i];
}
return null;
}
/*
* Return a string containing a list of all connection methods
* that is suitable for inclusion in an error message.
*/
private static String getPluggedInConnectionMethodNames() {
IConnectionMethod[] methods = getPluggedInConnectionMethods();
StringBuffer methodNames = new StringBuffer();
for(int i=0; i<methods.length; i++) {
String name = methods[i].getName();
if (i>0)
methodNames.append(", ");//$NON-NLS-1$
methodNames.append(name);
}
return methodNames.toString();
}
/*
* Get the pluged-in authenticator from the plugin manifest.
*/
private static IUserAuthenticator getPluggedInAuthenticator() {
IExtension[] extensions = Platform.getExtensionRegistry().getExtensionPoint(CVSProviderPlugin.ID, CVSProviderPlugin.PT_AUTHENTICATOR).getExtensions();
if (extensions.length == 0)
return null;
IExtension extension = extensions[0];
IConfigurationElement[] configs = extension.getConfigurationElements();
if (configs.length == 0) {
CVSProviderPlugin.log(IStatus.ERROR, NLS.bind("User autheticator {0} is missing required fields", (new Object[] {extension.getUniqueIdentifier()})), null);//$NON-NLS-1$
return null;
}
try {
IConfigurationElement config = configs[0];
return (IUserAuthenticator) config.createExecutableExtension("run");//$NON-NLS-1$
} catch (CoreException ex) {
CVSProviderPlugin.log(IStatus.ERROR, NLS.bind("Unable to instantiate user authenticator {0}", (new Object[] {extension.getUniqueIdentifier()})), ex);//$NON-NLS-1$
return null;
}
}
/*
* Create a CVSRepositoryLocation from its composite parts.
*/
private CVSRepositoryLocation(IConnectionMethod method, String user, String password, String host, int port, String root, String encoding, boolean userFixed, boolean passwordFixed) {
this.method = method;
this.user = user;
this.password = password;
this.host = host;
this.port = port;
this.root = root;
// The username can be fixed only if one is provided
if (userFixed && (user != null))
this.userFixed = true;
// The password can only be fixed if the username is and a password is provided
if (userFixed && passwordFixed && (password != null))
this.passwordFixed = true;
if (encoding != null) {
setEncoding(encoding);
}
}
/*
* Create the connection to the remote server.
* If anything fails, an exception will be thrown and must
* be handled by the caller.
*/
private Connection createConnection(String password, IProgressMonitor monitor) throws CVSException {
IConnectionMethod methodToUse = method;
if (method.getName().equals("ext") && extProxy != null && !extProxy.equals(method.getName())) { //$NON-NLS-1$
methodToUse = getPluggedInConnectionMethod(extProxy);
}
Connection connection = new Connection(this, methodToUse.createConnection(this, password));
connection.open(monitor);
return connection;
}
/*
* Dispose of the receiver by clearing any cached authorization information.
* This method should only be invoked when the corresponding adapter is shut
* down or a connection is being validated.
*/
public void dispose() {
removeNode();
try {
if (hasPreferences()) {
internalGetPreferences().removeNode();
getParentPreferences().flush();
}
} catch (BackingStoreException e) {
CVSProviderPlugin.log(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_73, new String[] { getLocation(true) }), e);
}
}
/*
* Clear and flush the keyring entry associated with the receiver
*/
private void removeNode() {
ISecurePreferences node = getCVSNode();
if (node == null)
return;
try {
node.clear();
node.flush(); // save immediately
} catch (IllegalStateException e) {
CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
} catch (IOException e) {
CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
}
}
/*
* @see ICVSRepositoryLocation#getHost()
*/
public String getHost() {
return host;
}
/*
* @see IRepositoryLocation#getLocation()
*
* The username is included if it is fixed.
* The password is never included even if it is fixed.
* The port is included if it is not the default port.
*/
public String getLocation() {
return getLocation(false);
}
public String getLocation(boolean forDisplay) {
return COLON + method.getName() + COLON +
(userFixed?(user +
((passwordFixed && !forDisplay)?(COLON + password):"")//$NON-NLS-1$
+ HOST_SEPARATOR):"") +//$NON-NLS-1$
host + COLON +
((port == USE_DEFAULT_PORT)?"":(Integer.valueOf(port).toString())) + //$NON-NLS-1$
root;
}
@Override
public IConnectionMethod getMethod() {
return method;
}
@Override
public int getPort() {
return port;
}
@Override
public String getEncoding() {
if (hasPreferences()) {
return internalGetPreferences().get(PREF_SERVER_ENCODING, getDefaultEncoding());
} else {
return getDefaultEncoding();
}
}
@Override
public void setEncoding(String encoding) {
if (encoding == null || encoding == getDefaultEncoding()) {
if (hasPreferences()) {
internalGetPreferences().remove(PREF_SERVER_ENCODING);
}
} else {
ensurePreferencesStored();
internalGetPreferences().put(PREF_SERVER_ENCODING, encoding);
flushPreferences();
}
}
@Override
public ICVSRemoteResource[] members(CVSTag tag, boolean modules, IProgressMonitor progress) throws CVSException {
try {
if (modules) {
return RemoteModule.getRemoteModules(this, tag, progress);
} else {
RemoteFolder root = new RemoteFolder(null, this, ICVSRemoteFolder.REPOSITORY_ROOT_FOLDER_NAME, tag);
ICVSRemoteResource[] resources = root.members(progress);
// There is the off chance that there is a file in the root of the repository.
// This is not supported by cvs so we need to make sure there are no files
List<ICVSRemoteResource> folders = new ArrayList<ICVSRemoteResource>(resources.length);
for (int i = 0; i < resources.length; i++) {
ICVSRemoteResource remoteResource = resources[i];
if (remoteResource.isContainer()) {
folders.add(remoteResource);
}
}
return folders.toArray(new ICVSRemoteResource[folders.size()]);
}
} catch (CVSException e){
// keep current CVSException
throw e;
} catch(TeamException e1) {
throw new CVSException(e1.getStatus());
}
}
@Override
public ICVSRemoteFolder getRemoteFolder(String remotePath, CVSTag tag) {
return new RemoteFolder(null, this, remotePath, tag);
}
@Override
public ICVSRemoteFile getRemoteFile(String remotePath, CVSTag tag) {
IPath path = new Path(null, remotePath);
RemoteFolderTree remoteFolder = new RemoteFolderTree(null, this, path.removeLastSegments(1).toString(), tag);
RemoteFile remoteFile = new RemoteFile(remoteFolder, Update.STATE_ADDED_LOCAL, path.lastSegment(), null, null, tag);
remoteFolder.setChildren(new ICVSRemoteResource[] { remoteFile });
return remoteFile;
}
@Override
public String getRootDirectory() {
return root;
}
/*
* @see ICVSRepositoryLocation#getTimeout()
*
* For the time being, the timeout value is a system wide value
* associated with the CVSPlugin singleton.
*/
public int getTimeout() {
return CVSProviderPlugin.getPlugin().getTimeout();
}
@Override
public IUserInfo getUserInfo(boolean makeUsernameMutable) {
return new UserInfo(getUsername(), password, makeUsernameMutable ? true : isUsernameMutable());
}
@Override
public String getUsername() {
// If the username is mutable, get it from the cache if it's there
if (user == null && isUsernameMutable()) {
retrieveUsername();
}
return user == null ? "" : user; //$NON-NLS-1$
}
@Override
public boolean isUsernameMutable() {
return !userFixed;
}
/*
* Open a connection to the repository represented by the receiver.
* If the username or password are not fixed, openConnection will
* use the plugged-in authenticator to prompt for the username and/or
* password if one has not previously been provided or if the previously
* supplied username and password are invalid.
*
* This method is synchronized to ensure that authentication with the
* remote server is serialized. This is needed to avoid the situation where
* multiple failed authentications occur and result in the remote account
* being locked. The CVSProviderPlugin enforces that there is one instance
* of a CVSRepositoryLocation per remote location thus this method is called
* for any connection made to this remote location.
*/
public Connection openConnection(IProgressMonitor monitor) throws CVSException {
// Get the lock for the host to ensure that we are not connecting to the same host concurrently.
Policy.checkCanceled(monitor);
ILock hostLock;
synchronized(hostLocks) {
hostLock = hostLocks.get(getHost());
if (hostLock == null) {
hostLock = Job.getJobManager().newLock();
hostLocks.put(getHost(), hostLock);
}
}
try {
boolean acquired = false;
int count = 0;
int timeout = CVSProviderPlugin.getPlugin().getTimeout();
while (!acquired) {
try {
acquired = hostLock.acquire(1000);
} catch (InterruptedException e) {
// Ignore
}
if (timeout > 0 && count > timeout) {
throw new CVSCommunicationException(NLS.bind(CVSMessages.CVSRepositoryLocation_72, getHost()));
}
count++;
Policy.checkCanceled(monitor);
}
// Allow two ticks in case of a retry
monitor.beginTask(NLS.bind(CVSMessages.CVSRepositoryLocation_openingConnection, new String[] { getHost() }), 2);
ensureLocationCached();
boolean cacheNeedsUpdate = false;
// If the previous connection failed, prompt before attempting to connect
if (previousAuthenticationFailed) {
promptForUserInfo(null);
// The authentication information has been change so update the cache
cacheNeedsUpdate = true;
}
while (true) {
try {
// The following will throw an exception if authentication fails
String password = this.password;
if (password == null) {
// If the instance has no password, obtain it from the cache
password = retrievePassword();
}
if (user == null) {
// This is possible if the cache was cleared somehow for a location with a mutable username
throw new CVSAuthenticationException(CVSMessages.CVSRepositoryLocation_usernameRequired, CVSAuthenticationException.RETRY, this, null);
}
//if (password == null)
// password = "";//$NON-NLS-1$
Connection connection = createConnection(password, monitor);
if (cacheNeedsUpdate)
updateCachedLocation();
previousAuthenticationFailed = false;
return connection;
} catch (CVSAuthenticationException ex) {
previousAuthenticationFailed = true;
if (ex.getRetryStatus() == CVSAuthenticationException.RETRY) {
String message = ex.getMessage();
promptForUserInfo(message);
// The authentication information has been change so update the cache
cacheNeedsUpdate = true;
} else {
throw ex;
}
}
}
} finally {
hostLock.release();
monitor.done();
}
}
/*
* Prompt for the user authentication information (i.e. user name and password).
*/
private void promptForUserInfo(String message) throws CVSException {
IUserAuthenticator authenticator = getAuthenticator();
if (authenticator == null) {
throw new CVSAuthenticationException(CVSMessages.CVSRepositoryLocation_noAuthenticator, CVSAuthenticationException.NO_RETRY,this);//
}
authenticator.promptForUserInfo(this, this, message);
}
/*
* Ensure that this location is in the known repositories list
* and that the authentication information matches what is in the
* cache, if this instance is not the instance in the cache.
*/
private void ensureLocationCached() {
String location = getLocation();
KnownRepositories repositories = KnownRepositories.getInstance();
if (repositories.isKnownRepository(location)) {
try {
// The repository is already known.
// Ensure that the authentication information of this
// location matches that of the known location
setAuthenticationInformation((CVSRepositoryLocation)repositories.getRepository(location));
} catch (CVSException e) {
// Log the exception and continue
CVSProviderPlugin.log(e);
}
} else {
// The repository is not known so record it so any authentication
// information the user may provide is remembered
repositories.addRepository(this, true /* broadcast */);
}
}
/*
* Set the authentication information of this instance such that it matches the
* provided instances.
*/
private void setAuthenticationInformation(CVSRepositoryLocation other) {
if (other != this) {
// The instances differ so copy from the other location to this one
if (other.getUserInfoCached()) {
// The user info is cached for the other instance
// so null all the values in this instance so the
// information is obtained from the cache
this.allowCaching = true;
if (!userFixed) this.user = null;
if (!passwordFixed) this.password = null;
} else {
// The user info is not cached for the other instance so
// copy the authentication information into this instance
setAllowCaching(false); /* this will clear any cached values */
// Only copy the username and password if they are not fixed.
// (If they are fixed, they would be included in the location
// identifier and therefore must already match)
if (!other.userFixed)
this.user = other.user;
if (!other.passwordFixed)
this.password = other.password;
}
}
}
/*
* The connection was successfully made. Update the cached
* repository location if it is a different instance than
* this location.
*/
private void updateCachedLocation() {
try {
CVSRepositoryLocation known = (CVSRepositoryLocation)KnownRepositories.getInstance().getRepository(getLocation());
known.setAuthenticationInformation(this);
} catch (CVSException e) {
// Log the exception and continue
CVSProviderPlugin.log(e);
}
}
/*
* Implementation of inherited toString()
*/
public String toString() {
return getLocation(true);
}
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CVSRepositoryLocation)) return false;
return getLocation().equals(((CVSRepositoryLocation)o).getLocation());
}
public int hashCode() {
return getLocation().hashCode();
}
/*
* Set the username of the receiver if the username is mutable. Return the
* username from the keyring if available.
*/
private String retrieveUsername() {
ISecurePreferences node = getCVSNode();
if (node == null)
return null;
try {
String username = node.get(USERNAME_KEY, null);
if (username != null && isUsernameMutable())
setUsername(username);
return username;
} catch (StorageException e) { // most likely invalid keyring password or corrupted data
CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
}
return null;
}
/*
* Return the cached password from the keyring.
* Also, set the username of the receiver if the username is mutable
*/
private String retrievePassword() {
ISecurePreferences node = getCVSNode();
if (node == null)
return null;
try {
retrieveUsername();
return node.get(PASSWORD_KEY, null);
} catch (StorageException e) { // most likely invalid keyring password or corrupted data
CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
}
return null;
}
@Override
public void setPassword(String password) {
if (passwordFixed)
throw new UnsupportedOperationException();
// We set the password here but it will be cleared
// if the user info is cached using updateCache()
this.password = password;
// The password has been changed, reset the flag, so we won't
// prompt before attempting to connect
previousAuthenticationFailed = false;
}
@Override
public void setUsername(String user) {
if (userFixed)
throw new UnsupportedOperationException();
this.user = user;
}
public void setUserMuteable(boolean muteable) {
userFixed = !muteable;
}
public void setAllowCaching(boolean value) {
allowCaching = value;
if (allowCaching) {
updateCache();
} else {
if (password == null)
password = retrievePassword();
removeNode();
}
}
public void updateCache() {
// Nothing to cache if the password is fixed
if (passwordFixed || ! allowCaching) return;
// Nothing to cache if the password is null and the user is fixed
if (password == null && userFixed) return;
if (updateCache(user, password)) {
// If the cache was updated, null the password field
// so we will obtain the password from the cache when needed
password = null;
}
ensurePreferencesStored();
}
/*
* Cache the user info in the keyring. Return true if the operation
* succeeded and false otherwise. If an error occurs, it will be logged.
*/
private boolean updateCache(String username, String password) {
ISecurePreferences node = getCVSNode();
if (node == null)
return false;
try {
node.put(USERNAME_KEY, username, false);
node.put(PASSWORD_KEY, password, true);
node.flush();
} catch (StorageException e) {
CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
return false;
} catch (IOException e) {
CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
return false;
}
return true;
}
/*
* Validate that the receiver contains valid information for
* making a connection. If the receiver contains valid
* information, the method returns. Otherwise, an exception
* indicating the problem is throw.
*/
public void validateConnection(IProgressMonitor monitor) throws CVSException {
try {
monitor = Policy.monitorFor(monitor);
monitor.beginTask(null, 100);
ICVSFolder root = CVSWorkspaceRoot.getCVSFolderFor(ResourcesPlugin.getWorkspace().getRoot());
Session session = new Session(this, root, false /* output to console */);
session.open(Policy.subMonitorFor(monitor, 50), false /* read-only */);
try {
IStatus status = Command.VERSION.execute(session, this, Policy.subMonitorFor(monitor, 50));
// Log any non-ok status
if (! status.isOK()) {
CVSProviderPlugin.log(status);
}
} finally {
session.close();
monitor.done();
}
} catch (CVSException e) {
// If the validation failed, dispose of any cached info
dispose();
throw e;
}
}
/**
* Return the server platform type. It will be one of the following:
* UNDETERMINED_PLATFORM: The platform has not been determined
* CVS_SERVER: The platform is regular CVS server
* CVSNT_SERVER: The platform in CVSNT
* If UNDETERMINED_PLATFORM is returned, the platform can be determined
* using the Command.VERSION command.
*/
public int getServerPlatform() {
return serverPlatform;
}
/**
* This method is called from Command.VERSION to set the platform type.
*/
public void setServerPlaform(int serverType) {
// Second, check the code of the status itself to see if it is NT
switch (serverType) {
case CVS_SERVER:
case CVSNT_SERVER:
case UNKNOWN_SERVER:
case UNSUPPORTED_SERVER:
serverPlatform = serverType;
break;
default:
// We had an error status with no info about the server.
// Mark it as undetermined.
serverPlatform = UNDETERMINED_PLATFORM;
}
}
@Override
public void flushUserInfo() {
removeNode();
}
/*
* Return the command string that is to be used by the EXT connection method.
*/
String[] getExtCommand(String password) throws IOException {
// Get the user specified connection parameters
String CVS_RSH = CVSProviderPlugin.getPlugin().getCvsRshCommand();
String CVS_RSH_PARAMETERS = CVSProviderPlugin.getPlugin().getCvsRshParameters();
String CVS_SERVER = CVSProviderPlugin.getPlugin().getCvsServer();
if(CVS_RSH == null || CVS_SERVER == null) {
throw new IOException(CVSMessages.EXTServerConnection_varsNotSet);
}
// If there is only one token, assume it is the command and use the default parameters and order
if (CVS_RSH_PARAMETERS == null || CVS_RSH_PARAMETERS.length() == 0) {
if (port != USE_DEFAULT_PORT)
throw new IOException(CVSMessages.EXTServerConnection_invalidPort);
return new String[] {CVS_RSH, host, "-l", user, CVS_SERVER, INVOKE_SVR_CMD}; //$NON-NLS-1$
}
// Substitute any variables for their appropriate values
CVS_RSH_PARAMETERS = stringReplace(CVS_RSH_PARAMETERS, USER_VARIABLE, user);
CVS_RSH_PARAMETERS = stringReplace(CVS_RSH_PARAMETERS, PASSWORD_VARIABLE, password);
CVS_RSH_PARAMETERS = stringReplace(CVS_RSH_PARAMETERS, HOST_VARIABLE, host);
CVS_RSH_PARAMETERS = stringReplace(CVS_RSH_PARAMETERS, PORT_VARIABLE, Integer.valueOf(port).toString());
// Build the command list to be sent to the OS.
List<String> commands = new ArrayList<String>();
commands.add(CVS_RSH);
StringTokenizer tokenizer = new StringTokenizer(CVS_RSH_PARAMETERS);
while (tokenizer.hasMoreTokens()) {
String next = tokenizer.nextToken();
commands.add(next);
}
commands.add(CVS_SERVER);
commands.add(INVOKE_SVR_CMD);
return commands.toArray(new String[commands.size()]);
}
/*
* Replace all occurrences of oldString with newString
*/
private String stringReplace(String string, String oldString, String newString) {
int index = string.toLowerCase().indexOf(oldString);
if (index == -1) return string;
return stringReplace(
string.substring(0, index) + newString + string.substring(index + oldString.length()),
oldString, newString);
}
/**
* Return the server message with the prefix removed.
* Server aborted messages typically start with
* "cvs server: ..."
* "cvs [server aborted]: ..."
* "cvs rtag: ..."
*/
public String getServerMessageWithoutPrefix(String errorLine, String prefix) {
String message = errorLine;
int firstSpace = message.indexOf(' ');
if(firstSpace != -1) {
// remove the program name and the space
message = message.substring(firstSpace + 1);
// Quick fix to handle changes in server message format (see Bug 45138)
if (prefix.startsWith("[")) { //$NON-NLS-1$
// This is the server aborted message
// Remove the pattern "[command_name aborted]: "
int closingBracket = message.indexOf("]: "); //$NON-NLS-1$
if (closingBracket == -1) return null;
// get what is inside the brackets
String realPrefix = message.substring(1, closingBracket);
// check that there is two words and the second word is "aborted"
int space = realPrefix.indexOf(' ');
if (space == -1) return null;
if (realPrefix.indexOf(' ', space +1) != -1) return null;
if (!realPrefix.substring(space +1).equals("aborted")) return null; //$NON-NLS-1$
// It's a match, return the rest of the line
message = message.substring(closingBracket + 2);
if (message.charAt(0) == ' ') {
message = message.substring(1);
}
return message;
} else {
// This is the server command message
// Remove the pattern "command_name: "
int colon = message.indexOf(": "); //$NON-NLS-1$
if (colon == -1) return null;
// get what is before the colon
String realPrefix = message.substring(0, colon);
// ensure that it is a single word
if (realPrefix.indexOf(' ') != -1) return null;
message = message.substring(colon + 1);
if (message.charAt(0) == ' ') {
message = message.substring(1);
}
return message;
}
}
// This is not a server message with the desired prefix
return null;
}
@Override
public IUserAuthenticator getUserAuthenticator() {
return getAuthenticator();
}
@Override
public void setUserAuthenticator(IUserAuthenticator authenticator) {
CVSRepositoryLocation.authenticator = authenticator;
}
/*
* Return the preferences node for this repository
*/
public Preferences getPreferences() {
if (!hasPreferences()) {
ensurePreferencesStored();
}
return internalGetPreferences();
}
private Preferences internalGetPreferences() {
return getParentPreferences().node(getPreferenceName());
}
private boolean hasPreferences() {
try {
return getParentPreferences().nodeExists(getPreferenceName());
} catch (BackingStoreException e) {
CVSProviderPlugin.log(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_74, new String[] { getLocation(true) }), e);
return false;
}
}
/**
* Return a unique name that identifies this location but
* does not contain any slashes (/). Also, do not use ':'.
* Although a valid path character, the initial core implementation
* didn't handle it well.
*/
private String getPreferenceName() {
return getLocation().replace('/', '%').replace(':', '%');
}
public void storePreferences() {
Preferences prefs = internalGetPreferences();
// Must store at least one preference in the node
prefs.put(PREF_LOCATION, getLocation());
flushPreferences();
}
private void flushPreferences() {
try {
internalGetPreferences().flush();
} catch (BackingStoreException e) {
CVSProviderPlugin.log(IStatus.ERROR, NLS.bind(CVSMessages.CVSRepositoryLocation_75, new String[] { getLocation(true) }), e);
}
}
private void ensurePreferencesStored() {
if (!hasPreferences()) {
storePreferences();
}
}
@Override
public boolean getUserInfoCached() {
ISecurePreferences node = getCVSNode();
if (node == null)
return false;
try {
String password = node.get(PASSWORD_KEY, null);
return (password != null);
} catch (StorageException e) { // most likely invalid keyring password or corrupted data
CVSProviderPlugin.log(IStatus.ERROR, e.getMessage(), e);
}
return false;
}
/**
* At this time information is saved in a simplistic flat form. In future, this
* can be modified into a hierarchy of storing information in "connections"
* where "connection" would combine "server" and "account" information (allowing
* user to have the same password for different connections on the server).
*
* Hopefully, we'll get some simplified notion of "account" from Higgins into Equinox
* and then we'll be able to re-use it.
*
* For now, the structure is rather simple:
* node: "CVS" "/CVS/"
* node: account_name name combines all attributes
* | value: login
* | value: password
*/
private ISecurePreferences getCVSNode() {
ISecurePreferences preferences = SecurePreferencesFactory.getDefault();
if (preferences == null)
return null;
String accountName = EncodingUtils.encodeSlashes(getLocation(true));
String path = cvsNameSegment + accountName;
try {
return preferences.node(path);
} catch (IllegalArgumentException e) {
return null; // invalid path
}
}
}