/*******************************************************************************
 * Copyright (c) 2007, 2015 IBM Corporation and others.
 * 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 API and implementation
 * Patrick Tasse - [462418] use stored password on non-preferred password based authentication
 * Martin Oberhuber - [468889] Support Eclipse older than Mars
 *******************************************************************************/
package org.eclipse.remote.internal.jsch.core;

import java.net.PasswordAuthentication;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jsch.core.IJSchService;
import org.eclipse.osgi.util.NLS;
import org.eclipse.remote.core.IRemoteConnection;
import org.eclipse.remote.core.IRemoteConnectionChangeListener;
import org.eclipse.remote.core.IRemoteConnectionControlService;
import org.eclipse.remote.core.IRemoteConnectionHostService;
import org.eclipse.remote.core.IRemoteConnectionPropertyService;
import org.eclipse.remote.core.IRemoteConnectionWorkingCopy;
import org.eclipse.remote.core.IRemotePortForwardingService;
import org.eclipse.remote.core.IRemoteProcessBuilder;
import org.eclipse.remote.core.IRemoteProcessService;
import org.eclipse.remote.core.IUserAuthenticatorService;
import org.eclipse.remote.core.RemoteConnectionChangeEvent;
import org.eclipse.remote.core.RemoteServicesUtils;
import org.eclipse.remote.core.exception.AddressInUseException;
import org.eclipse.remote.core.exception.RemoteConnectionException;
import org.eclipse.remote.core.exception.UnableToForwardPortException;
import org.eclipse.remote.internal.jsch.core.commands.ExecCommand;
import org.eclipse.remote.internal.jsch.core.messages.Messages;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;

/**
 * @since 5.0
 */
