blob: fc00a167eb2900c2f806396f2a3eddc23d34a4bb [file] [log] [blame]
/******************************************************************************
* Copyright (c) 2006 IBM Corporation.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - Initial Implementation
*
*****************************************************************************/
package org.eclipse.ptp.remotetools.internal.ssh;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.ptp.remotetools.core.AuthToken;
import org.eclipse.ptp.remotetools.core.IRemoteConnection;
import org.eclipse.ptp.remotetools.core.IRemoteExecutionManager;
import org.eclipse.ptp.remotetools.core.IRemoteOperation;
import org.eclipse.ptp.remotetools.core.KeyAuthToken;
import org.eclipse.ptp.remotetools.core.PasswdAuthToken;
import org.eclipse.ptp.remotetools.exception.LocalPortBoundException;
import org.eclipse.ptp.remotetools.exception.RemoteConnectionException;
import org.eclipse.ptp.remotetools.internal.common.RemoteTunnel;
import org.eclipse.ptp.remotetools.internal.core.ConnectionProperties;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
/**
* The connection to a remote host.
* Manages a pool of connection used to open execution channels, since some SSH server
* showed to impose a limit of execution channels per connection.
*
* @author Richard Maciel and Daniel Felix Ferber.
*/
public class Connection implements IRemoteConnection {
/**
* Data structure to access the ssh library.
*/
private JSch jsch;
/**
* A connection to the remote host. The default connections is always created, but more
* connections may be added to the pool on demand.
*/
Session defaultSession;
private AuthToken authToken;
//private String password;
private String username;
private String hostname;
private int port;
private int timeout;
private String cipherType;
private SSHUserInfo sshuserinfo = new SSHUserInfo();
/**
* All executions managers created for this connection.
*/
private ArrayList executionManagers;
/**
* Tunnels to remote host.
*/
Set tunnels;
/**
* Executions on remote host.
*/
Set executions;
/**
* Default sftp channel shared by all executions managers and file tools.
*/
ChannelSftp sftpChannel;
/**
* Hashtable that keeps all remote executions that can be killed.
* The table is indexed by Internal PID.
*/
Hashtable activeProcessTable;
/**
* The internal identification number for the next remote execution.
*/
private int nextInternalPID;
/**
* The control connection.
*/
private ControlChannel controlChannel;
/**
* A job that watches for finished executions.
*/
private ExecutionObserver executionObserver;
/**
* Array of all connections and how many pty channels were used.
*/
ArrayList connectionPool = new ArrayList();
private class ConnectionSlot {
Session session = null;
int numberUsedChannels = 0;
ConnectionSlot(Session session) {
this.session = session;
}
ConnectionSlot(Session session, int initialLoad) {
this.session = session;
this.numberUsedChannels = initialLoad;
}
}
/**
* Maps a channel to the connection where it was created.
*/
HashMap channelToConnectioPool;
/**
* Locks used on synchronized operations.
*/
protected ConnectionLocks connectionLocks;
RemotePortForwardingPool forwardingPool;
/**
* Default constructor
*
* @param authToken
* @param hostname
* @param port
* @param timeout
* @param cipherType
*/
public Connection(AuthToken authToken, String hostname, int port, String cipherType, int timeout) {
this.jsch = new JSch();
this.authToken = authToken;
this.username = authToken.getUsername();
this.hostname = hostname;
this.port = port;
this.timeout = timeout;
this.cipherType = cipherType;
// Convert information for the UserInfo class used by JSch
if(authToken instanceof PasswdAuthToken) {
sshuserinfo.isPasswdBased = true;
sshuserinfo.password = ((PasswdAuthToken)authToken).getPassword();
} else if(authToken instanceof KeyAuthToken) {
KeyAuthToken token = (KeyAuthToken)authToken;
sshuserinfo.isPasswdBased = false;
sshuserinfo.passphrase = token.getPassphrase();
} else {
throw new RuntimeException(Messages.Connection_AuthenticationTypeNotSupported);
}
}
public Connection(AuthToken authToken, String hostname, int port, String cipherType) {
this(authToken, hostname, port, CipherTypes.CIPHER_DEFAULT, ConnectionProperties.defaultTimeout);
}
public Connection(AuthToken authToken, String hostname) {
this(authToken, hostname, ConnectionProperties.defaultPort,
CipherTypes.CIPHER_DEFAULT, ConnectionProperties.defaultTimeout);
}
public Connection(AuthToken authToken, String hostname, int port) {
this(authToken, hostname, port, CipherTypes.CIPHER_DEFAULT, ConnectionProperties.defaultTimeout);
}
public synchronized void connect() throws RemoteConnectionException {
this.nextInternalPID = 0;
/*
* Insert key information if necessary
*/
if(authToken instanceof KeyAuthToken) {
KeyAuthToken token = (KeyAuthToken)authToken;
try {
jsch.addIdentity(token.getKeyPath().getAbsolutePath());
} catch (JSchException e) {
throw new RemoteConnectionException(Messages.Connection_Connect_InvalidPrivateKey, e);
}
}
/*
* Create session.
*/
try {
defaultSession = jsch.getSession(username, hostname, port);
defaultSession.setUserInfo(sshuserinfo);
} catch (JSchException e) {
disconnect();
throw new RemoteConnectionException(Messages.Connection_Connect_FailedCreateSession, e);
}
setSessionCipherType(defaultSession);
/*
* Connect to remote host.
*/
try {
defaultSession.connect(timeout);
} catch (JSchException e) {
disconnect();
throw new RemoteConnectionException(Messages.Connection_Connect_FailedConnect, e);
} catch (Exception e) {
disconnect();
throw new RemoteConnectionException(Messages.Connection_Connect_FailedUnsupportedKeySize, e);
}
/*
* Create control execution channel.
*/
try {
controlChannel = new ControlChannel(this);
controlChannel.open();
} catch (RemoteConnectionException e) {
disconnect();
throw new RemoteConnectionException(Messages.Connection_Connect_FailedCreateControlChannel, e);
}
/*
* Create convenience sftp channel.
*/
try {
sftpChannel = (ChannelSftp) defaultSession.openChannel("sftp"); //$NON-NLS-1$
sftpChannel.connect();
} catch (JSchException e) {
disconnect();
throw new RemoteConnectionException(Messages.Connection_Connect_FailedCreateSFTPConnection, e);
}
/*
* Create observer job but do not schedule it
*/
executionObserver = new ExecutionObserver(this);
executionObserver.setPriority(Job.DECORATE);
executionObserver.setSystem(true);
/*
* The default session cannot be fully used for connection pool,
* since some channels are already using pty.
*/
connectionPool = new ArrayList();
ConnectionSlot slot = new ConnectionSlot(defaultSession, ConnectionProperties.initialDefaultSessionLoad);
connectionPool.add(slot);
executionManagers = new ArrayList();
tunnels = new HashSet();
executions = new HashSet();
activeProcessTable = new Hashtable();
channelToConnectioPool = new HashMap();
forwardingPool = new RemotePortForwardingPool(this);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ptp.remotetools.IRemoteConnection#getRemoteExecutionManager()
*/
public synchronized IRemoteExecutionManager createRemoteExecutionManager() throws RemoteConnectionException {
ExecutionManager e = new ExecutionManager(this);
executionManagers.add(e);
return e;
}
synchronized protected void releaseExcutionManager(ExecutionManager manager) {
executionManagers.remove(manager);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ptp.remotetools.IRemoteConnection#disconnect()
*/
public synchronized void disconnect() {
/*
* First, cancel all ongoing executions and tunnels created by managers, by closing their execution managers.
*/
if (executionManagers != null) {
Iterator iterator = executionManagers.iterator();
while (iterator.hasNext()) {
ExecutionManager manager = (ExecutionManager) iterator.next();
manager.close();
iterator = executionManagers.iterator();
}
}
if (executionObserver != null) {
executionObserver.cancel();
}
/*
* Then, close channels created by the connection.
*/
if (controlChannel != null) {
controlChannel.close();
}
if (sftpChannel != null) {
sftpChannel.disconnect();
}
/*
* Close all connections from the pool.
*/
if (connectionPool != null) {
Iterator iterator = connectionPool.iterator();
while (iterator.hasNext()) {
ConnectionSlot slot = (ConnectionSlot) iterator.next();
slot.session.disconnect();
}
}
/*
* Reset internal state variables.
*/
defaultSession = null;
sftpChannel = null;
controlChannel = null;
connectionPool = null;
executionObserver = null;
executions = null;
executionManagers = null;
tunnels = null;
if (forwardingPool != null) {
forwardingPool.disconnect();
forwardingPool = null;
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.ptp.remotetools.IRemoteConnection#isConnected()
*/
public synchronized boolean isConnected() {
if (defaultSession == null) {
return false;
} else {
return defaultSession.isConnected();
}
}
public String getHostname() {
return hostname;
}
/*public String getPassword() {
return password;
}*/
public int getPort() {
return port;
}
public int getTimeout() {
return timeout;
}
public String getUsername() {
return username;
}
/**
* This class is required by the jsch library.
*
* @author Richard Maciel
*
*/
private class SSHUserInfo implements UserInfo, UIKeyboardInteractive {
public String password;
public String passphrase;
public boolean isPasswdBased;
private SSHUserInfo() { }
public String getPassword() {
return password;
}
public boolean promptYesNo(String str) {
// Always accept host identity
return true;
}
public String getPassphrase() {
return passphrase;
}
public boolean promptPassphrase(String message) {
return !isPasswdBased;
}
public boolean promptPassword(String message) {
return isPasswdBased;
}
public void showMessage(String message) {
}
public String[] promptKeyboardInteractive(final String destination,
final String name, final String instruction,
final String[] prompt, final boolean[] echo) {
if (prompt.length != 1
|| echo[0] != false
|| password == null) {
return null;
}
String[] response = new String[1];
response[0] = password;
return response;
}
}
/**
* Creates a new execution channel. The channel may or may not be managed by the connection pool.
* All execution channels that require PTY must be managed by the pool, except really special cases,
* like the control channel and default SFTP channel.
* Channels without PTY may be managed, by it is recommended no to be. This will allocate them into the
* default session, since an unlimited number of not PTY channels can be allocated into the default session.
* @param isInConnectionPool As described.
* @return The requested execution channel.
* @throws RemoteConnectionException If the allocation of new channel failed of if it was not possible
* to create a new ssh session for the new channel.
*/
protected ChannelExec createExecChannel(boolean isInConnectionPool) throws RemoteConnectionException {
if (isInConnectionPool) {
/*
* Search for the first available connection slot or create a new one if all are full.
*/
ConnectionSlot suggestedSlot = null;
Iterator iterator = connectionPool.iterator();
while (iterator.hasNext()) {
ConnectionSlot slot = (ConnectionSlot) iterator.next();
if (slot.numberUsedChannels < ConnectionProperties.maxChannelsPerConnection) {
suggestedSlot = slot;
break;
}
}
if (suggestedSlot == null) {
suggestedSlot = createConnecitonSlot();
}
/*
* Create the channel and update the pool.
*/
ChannelExec channel;
try {
channel = (ChannelExec) suggestedSlot.session.openChannel("exec"); //$NON-NLS-1$
} catch (JSchException e) {
throw new RemoteConnectionException(Messages.Connection_CreateExecChannel_FailedCreateNewExecChannel, e);
}
suggestedSlot.numberUsedChannels++;
channelToConnectioPool.put(channel, suggestedSlot);
return channel;
} else {
/*
* Create a channel on the default channel.
*/
try {
return (ChannelExec) defaultSession.openChannel("exec"); //$NON-NLS-1$
} catch (JSchException e) {
throw new RemoteConnectionException(Messages.Connection_CreateExecChannel_FailedCreateNewExecChannel, e);
}
}
}
/**
* Remove a channel from the pool, leaving the slot available for another channel.
* @param channel
*/
protected void releaseChannel(Channel channel) {
/*
* The channel may or may not be in the connection pool, depending how it was created.
* Ant any case, always disconnect the channel.
*/
channel.disconnect();
ConnectionSlot slot = (ConnectionSlot) channelToConnectioPool.remove(channel);
if (slot != null) {
slot.numberUsedChannels--;
}
}
/**
* Create a forwarding from a remote port to a local port.
* @param localPort
* @param addressOnRemoteHost
* @param portOnRemoteHost
* @return
* @throws RemoteConnectionException
*/
protected RemoteTunnel createTunnel(int localPort, String addressOnRemoteHost, int portOnRemoteHost)
throws RemoteConnectionException, LocalPortBoundException {
RemoteTunnel tunnel = new RemoteTunnel(localPort, portOnRemoteHost, addressOnRemoteHost);
if (tunnels.contains(tunnel)) {
throw new LocalPortBoundException(Messages.Connection_CreateTunnel_TunnelPortAlreadyAlloced);
}
try {
defaultSession.setPortForwardingL(tunnel.getLocalPort(), tunnel.getAddressOnRemoteHost(), tunnel
.getPortOnRemoteHost());
} catch (JSchException e) {
if(e.getMessage().matches("PortForwardingL: local port .* is already registered.")) { //$NON-NLS-1$
// Selected local port is already bound.
throw new LocalPortBoundException(Messages.Connection_CreateTunnel_TunnelPortAlreadyAlloced);
}
throw new RemoteConnectionException(Messages.Connection_CreateTunnel_FailedCreateTunnel, e);
}
tunnels.add(tunnel);
return tunnel;
}
/**
* Release the forwarding of the remote port.
* @param tunnel
* @throws RemoteConnectionException
*/
protected void releaseTunnel(RemoteTunnel tunnel) throws RemoteConnectionException {
if (!tunnels.contains(tunnel)) {
throw new RemoteConnectionException(Messages.Connection_ReleaseTunnel_PortNotAllocedForTunnel);
}
try {
RemoteTunnel remoteTunnel = tunnel;
defaultSession.delPortForwardingL(remoteTunnel.getLocalPort());
} catch (JSchException e) {
throw new RemoteConnectionException(Messages.Connection_ReleaseTunnel_FailedRemoveTunnel, e);
}
tunnels.remove(tunnel);
}
/**
* Gets the SFTP channel that may be used by the internal implementation to do file system operations
* on the remote host.
* @return
*/
protected ChannelSftp getDefaultSFTPChannel() {
return sftpChannel;
}
protected synchronized int createNextPIID() {
return ++nextInternalPID % Integer.MAX_VALUE;
}
protected synchronized void setPID(int piid, int pid) {
// Look for the object which key is PIID.
KillableExecution rce = (KillableExecution) activeProcessTable.get(new Integer(piid));
if (rce != null) {
// Process could be already finished and removed fro mthe table.
rce.setPID(pid);
}
}
protected synchronized void registerObservedExecution(IRemoteOperation operation) {
if (operation instanceof KillableExecution) {
KillableExecution killableExecution = (KillableExecution) operation;
activeProcessTable.put(new Integer(killableExecution.getInternaID()), killableExecution);
executionObserver.newCommand();
}
}
protected synchronized void unregisterObservedExecution(IRemoteOperation operation) {
if (operation instanceof KillableExecution) {
KillableExecution killableExecution = (KillableExecution) operation;
activeProcessTable.remove(new Integer(killableExecution.getInternaID()));
}
}
protected String getKillablePrefix(KillableExecution execution) {
return controlChannel.getKillablePrefix(execution.getInternaID());
}
/**
* Sends a KILL signal to the remote killable execution.
* @param execution
*/
protected void killExecution(KillableExecution execution) {
controlChannel.killRemoteProcess(execution.getPID());
}
/**
* Open a new connection to the remote host and add this connection to the pool.
* @return A pool entry.
* @throws RemoteConnectionException
*/
private ConnectionSlot createConnecitonSlot() throws RemoteConnectionException {
/*
* Create a Jsch session, with the same authentication values as the default session.
*/
Session newSession = null;
try {
newSession = jsch.getSession(username, hostname, port);
newSession.setUserInfo(sshuserinfo);
setSessionCipherType(newSession);
} catch (JSchException e) {
throw new RemoteConnectionException(Messages.Connection_CreateConnectionSlot_FailedCreateNewSession, e);
}
/*
* Connect to remote host.
*/
try {
newSession.connect(timeout);
} catch (JSchException e) {
throw new RemoteConnectionException(Messages.Connection_CreateConnectionSlot_FailedConnectNewSession, e);
}
/*
* Create slot and add to the pool.
*/
ConnectionSlot slot = new ConnectionSlot(newSession);
connectionPool.add(slot);
return slot;
}
/**
* Select a cipher for the session based on the cipherType attribute
*
* @param session Session that will have its cipher altered.
*/
private void setSessionCipherType(Session session) {
/*
* If the user selected a cipher other than the default, setup the cipher
*
*/
// TODO: Let the user select a list of ciphers, instead of only one
if(!cipherType.equals(CipherTypes.CIPHER_DEFAULT)) {
// Verify if the cipher is supported. Throw an exception if it isnt.
if(!CipherTypes.getCipherTypesMap().containsKey(cipherType)) {
// TODO: Throw a real exception, not a runtime one
throw new RuntimeException(Messages.Connection_SetCipherType_CipherNotSupported);
}
// Set the session's cipher
Hashtable config=new Hashtable();
config.put("cipher.s2c", cipherType); //$NON-NLS-1$
config.put("cipher.c2s", cipherType); //$NON-NLS-1$
session.setConfig(config);
}
}
/**
* Performs a sanity test to make sure that the connection is alive and has a valid state.
* <p>
* The connection may get dropped due some external interference, like loosing physical
* access to the remote machine.
* Or the connection may drop some channel, as it may be caused by a misbehavior
* of the remote SSH server or by a bug in the local SSH implementation.
* <p>
* If some problem is detected, then an {@link RemoteConnectionException} is thrown.
* Else, the method returns.
*
* @throws RemoteConnectionException The connection was entirely dropped or some channel got lost.
*/
protected void test() throws RemoteConnectionException {
/*
* Check all SSH sessions
*/
Iterator iterator = connectionPool.iterator();
while (iterator.hasNext()) {
ConnectionSlot slot = (ConnectionSlot) iterator.next();
if (! slot.session.isConnected()) {
throw new RemoteConnectionException("SSH connection to remote host was lost");
}
}
/*
* Check SFTP channel.
*/
if (! sftpChannel.isConnected()) {
throw new RemoteConnectionException("SFTP connection to remote host was lost");
}
/*
* Check control channel.
*/
if (! controlChannel.shell.isConnected()) {
throw new RemoteConnectionException("Control channel connection to remote host was lost");
}
}
}