diff options
author | Roland Schulz | 2014-09-30 04:36:51 +0000 |
---|---|---|
committer | John Eblen | 2014-10-17 16:29:45 +0000 |
commit | c563ea3f0e8967bd7a57ba454e95704797b1e9e3 (patch) | |
tree | 23da811a5019ec126cf128b5b0325a17f6f0430f | |
parent | 5ee4fa818a1313d838220e3737431625c9e29fea (diff) | |
download | org.eclipse.remote-c563ea3f0e8967bd7a57ba454e95704797b1e9e3.tar.gz org.eclipse.remote-c563ea3f0e8967bd7a57ba454e95704797b1e9e3.tar.xz org.eclipse.remote-c563ea3f0e8967bd7a57ba454e95704797b1e9e3.zip |
Bug 345329 - Add SSH proxy
Allows to connect over any ssh server as gateway or over any other
proxy by executing a local or remote command (such as netcat, corkscrew, ...).
Change-Id: I413c22cb588d8560d8db9ac877fb04f8331456aa
10 files changed, 893 insertions, 15 deletions
diff --git a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/ArgumentParser.java b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/ArgumentParser.java new file mode 100644 index 0000000..3fa3515 --- /dev/null +++ b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/ArgumentParser.java @@ -0,0 +1,367 @@ +/******************************************************************************* + * Copyright (c) 2007 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 + *******************************************************************************/ +package org.eclipse.remote.internal.jsch.core; + +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * Utilitiy class for managing command line arguments. + * + */ +public class ArgumentParser { + private static List<String> parseCommandline(String commandline) { + ArrayList<String> result = new ArrayList<String>(); + StringCharacterIterator iterator = new StringCharacterIterator(commandline); + + for (iterator.first(); iterator.current() != CharacterIterator.DONE; iterator.next()) { + + // Restart to skip white space + if (Character.isWhitespace(iterator.current())) { + continue; + } + + // Read token + StringBuffer buffer = new StringBuffer(); + token_reader: for (; iterator.current() != CharacterIterator.DONE; iterator.next()) { + char tokenChar = iterator.current(); + + // A white space terminates the token + if (Character.isWhitespace(tokenChar)) { + break token_reader; + } + + // Handle character that composes the token + switch (tokenChar) { + case '"': { + /* + * Read all text within double quotes or until end of + * string. Allows escaping. + */ + iterator.next(); // Skip quote + quoted_reader: while ((iterator.current() != CharacterIterator.DONE) && (iterator.current() != '"')) { + char innerChar = iterator.current(); + switch (innerChar) { + case '\\': + char nextChar = iterator.next(); + switch (nextChar) { + case CharacterIterator.DONE: + break quoted_reader; + case '"': + // Add the character, but remove the escape + buffer.append(nextChar); + iterator.next(); + continue quoted_reader; + default: + // Add the character and keep escape + buffer.append(innerChar); + buffer.append(nextChar); + iterator.next(); + continue quoted_reader; + } + default: + buffer.append(innerChar); + iterator.next(); + continue quoted_reader; + } + } + continue token_reader; + } + case '\'': { + /* + * Read all text within single quotes or until end of + * string. No escaping. + */ + iterator.next(); // Skip the quote + while ((iterator.current() != CharacterIterator.DONE) && (iterator.current() != '\'')) { + buffer.append(iterator.current()); + iterator.next(); + } + continue token_reader; + } + case '\\': { + /* + * Read escaped char. + */ + char nextChar = iterator.next(); + switch (nextChar) { + case CharacterIterator.DONE: + break token_reader; + case '\n': + // Ignore newline. Both lines are concatenated. + continue token_reader; + default: + // Add the character, but remove the escape + buffer.append(nextChar); + continue token_reader; + } + } + default: + /* + * Any other char, add to the buffer. + */ + buffer.append(tokenChar); + continue token_reader; + } + } + result.add(buffer.toString()); + } + + return result; + } + + private final List<String> tokens; + + /** + * Create a command line representation from an array of strings. The first + * element of the list is assumed to be the command, the remaining, the + * arguments. The elements are not parsed not (un)escaped., but taked as the + * are. + * + */ + public ArgumentParser(List<String> tokenList) { + this.tokens = new ArrayList<String>(tokenList); + } + + /** + * Create a command line representation from the string with a shell command + * line. The command line is parsed and split on spaces. Quoted or escaped + * spaces are preserved.. + * + */ + public ArgumentParser(String commandline) { + this.tokens = parseCommandline(commandline); + } + + /** + * Create a command line representation from an array of strings. The first + * element of the array is assumed to be the command, the remaining, the + * arguments. The elements are not parsed not (un)escaped., but taked as the + * are. + * + */ + public ArgumentParser(String tokenArray[]) { + this(Arrays.asList(tokenArray)); + } + + /** + * Create a command line representation from the command and an list of + * parameters. The elements are not parsed not (un)escaped., but taked as + * the are. + * + */ + public ArgumentParser(String command, List<String> parameterList) { + this.tokens = new ArrayList<String>(); + this.tokens.add(command); + this.tokens.addAll(parameterList); + } + + /** + * Create a command line representation from the command and an array of + * parameters. The elements are not parsed not (un)escaped., but taked as + * the are. + * + */ + public ArgumentParser(String command, String parameterArray[]) { + this(command, Arrays.asList(parameterArray)); + } + + private StringBuffer escapeToken(String token, boolean fullEscape) { + StringBuffer buffer = new StringBuffer(); + StringCharacterIterator iter = new StringCharacterIterator(token); + for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { + if (Character.isWhitespace(c)) { + buffer.append('\\'); + buffer.append(c); + continue; + } + switch (c) { + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + case '|': + case '\\': + case '*': + case '&': + case '^': + case '%': + case '$': + case '#': + case '@': + case '!': + case '~': + case '`': + case '\'': + case '"': + case ':': + case ';': + case '?': + case '>': + case '<': + case '\n': + if (fullEscape) { + buffer.append('\\'); + } + buffer.append(c); + continue; + case ' ': + buffer.append('\\'); + buffer.append(c); + continue; + default: + buffer.append(c); + continue; + } + } + return buffer; + } + + /** + * Returns the command of the command line, assuming that the first entry is + * always the command. + * + * @return The command or null if the command lines has no command nor + * arguments. + */ + public String getCommand() { + if (this.tokens.size() == 0) { + return null; + } + return this.tokens.get(0); + } + + /** + * Convert all tokens in a full command line that can be executed in a + * shell. + * + * @param fullEscape + * If every special character shall be escaped. If false, only + * white spaces are escaped and the shell will interpret the + * special chars. If true, then all special chars are quoted. + */ + public String getCommandLine(boolean fullEscape) { + StringBuffer buffer = new StringBuffer(); + Iterator<String> iterator = this.tokens.iterator(); + boolean first = true; + while (iterator.hasNext()) { + String token = iterator.next(); + if (!first) { + buffer.append(' '); + } else { + first = false; + } + buffer.append(escapeToken(token, fullEscape)); + } + return buffer.toString(); + } + + /** + * Returns the command of the command line, assuming that the first entry is + * always the command. + * + * @return The command or null if the command lines has no command nor + * arguments. + * @param fullEscape + * If every special character shall be escaped. If false, only + * white spaces are escaped and the shell will interpret the + * special chars. If true, then all special chars are quoted. + */ + public String getEscapedCommand(boolean fullEscalpe) { + if (this.tokens.size() == 0) { + return null; + } + return escapeToken(this.tokens.get(0), fullEscalpe).toString(); + } + + /** + * Returns a list of all arguments, assuming that the first entry is the + * command name. + * + * @return The Array or null if the command lines has no command nor + * arguments. + */ + public String[] getParameterArray() { + if (this.tokens.size() == 0) { + return null; + } + return this.tokens.subList(1, this.tokens.size()).toArray(new String[this.tokens.size() - 1]); + } + + /** + * Returns a list of all arguments, assuming that the first entry is the + * command name. + * + * @return The List or null if the command lines has no command nor + * arguments. + */ + public List<String> getParameterList() { + if (this.tokens.size() == 0) { + return null; + } + return new ArrayList<String>(this.tokens.subList(1, this.tokens.size())); + } + + /** + * Returns the total number of entries. + * + * @return + */ + public int getSize() { + return this.tokens.size(); + } + + /** + * Returns a List of all entries of the command line. + * + * @return The Array + */ + public String[] getTokenArray() { + return this.tokens.toArray(new String[this.tokens.size()]); + } + + /** + * Returns a List of all entries of the command line. + * + * @return The List + */ + public List<String> getTokenList() { + return new ArrayList<String>(this.tokens); + } + + /** + * Returns a representation of the command line for debug purposes. + */ + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("<"); //$NON-NLS-1$ + Iterator<String> iterator = this.tokens.iterator(); + boolean first = true; + while (iterator.hasNext()) { + String token = iterator.next(); + if (!first) { + buffer.append('\n'); + } else { + first = false; + } + buffer.append(token); + } + buffer.append(">"); //$NON-NLS-1$ + return buffer.toString(); + } +} diff --git a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnection.java b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnection.java index 1dc07a8..7df7a7a 100644 --- a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnection.java +++ b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnection.java @@ -27,18 +27,21 @@ import org.eclipse.osgi.util.NLS; import org.eclipse.remote.core.IRemoteConnection; import org.eclipse.remote.core.IRemoteConnectionChangeEvent; import org.eclipse.remote.core.IRemoteConnectionChangeListener; +import org.eclipse.remote.core.IRemoteConnectionManager; import org.eclipse.remote.core.IRemoteConnectionWorkingCopy; import org.eclipse.remote.core.IRemoteFileManager; import org.eclipse.remote.core.IRemoteProcess; import org.eclipse.remote.core.IRemoteProcessBuilder; import org.eclipse.remote.core.IRemoteServices; import org.eclipse.remote.core.IUserAuthenticator; +import org.eclipse.remote.core.RemoteServices; 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.JSchException; @@ -388,7 +391,7 @@ public class JSchConnection implements IRemoteConnection { /* * (non-Javadoc) * - * @see org.eclipse.remote.core.IRemoteConnection#forwardRemotePort(java. lang.String, int, + * @see org.eclipse.remote.core.IRemoteConnection#forwardRemotePort(java.lang.String, int, * org.eclipse.core.runtime.IProgressMonitor) */ @Override @@ -581,6 +584,38 @@ public class JSchConnection implements IRemoteConnection { return fProperties.get(key); } + /** + * Gets the proxy command. For no proxy command an empty string is returned. + * + * @return proxy command + */ + public String getProxyCommand() { + return fAttributes.getAttribute(JSchConnectionAttributes.PROXYCOMMAND_ATTR, EMPTY_STRING); + } + + /** + * Gets the proxy connection. If no proxy is used it returns a local connection. + * + * @return proxy connection + */ + public IRemoteConnection getProxyConnection() { + String proxyConnectionName = getProxyConnectionName(); + if (proxyConnectionName.equals(EMPTY_STRING)) { + return RemoteServices.getLocalServices().getConnectionManager().getConnection( + IRemoteConnectionManager.LOCAL_CONNECTION_NAME); + } + return fManager.getConnection(proxyConnectionName); + } + + /** + * Gets the proxy connection name + * + * @return proxy connection name + */ + public String getProxyConnectionName() { + return fAttributes.getAttribute(JSchConnectionAttributes.PROXYCONNECTION_ATTR, EMPTY_STRING); + } + /* * (non-Javadoc) * @@ -612,6 +647,22 @@ public class JSchConnection implements IRemoteConnection { return fSftpChannel; } + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Session#getStreamForwarder(java.lang.String, int) + */ + 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); + } + } + public int getTimeout() { return fAttributes.getInt(JSchConnectionAttributes.TIMEOUT_ATTR, DEFAULT_TIMEOUT); } @@ -792,7 +843,19 @@ public class JSchConnection implements IRemoteConnection { if (isPasswordAuth()) { session.setPassword(getPassword()); } - fJSchService.connect(session, getTimeout() * 1000, progress.newChild(10)); + if (getProxyCommand().equals(EMPTY_STRING) && getProxyConnectionName().equals(EMPTY_STRING)) { + fJSchService.connect(session, getTimeout() * 1000, progress.newChild(10)); // connect without proxy + } else { + if (getProxyCommand().equals(EMPTY_STRING)) { + 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; diff --git a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionAttributes.java b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionAttributes.java index 75ae166..ef6b43d 100644 --- a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionAttributes.java +++ b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionAttributes.java @@ -31,6 +31,8 @@ public class JSchConnectionAttributes { 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 KEYFILE_ATTR = "JSCH_KEYFILE_ATTR"; //$NON-NLS-1$ diff --git a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionProxyFactory.java b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionProxyFactory.java new file mode 100644 index 0000000..d0e33d4 --- /dev/null +++ b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionProxyFactory.java @@ -0,0 +1,311 @@ +/******************************************************************************* + * Copyright (c) 2014 University of Tennessee 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: + * University of Tennessee (Roland Schulz) - Initial API and implementation + *******************************************************************************/ +package org.eclipse.remote.internal.jsch.core; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.text.MessageFormat; +import java.util.List; + +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.remote.core.IRemoteConnection; +import org.eclipse.remote.core.IRemoteProcess; +import org.eclipse.remote.core.IRemoteProcessBuilder; +import org.eclipse.remote.core.exception.RemoteConnectionException; +import org.eclipse.remote.internal.jsch.core.messages.Messages; + +import com.jcraft.jsch.Channel; +import com.jcraft.jsch.Proxy; +import com.jcraft.jsch.SocketFactory; + +/** + * Creates a JSch Proxy. Supports both command proxies, as well as the ssh build-in + * stream forwarding. + * + * @author rschulz + * + */ +public class JSchConnectionProxyFactory { + private static class CommandProxy implements Proxy { + private String command; + private IRemoteProcess process; + private IRemoteConnection connection; + private IProgressMonitor monitor; + + private CommandProxy(IRemoteConnection connection, String command, IProgressMonitor monitor) { + if (command == null || connection == null || monitor == null) + throw new IllegalArgumentException(); + this.command = command; + this.connection = connection; + this.monitor = monitor; + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#close() + */ + @Override + public void close() { + process.destroy(); + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#connect(com.jcraft.jsch.SocketFactory, java.lang.String, int, int) + */ + @Override + public void connect(SocketFactory socket_factory, String host, + int port, int timeout) throws IOException { + assert connection != null : "connect should only be called once"; //$NON-NLS-1$ + try { + if (timeout == 0) { + timeout = 10000; // default to 10s + } + final int waitTime = 50; + final int waitSteps = timeout / waitTime; + SubMonitor subMon; + + // Open connection if it isn't already opened + if (!connection.isOpen()) { + subMon = SubMonitor.convert(monitor, waitSteps * 2); + try { + connection.open(subMon.newChild(waitSteps)); + } catch (RemoteConnectionException e) { + throw new IOException(e); + } + } else { + subMon = SubMonitor.convert(monitor, waitSteps); + } + + // Start command + command = command.replace("%h", host); //$NON-NLS-1$ + command = command.replace("%p", Integer.toString(port)); //$NON-NLS-1$ + List<String> cmd = new ArgumentParser(command).getTokenList(); + IRemoteProcessBuilder pb = connection.getProcessBuilder(cmd); + process = pb.start(); + + // Wait on command to produce stdout output + long endTime = System.currentTimeMillis() + timeout; + boolean bOutputAvailable, bProcessComplete, bTimedOut, bCanceled; + do { + try { + Thread.sleep(waitTime); + subMon.worked(1); + } catch (InterruptedException e) { + /* ignore */ + } + bOutputAvailable = (getInputStream().available() != 0); + bProcessComplete = process.isCompleted(); + bTimedOut = System.currentTimeMillis() > endTime; + bCanceled = subMon.isCanceled(); + } while (!bOutputAvailable && !bProcessComplete && !bTimedOut && !bCanceled); + + // If no output was produced before process died, throw an exception with the stderr output + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + if (getInputStream().available() == 0 || process.isCompleted()) { + String msg = ""; //$NON-NLS-1$ + while (bufferedReader.ready()) { + msg += (char) bufferedReader.read(); + } + msg = msg.trim(); + + if (!process.isCompleted()) { + process.destroy(); + } + + String cause = Messages.JSchConnectionProxyFactory_failed; + if (bTimedOut) { + cause = Messages.JSchConnectionProxyFactory_timedOut; + } else if (bCanceled) { + cause = Messages.JSchConnectionProxyFactory_wasCanceled; + } + throw new IOException(MessageFormat.format(Messages.JSchConnectionProxyFactory_ProxyCommandFailed, + command, cause, msg)); + } + + // Dump the stderr to log + new Thread() { + @Override + public void run() { + final ILog log = Activator.getDefault().getLog(); + String line; + try { + while ((line = bufferedReader.readLine()) != null) { + log.log(new Status(IStatus.INFO, Activator.getUniqueIdentifier(), + IStatus.OK, line, null)); + } + } catch (IOException e) { + Activator.log(e); + } + }; + }.start(); + } finally { + // Not valid to call connect again or for any other command to access these variables. Setting to null to ensure. + connection = null; + } + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#getInputStream() + */ + @Override + public InputStream getInputStream() { + return process.getInputStream(); + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#getOutputStream() + */ + @Override + public OutputStream getOutputStream() { + return process.getOutputStream(); + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#getSocket() + */ + @Override + public Socket getSocket() { + return null; + } + } + + private static class SSHForwardProxy implements Proxy { + private Channel channel; + private JSchConnection connection; + private IProgressMonitor monitor; + + private SSHForwardProxy(IRemoteConnection proxyConnection, IProgressMonitor monitor) { + if (proxyConnection == null || monitor == null) + throw new IllegalArgumentException(); + this.connection = (JSchConnection) proxyConnection; // only JSch supported for ForwardProxy + this.monitor = monitor; + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#close() + */ + @Override + public void close() { + channel.disconnect(); + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#connect(com.jcraft.jsch.SocketFactory, java.lang.String, int, int) + */ + @Override + public void connect(SocketFactory socket_factory, String host, int port, + int timeout) throws Exception { + assert connection != null : "connect should only be called once"; //$NON-NLS-1$ + try { + if (!connection.isOpen()) { + try { + connection.open(monitor); + } catch (RemoteConnectionException e) { + throw new IOException(e); + } + } + channel = connection.getStreamForwarder(host, port); + } finally { + // Not valid to call connect again or for any other command to access these variables. Setting to null to ensure. + connection = null; + } + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#getInputStream() + */ + @Override + public InputStream getInputStream() { + try { + return channel.getInputStream(); + } catch (IOException e) { + Activator.log(e); + return null; + } + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#getOutputStream() + */ + @Override + public OutputStream getOutputStream() { + try { + return channel.getOutputStream(); + } catch (IOException e) { + Activator.log(e); + return null; + } + } + + /* + * (non-Javadoc) + * + * @see com.jcraft.jsch.Proxy#getSocket() + */ + @Override + public Socket getSocket() { + return null; + } + } + + /** + * Creates a (local or remote) command proxy. + * + * @param connection + * Any (local or remote) connection. Cannot be null. + * @param command + * A valid proxy command. Cannot be null or empty. + * @param monitor + * A valid progress monitor. Cannot be null. + * @return ssh proxy + */ + public static Proxy createCommandProxy(IRemoteConnection connection, String command, IProgressMonitor monitor) { + return new CommandProxy(connection, command, monitor); + } + + /** + * Creates a ssh forward proxy. + * + * @param proxyConnection + * The Jsch proxy connection. Cannot be null. + * @param monitor + * A valid progress monitor. Cannot be null. + * @return ssh proxy + */ + public static Proxy createForwardProxy(IRemoteConnection proxyConnection, IProgressMonitor monitor) { + return new SSHForwardProxy(proxyConnection, monitor); + } +} diff --git a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionWorkingCopy.java b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionWorkingCopy.java index 6b6fd8e..8a75fed 100644 --- a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionWorkingCopy.java +++ b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/JSchConnectionWorkingCopy.java @@ -110,6 +110,26 @@ public class JSchConnectionWorkingCopy extends JSchConnection implements IRemote /* * (non-Javadoc) * + * @see org.eclipse.remote.internal.jsch.core.JSchConnection#getProxyCommand() + */ + @Override + public String getProxyCommand() { + return fWorkingAttributes.getAttribute(JSchConnectionAttributes.PROXYCOMMAND_ATTR, EMPTY_STRING); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.remote.internal.jsch.core.JSchConnection#getProxyConnectionName() + */ + @Override + public String getProxyConnectionName() { + return fWorkingAttributes.getAttribute(JSchConnectionAttributes.PROXYCONNECTION_ATTR, EMPTY_STRING); + } + + /* + * (non-Javadoc) + * * @see org.eclipse.remote.internal.jsch.core.JSchConnection#getTimeout() */ @Override @@ -255,6 +275,28 @@ public class JSchConnectionWorkingCopy extends JSchConnection implements IRemote fWorkingAttributes.setAttribute(JSchConnectionAttributes.PORT_ATTR, Integer.toString(port)); } + /** + * Sets the proxy command. Set to empty string for no command + * + * @param proxyCommand + * proxy command + */ + public void setProxyCommand(String proxyCommand) { + fIsDirty = true; + fWorkingAttributes.setAttribute(JSchConnectionAttributes.PROXYCOMMAND_ATTR, proxyCommand); + } + + /** + * Sets the proxy connection name. Set to empty string for no proxy connection + * + * @param proxyCommand + * proxy connection name + */ + public void setProxyConnectionName(String proxyConnectionName) { + fIsDirty = true; + fWorkingAttributes.setAttribute(JSchConnectionAttributes.PROXYCONNECTION_ATTR, proxyConnectionName); + } + public void setTimeout(int timeout) { fIsDirty = true; fWorkingAttributes.setAttribute(JSchConnectionAttributes.TIMEOUT_ATTR, Integer.toString(timeout)); diff --git a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/messages/Messages.java b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/messages/Messages.java index 30993cb..221a3e8 100755 --- a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/messages/Messages.java +++ b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/messages/Messages.java @@ -38,6 +38,10 @@ public class Messages extends NLS { public static String JSchConnectionManager_connection_with_name_exists; public static String JSchConnectionManager_cannotRemoveOpenConnection; public static String JSchConnectionManager_invalidConnectionType; + public static String JSchConnectionProxyFactory_failed; + public static String JSchConnectionProxyFactory_ProxyCommandFailed; + public static String JSchConnectionProxyFactory_timedOut; + public static String JSchConnectionProxyFactory_wasCanceled; public static String JSchProcessBuilder_Connection_is_not_open; public static String JschFileStore_Connection_is_not_open; public static String JschFileStore_File_doesnt_exist; diff --git a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/messages/messages.properties b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/messages/messages.properties index aaf04da..b9152f9 100755 --- a/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/messages/messages.properties +++ b/bundles/org.eclipse.remote.jsch.core/src/org/eclipse/remote/internal/jsch/core/messages/messages.properties @@ -28,6 +28,10 @@ JSchConnection_username_must_be_set=Username must be set before opening connecti JSchConnectionManager_connection_with_name_exists=A connection with name \"{0}\" already exists JSchConnectionManager_cannotRemoveOpenConnection=Cannot remove an open connection JSchConnectionManager_invalidConnectionType=Invalid connection type +JSchConnectionProxyFactory_failed=failed +JSchConnectionProxyFactory_ProxyCommandFailed=Proxy command "{0}" {1} and printed message "{2}" +JSchConnectionProxyFactory_timedOut=timed out +JSchConnectionProxyFactory_wasCanceled=was canceled JSchProcessBuilder_Connection_is_not_open=Connection is not open JschFileStore_Connection_is_not_open=Connection is not open JschFileStore_File_doesnt_exist=File {0} doesn't exist diff --git a/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/messages/Messages.java b/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/messages/Messages.java index 24e9db4..d14e5e2 100755 --- a/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/messages/Messages.java +++ b/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/messages/Messages.java @@ -25,6 +25,12 @@ public class Messages extends NLS { public static String JSchConnectionPage_Edit_Connection; public static String JSchConnectionPage_Edit_properties_of_an_existing_connection; public static String JSchConnectionPage_Please_enter_name_for_connection; + public static String JSchConnectionPage_Proxy; + public static String JSchConnectionPage_Help; + public static String JSchConnectionPage_SelectCommand; + public static String JSchConnectionPage_SelectConnection; + public static String JSchConnectionPage_Settings0; + public static String JSchConnectionPage_selectProxyConnection; public static String JSchFileSystemContributor_0; public static String JSchNewConnectionPage_Advanced; public static String JSchNewConnectionPage_Connection_name; diff --git a/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/messages/messages.properties b/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/messages/messages.properties index 89e22f3..6f02a26 100755 --- a/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/messages/messages.properties +++ b/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/messages/messages.properties @@ -12,6 +12,12 @@ JSchConnectionPage_A_connection_with_that_name_already_exists=A connection with JSchConnectionPage_Edit_Connection=Edit Connection JSchConnectionPage_Edit_properties_of_an_existing_connection=Edit properties of an existing connection JSchConnectionPage_Please_enter_name_for_connection=Please enter a name for the connection +JSchConnectionPage_Proxy=SSH Proxy Settings +JSchConnectionPage_Help=If 'Local' is selected and proxy command is empty, no proxy is used.\nSee <a href=\"org.eclipse.ui.net.NetPreferences\">Network Connections</a> for SOCKS and HTTP proxy options. +JSchConnectionPage_SelectCommand=Enter a local or remote command such as 'nc %h %p'. Can be empty for an ssh gateway. +JSchConnectionPage_SelectConnection=Select 'Remote' for an ssh gateway or a remote proxy command. +JSchConnectionPage_Settings0=Connection Settings +JSchConnectionPage_selectProxyConnection=Please select a proxy connection JSchFileSystemContributor_0=Browse File System JSchNewConnectionPage_Advanced=Advanced JSchNewConnectionPage_Connection_name=Connection name: diff --git a/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/wizards/JSchConnectionPage.java b/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/wizards/JSchConnectionPage.java index d45f0e8..efad260 100755 --- a/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/wizards/JSchConnectionPage.java +++ b/bundles/org.eclipse.remote.jsch.ui/src/org/eclipse/remote/internal/jsch/ui/wizards/JSchConnectionPage.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Set; import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.remote.core.IRemoteConnection; import org.eclipse.remote.core.IRemoteConnectionManager; import org.eclipse.remote.core.RemoteServices; import org.eclipse.remote.core.exception.RemoteConnectionException; @@ -25,6 +26,7 @@ import org.eclipse.remote.internal.jsch.core.JSchConnection; import org.eclipse.remote.internal.jsch.core.JSchConnectionAttributes; import org.eclipse.remote.internal.jsch.core.JSchConnectionWorkingCopy; import org.eclipse.remote.internal.jsch.ui.messages.Messages; +import org.eclipse.remote.ui.widgets.RemoteConnectionWidget; import org.eclipse.remote.ui.widgets.RemoteFileWidget; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; @@ -41,6 +43,7 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.forms.events.ExpansionEvent; import org.eclipse.ui.forms.events.IExpansionListener; @@ -74,6 +77,8 @@ public class JSchConnectionPage extends WizardPage { private final IRemoteConnectionManager fConnectionManager; private final DataModifyListener fDataModifyListener = new DataModifyListener(); + private RemoteConnectionWidget fProxyConnectionWidget; + private Text fProxyCommandText; public JSchConnectionPage(IRemoteConnectionManager connMgr) { super(Messages.JSchNewConnectionPage_New_Connection); @@ -82,7 +87,7 @@ public class JSchConnectionPage extends WizardPage { } /** - * Create controls for the bottom (hideable) composite + * Create controls for the bottom (hideable) advanced composite * * @param mold * @@ -96,13 +101,15 @@ public class JSchConnectionPage extends WizardPage { @Override public void expansionStateChanged(ExpansionEvent e) { - Point newSize = parent.computeSize(SWT.DEFAULT, SWT.DEFAULT); - Point currentSize = parent.getSize(); - int deltaY = newSize.y - currentSize.y; - Point shellSize = getShell().getSize(); - shellSize.y += deltaY; - getShell().setSize(shellSize); - getShell().layout(true, true); + for (int i = 0; i < 2; i++) { // sometimes the size compute isn't correct on first try + Point newSize = parent.computeSize(SWT.DEFAULT, SWT.DEFAULT); + Point currentSize = parent.getSize(); + int deltaY = newSize.y - currentSize.y; + Point shellSize = getShell().getSize(); + shellSize.y += deltaY; + getShell().setSize(shellSize); + getShell().layout(true, true); + } } @Override @@ -112,25 +119,36 @@ public class JSchConnectionPage extends WizardPage { }); Composite advancedComp = new Composite(expComp, SWT.NONE); - advancedComp.setLayout(new GridLayout(2, false)); + advancedComp.setLayout(new GridLayout(1, false)); advancedComp.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false)); - Label portLabel = new Label(advancedComp, SWT.NONE); + Group settingsComp = new Group(advancedComp, SWT.NONE); + settingsComp.setText(Messages.JSchConnectionPage_Settings0); + settingsComp.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + settingsComp.setLayout(new GridLayout(2, false)); + + Label portLabel = new Label(settingsComp, SWT.NONE); portLabel.setText(Messages.JSchNewConnectionPage_Port); portLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); - fPortText = new Text(advancedComp, SWT.BORDER | SWT.SINGLE); + fPortText = new Text(settingsComp, SWT.BORDER | SWT.SINGLE); fPortText.setText(Integer.toString(JSchConnection.DEFAULT_PORT)); fPortText.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false)); setTextFieldWidthInChars(fPortText, 5); - Label timeoutLabel = new Label(advancedComp, SWT.NONE); + Label timeoutLabel = new Label(settingsComp, SWT.NONE); timeoutLabel.setText(Messages.JSchNewConnectionPage_Timeout); timeoutLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false)); - fTimeoutText = new Text(advancedComp, SWT.BORDER | SWT.SINGLE); + fTimeoutText = new Text(settingsComp, SWT.BORDER | SWT.SINGLE); fTimeoutText.setText(Integer.toString(JSchConnection.DEFAULT_TIMEOUT)); fTimeoutText.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false)); setTextFieldWidthInChars(fTimeoutText, 5); + Group proxyComp = new Group(advancedComp, SWT.NONE); + proxyComp.setText(Messages.JSchConnectionPage_Proxy); + proxyComp.setLayout(new GridLayout(1, false)); + proxyComp.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + createProxyControls(proxyComp); + expComp.setClient(advancedComp); } @@ -248,6 +266,33 @@ public class JSchConnectionPage extends WizardPage { updateEnablement(); } + /** + * Create controls for the bottom (hideable) proxy composite + * + * @param mold + * + */ + private void createProxyControls(final Composite proxyComp) { + Label lblConnection = new Label(proxyComp, SWT.WRAP); + lblConnection.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + lblConnection.setText(Messages.JSchConnectionPage_SelectConnection); + + fProxyConnectionWidget = new RemoteConnectionWidget(proxyComp, SWT.NONE, null, 0, null); + + Label lblCommand = new Label(proxyComp, SWT.WRAP); + lblCommand.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + lblCommand.setText(Messages.JSchConnectionPage_SelectCommand); + + fProxyCommandText = new Text(proxyComp, SWT.BORDER); + fProxyCommandText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Link link = new Link(proxyComp, SWT.WRAP); + final GridData linkLayoutData = new GridData(GridData.FILL_HORIZONTAL); + link.setLayoutData(linkLayoutData); + linkLayoutData.widthHint = 400; + link.setText(Messages.JSchConnectionPage_Help); + } + public JSchConnectionWorkingCopy getConnection() { return fConnection; } @@ -285,6 +330,9 @@ public class JSchConnectionPage extends WizardPage { fPassphraseText.setText(fConnection.getPassphrase()); fFileWidget.setLocationPath(fConnection.getKeyFile()); } + fProxyCommandText.setText(fConnection.getProxyCommand()); + + fProxyConnectionWidget.setConnection(fConnection.getProxyConnection()); } else { fConnectionName.setText(fInitialName); String host = fInitialAttributes.get(JSchConnectionAttributes.ADDRESS_ATTR); @@ -319,6 +367,8 @@ public class JSchConnectionPage extends WizardPage { if (file != null) { fFileWidget.setLocationPath(file); } + fProxyConnectionWidget.setConnection(RemoteServices.getLocalServices().getConnectionManager().getConnection( + IRemoteConnectionManager.LOCAL_CONNECTION_NAME)); } } @@ -331,6 +381,14 @@ public class JSchConnectionPage extends WizardPage { fPassphraseText.addModifyListener(fDataModifyListener); fPortText.addModifyListener(fDataModifyListener); fTimeoutText.addModifyListener(fDataModifyListener); + fProxyCommandText.addModifyListener(fDataModifyListener); + fProxyConnectionWidget.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + validateFields(); + getContainer().updateButtons(); + } + }); } public void setAddress(String address) { @@ -420,6 +478,17 @@ public class JSchConnectionPage extends WizardPage { if (fConnection.getPort() != port) { fConnection.setPort(port); } + if (!fConnection.getProxyCommand().equals(fProxyCommandText.getText().trim())) { + fConnection.setProxyCommand(fProxyCommandText.getText().trim()); + } + IRemoteConnection proxyConnection = fProxyConnectionWidget.getConnection(); + String proxyConnectionName = ""; //$NON-NLS-1$ + if (proxyConnection != null && proxyConnection.getRemoteServices() != RemoteServices.getLocalServices()) { + proxyConnectionName = proxyConnection.getName(); + } + if (!fConnection.getProxyConnectionName().equals(proxyConnectionName)) { + fConnection.setProxyConnectionName(proxyConnectionName); + } } } @@ -461,9 +530,13 @@ public class JSchConnectionPage extends WizardPage { if (message == null) { message = validatePasskey(); } + if (message == null && fProxyConnectionWidget.getConnection() == null) { + message = Messages.JSchConnectionPage_selectProxyConnection; + } if (message == null) { message = validateAdvanced(); } + setErrorMessage(message); setPageComplete(message == null); } |