public class JSchConnection implements IRemoteConnectionControlService, IRemoteConnectionPropertyService,
		IRemotePortForwardingService, IRemoteProcessService, IRemoteConnectionHostService, IRemoteConnectionChangeListener {
	// Connection Type ID
	public static final String JSCH_ID = "org.eclipse.remote.JSch"; //$NON-NLS-1$

	// Attributes
	public static final String ADDRESS_ATTR = "JSCH_ADDRESS_ATTR"; //$NON-NLS-1$
	public static final String USERNAME_ATTR = "JSCH_USERNAME_ATTR"; //$NON-NLS-1$
	public static final String PASSWORD_ATTR = "JSCH_PASSWORD_ATTR"; //$NON-NLS-1$
	public static final String PORT_ATTR = "JSCH_PORT_ATTR"; //$NON-NLS-1$
	public static final String PROXYCONNECTION_ATTR = "JSCH_PROXYCONNECTION_ATTR"; //$NON-NLS-1$
	public static final String PROXYCOMMAND_ATTR = "JSCH_PROXYCOMMAND_ATTR"; //$NON-NLS-1$
	public static final String IS_PASSWORD_ATTR = "JSCH_IS_PASSWORD_ATTR"; //$NON-NLS-1$
	public static final String PASSPHRASE_ATTR = "JSCH_PASSPHRASE_ATTR"; //$NON-NLS-1$
	public static final String TIMEOUT_ATTR = "JSCH_TIMEOUT_ATTR"; //$NON-NLS-1$
	public static final String USE_LOGIN_SHELL_ATTR = "JSCH_USE_LOGIN_SHELL_ATTR"; //$NON-NLS-1$
	public static final String LOGIN_SHELL_COMMAND_ATTR = "JSCH_LOGIN_SHELL_COMMAND_ATTR"; //$NON-NLS-1$

	/**
	 * Class to supply credentials from connection attributes without user interaction.
	 */
	private class JSchUserInfo implements UserInfo, UIKeyboardInteractive {
		private boolean firstTryPassphrase = true;

		/*
		 * (non-Javadoc)
		 *
		 * @see com.jcraft.jsch.UserInfo#getPassphrase()
		 */
		@Override
		public String getPassphrase() {
			if (logging) {
				System.out.println("getPassphrase"); //$NON-NLS-1$
			}
			return JSchConnection.this.getPassphrase();
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see com.jcraft.jsch.UserInfo#getPassword()
		 */
		@Override
		public String getPassword() {
			if (logging) {
				System.out.println("getPassword"); //$NON-NLS-1$
			}
			return JSchConnection.this.getPassword();
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see com.jcraft.jsch.UIKeyboardInteractive#promptKeyboardInteractive(java.lang.String, java.lang.String,
		 * java.lang.String, java.lang.String[], boolean[])
		 */
		@Override
		public String[] promptKeyboardInteractive(String destination, String name, String instruction, String[] prompt,
				boolean[] echo) {
			if (logging) {
				System.out.println("promptKeyboardInteractive:" + destination + ":" + name + ":" + instruction); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
				for (String p : prompt) {
					System.out.println(" " + p); //$NON-NLS-1$
				}
			}

			IUserAuthenticatorService authService = fRemoteConnection.getService(IUserAuthenticatorService.class);
			if (authService != null) {
				String[] result = authService.prompt(destination, name, instruction, prompt, echo);
				if (result != null) {
					if (prompt.length == 1 && prompt[0].trim().equalsIgnoreCase("password:")) { //$NON-NLS-1$
						IRemoteConnectionWorkingCopy wc = fRemoteConnection.getWorkingCopy();
						wc.setSecureAttribute(PASSWORD_ATTR, result[0]);
						try {
							wc.save();
						} catch (RemoteConnectionException e) {
							Activator.log(e.getStatus());
						}
					}
				}
				return result;
			}
			return null;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see com.jcraft.jsch.UserInfo#promptPassphrase(java.lang.String)
		 */
		@Override
		public boolean promptPassphrase(String message) {
			if (logging) {
				System.out.println("promptPassphrase:" + message); //$NON-NLS-1$
			}
			if (firstTryPassphrase && !getPassphrase().equals("")) { //$NON-NLS-1$
				firstTryPassphrase = false;
				return true;
			}
			IUserAuthenticatorService authService = fRemoteConnection.getService(IUserAuthenticatorService.class);
			if (authService != null) {
				PasswordAuthentication auth = authService.prompt(getUsername(), message);
				if (auth == null) {
					return false;
				}
				IRemoteConnectionWorkingCopy wc = fRemoteConnection.getWorkingCopy();
				wc.setAttribute(USERNAME_ATTR, auth.getUserName());
				wc.setSecureAttribute(PASSPHRASE_ATTR, new String(auth.getPassword()));
				try {
					wc.save();
				} catch (RemoteConnectionException e) {
					Activator.log(e.getStatus());
				}
				return true;
			}
			return false;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see com.jcraft.jsch.UserInfo#promptPassword(java.lang.String)
		 */
		@Override
		public boolean promptPassword(String message) {
			if (logging) {
				System.out.println("promptPassword:" + message); //$NON-NLS-1$
			}
			IUserAuthenticatorService authService = fRemoteConnection.getService(IUserAuthenticatorService.class);
			if (authService != null) {
				PasswordAuthentication auth = authService.prompt(getUsername(), message);
				if (auth == null) {
					return false;
				}
				IRemoteConnectionWorkingCopy wc = fRemoteConnection.getWorkingCopy();
				wc.setAttribute(USERNAME_ATTR, auth.getUserName());
				wc.setSecureAttribute(PASSWORD_ATTR, new String(auth.getPassword()));
				try {
					wc.save();
				} catch (RemoteConnectionException e) {
					Activator.log(e.getStatus());
				}
				return true;
			}
			return false;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see com.jcraft.jsch.UserInfo#promptYesNo(java.lang.String)
		 */
		@Override
		public boolean promptYesNo(String message) {
			if (logging) {
				System.out.println("promptYesNo:" + message); //$NON-NLS-1$
			}
			IUserAuthenticatorService authService = fRemoteConnection.getService(IUserAuthenticatorService.class);
			if (authService != null) {
				int prompt = authService.prompt(IUserAuthenticatorService.QUESTION, Messages.AuthInfo_Authentication_message,
						message, new int[] { IUserAuthenticatorService.YES, IUserAuthenticatorService.NO },
						IUserAuthenticatorService.YES);
				return prompt == IUserAuthenticatorService.YES;
			}
			return true;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see com.jcraft.jsch.UserInfo#showMessage(java.lang.String)
		 */
		@Override
		public void showMessage(String message) {
			if (logging) {
				System.out.println("showMessage:" + message); //$NON-NLS-1$
			}
			IUserAuthenticatorService authService = fRemoteConnection.getService(IUserAuthenticatorService.class);
			if (authService != null) {
				authService.prompt(IUserAuthenticatorService.INFORMATION, Messages.AuthInfo_Authentication_message, message,
						new int[] { IUserAuthenticatorService.OK }, IUserAuthenticatorService.OK);
			}
		}
	}

	private final boolean logging = false;

	public static final int DEFAULT_PORT = 22;
	public static final int DEFAULT_TIMEOUT = 0;
	public static final boolean DEFAULT_IS_PASSWORD = false;
	public static final boolean DEFAULT_USE_LOGIN_SHELL = true;
	public static final String DEFAULT_LOGIN_SHELL_COMMAND = "/bin/bash -l -c '{0}'"; //$NON-NLS-1$
	public static final String EMPTY_STRING = ""; //$NON-NLS-1$

	private String fWorkingDir;

	private final IRemoteConnection fRemoteConnection;
	private final IJSchService fJSchService;

	private final Map<String, String> fEnv = new HashMap<>();
	private final Map<String, String> fProperties = new HashMap<>();
	private final List<Session> fSessions = new ArrayList<>();

	private ChannelSftp fSftpCommandChannel;
	private boolean isFullySetup; // including sftp channel and environment

	private static final Map<IRemoteConnection, JSchConnection> connectionMap = new HashMap<>();

	public JSchConnection(IRemoteConnection connection) {
		fRemoteConnection = connection;
		fJSchService = Activator.getDefault().getService();
		connection.addConnectionChangeListener(this);
	}

	@Override
	public void connectionChanged(RemoteConnectionChangeEvent event) {
		if (event.getType() == RemoteConnectionChangeEvent.CONNECTION_REMOVED) {
			synchronized (connectionMap) {
				connectionMap.remove(event.getConnection());
			}
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnection.Service#getRemoteConnection()
	 */
	@Override
	public IRemoteConnection getRemoteConnection() {
		return fRemoteConnection;
	}

	public static class Factory implements IRemoteConnection.Service.Factory {
		/*
		 * (non-Javadoc)
		 *
		 * @see org.eclipse.remote.core.IRemoteConnection.Service.Factory#getService(org.eclipse.remote.core.IRemoteConnection,
		 * java.lang.Class)
		 */
		@Override
		@SuppressWarnings("unchecked")
		public <T extends IRemoteConnection.Service> T getService(IRemoteConnection connection, Class<T> service) {
			// This little trick creates an instance of this class for a connection
			// then for each interface it implements, it returns the same object.
			// This works because the connection caches the service so only one gets created.
			// As a side effect, it makes this class a service too which can be used
			// by the this plug-in
			if (JSchConnection.class.equals(service)) {
				synchronized (connectionMap) {
					JSchConnection jschConnection = connectionMap.get(connection);
					if (jschConnection == null) {
						jschConnection = new JSchConnection(connection);
						connectionMap.put(connection, jschConnection);
					}
					return (T) jschConnection;
				}
			} else if (IRemoteConnectionControlService.class.equals(service)
					|| IRemoteConnectionPropertyService.class.equals(service) || IRemotePortForwardingService.class.equals(service)
					|| IRemoteProcessService.class.equals(service) || IRemoteConnectionHostService.class.equals(service)) {
				return (T) connection.getService(JSchConnection.class);
			} else {
				return null;
			}
		}
	}

	private boolean checkConfiguration(Session session, IProgressMonitor monitor) throws RemoteConnectionException {
		SubMonitor subMon = SubMonitor.convert(monitor, 10);
		ChannelSftp sftp;
		try {
			/*
			 * First, check if sftp is supported at all. This is required for EFS, so throw exception if not supported.
			 */
			sftp = openSftpChannel(session);
		} catch (RemoteConnectionException e) {
			throw new RemoteConnectionException(Messages.JSchConnection_Remote_host_does_not_support_sftp);
		}
		/*
		 * While sftp channel is open, try opening an exec channel. If it doesn't succeed, then MaxSession is < 2 so we need at
		 * least one additional session.
		 */
		try {
			loadEnv(subMon.newChild(10));
		} catch (RemoteConnectionException e) {
			if (e.getMessage().contains("channel is not opened")) { //$NON-NLS-1$
				return false;
			}
		} finally {
			if (sftp != null) {
				sftp.disconnect();
			}
		}
		return true;
	}

	/**
	 * @throws RemoteConnectionException
	 */
	private void checkIsConfigured() throws RemoteConnectionException {
		if (fRemoteConnection.getAttribute(ADDRESS_ATTR) == null) {
			throw new RemoteConnectionException(Messages.JSchConnection_remote_address_must_be_set);
		}
		if (fRemoteConnection.getAttribute(USERNAME_ATTR) == null) {
			throw new RemoteConnectionException(Messages.JSchConnection_username_must_be_set);
		}
	}

	private synchronized void cleanup() {
		if (fSftpCommandChannel != null) {
			if (fSftpCommandChannel.isConnected()) {
				fSftpCommandChannel.disconnect();
			}
			fSftpCommandChannel = null;
		}
		for (Session session : fSessions) {
			if (session.isConnected()) {
				session.disconnect();
			}
		}
		fSessions.clear();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnectionControlService#close()
	 */
	@Override
	public synchronized void close() {
		cleanup();
		fRemoteConnection.fireConnectionChangeEvent(RemoteConnectionChangeEvent.CONNECTION_CLOSED);
	}

	/**
	 * Execute the command and return the result as a string.
	 *
	 * @param cmd
	 *            command to execute
	 * @param monitor
	 *            progress monitor
	 * @return result of command
	 * @throws RemoteConnectionException
	 */
	private String executeCommand(String cmd, IProgressMonitor monitor) throws RemoteConnectionException {
		ExecCommand exec = new ExecCommand(this);
		monitor.subTask(NLS.bind(Messages.JSchConnection_Executing_command, cmd));
		return exec.setCommand(cmd).getResult(monitor).trim();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemotePortForwardingService#forwardLocalPort(int, java.lang.String, int)
	 */
	@Override
	public void forwardLocalPort(int localPort, String fwdAddress, int fwdPort) throws RemoteConnectionException {
		if (!isOpen()) {
			throw new RemoteConnectionException(Messages.JSchConnection_connectionNotOpen);
		}
		try {
			fSessions.get(0).setPortForwardingL(localPort, fwdAddress, fwdPort);
		} catch (JSchException e) {
			throw new RemoteConnectionException(e.getMessage());
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemotePortForwardingService#forwardLocalPort(java.lang.String, int,
	 * org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public int forwardLocalPort(String fwdAddress, int fwdPort, IProgressMonitor monitor) throws RemoteConnectionException {
		if (!isOpen()) {
			throw new RemoteConnectionException(Messages.JSchConnection_connectionNotOpen);
		}
		SubMonitor progress = SubMonitor.convert(monitor, 10);
		progress.beginTask(Messages.JSchConnection_forwarding, 10);
		/*
		 * Start with a different port number, in case we're doing this all on localhost.
		 */
		int localPort = fwdPort + 1;

		/*
		 * Try to find a free port on the remote machine. This take a while, so allow it to be canceled. If we've tried all
		 * ports (which could take a very long while) then bail out.
		 */
		while (!progress.isCanceled()) {
			try {
				forwardLocalPort(localPort, fwdAddress, fwdPort);
			} catch (AddressInUseException e) {
				if (++localPort == fwdPort) {
					throw new UnableToForwardPortException(Messages.JSchConnection_remotePort);
				}
				progress.worked(1);
			}
			return localPort;
		}
		return -1;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemotePortForwardingService#forwardRemotePort(int, java.lang.String, int)
	 */
	@Override
	public void forwardRemotePort(int remotePort, String fwdAddress, int fwdPort) throws RemoteConnectionException {
		if (!isOpen()) {
			throw new RemoteConnectionException(Messages.JSchConnection_connectionNotOpen);
		}
		try {
			fSessions.get(0).setPortForwardingR(remotePort, fwdAddress, fwdPort);
		} catch (JSchException e) {
			throw new RemoteConnectionException(e.getMessage());
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemotePortForwardingService#forwardRemotePort(java.lang.String, int,
	 * org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public int forwardRemotePort(String fwdAddress, int fwdPort, IProgressMonitor monitor) throws RemoteConnectionException {
		if (!isOpen()) {
			throw new RemoteConnectionException(Messages.JSchConnection_connectionNotOpen);
		}
		SubMonitor progress = SubMonitor.convert(monitor, 10);
		progress.beginTask(Messages.JSchConnection_forwarding, 10);
		/*
		 * Start with a different port number, in case we're doing this all on localhost.
		 */
		int remotePort = fwdPort + 1;
		/*
		 * Try to find a free port on the remote machine. This take a while, so allow it to be canceled. If we've tried all
		 * ports (which could take a very long while) then bail out.
		 */
		while (!progress.isCanceled()) {
			try {
				forwardRemotePort(remotePort, fwdAddress, fwdPort);
				return remotePort;
			} catch (AddressInUseException e) {
				if (++remotePort == fwdPort) {
					throw new UnableToForwardPortException(Messages.JSchConnection_remotePort);
				}
				progress.worked(1);
			}
		}
		return -1;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnectionHostService#getHostname()
	 */
	@Override
	public String getHostname() {
		return fRemoteConnection.getAttribute(ADDRESS_ATTR);
	}

	/**
	 * Get the result of executing a pwd command.
	 *
	 * @return current working directory
	 */
	private String getCwd(IProgressMonitor monitor) {
		SubMonitor subMon = SubMonitor.convert(monitor, 10);
		try {
			return executeCommand("pwd", subMon.newChild(10)); //$NON-NLS-1$
		} catch (RemoteConnectionException e) {
			// Ignore
		}
		return null;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteProcessService#getEnv()
	 */
	@Override
	public Map<String, String> getEnv() {
		return Collections.unmodifiableMap(fEnv);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteProcessService#getEnv(java.lang.String)
	 */
	@Override
	public String getEnv(String name) {
		return getEnv().get(name);
	}

	/**
	 * Open an exec channel to the remote host.
	 *
	 * @return exec channel or null if the progress monitor was cancelled
	 *
	 * @throws RemoteConnectionException
	 *             if a channel could not be opened
	 */
	public ChannelExec getExecChannel() throws RemoteConnectionException {
		try {
			return (ChannelExec) fSessions.get(0).openChannel("exec"); //$NON-NLS-1$
		} catch (JSchException e) {
			throw new RemoteConnectionException(e.getMessage());
		}
	}

	/**
	 * Open a shell channel to the remote host.
	 *
	 * @return shell channel or null if the progress monitor was cancelled
	 *
	 * @throws RemoteConnectionException
	 *             if a channel could not be opened
	 */
	public ChannelShell getShellChannel() throws RemoteConnectionException {
		try {
			return (ChannelShell) fSessions.get(0).openChannel("shell"); //$NON-NLS-1$
		} catch (JSchException e) {
			throw new RemoteConnectionException(e.getMessage());
		}
	}

	public String getPassphrase() {
		return fRemoteConnection.getSecureAttribute(PASSPHRASE_ATTR);
	}

	public String getPassword() {
		return fRemoteConnection.getSecureAttribute(PASSWORD_ATTR);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnectionHostService#getPort()
	 */
	@Override
	public int getPort() {
		String portStr = fRemoteConnection.getAttribute(PORT_ATTR);
		return !portStr.isEmpty() ? Integer.parseInt(portStr) : DEFAULT_PORT;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteProcessService#getProcessBuilder(java.util.List)
	 */
	@Override
	public IRemoteProcessBuilder getProcessBuilder(List<String> command) {
		if (!isOpen()) {
			return null;
		}
		return new JSchProcessBuilder(getRemoteConnection(), command);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteProcessService#getProcessBuilder(java.lang.String[])
	 */
	@Override
	public IRemoteProcessBuilder getProcessBuilder(String... command) {
		if (!isOpen()) {
			return null;
		}
		return new JSchProcessBuilder(getRemoteConnection(), command);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnectionPropertyService#getProperty(java.lang.String)
	 */
	@Override
	public String getProperty(String key) {
		return fProperties.get(key);
	}

	/**
	 * Get the login shell command if useLoginShell is true
	 *
	 * @return login shell command
	 */
	public String getLoginShellCommand() {
		String loginShell = fRemoteConnection.getAttribute(LOGIN_SHELL_COMMAND_ATTR);
		return loginShell.isEmpty() ? DEFAULT_LOGIN_SHELL_COMMAND : loginShell;
	}

	/**
	 * Gets the proxy command. For no proxy command null is returned.
	 *
	 * @return proxy command
	 */
	public String getProxyCommand() {
		return fRemoteConnection.getAttribute(PROXYCOMMAND_ATTR);
	}

	/**
	 * Gets the proxy connection. If no proxy connection is used it returns null.
	 *
	 * @return proxy connection
	 */
	public JSchConnection getProxyConnection() {
		String proxyConnectionName = getProxyConnectionName();
		if (proxyConnectionName.isEmpty()) {
			return null;
		}
		return fRemoteConnection.getConnectionType().getConnection(proxyConnectionName).getService(JSchConnection.class);
	}

	/**
	 * Gets the proxy connection name
	 *
	 * @return proxy connection name. If no proxy is used returns null.
	 */
	public String getProxyConnectionName() {
		return fRemoteConnection.getAttribute(PROXYCONNECTION_ATTR);
	}

	/**
	 * Open an sftp command channel to the remote host. This channel is for commands that do not require any
	 * state being preserved and should not be closed. Long running commands (such as get/put) should use a separate channel
	 * obtained via {#link #newSftpChannel()}.
	 * 
	 * Always use the second session if available.
	 *
	 * @return sftp channel
	 * @throws RemoteConnectionException
	 *             if a channel could not be opened
	 */
	public ChannelSftp getSftpCommandChannel() throws RemoteConnectionException {
		if (fSftpCommandChannel == null || fSftpCommandChannel.isClosed()) {
			fSftpCommandChannel = newSftpChannel();
		}
		return fSftpCommandChannel;
	}

	/**
	 * Open a channel for long running commands. This channel should be closed when the command is completed.
	 * 
	 * @return sftp channel
	 * @throws RemoteConnectionException
	 *             if a channel could not be opened
	 */
	public ChannelSftp newSftpChannel() throws RemoteConnectionException {
		Session session = fSessions.get(0);
		if (fSessions.size() > 1) {
			session = fSessions.get(1);
		}
		ChannelSftp channel = openSftpChannel(session);
		if (channel == null) {
			throw new RemoteConnectionException(Messages.JSchConnection_Unable_to_open_sftp_channel);
		}
		return channel;
	}

	public Channel getStreamForwarder(String host, int port) throws RemoteConnectionException {
		try {
			Channel channel = fSessions.get(0).getStreamForwarder(host, port);
			channel.connect();
			return channel;
		} catch (JSchException e) {
			throw new RemoteConnectionException(e);
		}
	}

	@Override
	public int getTimeout() {
		String str = fRemoteConnection.getAttribute(TIMEOUT_ATTR);
		return !str.isEmpty() ? Integer.parseInt(str) : DEFAULT_TIMEOUT;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnectionHostService#getUsername()
	 */
	@Override
	public String getUsername() {
		return fRemoteConnection.getAttribute(USERNAME_ATTR);
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteProcessService#getWorkingDirectory()
	 */
	@Override
	public String getWorkingDirectory() {
		if (!isOpen()) {
			return "/"; //$NON-NLS-1$
		}
		if (fWorkingDir == null) {
			return "/"; //$NON-NLS-1$
		}
		return fWorkingDir;
	}

	/**
	 * Test if the connection has a valid open session. Doesn't check whether the connection is fully setup.
	 *
	 * @return true if a valid session is available.
	 */
	public boolean hasOpenSession() {
		boolean hasOpenSession = fSessions.size() > 0;
		if (hasOpenSession) {
			for (Session session : fSessions) {
				hasOpenSession &= session.isConnected();
			}
		}
		if (!hasOpenSession) {
			cleanup(); // Cleanup if session is closed
		}
		return hasOpenSession;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnectionControlService#isOpen()
	 */
	@Override
	public boolean isOpen() {
		return hasOpenSession() && isFullySetup;
	}

	public boolean isPasswordAuth() {
		String str = fRemoteConnection.getAttribute(IS_PASSWORD_ATTR);
		return !str.isEmpty() ? Boolean.parseBoolean(str) : DEFAULT_IS_PASSWORD;
	}

	private void loadEnv(IProgressMonitor monitor) throws RemoteConnectionException {
		SubMonitor subMon = SubMonitor.convert(monitor, 10);
		String env = executeCommand("printenv", subMon.newChild(10)); //$NON-NLS-1$
		String[] vars = env.split("\n"); //$NON-NLS-1$
		for (String var : vars) {
			String[] kv = var.split("="); //$NON-NLS-1$
			if (kv.length == 2) {
				fEnv.put(kv[0], kv[1]);
			}
		}
	}

	/**
	 *
	 * Load the following hard-coded properties at runtime:
	 *
	 * <dl>
	 * <dt>file.separator
	 * <dd>File separator character of the (remote) connection. Hardcoded "/" (forward slash).
	 * <dt>path.separator
	 * <dd>Path separator character of the (remote) connection. Hardcoded ":" (colon).
	 * <dt>line.separator
	 * <dd>Line separator character of the (remote) connection. Hardcoded "\n" (new-line).
	 * <dt>user.home
	 * <dd>User home directory on the (remote) connection.
	 * <dt>os.name
	 * <dd>Operating system name of the (remote) connection. For example, given results from the "uname" command:
	 * <ul>
	 * <li>Linux</li>
	 * <li>AIX</li>
	 * <li>Mac OS X - if results equal "Darwin" then results from "sw_vers -productName"</li>
	 * <li>everything else - results from "uname" command</li>
	 * </ul>
	 * <dt>os.version
	 * <dd>Operating system version of the (remote) connection. For example:
	 * <ul>
	 * <li>For Linux - results from "uname -r" such as "2.6.32-279.2.1.el6.x86_64"</li>
	 * <li>For AIX - results from "oslevel" such as "7.1.0.0"</li>
	 * <li>For Mac OS X - results from "sw_vers -productVersion" such as "10.8.3"</li>
	 * <li>For everything else - "unknown"</li>
	 * </ul>
	 * <dt>os.arch
	 * <dd>Machine architecture of the (remote) connection. For example:
	 * <ul>
	 * <li>For Linux - results from "uname -m" such as "x86_64"</li>
	 * <li>For AIX - if results from "uname -p" equals "powerpc"
	 * <ul style="list-style: none;">
	 * <li>then if "prtconf -k" contains "64-bit" then "ppc64" else "ppc"</li>
	 * <li>else the result from "uname -p"</li>
	 * </ul>
	 * </li>
	 * <li>For Mac OS X - if results from "uname -m" equals "i386"
	 * <ul style="list-style: none;">
	 * <li>then if results from "sysctl -n hw.optional.x86_64" equals "1" then "x86_64" else the results from "uname -m"</li>
	 * <li>else the results from "uname -m"</li>
	 * </ul>
	 * </li>
	 * <li>For everything else - "unknown"</li>
	 * </ul>
	 * <dl>
	 *
	 */
	private void loadProperties(IProgressMonitor monitor) throws RemoteConnectionException {
		SubMonitor subMon = SubMonitor.convert(monitor, 100);
		fProperties.put(IRemoteConnection.FILE_SEPARATOR_PROPERTY, "/"); //$NON-NLS-1$
		fProperties.put(IRemoteConnection.PATH_SEPARATOR_PROPERTY, ":"); //$NON-NLS-1$
		fProperties.put(IRemoteConnection.LINE_SEPARATOR_PROPERTY, "\n"); //$NON-NLS-1$
		fProperties.put(IRemoteConnection.USER_HOME_PROPERTY, getWorkingDirectory());

		String osVersion;
		String osArch;
		String encoding;

		String osName = executeCommand("uname", subMon.newChild(10)); //$NON-NLS-1$
		switch (osName.toLowerCase()) {
		case "linux": //$NON-NLS-1$
			osArch = executeCommand("uname -m", subMon.newChild(10)); //$NON-NLS-1$
			osVersion = executeCommand("uname -r", subMon.newChild(10)); //$NON-NLS-1$
			encoding = executeCommand("locale charmap", subMon.newChild(10)); //$NON-NLS-1$
			break;

		case "darwin": //$NON-NLS-1$
			osName = executeCommand("sw_vers -productName", subMon.newChild(10)); //$NON-NLS-1$
			osVersion = executeCommand("sw_vers -productVersion", subMon.newChild(10)); //$NON-NLS-1$
			osArch = executeCommand("uname -m", subMon.newChild(10)); //$NON-NLS-1$
			if (osArch.equalsIgnoreCase("i386")) { //$NON-NLS-1$
				String opt = executeCommand("sysctl -n hw.optional.x86_64", subMon.newChild(10)); //$NON-NLS-1$
				if (opt.equals("1")) { //$NON-NLS-1$
					osArch = "x86_64"; //$NON-NLS-1$
				}
			}
			encoding = executeCommand("locale charmap", subMon.newChild(10)); //$NON-NLS-1$
			break;

		case "aix": //$NON-NLS-1$
			osArch = executeCommand("uname -p", subMon.newChild(10)); //$NON-NLS-1$
			osVersion = executeCommand("oslevel", subMon.newChild(10)); //$NON-NLS-1$
			if (osArch.equalsIgnoreCase("powerpc")) { //$NON-NLS-1$
				/* Make the architecture match what Linux produces: either ppc or ppc64 */
				osArch = "ppc"; //$NON-NLS-1$
				/* Get Kernel type either 32-bit or 64-bit */
				String opt = executeCommand("prtconf -k", subMon.newChild(10)); //$NON-NLS-1$
				if (opt.indexOf("64-bit") > 0) { //$NON-NLS-1$
					osArch += "64"; //$NON-NLS-1$
				}
			}
			encoding = executeCommand("locale charmap", subMon.newChild(10)); //$NON-NLS-1$
			break;

		case "qnx": //$NON-NLS-1$
			osArch = executeCommand("uname -p", subMon.newChild(10)); //$NON-NLS-1$
			osVersion = executeCommand("uname -r", subMon.newChild(10)); //$NON-NLS-1$
			encoding = "UTF-8"; //$NON-NLS-1$
			break;

		default:
			osVersion = "unknown"; //$NON-NLS-1$
			osArch = "unknown"; //$NON-NLS-1$
			encoding = "unknown"; //$NON-NLS-1$
			break;
		}

		fProperties.put(IRemoteConnection.OS_NAME_PROPERTY, osName);
		fProperties.put(IRemoteConnection.OS_VERSION_PROPERTY, osVersion);
		fProperties.put(IRemoteConnection.OS_ARCH_PROPERTY, osArch);
		fProperties.put(IRemoteConnection.LOCALE_CHARMAP_PROPERTY, encoding);
	}

	private Session newSession(IProgressMonitor monitor) throws RemoteConnectionException {
		SubMonitor progress = SubMonitor.convert(monitor, 10);
		try {
			Session session = fJSchService.createSession(getHostname(), getPort(), getUsername());
			session.setUserInfo(new JSchUserInfo());
			if (isPasswordAuth()) {
				session.setConfig("PreferredAuthentications", "password,keyboard-interactive,gssapi-with-mic,publickey"); //$NON-NLS-1$ //$NON-NLS-2$
			} else {
				session.setConfig("PreferredAuthentications", "publickey,gssapi-with-mic,password,keyboard-interactive"); //$NON-NLS-1$ //$NON-NLS-2$
			}
			String password = getPassword();
			if (!password.isEmpty()) {
				session.setPassword(password);
			}
			if (getProxyCommand().isEmpty() && getProxyConnectionName().isEmpty()) {
				fJSchService.connect(session, getTimeout() * 1000, progress.newChild(10)); // connect without proxy
			} else {
				if (getProxyCommand().isEmpty()) {
					session.setProxy(JSchConnectionProxyFactory.createForwardProxy(getProxyConnection(), progress.newChild(10)));
					fJSchService.connect(session, getTimeout() * 1000, progress.newChild(10));
				} else {
					session.setProxy(JSchConnectionProxyFactory.createCommandProxy(getProxyConnection(), getProxyCommand(),
							progress.newChild(10)));
					session.connect(getTimeout() * 1000); // the fJSchService doesn't pass the timeout correctly
				}
			}
			if (!progress.isCanceled()) {
				fSessions.add(session);
				return session;
			}
			return null;
		} catch (OperationCanceledException e) {
			throw new RemoteConnectionException(Messages.JSchConnection_0);
		} catch (JSchException e) {
			throw new RemoteConnectionException(e.getMessage());
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnectionControlService#open(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public void open(IProgressMonitor monitor) throws RemoteConnectionException {
		open(monitor, true);
	}

	/**
	 * Open ssh connection without full setup (environment, sftp)
	 *
	 * @see org.eclipse.remote.core.IRemoteConnection#open()
	 *
	 * @param monitor
	 * @throws RemoteConnectionException
	 */
	public void openMinimal(IProgressMonitor monitor) throws RemoteConnectionException {
		open(monitor, false);
	}

	/**
	 * @see org.eclipse.remote.core.IRemoteConnection#open()
	 *
	 * @param monitor
	 * @param setupFully
	 *            open a full featured connection (environment query and sftp)
	 * @throws RemoteConnectionException
	 */
	private void open(IProgressMonitor monitor, boolean setupFully) throws RemoteConnectionException {
		SubMonitor subMon = SubMonitor.convert(monitor, 60);
		if (!hasOpenSession()) {
			checkIsConfigured();
			newSession(subMon.newChild(10));
			if (subMon.isCanceled()) {
				throw new RemoteConnectionException(Messages.JSchConnection_Connection_was_cancelled);
			}
			isFullySetup = false;
		}
		if (setupFully && !isFullySetup) { // happens on the first open with setupFully==true, which might not be the first open
			isFullySetup = true;
			// getCwd checks the exec channel before checkConfiguration checks the sftp channel
			fWorkingDir = getCwd(subMon.newChild(10));
			try {
				if (!checkConfiguration(fSessions.get(0), subMon.newChild(20))) {
					newSession(subMon.newChild(10));
					loadEnv(subMon.newChild(10));
				}
			} catch (RemoteConnectionException e) {
				// Do not throw exception now, it will be thrown if FileService is accessed.
			}
			loadProperties(subMon.newChild(10));
			fRemoteConnection.fireConnectionChangeEvent(RemoteConnectionChangeEvent.CONNECTION_OPENED);
		}
	}

	private ChannelSftp openSftpChannel(Session session) throws RemoteConnectionException {
		try {
			ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); //$NON-NLS-1$
			channel.connect();
			return channel;
		} catch (JSchException e) {
			throw new RemoteConnectionException(e.getMessage());
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemotePortForwardingService#removeLocalPortForwarding(int)
	 */
	@Override
	public void removeLocalPortForwarding(int port) throws RemoteConnectionException {
		if (!isOpen()) {
			throw new RemoteConnectionException(Messages.JSchConnection_connectionNotOpen);
		}
		try {
			fSessions.get(0).delPortForwardingL(port);
		} catch (JSchException e) {
			throw new RemoteConnectionException(e.getMessage());
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemotePortForwardingService#removeRemotePortForwarding(int)
	 */
	@Override
	public void removeRemotePortForwarding(int port) throws RemoteConnectionException {
		if (!isOpen()) {
			throw new RemoteConnectionException(Messages.JSchConnection_connectionNotOpen);
		}
		try {
			fSessions.get(0).delPortForwardingR(port);
		} catch (JSchException e) {
			throw new RemoteConnectionException(e.getMessage());
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.eclipse.remote.core.IRemoteProcessService#setWorkingDirectory(java.lang.String)
	 */
	@Override
	public void setWorkingDirectory(String path) {
		if (RemoteServicesUtils.posixPath(path).isAbsolute()) {
			fWorkingDir = path;
		}
	}

	@Override
	public boolean useLoginShell() {
		String str = fRemoteConnection.getAttribute(USE_LOGIN_SHELL_ATTR);
		return !str.isEmpty() ? Boolean.parseBoolean(str) : DEFAULT_USE_LOGIN_SHELL;
	}

	@Override
	public void setHostname(String hostname) {
		if (fRemoteConnection instanceof IRemoteConnectionWorkingCopy) {
			IRemoteConnectionWorkingCopy wc = (IRemoteConnectionWorkingCopy) fRemoteConnection;
			wc.setAttribute(ADDRESS_ATTR, hostname);
		}
	}

	@Override
	public void setPassphrase(String passphrase) {
		if (fRemoteConnection instanceof IRemoteConnectionWorkingCopy) {
			IRemoteConnectionWorkingCopy wc = (IRemoteConnectionWorkingCopy) fRemoteConnection;
			wc.setSecureAttribute(PASSPHRASE_ATTR, passphrase);
		}
	}

	@Override
	public void setPassword(String password) {
		if (fRemoteConnection instanceof IRemoteConnectionWorkingCopy) {
			IRemoteConnectionWorkingCopy wc = (IRemoteConnectionWorkingCopy) fRemoteConnection;
			wc.setSecureAttribute(PASSWORD_ATTR, password);
		}
	}

	@Override
	public void setPort(int port) {
		if (fRemoteConnection instanceof IRemoteConnectionWorkingCopy) {
			IRemoteConnectionWorkingCopy wc = (IRemoteConnectionWorkingCopy) fRemoteConnection;
			wc.setAttribute(PORT_ATTR, Integer.toString(port));
		}
	}

	@Override
	public void setTimeout(int timeout) {
		if (fRemoteConnection instanceof IRemoteConnectionWorkingCopy) {
			IRemoteConnectionWorkingCopy wc = (IRemoteConnectionWorkingCopy) fRemoteConnection;
			wc.setAttribute(TIMEOUT_ATTR, Integer.toString(timeout));
		}
	}

	@Override
	public void setUseLoginShell(boolean useLogingShell) {
		if (fRemoteConnection instanceof IRemoteConnectionWorkingCopy) {
			IRemoteConnectionWorkingCopy wc = (IRemoteConnectionWorkingCopy) fRemoteConnection;
			wc.setAttribute(USE_LOGIN_SHELL_ATTR, Boolean.toString(useLogingShell));
		}
	}

	@Override
	public void setUsePassword(boolean usePassword) {
		if (fRemoteConnection instanceof IRemoteConnectionWorkingCopy) {
			IRemoteConnectionWorkingCopy wc = (IRemoteConnectionWorkingCopy) fRemoteConnection;
			wc.setAttribute(IS_PASSWORD_ATTR, Boolean.toString(usePassword));
		}
	}

	@Override
	public void setUsername(String username) {
		if (fRemoteConnection instanceof IRemoteConnectionWorkingCopy) {
			IRemoteConnectionWorkingCopy wc = (IRemoteConnectionWorkingCopy) fRemoteConnection;
			wc.setAttribute(USERNAME_ATTR, username);
		}
	}

}
