/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sshd.client.scp; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.PosixFilePermission; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.EnumSet; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.jcraft.jsch.ChannelExec; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.common.Factory; import org.apache.sshd.common.channel.Channel; import org.apache.sshd.common.file.FileSystemFactory; import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; import org.apache.sshd.common.random.Random; import org.apache.sshd.common.scp.ScpException; import org.apache.sshd.common.scp.ScpFileOpener; import org.apache.sshd.common.scp.ScpHelper; import org.apache.sshd.common.scp.ScpTransferEventListener; import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener; import org.apache.sshd.common.session.Session; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.OsUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.server.Command; import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.scp.ScpCommand; import org.apache.sshd.server.scp.ScpCommandFactory; import org.apache.sshd.util.test.BaseTestSupport; import org.apache.sshd.util.test.JSchLogger; import org.apache.sshd.util.test.SimpleUserInfo; import org.apache.sshd.util.test.Utils; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; import ch.ethz.ssh2.Connection; import ch.ethz.ssh2.ConnectionInfo; import ch.ethz.ssh2.SCPClient; /** * Test for SCP support. * * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class ScpTest extends BaseTestSupport { private static final ScpTransferEventListener DEBUG_LISTENER = new ScpTransferEventListener() { @Override public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) { logEvent("starFolderEvent", op, file, false, -1L, perms, null); } @Override public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) { logEvent("startFileEvent", op, file, true, length, perms, null); } @Override public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) { logEvent("endFolderEvent", op, file, false, -1L, perms, thrown); } @Override public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) { logEvent("endFileEvent", op, file, true, length, perms, thrown); } private void logEvent(String type, FileOperation op, Path path, boolean isFile, long length, Collection<PosixFilePermission> perms, Throwable t) { if (!OUTPUT_DEBUG_MESSAGES) { return; // just in case } StringBuilder sb = new StringBuilder(Byte.MAX_VALUE); sb.append('\t').append(type) .append('[').append(op).append(']') .append(' ').append(isFile ? "File" : "Directory").append('=').append(path) .append(' ').append("length=").append(length) .append(' ').append("perms=").append(perms); if (t != null) { sb.append(' ').append("ERROR=").append(t.getClass().getSimpleName()).append(": ").append(t.getMessage()); } outputDebugMessage(sb.toString()); } }; private static SshServer sshd; private static int port; private static SshClient client; private final FileSystemFactory fileSystemFactory; public ScpTest() throws IOException { Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); fileSystemFactory = new VirtualFileSystemFactory(parentPath); } @BeforeClass public static void setupClientAndServer() throws Exception { JSchLogger.init(); sshd = Utils.setupTestServer(ScpTest.class); sshd.setCommandFactory(new ScpCommandFactory()); sshd.start(); port = sshd.getPort(); client = Utils.setupTestClient(ScpTest.class); client.start(); } @AfterClass public static void tearDownClientAndServer() throws Exception { if (sshd != null) { try { sshd.stop(true); } finally { sshd = null; } } if (client != null) { try { client.stop(); } finally { client = null; } } } @Before public void setUp() throws Exception { sshd.setFileSystemFactory(fileSystemFactory); } @Test public void testNormalizedScpRemotePaths() throws Exception { Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local")); Path localFile = localDir.resolve("file.txt"); byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); Path remoteFile = remoteDir.resolve(localFile.getFileName().toString()); String localPath = localFile.toString(); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile); String[] remoteComps = GenericUtils.split(remotePath, '/'); Factory<? extends Random> factory = client.getRandomFactory(); Random rnd = factory.create(); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); StringBuilder sb = new StringBuilder(remotePath.length() + Long.SIZE); for (int i = 0; i < Math.max(Long.SIZE, remoteComps.length); i++) { if (sb.length() > 0) { sb.setLength(0); // start again } sb.append(remoteComps[0]); for (int j = 1; j < remoteComps.length; j++) { String name = remoteComps[j]; slashify(sb, rnd); sb.append(name); } slashify(sb, rnd); String path = sb.toString(); scp.upload(localPath, path); assertTrue("Remote file not ready for " + path, waitForFile(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L))); byte[] actual = Files.readAllBytes(remoteFile); assertArrayEquals("Mismatched uploaded data for " + path, data, actual); Files.delete(remoteFile); assertFalse("Remote file (" + remoteFile + ") not deleted for " + path, Files.exists(remoteFile)); } } } private static int slashify(StringBuilder sb, Random rnd) { int slashes = 1 /* at least one slash */ + rnd.random(Byte.SIZE); for (int k = 0; k < slashes; k++) { sb.append('/'); } return slashes; } @Test public void testUploadAbsoluteDriveLetter() throws Exception { Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local")); Path localFile = localDir.resolve("file-1.txt"); byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); Path remoteFile = remoteDir.resolve(localFile.getFileName().toString()); String localPath = localFile.toString(); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); scp.upload(localPath, remotePath); assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L)); Path secondRemote = remoteDir.resolve("file-2.txt"); String secondPath = Utils.resolveRelativeRemotePath(parentPath, secondRemote); scp.upload(localPath, secondPath); assertFileLength(secondRemote, data.length, TimeUnit.SECONDS.toMillis(5L)); Path pathRemote = remoteDir.resolve("file-path.txt"); String pathPath = Utils.resolveRelativeRemotePath(parentPath, pathRemote); scp.upload(localFile, pathPath); assertFileLength(pathRemote, data.length, TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpUploadOverwrite() throws Exception { try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL; Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local")); Path localFile = localDir.resolve("file.txt"); Utils.writeFile(localFile, data); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); Path remoteFile = remoteDir.resolve(localFile.getFileName()); Utils.writeFile(remoteFile, data + data); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile); scp.upload(localFile.toString(), remotePath); assertFileLength(remoteFile, data.length(), TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpUploadZeroLengthFile() throws Exception { Path targetPath = detectTargetFolder(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local")); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); Path zeroLocal = localDir.resolve("zero.txt"); try (FileChannel fch = FileChannel.open(zeroLocal, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { if (fch.size() > 0L) { fch.truncate(0L); } } assertEquals("Non-zero size for local file=" + zeroLocal, 0L, Files.size(zeroLocal)); Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName()); if (Files.exists(zeroRemote)) { Files.delete(zeroRemote); } try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote); scp.upload(zeroLocal.toString(), remotePath); assertFileLength(zeroRemote, 0L, TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpDownloadZeroLengthFile() throws Exception { Path targetPath = detectTargetFolder(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local")); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); Path zeroLocal = localDir.resolve(getCurrentTestName()); if (Files.exists(zeroLocal)) { Files.delete(zeroLocal); } Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName()); try (FileChannel fch = FileChannel.open(zeroRemote, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { if (fch.size() > 0L) { fch.truncate(0L); } } assertEquals("Non-zero size for remote file=" + zeroRemote, 0L, Files.size(zeroRemote)); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote); scp.download(remotePath, zeroLocal.toString()); assertFileLength(zeroLocal, 0L, TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpNativeOnSingleFile() throws Exception { String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL; Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local")); Path localOutFile = localDir.resolve("file-1.txt"); Path remoteDir = scpRoot.resolve("remote"); Path remoteOutFile = remoteDir.resolve(localOutFile.getFileName()); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); Utils.writeFile(localOutFile, data); assertFalse("Remote folder already exists: " + remoteDir, Files.exists(remoteDir)); String localOutPath = localOutFile.toString(); String remoteOutPath = Utils.resolveRelativeRemotePath(parentPath, remoteOutFile); outputDebugMessage("Expect upload failure %s => %s", localOutPath, remoteOutPath); try { scp.upload(localOutPath, remoteOutPath); fail("Expected IOException for 1st time " + remoteOutPath); } catch (IOException e) { // ok } assertHierarchyTargetFolderExists(remoteDir); outputDebugMessage("Expect upload success %s => %s", localOutPath, remoteOutPath); scp.upload(localOutPath, remoteOutPath); assertFileLength(remoteOutFile, data.length(), TimeUnit.SECONDS.toMillis(5L)); Path secondLocal = localDir.resolve(localOutFile.getFileName()); String downloadTarget = Utils.resolveRelativeRemotePath(parentPath, secondLocal); outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget); scp.download(remoteOutPath, downloadTarget); assertFileLength(secondLocal, data.length(), TimeUnit.SECONDS.toMillis(5L)); Path localPath = localDir.resolve("file-path.txt"); downloadTarget = Utils.resolveRelativeRemotePath(parentPath, localPath); outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget); scp.download(remoteOutPath, downloadTarget); assertFileLength(localPath, data.length(), TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpNativeOnMultipleFiles() throws Exception { try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local")); Path local1 = localDir.resolve("file-1.txt"); byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL); Path local2 = localDir.resolve("file-2.txt"); Files.write(local2, data); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); Path remote1 = remoteDir.resolve(local1.getFileName()); String remote1Path = Utils.resolveRelativeRemotePath(parentPath, remote1); String[] locals = {local1.toString(), local2.toString()}; try { scp.upload(locals, remote1Path); fail("Unexpected upload success to missing remote file: " + remote1Path); } catch (IOException e) { // Ok } Files.write(remote1, data); try { scp.upload(locals, remote1Path); fail("Unexpected upload success to existing remote file: " + remote1Path); } catch (IOException e) { // Ok } Path remoteSubDir = assertHierarchyTargetFolderExists(remoteDir.resolve("dir")); scp.upload(locals, Utils.resolveRelativeRemotePath(parentPath, remoteSubDir)); Path remoteSub1 = remoteSubDir.resolve(local1.getFileName()); assertFileLength(remoteSub1, data.length, TimeUnit.SECONDS.toMillis(5L)); Path remoteSub2 = remoteSubDir.resolve(local2.getFileName()); assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(5L)); String[] remotes = { Utils.resolveRelativeRemotePath(parentPath, remoteSub1), Utils.resolveRelativeRemotePath(parentPath, remoteSub2), }; try { scp.download(remotes, Utils.resolveRelativeRemotePath(parentPath, local1)); fail("Unexpected download success to existing local file: " + local1); } catch (IOException e) { // Ok } Path localSubDir = localDir.resolve("dir"); try { scp.download(remotes, localSubDir); fail("Unexpected download success to non-existing folder: " + localSubDir); } catch (IOException e) { // Ok } assertHierarchyTargetFolderExists(localSubDir); scp.download(remotes, localSubDir); assertFileLength(localSubDir.resolve(remoteSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(5L)); assertFileLength(localSubDir.resolve(remoteSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpNativeOnRecursiveDirs() throws Exception { try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = scpRoot.resolve("local"); Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir")); Path localSub1 = localSubDir.resolve("file-1.txt"); byte[] data = Utils.writeFile(localSub1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL); Path localSub2 = localSubDir.resolve("file-2.txt"); Files.write(localSub2, data); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); scp.upload(localSubDir, Utils.resolveRelativeRemotePath(parentPath, remoteDir), ScpClient.Option.Recursive); Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName()); assertFileLength(remoteSubDir.resolve(localSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(5L)); assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(5L)); Utils.deleteRecursive(localSubDir); scp.download(Utils.resolveRelativeRemotePath(parentPath, remoteSubDir), localDir, ScpClient.Option.Recursive); assertFileLength(localSub1, data.length, TimeUnit.SECONDS.toMillis(5L)); assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpNativeOnDirWithPattern() throws Exception { try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local")); Path local1 = localDir.resolve("file-1.txt"); byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL); Path local2 = localDir.resolve("file-2.txt"); Files.write(local2, data); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir); scp.upload(localDir.toString() + File.separator + "*", remotePath); assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(5L)); assertFileLength(remoteDir.resolve(local2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(5L)); Files.delete(local1); Files.delete(local2); scp.download(remotePath + "/*", localDir); assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(5L)); assertFileLength(local2, data.length, TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpNativeOnMixedDirAndFiles() throws Exception { try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = scpRoot.resolve("local"); Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir")); Path local1 = localDir.resolve("file-1.txt"); byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL); Path localSub2 = localSubDir.resolve("file-2.txt"); Files.write(localSub2, data); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir); scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive); assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(5L)); Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName()); assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(5L)); Files.delete(local1); Utils.deleteRecursive(localSubDir); scp.download(remotePath + "/*", localDir); assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(5L)); assertFalse("Unexpected recursive local file: " + localSub2, Files.exists(localSub2)); Files.delete(local1); scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive); assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(5L)); assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(5L)); } } @Test public void testScpNativePreserveAttributes() throws Exception { try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path localDir = scpRoot.resolve("local"); Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir")); // convert everything to seconds since this is the SCP timestamps granularity final long lastModMillis = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1); final long lastModSecs = TimeUnit.MILLISECONDS.toSeconds(lastModMillis); Path local1 = localDir.resolve("file-1.txt"); byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL); File lclFile1 = local1.toFile(); boolean lcl1ModSet = lclFile1.setLastModified(lastModMillis); lclFile1.setExecutable(true, true); lclFile1.setWritable(false, false); Path localSub2 = localSubDir.resolve("file-2.txt"); Files.write(localSub2, data); File lclSubFile2 = localSub2.toFile(); boolean lclSub2ModSet = lclSubFile2.setLastModified(lastModMillis); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir); scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes); Path remote1 = remoteDir.resolve(local1.getFileName()); assertFileLength(remote1, data.length, TimeUnit.SECONDS.toMillis(5L)); File remFile1 = remote1.toFile(); assertLastModifiedTimeEquals(remFile1, lcl1ModSet, lastModSecs); Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName()); Path remoteSub2 = remoteSubDir.resolve(localSub2.getFileName()); assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(5L)); File remSubFile2 = remoteSub2.toFile(); assertLastModifiedTimeEquals(remSubFile2, lclSub2ModSet, lastModSecs); Utils.deleteRecursive(localDir); assertHierarchyTargetFolderExists(localDir); scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes); assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(5L)); assertLastModifiedTimeEquals(lclFile1, lcl1ModSet, lastModSecs); assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(5L)); assertLastModifiedTimeEquals(lclSubFile2, lclSub2ModSet, lastModSecs); } } @Test public void testStreamsUploadAndDownload() throws Exception { try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = createScpClient(session); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); Path remoteFile = remoteDir.resolve("file.txt"); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile); byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8); outputDebugMessage("Upload data to %s", remotePath); scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null); assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L)); byte[] uploaded = Files.readAllBytes(remoteFile); assertArrayEquals("Mismatched uploaded data", data, uploaded); outputDebugMessage("Download data from %s", remotePath); byte[] downloaded = scp.downloadBytes(remotePath); assertArrayEquals("Mismatched downloaded data", uploaded, downloaded); } } @Test // see SSHD-649 public void testScpFileOpener() throws Exception { class TrackingFileOpener extends DefaultScpFileOpener { private final AtomicInteger readCount = new AtomicInteger(0); private final AtomicInteger writeCount = new AtomicInteger(0); TrackingFileOpener() { super(); } public AtomicInteger getReadCount() { return readCount; } public AtomicInteger getWriteCount() { return writeCount; } @Override public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException { int count = readCount.incrementAndGet(); outputDebugMessage("openRead(%s)[%s] count=%d", session, file, count); return super.openRead(session, file, options); } @Override public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException { int count = writeCount.incrementAndGet(); outputDebugMessage("openWrite(%s)[%s] count=%d", session, file, count); return super.openWrite(session, file, options); } } ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory(); ScpFileOpener opener = factory.getScpFileOpener(); TrackingFileOpener serverOpener = new TrackingFileOpener(); factory.setScpFileOpener(serverOpener); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); TrackingFileOpener clientOpener = new TrackingFileOpener(); ScpClient scp = session.createScpClient(clientOpener); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot); Path localFile = remoteDir.resolve("data.txt"); byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8); Files.write(localFile, data); Path remoteFile = remoteDir.resolve("upload.txt"); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile); outputDebugMessage("Upload data to %s", remotePath); scp.upload(localFile, remotePath); assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L)); AtomicInteger serverRead = serverOpener.getReadCount(); assertEquals("Mismatched server upload open read count", 0, serverRead.get()); AtomicInteger serverWrite = serverOpener.getWriteCount(); assertEquals("Mismatched server upload write count", 1, serverWrite.getAndSet(0)); AtomicInteger clientRead = clientOpener.getReadCount(); assertEquals("Mismatched client upload read count", 1, clientRead.getAndSet(0)); AtomicInteger clientWrite = clientOpener.getWriteCount(); assertEquals("Mismatched client upload write count", 0, clientWrite.get()); Files.delete(localFile); scp.download(remotePath, localFile); assertFileLength(localFile, data.length, TimeUnit.SECONDS.toMillis(5L)); assertEquals("Mismatched server download open read count", 1, serverRead.getAndSet(0)); assertEquals("Mismatched server download write count", 0, serverWrite.get()); assertEquals("Mismatched client download read count", 0, clientRead.get()); assertEquals("Mismatched client download write count", 1, clientWrite.getAndSet(0)); } finally { factory.setScpFileOpener(opener); } } @Test // see SSHD-628 public void testScpExitStatusPropagation() throws Exception { final int testExitValue = 7365; class InternalScpCommand extends ScpCommand implements ExitCallback { private ExitCallback delegate; InternalScpCommand(String command, ExecutorService executorService, boolean shutdownOnExit, int sendSize, int receiveSize, ScpFileOpener opener, ScpTransferEventListener eventListener) { super(command, executorService, shutdownOnExit, sendSize, receiveSize, opener, eventListener); } @Override protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException { outputDebugMessage("writeCommandResponseMessage(%s) status=%d", command, exitValue); super.writeCommandResponseMessage(command, testExitValue, exitMessage); } @Override public void setExitCallback(ExitCallback callback) { delegate = callback; super.setExitCallback(this); } @Override public void onExit(int exitValue) { onExit(exitValue, Integer.toString(exitValue)); } @Override public void onExit(int exitValue, String exitMessage) { outputDebugMessage("onExit(%s) status=%d", this, exitValue); if (exitValue == ScpHelper.OK) { delegate.onExit(testExitValue, exitMessage); } else { delegate.onExit(exitValue, exitMessage); } } } ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory(); sshd.setCommandFactory(new ScpCommandFactory() { @Override public Command createCommand(String command) { ValidateUtils.checkTrue(command.startsWith(ScpHelper.SCP_COMMAND_PREFIX), "Bad SCP command: %s", command); return new InternalScpCommand(command, getExecutorService(), isShutdownOnExit(), getSendBufferSize(), getReceiveBufferSize(), DefaultScpFileOpener.INSTANCE, ScpTransferEventListener.EMPTY); } }); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); ScpClient scp = session.createScpClient(); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); Path remoteFile = remoteDir.resolve("file.txt"); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile); byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8); outputDebugMessage("Upload data to %s", remotePath); try { scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null); outputDebugMessage("Upload success to %s", remotePath); } catch (ScpException e) { Integer exitCode = e.getExitStatus(); assertNotNull("No upload exit status", exitCode); assertEquals("Mismatched upload exit status", testExitValue, exitCode.intValue()); } if (Files.deleteIfExists(remoteFile)) { outputDebugMessage("Deleted remote file %s", remoteFile); } try (OutputStream out = Files.newOutputStream(remoteFile)) { out.write(data); } try { byte[] downloaded = scp.downloadBytes(remotePath); outputDebugMessage("Download success to %s: %s", remotePath, new String(downloaded, StandardCharsets.UTF_8)); } catch (ScpException e) { Integer exitCode = e.getExitStatus(); assertNotNull("No download exit status", exitCode); assertEquals("Mismatched download exit status", testExitValue, exitCode.intValue()); } } finally { sshd.setCommandFactory(factory); } } // see http://stackoverflow.com/questions/2717936/file-createnewfile-creates-files-with-last-modified-time-before-actual-creatio // See https://msdn.microsoft.com/en-us/library/ms724290(VS.85).aspx private static void assertLastModifiedTimeEquals(File file, boolean modSuccess, long expectedSeconds) { long expectedMillis = TimeUnit.SECONDS.toMillis(expectedSeconds); long actualMillis = file.lastModified(); long actualSeconds = TimeUnit.MILLISECONDS.toSeconds(actualMillis); // if failed to set the local file time, don't expect it to be the same if (!modSuccess) { System.err.append("Failed to set last modified time of ").append(file.getAbsolutePath()) .append(" to ").append(String.valueOf(expectedMillis)) .append(" - ").println(new Date(expectedMillis)); System.err.append("\t\t").append("Current value: ").append(String.valueOf(actualMillis)) .append(" - ").println(new Date(actualMillis)); return; } if (OsUtils.isWin32()) { // The NTFS file system delays updates to the last access time for a file by up to 1 hour after the last access if (expectedSeconds != actualSeconds) { System.err.append("Mismatched last modified time for ").append(file.getAbsolutePath()) .append(" - expected=").append(String.valueOf(expectedSeconds)) .append('[').append(new Date(expectedMillis).toString()).append(']') .append(", actual=").append(String.valueOf(actualSeconds)) .append('[').append(new Date(actualMillis).toString()).append(']') .println(); } } else { assertEquals("Mismatched last modified time for " + file.getAbsolutePath(), expectedSeconds, actualSeconds); } } @Test public void testJschScp() throws Exception { com.jcraft.jsch.Session session = getJschSession(); try { String data = getCurrentTestName() + "\n"; String unixDir = "target/scp"; String fileName = getCurrentTestName() + ".txt"; String unixPath = unixDir + "/" + fileName; File root = new File(unixDir); File target = new File(unixPath); Utils.deleteRecursive(root); root.mkdirs(); assertTrue(root.exists()); target.delete(); assertFalse(target.exists()); sendFile(session, unixPath, fileName, data); assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(5L)); target.delete(); assertFalse(target.exists()); sendFile(session, unixDir, fileName, data); assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(5L)); sendFileError(session, "target", ScpHelper.SCP_COMMAND_PREFIX, data); readFileError(session, unixDir); assertEquals("Mismatched file data", data, readFile(session, unixPath, target.length())); assertEquals("Mismatched dir data", data, readDir(session, unixDir, fileName, target.length())); target.delete(); root.delete(); sendDir(session, "target", ScpHelper.SCP_COMMAND_PREFIX, fileName, data); assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(5L)); } finally { session.disconnect(); } } protected com.jcraft.jsch.Session getJschSession() throws JSchException { JSch sch = new JSch(); com.jcraft.jsch.Session session = sch.getSession(getCurrentTestName(), TEST_LOCALHOST, port); session.setUserInfo(new SimpleUserInfo(getCurrentTestName())); session.connect(); return session; } @Test public void testWithGanymede() throws Exception { Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName()); Utils.deleteRecursive(scpRoot); byte[] expected = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8); Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote")); String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir); String fileName = "file.txt"; Path remoteFile = remoteDir.resolve(fileName); String mode = ScpHelper.getOctalPermissions(EnumSet.of( PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE )); ch.ethz.ssh2.log.Logger.enabled = true; final Connection conn = new Connection(TEST_LOCALHOST, port); try { ConnectionInfo info = conn.connect(null, (int) TimeUnit.SECONDS.toMillis(5L), (int) TimeUnit.SECONDS.toMillis(11L)); outputDebugMessage("Connected: kex=%s, key-type=%s, c2senc=%s, s2cenc=%s, c2mac=%s, s2cmac=%s", info.keyExchangeAlgorithm, info.serverHostKeyAlgorithm, info.clientToServerCryptoAlgorithm, info.serverToClientCryptoAlgorithm, info.clientToServerMACAlgorithm, info.serverToClientMACAlgorithm); assertTrue("Failed to authenticate", conn.authenticateWithPassword(getCurrentTestName(), getCurrentTestName())); SCPClient scpClient = new SCPClient(conn); try (OutputStream output = scpClient.put(fileName, expected.length, remotePath, mode)) { output.write(expected); } assertTrue("Remote file not created: " + remoteFile, Files.exists(remoteFile)); byte[] remoteData = Files.readAllBytes(remoteFile); assertArrayEquals("Mismatched remote put data", expected, remoteData); Arrays.fill(remoteData, (byte) 0); // make sure we start with a clean slate try (InputStream input = scpClient.get(remotePath + "/" + fileName)) { int readLen = input.read(remoteData); assertEquals("Mismatched remote get data size", expected.length, readLen); // make sure we reached EOF assertEquals("Unexpected extra data after read expected size", -1, input.read()); } assertArrayEquals("Mismatched remote get data", expected, remoteData); } finally { conn.close(); } } protected String readFile(com.jcraft.jsch.Session session, String path, long expectedSize) throws Exception { ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC); c.setCommand("scp -f " + path); c.connect(); int namePos = path.lastIndexOf('/'); String fileName = (namePos >= 0) ? path.substring(namePos + 1) : path; try (OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream()) { os.write(0); os.flush(); String header = readLine(is); assertEquals("Mismatched header for " + path, "C0644 " + expectedSize + " " + fileName, header); int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6))); os.write(0); os.flush(); byte[] buffer = new byte[length]; length = is.read(buffer, 0, buffer.length); assertEquals("Mismatched read data length for " + path, length, buffer.length); assertAckReceived(is, "Read data of " + path); os.write(0); os.flush(); return new String(buffer, StandardCharsets.UTF_8); } finally { c.disconnect(); } } protected String readDir(com.jcraft.jsch.Session session, String path, String fileName, long expectedSize) throws Exception { ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC); c.setCommand("scp -r -f " + path); c.connect(); try (OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream()) { os.write(0); os.flush(); String header = readLine(is); assertTrue("Bad header prefix for " + path + ": " + header, header.startsWith("D0755 0 ")); os.write(0); os.flush(); header = readLine(is); assertEquals("Mismatched dir header for " + path, "C0644 " + expectedSize + " " + fileName, header); int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6))); os.write(0); os.flush(); byte[] buffer = new byte[length]; length = is.read(buffer, 0, buffer.length); assertEquals("Mismatched read buffer size for " + path, length, buffer.length); assertAckReceived(is, "Read date of " + path); os.write(0); os.flush(); header = readLine(is); assertEquals("Mismatched end value for " + path, "E", header); os.write(0); os.flush(); return new String(buffer, StandardCharsets.UTF_8); } finally { c.disconnect(); } } protected void readFileError(com.jcraft.jsch.Session session, String path) throws Exception { ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC); String command = "scp -f " + path; c.setCommand(command); c.connect(); try (OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream()) { os.write(0); os.flush(); assertEquals("Mismatched response for command: " + command, 2, is.read()); } finally { c.disconnect(); } } protected void sendFile(com.jcraft.jsch.Session session, String path, String name, String data) throws Exception { ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC); String command = "scp -t " + path; c.setCommand(command); c.connect(); try (OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream()) { assertAckReceived(is, command); assertAckReceived(os, is, "C7777 " + data.length() + " " + name); os.write(data.getBytes(StandardCharsets.UTF_8)); os.flush(); assertAckReceived(is, "Sent data (length=" + data.length() + ") for " + path + "[" + name + "]"); os.write(0); os.flush(); Thread.sleep(100); } finally { c.disconnect(); } } protected void assertAckReceived(OutputStream os, InputStream is, String command) throws IOException { os.write((command + "\n").getBytes(StandardCharsets.UTF_8)); os.flush(); assertAckReceived(is, command); } protected void assertAckReceived(InputStream is, String command) throws IOException { assertEquals("No ACK for command=" + command, 0, is.read()); } protected void sendFileError(com.jcraft.jsch.Session session, String path, String name, String data) throws Exception { ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC); String command = "scp -t " + path; c.setCommand(command); c.connect(); try (OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream()) { assertAckReceived(is, command); command = "C7777 " + data.length() + " " + name; os.write((command + "\n").getBytes(StandardCharsets.UTF_8)); os.flush(); assertEquals("Mismatched response for command=" + command, 2, is.read()); } finally { c.disconnect(); } } protected void sendDir(com.jcraft.jsch.Session session, String path, String dirName, String fileName, String data) throws Exception { ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC); String command = "scp -t -r " + path; c.setCommand(command); c.connect(); try (OutputStream os = c.getOutputStream(); InputStream is = c.getInputStream()) { assertAckReceived(is, command); assertAckReceived(os, is, "D0755 0 " + dirName); assertAckReceived(os, is, "C7777 " + data.length() + " " + fileName); os.write(data.getBytes(StandardCharsets.UTF_8)); os.flush(); assertAckReceived(is, "Send data of " + path); os.write(0); os.flush(); os.write("E\n".getBytes(StandardCharsets.UTF_8)); os.flush(); assertAckReceived(is, "Signal end of " + path); } finally { c.disconnect(); } } private static String readLine(InputStream in) throws IOException { try (OutputStream baos = new ByteArrayOutputStream()) { for (;;) { int c = in.read(); if (c == '\n') { return baos.toString(); } else if (c == -1) { throw new IOException("End of stream"); } else { baos.write(c); } } } } private static ScpClient createScpClient(ClientSession session) { return session.createScpClient(getScpTransferEventListener(session)); } private static ScpTransferEventListener getScpTransferEventListener(ClientSession session) { return OUTPUT_DEBUG_MESSAGES ? DEBUG_LISTENER : ScpTransferEventListener.EMPTY; } }