/*******************************************************************************
* Copyright (c) 2009 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
* Aptana Inc.
*******************************************************************************/
package org.eclipse.php.internal.server.core.tunneling;
import java.lang.reflect.InvocationTargetException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.php.internal.server.core.Activator;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
/**
* An SSH tunnel (port forwarding). This class creates a tunnel between a port
* on a remote host and a port on the local host.
*
* @author Shalom Gibly
*/
public class SSHTunnel {
public static final int CONNECTION_ERROR_CODE = 100;
public static final int CONNECTION_WARNING_CODE = 200;
public static final int CONNECTION_PASSWORD_CHANGED_CODE = 300;
private static final int SSH_DEFAULT_PORT = 22;
private static final String EMPTY_STRING = ""; //$NON-NLS-1$
private String remoteHost;
private String localHost;
private String userName;
private String password;
private int localPort;
private int remotePort;
private Session session;
/**
* Constructs a new SSH tunnel.
*
* @param localHost
* @param remoteHost
* @param userName
* (may be null or empty)
* @param password
* (may be null or empty)
* @param localPort
* @param remotePort
* @throws IllegalArgumentException
* In case one of the variables is null or off limits (null
* passwords are not verified).
*/
public SSHTunnel(String localHost, String remoteHost, String userName, String password, int localPort,
int remotePort) throws IllegalArgumentException {
validateInput(localHost, remoteHost, localPort, remotePort);
this.remoteHost = remoteHost;
this.localHost = localHost;
this.localPort = localPort;
this.remotePort = remotePort;
if (userName == null) {
this.userName = EMPTY_STRING;
} else {
this.userName = userName;
}
if (password == null) {
this.password = EMPTY_STRING;
} else {
this.password = password;
}
}
/**
* Create the tunnel connection. If the tunnel is already connected, nothing
* happens. This call will potentially change the password value of this
* class in case the user is prompted to enter a valid one. There are
* several return values possibilities to this call (as IStatus instances):
* <br>
* <ul>
* <li>Status OK - Signals that the connection was successful with no errors
* or warnings</li>
* <li>Status ERROR - Signals that the connection was unsuccessful</li>
* <li>Status WARNING - Signals that the connection was successful, however
* there are a few warning notifications that should be reviewed</li>
* <li>Status INFO - Signals that the connection was successful, however
* there was a modification to the connection data that is expressed in the
* INFO code (such as a password change data)</li>
* </ul>
*
* @return An IStatus that holds the creation result.
*/
@SuppressWarnings("unchecked")
public IStatus connect() {
IStatus status = Status.OK_STATUS;
if (isConnected()) {
return status;
}
// ssh -R 9000:localhost:9000 you@example.aptanacloud.com
int retry = 1;
String newPassword = null;
while (true) {
try {
session = SSHTunnelSession.getSession(userName, password, remoteHost, SSH_DEFAULT_PORT, null)
.getSession();
// This might throw exception because of binary compatibility
// issues.
// The JSCH API for the port forwarding was different in eclipse
// 3.2 and 3.4
Exception ex = null;
String actualPassword = session.getUserInfo().getPassword();
if (!password.equals(actualPassword)) {
newPassword = actualPassword;
}
Class sessionClass = session.getClass();
Class[] parameterTypes = new Class[] { int.class, String.class, int.class };
try {
// session.setPortForwardingR(rport, host, lport)
Object[] values = new Object[] { Integer.valueOf(remotePort), localHost,
Integer.valueOf(localPort) };
java.lang.reflect.Method mSetPortForwarding = sessionClass.getMethod("setPortForwardingR", //$NON-NLS-1$
parameterTypes);
mSetPortForwarding.invoke(session, values);
} catch (NoSuchMethodException nsme) {
// it will not be thrown.
} catch (InvocationTargetException e) {
// this can be thrown if we already binded the remote port.
// if this is the case, ignore it. If not, log it.
if (e.getCause() != null && e.getCause().getMessage() != null
&& e.getCause().getMessage().toLowerCase().indexOf(Messages.SSHTunnel_2) == -1) {
ex = e;
}
} catch (Exception e) {
ex = e;
}
if (ex != null) {
if (isConnected()) {
status = new Status(IStatus.WARNING, Activator.PLUGIN_ID, CONNECTION_WARNING_CODE,
Messages.SSHTunnel_3, ex);
} else {
status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, CONNECTION_ERROR_CODE,
Messages.SSHTunnel_4, ex);
}
}
} catch (JSchException ee) {
retry--;
if (retry < 0) {
status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, CONNECTION_ERROR_CODE,
Messages.SSHTunnel_5 + remoteHost, ee);
break;
}
if (session != null && session.isConnected()) {
// Make sure we disconnect before retrying
session.disconnect();
}
continue;
}
break;
}
if (newPassword != null) {
password = newPassword;
}
if (status.isOK() && newPassword != null) {
// Change the Status to INFO and deliver the accurate password to
// the caller.
status = new Status(IStatus.INFO, Activator.PLUGIN_ID, CONNECTION_PASSWORD_CHANGED_CODE, newPassword, null);
}
return status;
}
/**
* Disconnect the session.
*/
public void disconnect() {
if (session != null) {
session.disconnect();
}
}
/**
* Returns true if the session is alive; False, if it's not.
*
* @return True, iff the session is active.
*/
public boolean isConnected() {
if (session != null) {
return session.isConnected();
}
return false;
}
/**
* Returns the remote host.
*
* @return The remote host.
*/
public String getRemoteHost() {
return remoteHost;
}
/**
* Returns the local host.
*
* @return The local host.
*/
public String getLocalHost() {
return localHost;
}
/**
* Returns the user name.
*
* @return The user name.
*/
public String getUserName() {
return userName;
}
/**
* Returns the password. The returned password might differ from the
* original given one in case that the user was requested to change it while
* connecting.
*
* @return The password.
*/
public String getPassword() {
return password;
}
/**
* Returns the local port.
*
* @return The local port.
*/
public int getLocalPort() {
return localPort;
}
/**
* Returns the remote port.
*
* @return The remote port.
*/
public int getRemotePort() {
return remotePort;
}
/**
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return remoteHost.hashCode() * localHost.hashCode() * userName.hashCode() * (localPort + 1) * (remotePort + 1);
}
/**
* Check for SSHTunnel equality. Note that we do no compare passwords, as
* the password itself can be modified during connections.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object o) {
if (o instanceof SSHTunnel) {
SSHTunnel other = (SSHTunnel) o;
return remoteHost.equals(other.remoteHost) && localHost.equals(other.localHost)
&& userName.equals(other.userName) && localPort == other.localPort
&& remotePort == other.remotePort;
}
return false;
}
/*
* Validate the constructor's input.
*
* @param localHost
*
* @param remoteHost
*
* @param localPort
*
* @param remotePort
*
* @throws IllegalArgumentException
*/
private void validateInput(String localHost, String remoteHost, int localPort, int remotePort)
throws IllegalArgumentException {
if (localHost == null || remoteHost == null) {
throw new IllegalArgumentException("Null arument was passed to the SSHTunnel"); //$NON-NLS-1$
}
if (localPort < 0 || localPort > 65535 || remotePort < 0 || remotePort > 65535) {
throw new IllegalArgumentException("Illegal port was passed to the SSHTunnel"); //$NON-NLS-1$
}
// user names and password may be empty, so we do not check it here
}
}