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 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 passwords; + + @Singular + protected final Collection 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; } }