diff --git a/compute/src/main/java/org/jclouds/compute/functions/CreateSshClientOncePortIsListeningOnNode.java b/compute/src/main/java/org/jclouds/compute/functions/CreateSshClientOncePortIsListeningOnNode.java index 0ba1818d4e..ce0616717f 100644 --- a/compute/src/main/java/org/jclouds/compute/functions/CreateSshClientOncePortIsListeningOnNode.java +++ b/compute/src/main/java/org/jclouds/compute/functions/CreateSshClientOncePortIsListeningOnNode.java @@ -17,6 +17,7 @@ package org.jclouds.compute.functions; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import java.util.concurrent.TimeUnit; @@ -49,7 +50,7 @@ public class CreateSshClientOncePortIsListeningOnNode implements Function response : client.runScriptOnNodesMatching( runningInGroup(group), "hostname", @@ -507,7 +506,6 @@ public abstract class BaseComputeServiceLiveTest extends BaseComputeServiceConte assertNotNull(node.getCredentials()); if (node.getCredentials().identity != null) { assertNotNull(node.getCredentials().identity); - assertNotNull(node.getCredentials().credential); sshPing(node, taskName); } } diff --git a/core/src/main/java/org/jclouds/crypto/Pems.java b/core/src/main/java/org/jclouds/crypto/Pems.java index 65afc42e70..b343f82025 100644 --- a/core/src/main/java/org/jclouds/crypto/Pems.java +++ b/core/src/main/java/org/jclouds/crypto/Pems.java @@ -73,6 +73,7 @@ public class Pems { public static final String CERTIFICATE_X509_MARKER = "-----BEGIN CERTIFICATE-----"; public static final String PUBLIC_X509_MARKER = "-----BEGIN PUBLIC KEY-----"; public static final String PUBLIC_PKCS1_MARKER = "-----BEGIN RSA PUBLIC KEY-----"; + public static final String PROC_TYPE_ENCRYPTED = "Proc-Type: 4,ENCRYPTED"; private static class PemProcessor implements ByteProcessor { private interface ResultParser { diff --git a/core/src/main/java/org/jclouds/domain/LoginCredentials.java b/core/src/main/java/org/jclouds/domain/LoginCredentials.java index a1eca0a695..d94acd7f14 100644 --- a/core/src/main/java/org/jclouds/domain/LoginCredentials.java +++ b/core/src/main/java/org/jclouds/domain/LoginCredentials.java @@ -19,6 +19,7 @@ package org.jclouds.domain; import static org.jclouds.crypto.Pems.PRIVATE_PKCS1_MARKER; import static org.jclouds.crypto.Pems.PRIVATE_PKCS8_MARKER; +import org.jclouds.crypto.Pems; import org.jclouds.javax.annotation.Nullable; import com.google.common.base.Optional; @@ -155,6 +156,15 @@ public class LoginCredentials extends Credentials { return (privateKey != null) ? privateKey.orNull() : null; } + /** + * @return true if there is a private key attached that is not encrypted + */ + public boolean hasUnencryptedPrivateKey() { + return getPrivateKey() != null + && !getPrivateKey().isEmpty() + && !getPrivateKey().contains(Pems.PROC_TYPE_ENCRYPTED); + } + /** * @return the optional private ssh key of the user or null */ diff --git a/drivers/jsch/pom.xml b/drivers/jsch/pom.xml index f5c965285f..8835ea6dec 100644 --- a/drivers/jsch/pom.xml +++ b/drivers/jsch/pom.xml @@ -86,6 +86,16 @@ jsch compile + + com.jcraft + jsch.agentproxy.jsch + 0.0.7 + + + com.jcraft + jsch.agentproxy.connector-factory + 0.0.7 + diff --git a/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/JschSshClient.java b/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/JschSshClient.java index 5e58b486fa..aa7f9d4ce4 100644 --- a/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/JschSshClient.java +++ b/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/JschSshClient.java @@ -56,6 +56,7 @@ import org.jclouds.util.Closeables2; import org.jclouds.util.Strings2; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Splitter; @@ -66,6 +67,7 @@ import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; +import com.jcraft.jsch.agentproxy.Connector; /** * This class needs refactoring. It is not thread safe. @@ -124,25 +126,28 @@ public class JschSshClient implements SshClient { public JschSshClient(ProxyConfig proxyConfig, BackoffLimitedRetryHandler backoffLimitedRetryHandler, HostAndPort socket, - LoginCredentials loginCredentials, int timeout) { + LoginCredentials loginCredentials, int timeout, Optional agentConnector) { this.user = checkNotNull(loginCredentials, "loginCredentials").getUser(); this.host = checkNotNull(socket, "socket").getHostText(); checkArgument(socket.getPort() > 0, "ssh port must be greater then zero" + socket.getPort()); - checkArgument(loginCredentials.getPassword() != null || loginCredentials.getPrivateKey() != null, - "you must specify a password or a key"); + checkArgument(loginCredentials.getPassword() != null || loginCredentials.hasUnencryptedPrivateKey() || agentConnector.isPresent(), + "you must specify a password, a key or an SSH agent needs to be available"); this.backoffLimitedRetryHandler = checkNotNull(backoffLimitedRetryHandler, "backoffLimitedRetryHandler"); - if (loginCredentials.getPrivateKey() == null) { + if (loginCredentials.getPassword() != null) { this.toString = String.format("%s:pw[%s]@%s:%d", loginCredentials.getUser(), base16().lowerCase().encode(md5().hashString(loginCredentials.getPassword(), UTF_8).asBytes()), host, socket.getPort()); - } else { + } else if (loginCredentials.hasUnencryptedPrivateKey()) { String fingerPrint = fingerprintPrivateKey(loginCredentials.getPrivateKey()); String sha1 = sha1PrivateKey(loginCredentials.getPrivateKey()); this.toString = String.format("%s:rsa[fingerprint(%s),sha1(%s)]@%s:%d", loginCredentials.getUser(), - fingerPrint, sha1, host, socket.getPort()); + fingerPrint, sha1, host, socket.getPort()); + } else { + this.toString = String.format("%s:rsa[ssh-agent]@%s:%d", loginCredentials.getUser(), host, socket.getPort()); } sessionConnection = SessionConnection.builder().hostAndPort(HostAndPort.fromParts(host, socket.getPort())).loginCredentials( - loginCredentials).proxy(checkNotNull(proxyConfig, "proxyConfig")).connectTimeout(timeout).sessionTimeout(timeout).build(); + loginCredentials).proxy(checkNotNull(proxyConfig, "proxyConfig")).connectTimeout(timeout).sessionTimeout(timeout) + .agentConnector(agentConnector).build(); } @Override diff --git a/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/SessionConnection.java b/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/SessionConnection.java index 66d05d1332..f6465cb490 100644 --- a/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/SessionConnection.java +++ b/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/SessionConnection.java @@ -17,7 +17,6 @@ package org.jclouds.ssh.jsch; import static com.google.common.base.Objects.equal; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import org.jclouds.domain.Credentials; @@ -34,8 +33,13 @@ import com.jcraft.jsch.Proxy; import com.jcraft.jsch.ProxyHTTP; import com.jcraft.jsch.ProxySOCKS5; import com.jcraft.jsch.Session; +import com.jcraft.jsch.agentproxy.Connector; +import com.jcraft.jsch.agentproxy.RemoteIdentityRepository; public final class SessionConnection implements Connection { + + private Optional agentConnector; + public static Builder builder() { return new Builder(); } @@ -47,6 +51,7 @@ public final class SessionConnection implements Connection { private Optional proxy = Optional.absent(); private int connectTimeout; private int sessionTimeout; + private Optional agentConnector; /** * @see SessionConnection#getHostAndPort() @@ -114,7 +119,7 @@ public final class SessionConnection implements Connection { } public SessionConnection build() { - return new SessionConnection(hostAndPort, loginCredentials, proxy, connectTimeout, sessionTimeout); + return new SessionConnection(hostAndPort, loginCredentials, proxy, connectTimeout, sessionTimeout, agentConnector); } public Builder from(SessionConnection in) { @@ -122,15 +127,21 @@ public final class SessionConnection implements Connection { .connectTimeout(in.connectTimeout).sessionTimeout(in.sessionTimeout); } + public Builder agentConnector(Optional agentConnector) { + this.agentConnector = agentConnector; + return this; + } + } private SessionConnection(HostAndPort hostAndPort, LoginCredentials loginCredentials, Optional proxy, - int connectTimeout, int sessionTimeout) { + int connectTimeout, int sessionTimeout, Optional agentConnector) { this.hostAndPort = checkNotNull(hostAndPort, "hostAndPort"); this.loginCredentials = checkNotNull(loginCredentials, "loginCredentials for %", hostAndPort); this.connectTimeout = connectTimeout; this.sessionTimeout = sessionTimeout; this.proxy = checkNotNull(proxy, "proxy for %", hostAndPort); + this.agentConnector = checkNotNull(agentConnector, "agentConnector for %", hostAndPort); } private static final byte[] emptyPassPhrase = new byte[0]; @@ -160,11 +171,12 @@ public final class SessionConnection implements Connection { session.setTimeout(sessionTimeout); if (loginCredentials.getPrivateKey() == null) { session.setPassword(loginCredentials.getPassword()); - } else { - checkArgument(!loginCredentials.getPrivateKey().contains("Proc-Type: 4,ENCRYPTED"), - "JschSshClientModule does not support private keys that require a passphrase"); + } else if (loginCredentials.hasUnencryptedPrivateKey()) { byte[] privateKey = loginCredentials.getPrivateKey().getBytes(); jsch.addIdentity(loginCredentials.getUser(), privateKey, null, emptyPassPhrase); + } else if (agentConnector.isPresent()) { + JSch.setConfig("PreferredAuthentications", "publickey"); + jsch.setIdentityRepository(new RemoteIdentityRepository(agentConnector.get())); } java.util.Properties config = new java.util.Properties(); config.put("StrictHostKeyChecking", "no"); diff --git a/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/config/JschSshClientModule.java b/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/config/JschSshClientModule.java index 729da6de1f..dfb75a0a43 100644 --- a/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/config/JschSshClientModule.java +++ b/drivers/jsch/src/main/java/org/jclouds/ssh/jsch/config/JschSshClientModule.java @@ -28,11 +28,15 @@ import org.jclouds.ssh.SshClient; import org.jclouds.ssh.config.ConfiguresSshClient; import org.jclouds.ssh.jsch.JschSshClient; +import com.google.common.base.Optional; import com.google.common.net.HostAndPort; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Scopes; +import com.jcraft.jsch.agentproxy.AgentProxyException; +import com.jcraft.jsch.agentproxy.Connector; +import com.jcraft.jsch.agentproxy.ConnectorFactory; /** * @@ -50,6 +54,16 @@ public class JschSshClientModule extends AbstractModule { @Inject(optional = true) int timeout = 60000; + Optional agentConnector = getAgentConnector(); + + Optional getAgentConnector() { + try { + return Optional.of(ConnectorFactory.getDefault().createConnector()); + } catch (final AgentProxyException e) { + return Optional.absent(); + } + } + private final ProxyConfig proxyConfig; private final BackoffLimitedRetryHandler backoffLimitedRetryHandler; private final Injector injector; @@ -63,9 +77,14 @@ public class JschSshClientModule extends AbstractModule { @Override public SshClient create(HostAndPort socket, LoginCredentials credentials) { - SshClient client = new JschSshClient(proxyConfig, backoffLimitedRetryHandler, socket, credentials, timeout); + SshClient client = new JschSshClient(proxyConfig, backoffLimitedRetryHandler, socket, credentials, timeout, getAgentConnector()); injector.injectMembers(client);// add logger return client; } + + @Override + public boolean isAgentAvailable() { + return agentConnector.isPresent(); + } } } diff --git a/drivers/sshj/pom.xml b/drivers/sshj/pom.xml index 33125bf5b1..bc69e89942 100644 --- a/drivers/sshj/pom.xml +++ b/drivers/sshj/pom.xml @@ -104,6 +104,16 @@ + + com.jcraft + jsch.agentproxy.sshj + 0.0.7 + + + com.jcraft + jsch.agentproxy.connector-factory + 0.0.7 + diff --git a/drivers/sshj/src/main/java/org/jclouds/sshj/SSHClientConnection.java b/drivers/sshj/src/main/java/org/jclouds/sshj/SSHClientConnection.java index bf203787bf..f1f32e2efb 100644 --- a/drivers/sshj/src/main/java/org/jclouds/sshj/SSHClientConnection.java +++ b/drivers/sshj/src/main/java/org/jclouds/sshj/SSHClientConnection.java @@ -17,15 +17,19 @@ package org.jclouds.sshj; import static com.google.common.base.Objects.equal; +import static com.google.common.base.Preconditions.checkNotNull; import java.io.IOException; +import java.util.List; import javax.annotation.Resource; import javax.inject.Named; import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.common.Buffer.BufferException; import net.schmizz.sshj.transport.verification.PromiscuousVerifier; import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile; +import net.schmizz.sshj.userauth.method.AuthMethod; import org.jclouds.domain.LoginCredentials; import org.jclouds.logging.Logger; @@ -33,9 +37,18 @@ import org.jclouds.sshj.SshjSshClient.Connection; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; import com.google.common.net.HostAndPort; +import com.jcraft.jsch.agentproxy.AgentProxy; +import com.jcraft.jsch.agentproxy.Connector; +import com.jcraft.jsch.agentproxy.Identity; +import com.jcraft.jsch.agentproxy.sshj.AuthAgent; + public class SSHClientConnection implements Connection { + private Optional agentConnector; + public static Builder builder() { return new Builder(); } @@ -46,6 +59,7 @@ public class SSHClientConnection implements Connection { protected LoginCredentials loginCredentials; protected int connectTimeout; protected int sessionTimeout; + protected Optional agentConnector; /** * @see SSHClientConnection#getHostAndPort() @@ -79,8 +93,16 @@ public class SSHClientConnection implements Connection { return this; } + /** + * @see SSHClientConnection#getAgentConnector() + */ + public Builder agentConnector(Optional agentConnector) { + this.agentConnector = agentConnector; + return this; + } + public SSHClientConnection build() { - return new SSHClientConnection(hostAndPort, loginCredentials, connectTimeout, sessionTimeout); + return new SSHClientConnection(hostAndPort, loginCredentials, connectTimeout, sessionTimeout, agentConnector); } protected Builder fromSSHClientConnection(SSHClientConnection in) { @@ -90,11 +112,12 @@ public class SSHClientConnection implements Connection { } private SSHClientConnection(HostAndPort hostAndPort, LoginCredentials loginCredentials, int connectTimeout, - int sessionTimeout) { - this.hostAndPort = hostAndPort; - this.loginCredentials = loginCredentials; + int sessionTimeout, Optional agentConnector) { + this.hostAndPort = checkNotNull(hostAndPort, "hostAndPort"); + this.loginCredentials = checkNotNull(loginCredentials, "loginCredentials for %", hostAndPort); this.connectTimeout = connectTimeout; this.sessionTimeout = sessionTimeout; + this.agentConnector = checkNotNull(agentConnector, "agentConnector for %", hostAndPort); } @Resource @@ -136,10 +159,13 @@ public class SSHClientConnection implements Connection { ssh.connect(hostAndPort.getHostText(), hostAndPort.getPortOrDefault(22)); if (loginCredentials.getPassword() != null) { ssh.authPassword(loginCredentials.getUser(), loginCredentials.getPassword()); - } else { + } else if (loginCredentials.hasUnencryptedPrivateKey()) { OpenSSHKeyFile key = new OpenSSHKeyFile(); key.init(loginCredentials.getPrivateKey(), null); ssh.authPublickey(loginCredentials.getUser(), key); + } else if (agentConnector.isPresent()) { + AgentProxy proxy = new AgentProxy(agentConnector.get()); + ssh.auth(loginCredentials.getUser(), getAuthMethods(proxy)); } return ssh; } @@ -175,6 +201,14 @@ public class SSHClientConnection implements Connection { return sessionTimeout; } + /** + * + * @return Ssh agent connector + */ + public Optional getAgentConnector() { + return agentConnector; + } + /** * * @return the current ssh or {@code null} if not connected @@ -206,4 +240,12 @@ public class SSHClientConnection implements Connection { "sessionTimeout", sessionTimeout).toString(); } + private static List getAuthMethods(AgentProxy agent) throws BufferException { + ImmutableList.Builder identities = ImmutableList.builder(); + for (Identity identity : agent.getIdentities()) { + identities.add(new AuthAgent(agent, identity)); + } + return identities.build(); + } + } diff --git a/drivers/sshj/src/main/java/org/jclouds/sshj/SshjSshClient.java b/drivers/sshj/src/main/java/org/jclouds/sshj/SshjSshClient.java index 41155175c9..9bfb210891 100644 --- a/drivers/sshj/src/main/java/org/jclouds/sshj/SshjSshClient.java +++ b/drivers/sshj/src/main/java/org/jclouds/sshj/SshjSshClient.java @@ -68,6 +68,7 @@ import org.jclouds.util.Closeables2; import org.jclouds.util.Throwables2; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.base.Splitter; @@ -76,6 +77,7 @@ import com.google.common.base.Throwables; import com.google.common.collect.ImmutableMap; import com.google.common.net.HostAndPort; import com.google.inject.Inject; +import com.jcraft.jsch.agentproxy.Connector; /** * This class needs refactoring. It is not thread safe. @@ -141,25 +143,28 @@ public class SshjSshClient implements SshClient { private final BackoffLimitedRetryHandler backoffLimitedRetryHandler; public SshjSshClient(BackoffLimitedRetryHandler backoffLimitedRetryHandler, HostAndPort socket, - LoginCredentials loginCredentials, int timeout) { + LoginCredentials loginCredentials, int timeout, Optional agentConnector) { this.user = checkNotNull(loginCredentials, "loginCredentials").getUser(); this.host = checkNotNull(socket, "socket").getHostText(); checkArgument(socket.getPort() > 0, "ssh port must be greater then zero" + socket.getPort()); - checkArgument(loginCredentials.getPassword() != null || loginCredentials.getPrivateKey() != null, - "you must specify a password or a key"); + checkArgument(loginCredentials.getPassword() != null || loginCredentials.hasUnencryptedPrivateKey() || agentConnector.isPresent(), + "you must specify a password, a key or an SSH agent needs to be available"); this.backoffLimitedRetryHandler = checkNotNull(backoffLimitedRetryHandler, "backoffLimitedRetryHandler"); - if (loginCredentials.getPrivateKey() == null) { + if (loginCredentials.getPassword() != null) { this.toString = String.format("%s:pw[%s]@%s:%d", loginCredentials.getUser(), base16().lowerCase().encode(md5().hashString(loginCredentials.getPassword(), UTF_8).asBytes()), host, socket.getPort()); - } else { + } else if (loginCredentials.hasUnencryptedPrivateKey()) { String fingerPrint = fingerprintPrivateKey(loginCredentials.getPrivateKey()); String sha1 = sha1PrivateKey(loginCredentials.getPrivateKey()); this.toString = String.format("%s:rsa[fingerprint(%s),sha1(%s)]@%s:%d", loginCredentials.getUser(), fingerPrint, sha1, host, socket.getPort()); + } else { + this.toString = String.format("%s:rsa[ssh-agent]@%s:%d", loginCredentials.getUser(), + host, socket.getPort()); } sshClientConnection = SSHClientConnection.builder().hostAndPort(HostAndPort.fromParts(host, socket.getPort())) - .loginCredentials(loginCredentials).connectTimeout(timeout).sessionTimeout(timeout).build(); + .loginCredentials(loginCredentials).connectTimeout(timeout).sessionTimeout(timeout).agentConnector(agentConnector).build(); } @Override diff --git a/drivers/sshj/src/main/java/org/jclouds/sshj/config/SshjSshClientModule.java b/drivers/sshj/src/main/java/org/jclouds/sshj/config/SshjSshClientModule.java index a50c886fc8..4b378508e9 100644 --- a/drivers/sshj/src/main/java/org/jclouds/sshj/config/SshjSshClientModule.java +++ b/drivers/sshj/src/main/java/org/jclouds/sshj/config/SshjSshClientModule.java @@ -25,11 +25,15 @@ import org.jclouds.ssh.SshClient; import org.jclouds.ssh.config.ConfiguresSshClient; import org.jclouds.sshj.SshjSshClient; +import com.google.common.base.Optional; import com.google.common.net.HostAndPort; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Scopes; +import com.jcraft.jsch.agentproxy.AgentProxyException; +import com.jcraft.jsch.agentproxy.Connector; +import com.jcraft.jsch.agentproxy.ConnectorFactory; /** * @@ -42,11 +46,22 @@ public class SshjSshClientModule extends AbstractModule { bind(SshClient.Factory.class).to(Factory.class).in(Scopes.SINGLETON); } + private static class Factory implements SshClient.Factory { @Named(Constants.PROPERTY_CONNECTION_TIMEOUT) @Inject(optional = true) int timeout = 60000; + Optional agentConnector = getAgentConnector(); + + Optional getAgentConnector() { + try { + return Optional.of(ConnectorFactory.getDefault().createConnector()); + } catch (final AgentProxyException e) { + return Optional.absent(); + } + } + private final BackoffLimitedRetryHandler backoffLimitedRetryHandler; private final Injector injector; @@ -58,9 +73,15 @@ public class SshjSshClientModule extends AbstractModule { @Override public SshClient create(HostAndPort socket, LoginCredentials credentials) { - SshClient client = new SshjSshClient(backoffLimitedRetryHandler, socket, credentials, timeout); + SshClient client = new SshjSshClient(backoffLimitedRetryHandler, socket, credentials, timeout, getAgentConnector()); injector.injectMembers(client);// add logger return client; } + + @Override + public boolean isAgentAvailable() { + return agentConnector.isPresent(); + } + } } diff --git a/project/pom.xml b/project/pom.xml index 79d31f9dd6..e548aea855 100644 --- a/project/pom.xml +++ b/project/pom.xml @@ -469,6 +469,23 @@ com.google + + + + com.jcraft + jsch.agentproxy.core + 0.0.7 + + + com.jcraft + jsch.agentproxy.connector-factory + 0.0.7 + + + + com.jcraft.jsch.agentproxy + +