Skip to content

Commit

Permalink
Merge pull request #112 from gdgib/G2-1476-SSHFileSystem
Browse files Browse the repository at this point in the history
G2-1476 SSH FileSystem, multi-session client, & key support
  • Loading branch information
gdgib authored Oct 18, 2023
2 parents 930a7ea + 7f420fd commit 438251d
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
11 changes: 10 additions & 1 deletion gb-ssh/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

<name>Gearbox SSH</name>
<description>Library for SSH client &amp; server, including support for gearbox functional runners.</description>

<properties>
<sshd.version>2.7.0</sshd.version>
</properties>

<dependencies>
<dependency>
Expand All @@ -28,7 +32,12 @@
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId>
<version>2.7.0</version>
<version>${sshd.version}</version>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>${sshd.version}</version>
</dependency>
<dependency>
<groupId>com.g2forge.alexandria</groupId>
Expand Down
53 changes: 53 additions & 0 deletions gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHConfig.java
Original file line number Diff line number Diff line change
@@ -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.<String, Object>emptyMap());
} catch (IOException e) {
throw new RuntimeIOException("Failed to create SFTP filesystem for " + this, e);
}
}
}
33 changes: 33 additions & 0 deletions gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHCredentials.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
34 changes: 34 additions & 0 deletions gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRemote.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
35 changes: 22 additions & 13 deletions gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -113,7 +122,7 @@ public void close() {
} catch (IOException exception) {
throw new RuntimeIOException(exception);
} finally {
client.stop();
if (ownClient) client.stop();
}
}

Expand Down
18 changes: 0 additions & 18 deletions gb-ssh/src/main/java/com/g2forge/gearbox/ssh/SSHServer.java

This file was deleted.

30 changes: 15 additions & 15 deletions gb-ssh/src/test/java/com/g2forge/gearbox/ssh/TestSSHRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<SSHServer> handler = executor.submit(new Callable<SSHServer>() {
final Future<SSHConfig> handler = executor.submit(new Callable<SSHConfig>() {
@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;
}
Expand Down Expand Up @@ -80,24 +80,24 @@ protected ICommandConverterR_ createRenderer() {

@Override
protected IFunction1<CommandInvocation<IRedirect, IRedirect>, 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;
}
}

0 comments on commit 438251d

Please sign in to comment.