diff --git a/gb-browser/src/test/java/com/g2forge/gearbox/browser/example/TestExample.java b/gb-browser/src/test/java/com/g2forge/gearbox/browser/example/TestExample.java
index e7ce057..ea7e216 100644
--- a/gb-browser/src/test/java/com/g2forge/gearbox/browser/example/TestExample.java
+++ b/gb-browser/src/test/java/com/g2forge/gearbox/browser/example/TestExample.java
@@ -16,7 +16,7 @@ public void test() {
final ExampleHome home = site.open();
HAssert.assertEquals("Example Domain", home.getTitle());
final ExampleMoreInformation moreInformation = home.openMoreInformation();
- HAssert.assertEquals("IANA-managed Reserved Domains", moreInformation.getTitle());
+ HAssert.assertEquals("Example Domains", moreInformation.getTitle());
} catch (WebDriverException exception) {
// Don't try and run the test if firefox isn't installed
HAssume.assumeFalse(exception.getMessage().contains("Cannot find firefox binary in PATH"));
diff --git a/gb-ssh/pom.xml b/gb-ssh/pom.xml
index f1e5240..020c272 100644
--- a/gb-ssh/pom.xml
+++ b/gb-ssh/pom.xml
@@ -12,6 +12,10 @@
Gearbox SSH
Library for SSH client & server, including support for gearbox functional runners.
+
+
+ 2.7.0
+
@@ -28,7 +32,12 @@
org.apache.sshd
sshd-core
- 2.7.0
+ ${sshd.version}
+
+
+ org.apache.sshd
+ sshd-sftp
+ ${sshd.version}
com.g2forge.alexandria
diff --git a/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHConfig.java b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHConfig.java
new file mode 100644
index 0000000..424eedb
--- /dev/null
+++ b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHConfig.java
@@ -0,0 +1,53 @@
+package com.g2forge.gearbox.ssh;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.sftp.client.fs.SftpFileSystemClientSessionInitializer;
+import org.apache.sshd.sftp.client.fs.SftpFileSystemInitializationContext;
+import org.apache.sshd.sftp.client.fs.SftpFileSystemProvider;
+
+import com.g2forge.alexandria.java.core.helpers.HCollection;
+import com.g2forge.alexandria.java.io.RuntimeIOException;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+@Data
+@Builder(toBuilder = true)
+@RequiredArgsConstructor
+public class SSHConfig {
+ protected final SSHRemote remote;
+
+ protected final SSHCredentials credentials;
+
+ public FileSystem createFileSystem() {
+ final SftpFileSystemProvider provider = new SftpFileSystemProvider();
+ provider.setSftpFileSystemClientSessionInitializer(new SftpFileSystemClientSessionInitializer() {
+ @Override
+ public void authenticateClientSession(SftpFileSystemProvider provider, SftpFileSystemInitializationContext context, ClientSession session) throws IOException {
+ final String password = context.getCredentials().getPassword();
+ // If no password provided perhaps the client is set-up to use registered public keys
+ if (password != null) session.addPasswordIdentity(password);
+ getCredentials().configure(session);
+ session.auth().verify(context.getMaxAuthTime());
+ }
+ });
+
+ final SSHRemote remote = getRemote();
+ final Collection extends String> passwords = getCredentials().getPasswords();
+ final String password = passwords.isEmpty() ? null : HCollection.getFirst(passwords);
+ final URI uri = SftpFileSystemProvider.createFileSystemURI(remote.getHost(), remote.getPort(), remote.getUsername(), password);
+
+ try {
+ return provider.newFileSystem(uri, Collections.emptyMap());
+ } catch (IOException e) {
+ throw new RuntimeIOException("Failed to create SFTP filesystem for " + this, e);
+ }
+ }
+}
diff --git a/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHCredentials.java b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHCredentials.java
new file mode 100644
index 0000000..089646a
--- /dev/null
+++ b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHCredentials.java
@@ -0,0 +1,33 @@
+package com.g2forge.gearbox.ssh;
+
+import java.security.KeyPair;
+import java.util.Collection;
+
+import org.apache.sshd.client.ClientAuthenticationManager;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.Singular;
+import lombok.ToString;
+
+@Data
+@Builder(toBuilder = true)
+@RequiredArgsConstructor
+public class SSHCredentials {
+ @ToString.Exclude
+ @Singular
+ protected final Collection extends String> passwords;
+
+ @Singular
+ protected final Collection extends KeyPair> keys;
+
+ public void configure(ClientAuthenticationManager clientAuthenticationManager) {
+ if (getPasswords() != null) for (String password : getPasswords()) {
+ clientAuthenticationManager.addPasswordIdentity(password);
+ }
+ if (getKeys() != null) for (KeyPair key : getKeys()) {
+ clientAuthenticationManager.addPublicKeyIdentity(key);
+ }
+ }
+}
diff --git a/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRemote.java b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRemote.java
new file mode 100644
index 0000000..d3b7806
--- /dev/null
+++ b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRemote.java
@@ -0,0 +1,34 @@
+package com.g2forge.gearbox.ssh;
+
+import java.io.IOException;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.future.ConnectFuture;
+import org.apache.sshd.client.session.ClientSession;
+
+import com.g2forge.alexandria.java.io.RuntimeIOException;
+
+import lombok.Builder;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+@Data
+@Builder(toBuilder = true)
+@RequiredArgsConstructor
+public class SSHRemote {
+ protected final String username;
+
+ protected final String host;
+
+ protected final int port;
+
+ public ClientSession connect(SshClient client) {
+ try {
+ final ConnectFuture future = client.connect(getUsername(), getHost(), getPort());
+ if (!future.await()) throw new RuntimeIOException("Failed to connect to " + this);
+ return future.getSession();
+ } catch (IOException exception) {
+ throw new RuntimeIOException("Failed to connect to " + this, exception);
+ }
+ }
+}
diff --git a/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRunner.java b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRunner.java
index a7c609f..db168a7 100644
--- a/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRunner.java
+++ b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRunner.java
@@ -4,12 +4,12 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.util.EnumSet;
+import java.util.Objects;
import java.util.stream.Collectors;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ChannelExec;
import org.apache.sshd.client.channel.ClientChannelEvent;
-import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.session.ClientSession;
import com.g2forge.alexandria.annotations.note.Note;
@@ -30,18 +30,19 @@ public class SSHRunner implements IRunner, ICloseable {
protected boolean open = false;
- public SSHRunner(SSHServer server) {
- client = SshClient.setUpDefaultClient();
- client.start();
+ protected final boolean ownClient;
+
+ protected SSHRunner(final SshClient client, final boolean ownClient, final SSHConfig config) {
+ Objects.requireNonNull(client);
+ Objects.requireNonNull(config);
+
+ this.client = client;
+ this.ownClient = ownClient;
+ if (this.ownClient) client.start();
+
+ this.session = config.getRemote().connect(client);
+ if (config.getCredentials() != null) config.getCredentials().configure(session);
- try {
- final ConnectFuture future = client.connect(server.getUsername(), server.getHost(), server.getPort());
- if (!future.await()) throw new RuntimeIOException();
- session = future.getSession();
- } catch (IOException exception) {
- throw new RuntimeIOException(exception);
- }
- session.addPasswordIdentity(server.getPassword());
try {
session.auth().verify();
} catch (IOException exception) {
@@ -50,6 +51,14 @@ public SSHRunner(SSHServer server) {
open = true;
}
+ public SSHRunner(final SshClient client, final SSHConfig config) {
+ this(client, false, config);
+ }
+
+ public SSHRunner(final SSHConfig config) {
+ this(SshClient.setUpDefaultClient(), true, config);
+ }
+
@Note(type = NoteType.TODO, value = "IO redirection and working directories")
@Note(type = NoteType.TODO, value = "Environment variables")
@Override
@@ -113,7 +122,7 @@ public void close() {
} catch (IOException exception) {
throw new RuntimeIOException(exception);
} finally {
- client.stop();
+ if (ownClient) client.stop();
}
}
diff --git a/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHServer.java b/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHServer.java
deleted file mode 100644
index 9f2a991..0000000
--- a/gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHServer.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.g2forge.gearbox.ssh;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-
-@Builder
-@AllArgsConstructor
-@Data
-public class SSHServer {
- protected final String username;
-
- protected final String password;
-
- protected final String host;
-
- protected final int port;
-}
diff --git a/gb-ssh/src/test/java/com/g2forge/gearbox/ssh/TestSSHRunner.java b/gb-ssh/src/test/java/com/g2forge/gearbox/ssh/TestSSHRunner.java
index 6b0851f..89abb1e 100644
--- a/gb-ssh/src/test/java/com/g2forge/gearbox/ssh/TestSSHRunner.java
+++ b/gb-ssh/src/test/java/com/g2forge/gearbox/ssh/TestSSHRunner.java
@@ -27,26 +27,26 @@
import com.g2forge.gearbox.command.process.IProcess;
import com.g2forge.gearbox.command.process.redirect.IRedirect;
import com.g2forge.gearbox.command.test.ATestCommand;
-import com.g2forge.gearbox.ssh.SSHServer.SSHServerBuilder;
import lombok.Getter;
public class TestSSHRunner extends ATestCommand {
@Getter(lazy = true)
- private static final SSHServer server = createServer();
+ private static final SSHConfig config = createConfig();
- protected static SSHServer createServer() {
+ protected static SSHConfig createConfig() {
final ExecutorService executor = Executors.newSingleThreadExecutor();
- final Future handler = executor.submit(new Callable() {
+ final Future handler = executor.submit(new Callable() {
@Override
- public SSHServer call() throws Exception {
+ public SSHConfig call() throws Exception {
try {
- final SSHServerBuilder builder = SSHServer.builder();
- builder.username(new PropertyStringInput("ssh.username").fallback(new UserStringInput("SSH Username", false)).get());
- builder.password(new PropertyStringInput("ssh.password").fallback(new UserPasswordInput("SSH Password")).get());
- builder.host(new PropertyStringInput("ssh.host").fallback(new UserStringInput("SSH Host", false)).get());
- builder.port(Integer.valueOf(new PropertyStringInput("ssh.port").fallback(new UserStringInput("SSH Port", false)).get()));
- return builder.build();
+ final SSHRemote.SSHRemoteBuilder remote = SSHRemote.builder();
+ final SSHCredentials.SSHCredentialsBuilder credentials = SSHCredentials.builder();
+ remote.username(new PropertyStringInput("ssh.username").fallback(new UserStringInput("SSH Username", false)).get());
+ credentials.password(new PropertyStringInput("ssh.password").fallback(new UserPasswordInput("SSH Password")).get());
+ remote.host(new PropertyStringInput("ssh.host").fallback(new UserStringInput("SSH Host", false)).get());
+ remote.port(Integer.valueOf(new PropertyStringInput("ssh.port").fallback(new UserStringInput("SSH Port", false)).get()));
+ return new SSHConfig(remote.build(), credentials.build());
} catch (InputUnspecifiedException exception) {
return null;
}
@@ -80,24 +80,24 @@ protected ICommandConverterR_ createRenderer() {
@Override
protected IFunction1, IProcess> createRunner() {
- return new SSHRunner(getServer());
+ return new SSHRunner(getConfig());
}
@Test
public void cwd() {
- HAssume.assumeNotNull(getServer());
+ HAssume.assumeNotNull(getConfig());
final String cwd = HAssume.assumeNoException(new PropertyStringInput("sshtest.cwd").fallback(new UserStringInput("SSH Test CWD", false)));
HAssert.assertEquals(cwd, getUtils().pwd(Paths.get("./"), false).trim());
}
@Test
public void hostname() {
- HAssume.assumeNotNull(getServer());
+ HAssume.assumeNotNull(getConfig());
final String hostname = HAssume.assumeNoException(new PropertyStringInput("sshtest.hostname").fallback(new UserStringInput("SSH Test Hostname", false)));
HAssert.assertEquals(hostname, getUtils().echo(false, "${HOSTNAME}").trim());
}
protected boolean isValid() {
- return getServer() != null;
+ return getConfig() != null;
}
}