diff --git a/src/main/java/edu/unc/lib/boxc/migration/cdm/services/CdmFileRetrievalService.java b/src/main/java/edu/unc/lib/boxc/migration/cdm/services/CdmFileRetrievalService.java index 38722971..5bb63bdb 100644 --- a/src/main/java/edu/unc/lib/boxc/migration/cdm/services/CdmFileRetrievalService.java +++ b/src/main/java/edu/unc/lib/boxc/migration/cdm/services/CdmFileRetrievalService.java @@ -4,17 +4,13 @@ import edu.unc.lib.boxc.migration.cdm.model.CdmEnvironment; import edu.unc.lib.boxc.migration.cdm.model.MigrationProject; import edu.unc.lib.boxc.migration.cdm.services.ChompbConfigService.ChompbConfig; -import org.apache.sshd.client.SshClient; -import org.apache.sshd.common.SshException; -import org.apache.sshd.common.config.keys.FilePasswordProvider; +import edu.unc.lib.boxc.migration.cdm.util.SshClientService; import org.apache.sshd.scp.client.ScpClient; -import org.apache.sshd.scp.client.ScpClientCreator; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** @@ -89,27 +85,14 @@ public static Path getExportedCpdsPath(MigrationProject project) { * @param downloadBlock method containing download operations */ public void executeDownloadBlock(Consumer downloadBlock) { - SshClient client = SshClient.setUpDefaultClient(); - client.setFilePasswordProvider(FilePasswordProvider.of(sshPassword)); - client.start(); - var cdmEnvId = project.getProjectProperties().getCdmEnvironmentId(); - var cdmEnvConfig = chompbConfig.getCdmEnvironments().get(cdmEnvId); - try (var sshSession = client.connect(sshUsername, cdmEnvConfig.getSshHost(), cdmEnvConfig.getSshPort()) - .verify(SSH_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .getSession()) { - sshSession.addPasswordIdentity(sshPassword); - - sshSession.auth().verify(SSH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + var cdmEnvConfig = getCdmEnvironment(); + var sshService = new SshClientService(); + sshService.setSshHost(cdmEnvConfig.getSshHost()); + sshService.setSshPort(cdmEnvConfig.getSshPort()); + sshService.setSshUsername(sshUsername); + sshService.setSshPassword(sshPassword); - var scpClientCreator = ScpClientCreator.instance(); - var scpClient = scpClientCreator.createScpClient(sshSession); - downloadBlock.accept(scpClient); - } catch (IOException e) { - if (e instanceof SshException && e.getMessage().contains("No more authentication methods available")) { - throw new MigrationException("Authentication to server failed, check username or password"); - } - throw new MigrationException("Failed to establish ssh session", e); - } + sshService.executeScpBlock(downloadBlock); } private CdmEnvironment getCdmEnvironment() { diff --git a/src/main/java/edu/unc/lib/boxc/migration/cdm/services/SourceFilesToRemoteService.java b/src/main/java/edu/unc/lib/boxc/migration/cdm/services/SourceFilesToRemoteService.java new file mode 100644 index 00000000..8ab1628b --- /dev/null +++ b/src/main/java/edu/unc/lib/boxc/migration/cdm/services/SourceFilesToRemoteService.java @@ -0,0 +1,104 @@ +package edu.unc.lib.boxc.migration.cdm.services; + +import edu.unc.lib.boxc.migration.cdm.model.SourceFilesInfo; +import edu.unc.lib.boxc.migration.cdm.util.SshClientService; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.stream.Collectors; + +/** + * Service for transferring source files from the local server to a remote destination. + * @author bbpennel + */ +public class SourceFilesToRemoteService { + private SourceFileService sourceFileService; + private SshClientService sshClientService; + private int concurrentTransfers = 5; + + /** + * Transfer files from the source CDM server to the remote destination. + * Files are transferred in parallel, up to concurrentTransfers at a time. + * @param destinationPath + * @throws IOException + */ + public void transferFiles(Path destinationPath) throws IOException { + var sourceMappings = sourceFileService.loadMappings(); + final Path destinationBasePath = destinationPath.toAbsolutePath(); + // Get all the source paths as a thread safe queue + var sourcePaths = sourceMappings.getMappings().stream() + .map(SourceFilesInfo.SourceFileMapping::getFirstSourcePath) + .collect(Collectors.toList()); + var pathsDeque = new ConcurrentLinkedDeque(sourcePaths); + // For tracking if a parent directory has already been created + Set createdParentsSet = ConcurrentHashMap.newKeySet(); + // Create the remote destination directory + sshClientService.executeRemoteCommand("mkdir -p " + destinationBasePath); + createdParentsSet.add(destinationBasePath.toString()); + + var threads = new ArrayList(concurrentTransfers); + // Start threads for parallel transfer of files + for (int i = 0; i < concurrentTransfers; i++) { + var thread = createTransferThread(pathsDeque, destinationBasePath, createdParentsSet); + thread.start(); + threads.add(thread); + } + + // Wait for all threads to finish + threads.forEach(t -> { + try { + t.join(); + } catch (InterruptedException e) { + throw new RuntimeException("Thread interrupted", e); + } + }); + } + + private Thread createTransferThread(ConcurrentLinkedDeque pathsDeque, + Path destinationBasePath, + Set createdParentsSet) { + var thread = new Thread(() -> { + Path nextPath; + while ((nextPath = pathsDeque.poll()) != null) { + final Path sourcePath = nextPath; + sshClientService.executeSshBlock((sshClient) -> { + var sourceRelative = sourcePath.toAbsolutePath().toString().substring(1); + var destPath = destinationBasePath.resolve(sourceRelative); + var destParentPath = destPath.getParent(); + // Create the parent path if we haven't already done so + synchronized (createdParentsSet) { + if (!createdParentsSet.contains(destParentPath.toString())) { + createdParentsSet.add(destParentPath.toString()); + sshClientService.executeRemoteCommand("mkdir -p " + destPath.getParent()); + } + } + // Upload the file to the appropriate path on the remote server + sshClientService.executeScpBlock(sshClient, (scpClient) -> { + try { + scpClient.upload(sourcePath.toString(), destPath.toString()); + } catch (IOException e) { + throw new RuntimeException("Failed to transfer file " + sourcePath, e); + } + }); + }); + } + }); + return thread; + } + + public void setSourceFileService(SourceFileService sourceFileService) { + this.sourceFileService = sourceFileService; + } + + public void setSshClientService(SshClientService sshClientService) { + this.sshClientService = sshClientService; + } + + public void setConcurrentTransfers(int concurrentTransfers) { + this.concurrentTransfers = concurrentTransfers; + } +} diff --git a/src/main/java/edu/unc/lib/boxc/migration/cdm/util/SshClientService.java b/src/main/java/edu/unc/lib/boxc/migration/cdm/util/SshClientService.java new file mode 100644 index 00000000..40ad921f --- /dev/null +++ b/src/main/java/edu/unc/lib/boxc/migration/cdm/util/SshClientService.java @@ -0,0 +1,167 @@ +package edu.unc.lib.boxc.migration.cdm.util; + +import edu.unc.lib.boxc.migration.cdm.exceptions.MigrationException; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ClientChannel; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.io.resource.PathResource; +import org.apache.sshd.scp.client.ScpClient; +import org.apache.sshd.scp.client.ScpClientCreator; +import org.apache.sshd.common.util.security.SecurityUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.EnumSet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static java.util.Collections.singletonList; + +/** + * Service for executing remote commands and transfers + * @author bbpennel + */ +public class SshClientService { + private static final int SSH_TIMEOUT_SECONDS = 10; + + private String sshHost; + private int sshPort; + private String sshUsername; + private String sshPassword; + private Path sshKeyPath; + private KeyPair sshKeyPair; + + public void initialize() { + if (sshKeyPath != null) { + try { + sshKeyPair = SecurityUtils.loadKeyPairIdentities( + null, new PathResource(sshKeyPath), Files.newInputStream(sshKeyPath), null + ).iterator().next(); + } catch (IOException | GeneralSecurityException e) { + throw new MigrationException("Failed to load ssh key", e); + } + } + } + + private SshClient buildSshClient() { + SshClient client = SshClient.setUpDefaultClient(); + if (sshKeyPair != null) { + client.setKeyIdentityProvider(KeyPairProvider.wrap(singletonList(sshKeyPair))); + } else if (sshPassword != null) { + client.setFilePasswordProvider(FilePasswordProvider.of(sshPassword)); + } + return client; + } + + private void setupSessionAuthentication(ClientSession session) { + if (sshKeyPair != null) { + session.addPublicKeyIdentity(sshKeyPair); + } else if (sshPassword != null) { + session.addPasswordIdentity(sshPassword); + } + } + + /** + * Execute a remote command on the server + * @param command + * @return Response output from the command + */ + public String executeRemoteCommand(String command) { + var response = new AtomicReference(); + executeSshBlock(clientSession -> { + response.set(executeRemoteCommand(clientSession, command)); + }); + return response.get(); + } + + /** + * Execute a remote command on the server, using the provided session + * @param command + * @return Response output from the command + */ + public String executeRemoteCommand(ClientSession clientSession, String command) { + try (var responseStream = new ByteArrayOutputStream(); + ClientChannel channel = clientSession.createExecChannel(command)) { + + channel.setOut(responseStream); + channel.setErr(responseStream); + channel.open().verify(5, TimeUnit.SECONDS); + + channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED), 5000); + return responseStream.toString(); + } catch (Exception e) { + throw new MigrationException("Failed to execute remote command", e); + } + } + + /** + * Execute a block of code with an SSH session + * @param sshBlock + */ + public void executeSshBlock(Consumer sshBlock) { + SshClient client = buildSshClient(); + client.start(); + try (var sshSession = client.connect(sshUsername, sshHost, sshPort) + .verify(SSH_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .getSession()) { + setupSessionAuthentication(sshSession); + sshSession.auth().verify(SSH_TIMEOUT_SECONDS, TimeUnit.SECONDS); + sshBlock.accept(sshSession); + } catch (IOException e) { + if (e instanceof SshException && e.getMessage().contains("No more authentication methods available")) { + throw new MigrationException("Authentication to server failed, check username or password", e); + } + throw new MigrationException("Failed to establish ssh session", e); + } + } + + /** + * Execute a block of code that requires an SCP client + * @param scpBlock + */ + public void executeScpBlock(Consumer scpBlock) { + executeSshBlock(client -> { + executeScpBlock(client, scpBlock); + }); + } + + /** + * Execute a block of code that requires an SCP client, using the provided ssh session + * @param session + * @param scpBlock + */ + public void executeScpBlock(ClientSession session, Consumer scpBlock) { + var scpClientCreator = ScpClientCreator.instance(); + var scpClient = scpClientCreator.createScpClient(session); + scpBlock.accept(scpClient); + } + + public void setSshHost(String sshHost) { + this.sshHost = sshHost; + } + + public void setSshPort(int sshPort) { + this.sshPort = sshPort; + } + + public void setSshUsername(String sshUsername) { + this.sshUsername = sshUsername; + } + + public void setSshPassword(String sshPassword) { + this.sshPassword = sshPassword; + } + + public void setSshKeyPath(Path sshKeyPath) { + this.sshKeyPath = sshKeyPath; + } +} diff --git a/src/test/java/edu/unc/lib/boxc/migration/cdm/services/SourceFilesToRemoteServiceTest.java b/src/test/java/edu/unc/lib/boxc/migration/cdm/services/SourceFilesToRemoteServiceTest.java new file mode 100644 index 00000000..6949646b --- /dev/null +++ b/src/test/java/edu/unc/lib/boxc/migration/cdm/services/SourceFilesToRemoteServiceTest.java @@ -0,0 +1,128 @@ +package edu.unc.lib.boxc.migration.cdm.services; + +import edu.unc.lib.boxc.migration.cdm.exceptions.MigrationException; +import edu.unc.lib.boxc.migration.cdm.model.MigrationProject; +import edu.unc.lib.boxc.migration.cdm.options.AddSourceFileMappingOptions; +import edu.unc.lib.boxc.migration.cdm.test.BxcEnvironmentHelper; +import edu.unc.lib.boxc.migration.cdm.test.CdmEnvironmentHelper; +import edu.unc.lib.boxc.migration.cdm.test.TestSshServer; +import edu.unc.lib.boxc.migration.cdm.util.SshClientService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author bbpennel + */ +public class SourceFilesToRemoteServiceTest { + private static final String PROJECT_NAME = "proj"; + private SourceFilesToRemoteService service; + private SourceFileService sourceFileService; + private SshClientService sshClientService; + private TestSshServer testSshServer; + private MigrationProject project; + private Path projPath; + private Path remotePath; + private Path clientKeyPath; + + @TempDir + public Path tmpFolder; + + @BeforeEach + public void setUp() throws Exception { + projPath = tmpFolder.resolve(PROJECT_NAME); + Files.createDirectories(projPath); + remotePath = tmpFolder.resolve("remote"); + project = MigrationProjectFactory.createCdmMigrationProject( + projPath, PROJECT_NAME, null, "user", + CdmEnvironmentHelper.DEFAULT_ENV_ID, BxcEnvironmentHelper.DEFAULT_ENV_ID); + sourceFileService = new SourceFileService(); + sourceFileService.setProject(project); + clientKeyPath = Paths.get("src/test/resources/test_client_key"); + testSshServer = new TestSshServer(); + testSshServer.setClientKeyPath(clientKeyPath); + sshClientService = new SshClientService(); + sshClientService.setSshPort(42222); + sshClientService.setSshHost("127.0.0.1"); + sshClientService.setSshUsername("testuser"); + sshClientService.setSshKeyPath(clientKeyPath); + sshClientService.initialize(); + service = new SourceFilesToRemoteService(); + service.setConcurrentTransfers(2); + service.setSourceFileService(sourceFileService); + service.setSshClientService(sshClientService); + testSshServer.startServer(); + } + + @AfterEach + public void cleanup() throws Exception { + testSshServer.stopServer(); + } + + @Test + public void testTransferFiles() throws Exception { + var filePath1 = createTestFile("sources/file1.jpg", "file1"); + var filePath2 = createTestFile("sources/nest/path/file2.jpg", "file2"); + var filePath3 = createTestFile("sources/nest/path/file3.jpg", "file3"); + var filePath4 = createTestFile("sources/nest/file4.jpg", "file4"); + var filePath5 = createTestFile("sources/another/file5.jpg", "file5"); + AddSourceFileMappingOptions options = new AddSourceFileMappingOptions(); + options.setBasePath(tmpFolder.resolve("sources")); + options.setExtensions(Arrays.asList("jpg")); + sourceFileService.addToMapping(options); + + service.transferFiles(remotePath); + + // Verify that the files were transferred + assertTransferred(filePath1); + assertTransferred(filePath2); + assertTransferred(filePath3); + assertTransferred(filePath4); + assertTransferred(filePath5); + } + + @Test + public void testTransferFilesAuthFailure() throws Exception { + createTestFile("sources/file1.jpg", "file1"); + createTestFile("sources/nest/path/file2.jpg", "file2"); + AddSourceFileMappingOptions options = new AddSourceFileMappingOptions(); + options.setBasePath(tmpFolder.resolve("sources")); + options.setExtensions(Arrays.asList("jpg")); + sourceFileService.addToMapping(options); + + Path badClientKey = Paths.get("src/test/resources/test_client2_key"); + sshClientService.setSshKeyPath(badClientKey); + sshClientService.initialize(); + + var e = assertThrows(MigrationException.class, () -> service.transferFiles(remotePath)); + assertTrue(e.getMessage().contains("Authentication to server failed")); + + // Verify no files transferred + assertFalse(Files.exists(remotePath)); + } + + private Path createTestFile(String relativePath, String content) throws Exception { + Path file = tmpFolder.resolve(relativePath); + Files.createDirectories(file.getParent()); + Files.writeString(file, content); + return file; + } + + private void assertTransferred(Path sourcePath) throws IOException { + Path remoteFile = remotePath.resolve(sourcePath.toString().substring(1)); + assertTrue(Files.exists(remoteFile)); + assertEquals(Files.readString(remoteFile), Files.readString(sourcePath)); + } +} diff --git a/src/test/java/edu/unc/lib/boxc/migration/cdm/test/TestSshServer.java b/src/test/java/edu/unc/lib/boxc/migration/cdm/test/TestSshServer.java index 1bb655e6..034c0835 100644 --- a/src/test/java/edu/unc/lib/boxc/migration/cdm/test/TestSshServer.java +++ b/src/test/java/edu/unc/lib/boxc/migration/cdm/test/TestSshServer.java @@ -1,10 +1,20 @@ package edu.unc.lib.boxc.migration.cdm.test; +import org.apache.sshd.common.util.io.resource.PathResource; +import org.apache.sshd.common.util.security.SecurityUtils; import org.apache.sshd.scp.server.ScpCommandFactory; import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.command.Command; +import org.apache.sshd.server.command.CommandFactory; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.shell.ProcessShellFactory; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; /** * SSH server used for testing @@ -14,16 +24,35 @@ public class TestSshServer { public static final String PASSWORD = "supersecret"; private SshServer sshServer; + private Path clientKeyPath; public TestSshServer() throws IOException { sshServer = SshServer.setUpDefaultServer(); sshServer.setHost("127.0.0.1"); sshServer.setPort(42222); sshServer.setKeyPairProvider(new SimpleGeneratorHostKeyProvider()); - sshServer.setCommandFactory(new ScpCommandFactory()); + sshServer.setCommandFactory(new DelegatingCommandFactory()); sshServer.setPasswordAuthenticator((username, password, serverSession) -> { return username != null && PASSWORD.equals(password); }); + sshServer.setPublickeyAuthenticator((username, key, session) -> { + try { + KeyPair clientKeyPair = SecurityUtils.loadKeyPairIdentities( + null, + new PathResource(clientKeyPath), + Files.newInputStream(clientKeyPath), + null + ).iterator().next(); + + return key.equals(clientKeyPair.getPublic()); + } catch (IOException | GeneralSecurityException e) { + throw new RuntimeException(e); + } + }); + } + + public void setClientKeyPath(Path clientKeyPath) { + this.clientKeyPath = clientKeyPath; } public void startServer() throws IOException { @@ -33,4 +62,24 @@ public void startServer() throws IOException { public void stopServer() throws IOException { sshServer.stop(); } + + public class DelegatingCommandFactory implements CommandFactory { + + private final CommandFactory scpCommandFactory; + private final CommandFactory shellCommandFactory; + + public DelegatingCommandFactory() { + this.scpCommandFactory = new ScpCommandFactory(); + this.shellCommandFactory = (channel, command) -> new ProcessShellFactory(command, command.split(" ")).createShell(channel); + } + + @Override + public Command createCommand(ChannelSession channel, String command) throws IOException { + if (command.startsWith("scp")) { + return scpCommandFactory.createCommand(channel, command); + } else { + return shellCommandFactory.createCommand(channel, command); + } + } + } } diff --git a/src/test/resources/test_client2_key b/src/test/resources/test_client2_key new file mode 100644 index 00000000..dac47c34 --- /dev/null +++ b/src/test/resources/test_client2_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAw6koDLR6l8VIwJxlW9gDuElP7b9Fpp3ea5EW2mrjNnFvddDV +UfIY4Z75hjKgZC2CLc+Q1HatuUCn7W0t5G+IDfJP9iCIXxPKe9XONs9ltsEtLrXA +em0c8//h2xGlfCzjy1w1bnJOUxt27g8yHF/Kivfm/Bb4O+ZPg4sa5ycxTWINOa/o +DFnh3qM44csd8WnB5DvBE/0CSJNvYHS/9tWvd3CkcACD5F0sv8MBPNWaZu1aiKn8 +N4sV88+VB3cCvRYBYS427YHa1VWVEiKCirh1FmP0WB0IM8gJA0G9NcBx/AyBnjDx +8BpeqptYd3+xvDWpWDiHrqQvgDqK/Z2r9/fdDwIDAQABAoIBAAGLIxK+lbz+xG0o +jrAjyxu4EFZazOzcj/WfRmPgtfwm9u8h/5zO4R1eiK9jVNqVdVJxMb5LVWGBvhFk +bBlCO/uILtNNPGYrnMxPSnoqNq4zZezZaDRPCgxut1RoFjsdi4p+JrnBUxbav5xv +KEELDGj+Pac9cyXd0kDCZyrtpzjqCpCb3cwRmV0h+FFlYP0ypNquC0Sx2+aTi+jj +c9e/zizeTGIvlFlL4Ld9Idrl0LmbxFhoVUhofx2S7BjS7AnoNEyP3NbdhD7TmETS +J5ftYActcgbZGhsI4y4aVPurs1FnDyjdVHFF1Kebb6RhDL2ZbA0xM4NaX+lJ1mFQ +uRFzpNECgYEA749GC6rJgASr+s/sHm6ojUc9wodWa3umqnznp1eenojELJFhlRuY +gilSg3E+NqiD4m30v1hIbFgFJRlDoi1pKkTJrKRKJF2hiWDVhNRVpqLQX4XzFFAh +nMic1gaC7yTLZ3BMIyDIqAumsREinJAkmO3oOBPYa8Vyzy5XqjryO5kCgYEA0Rak +Fte2S7OsP96GLEkAj5ArFHdLrduO8Tz/HfgGFQ4hM8rVItaFR1+NCmg5/YezRYLa +CINE4Qg2i8kN0A7P5Rdcazmw+SUXCAVE3P20h91Opv8o/D1HPq3Dj4oDFllLt63m +JCzFH2ED4Wz0DG7VUnaRUsvfcLV1NI3cWM0ChucCgYEA2yjD+CCCv2+GtqpJZX3v +DXDDe5e85A/3ZblnZJywLHEKp4tJLtRULaAIMOAE/pQkM55MblEh0Jyl+E6opVHO +CDFuH1cdzS8rfTbtn9txkEUbRIiS3V6BoaGWeebzGj0bKMJ/pRN0/ufJ0+vNMlZc +ZNwhukvNjqrQYeIPqVjMCuECgYEAxG0AunTsTTH+IAJtCi2K2VQXJKLt9ebN4tPq +17yp+h3ME1v530Co9ORPG/fOgt95C1RigJzRmJaep7O5xjwkEpRfvlv0ZhryCBbr +GrE9aGX//eK4Hj9zLu8PSUnenKBHAcfc7R8iENiTTLOOkh+NSGsbGy1sO/Y8paKf +3sy6EwECgYEAzVOKd9NKVvr49XoDd9c8JTN0TPGi50WaLUEMFGn1wIsqwiSjBkAl +R8ehPnmac795fJUscQIoMU3UJKNo+JQZCsgrlw9ItclS7Vi9p9NL3aJuaoH8rpc2 +bYJg5yPLafJJE3bbKGJekrhKvLgJ3BwQLnJSpVf1v3iUnUUV5d+Dfxw= +-----END RSA PRIVATE KEY----- diff --git a/src/test/resources/test_client2_key.pub b/src/test/resources/test_client2_key.pub new file mode 100644 index 00000000..7b66373a --- /dev/null +++ b/src/test/resources/test_client2_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDqSgMtHqXxUjAnGVb2AO4SU/tv0Wmnd5rkRbaauM2cW910NVR8hjhnvmGMqBkLYItz5DUdq25QKftbS3kb4gN8k/2IIhfE8p71c42z2W2wS0utcB6bRzz/+HbEaV8LOPLXDVuck5TG3buDzIcX8qK9+b8Fvg75k+DixrnJzFNYg05r+gMWeHeozjhyx3xacHkO8ET/QJIk29gdL/21a93cKRwAIPkXSy/wwE81Zpm7VqIqfw3ixXzz5UHdwK9FgFhLjbtgdrVVZUSIoKKuHUWY/RYHQgzyAkDQb01wHH8DIGeMPHwGl6qm1h3f7G8NalYOIeupC+AOor9nav3990P chompb_c@example.com diff --git a/src/test/resources/test_client_key b/src/test/resources/test_client_key new file mode 100644 index 00000000..b0a2ff5f --- /dev/null +++ b/src/test/resources/test_client_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0jxXS/0Jm5/V80/WOHixz5gAbnEaEBKvX3BTyOJo1gl/X3pi +LTZrq+8D89NiC3HS83BLzU9l1siFurEMDNGFAwaKt592dbQV7UKytlGARvdLhfzD +UsauA2NjVYCFciatWKUdFp95o5HUQXZU/5hu7Opwoh9X22fWOPBP3y0INxh7sQ6g +iAAfQQIvqhE6jOWmbba4gQhkAA5Ew7Lqo8pFqDFvDPGIzmuUPfxHkCWEkZj0X8I2 +t+/BBxelGlbsNc40m0T/xVRtbVw9oOKOqJ2m4GaxJY004CF5EgA3bRKLoR0GA1M7 +Gis7sczNlHlY7Bi0IyJstTStQ8WFIhUmgpN+rQIDAQABAoIBAQCMFRDF3TDNtU7F +9Oh0cFdqT4naDRqkow6ftSTesZ3RIDryz4UjQyOzSGuFx8+IeKtq8eRQRRtCZhJV +NyskT2clgPJlL6eq/feuQ8b4nI2wu45jFOzA9wlz0IGbsys3yHWwnzQmotRWHREl +HUe9l0AyNHQgUmokE6g2AEn4FvVmX4Cub4HUYkbbQsRoFJmkTqNVHLFZUr501LTZ +1FMD5ycroTRjMejv0SP0X4KyscWRE9La5ZBJhqRJ/xSDr1kDEbgqHqaHWTVAe93E +jhbWnhxtb5YZkvlKBilMAlq9xRUH6t7EuQtOnL46Wg2klHBWMO3NI3VyeV9y/5sk +blmJJJ+BAoGBAO02GTNhUHIPHkqhpJB1aajjabHzwDIkyVlXDoWEAAANtp07CZls +KXuYmh2f1zyROyrKEmQwYQJVEJzuSd5SZM4wOgSiNjpeN7SrwLMP4Hl65qYTFU8D +GRryKK5ViOo91bfCRlI7X9/pHi60gG0WAiQ/FzZAsM6xcfDXlOdE6y+VAoGBAOLj +Qv8OOkS9tgdwuQGlwHcs1JLc5KD2qdBNPHYNsp3aBrJfIT7MNw2For/o6phexD1K ++UWCMaObePuKOcTIE6wujLllY2VZSLRiRJEdyWx0jgWgARXJ5qaqOG/V/zDM03jX +USXjaeWUVz9HU4Z+rw4ntk9M6mFNIc54LBMcjqy5AoGAcTX2hQ0WxbjtbhUDY2aG +DPKsSR/aRJRF/HuOFK7RCTWCSDwa8rwqeDB2rVjR8rglkuBqMqLcSa6rhRo86vxL +3BQsPHXmh6jb0UB/cXZMXe4IEo7dBp6l5rRYMgvkklb8nvz7btejhEvP+d5vxKBS +WVY7D8uEFsGdbaKWEGJWFJkCgYEAwTIdMUvvJ+uW3Z0JnDKEwbYFyIyZjcqb7Mhn +zGp7hthJTYedqniABF6fp+RwJpDDbyGxQpPLKvJPQXmBGWP5BGua3p9L1NgDBb6L +fjor8tMIwBdv9Rq38YEOj4RcSTSQedx2t9l+qcP3yuU0ZVjoPRhpr/rhtTrVp5dC +iH+PqdkCgYAJulDHuEB5d9GDt++EOjT21GnHpUwg7jGydWxH7RJCJFVSIPxpL4xV +oSWGrxgefetZQXfVADrpAw01OQsh4agQpOYeYniseRfUYiuJt/BEnC3OyUvG40vL +u57YjrdDNq2IZS0aM211eG3oZfQKeo6lnCSDcEqAntjrDP4ZLfSW4A== +-----END RSA PRIVATE KEY----- diff --git a/src/test/resources/test_client_key.pub b/src/test/resources/test_client_key.pub new file mode 100644 index 00000000..bcff85a4 --- /dev/null +++ b/src/test/resources/test_client_key.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSPFdL/Qmbn9XzT9Y4eLHPmABucRoQEq9fcFPI4mjWCX9femItNmur7wPz02ILcdLzcEvNT2XWyIW6sQwM0YUDBoq3n3Z1tBXtQrK2UYBG90uF/MNSxq4DY2NVgIVyJq1YpR0Wn3mjkdRBdlT/mG7s6nCiH1fbZ9Y48E/fLQg3GHuxDqCIAB9BAi+qETqM5aZttriBCGQADkTDsuqjykWoMW8M8YjOa5Q9/EeQJYSRmPRfwja378EHF6UaVuw1zjSbRP/FVG1tXD2g4o6onabgZrEljTTgIXkSADdtEouhHQYDUzsaKzuxzM2UeVjsGLQjImy1NK1DxYUiFSaCk36t chompb_c@example.com