/* * 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.subsystem.sftp; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.attribute.AclEntry; import java.nio.file.attribute.AclEntryFlag; import java.nio.file.attribute.AclEntryPermission; import java.nio.file.attribute.AclEntryType; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.apache.sshd.client.session.ClientSession; import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes; import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle; import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.subsystem.sftp.SftpConstants; import org.apache.sshd.common.subsystem.sftp.SftpHelper; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.server.Command; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.subsystem.sftp.AbstractSftpEventListenerAdapter; import org.apache.sshd.server.subsystem.sftp.DefaultGroupPrincipal; import org.apache.sshd.server.subsystem.sftp.SftpEventListener; import org.apache.sshd.server.subsystem.sftp.SftpSubsystem; import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; import org.apache.sshd.util.test.Utils; import org.junit.Before; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; /** * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a> */ @FixMethodOrder(MethodSorters.NAME_ASCENDING) @RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests public class SftpVersionsTest extends AbstractSftpClientTestSupport { private static final List<Integer> VERSIONS = Collections.unmodifiableList( IntStream.rangeClosed(SftpSubsystem.LOWER_SFTP_IMPL, SftpSubsystem.HIGHER_SFTP_IMPL) .boxed().collect(Collectors.toList())); private final int testVersion; public SftpVersionsTest(int version) throws IOException { testVersion = version; } @Parameters(name = "version={0}") public static Collection<Object[]> parameters() { return parameterize(VERSIONS); } @Before public void setUp() throws Exception { setupServer(); } public final int getTestedVersion() { return testVersion; } @Test public void testSftpVersionSelector() 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); try (SftpClient sftp = session.createSftpClient(getTestedVersion())) { assertEquals("Mismatched negotiated version", getTestedVersion(), sftp.getVersion()); } } } @Test // see SSHD-572 public void testSftpFileTimesUpdate() throws Exception { Path targetPath = detectTargetFolder(); Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName()); Path lclFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt"); Files.write(lclFile, getClass().getName().getBytes(StandardCharsets.UTF_8)); Path parentPath = targetPath.getParent(); String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclFile); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); try (SftpClient sftp = session.createSftpClient(getTestedVersion())) { Attributes attrs = sftp.lstat(remotePath); long expectedSeconds = TimeUnit.SECONDS.convert(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1L), TimeUnit.MILLISECONDS); attrs.getFlags().clear(); attrs.modifyTime(expectedSeconds); sftp.setStat(remotePath, attrs); attrs = sftp.lstat(remotePath); long actualSeconds = attrs.getModifyTime().to(TimeUnit.SECONDS); // 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(lclFile.toString()) .append(" - expected=").append(String.valueOf(expectedSeconds)) .append('[').append(new Date(TimeUnit.SECONDS.toMillis(expectedSeconds)).toString()).append(']') .append(", actual=").append(String.valueOf(actualSeconds)) .append('[').append(new Date(TimeUnit.SECONDS.toMillis(actualSeconds)).toString()).append(']') .println(); } } } } @Test // see SSHD-573 public void testSftpFileTypeAndPermissionsUpdate() throws Exception { Path targetPath = detectTargetFolder(); Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName()); Path subFolder = Files.createDirectories(lclSftp.resolve("sub-folder")); String subFolderName = subFolder.getFileName().toString(); Path lclFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt"); String lclFileName = lclFile.getFileName().toString(); Files.write(lclFile, getClass().getName().getBytes(StandardCharsets.UTF_8)); Path parentPath = targetPath.getParent(); String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclSftp); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); try (SftpClient sftp = session.createSftpClient(getTestedVersion())) { for (DirEntry entry : sftp.readDir(remotePath)) { String fileName = entry.getFilename(); if (".".equals(fileName) || "..".equals(fileName)) { continue; } Attributes attrs = validateSftpFileTypeAndPermissions(fileName, getTestedVersion(), entry.getAttributes()); if (subFolderName.equals(fileName)) { assertEquals("Mismatched sub-folder type", SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY, attrs.getType()); assertTrue("Sub-folder not marked as directory", attrs.isDirectory()); } else if (lclFileName.equals(fileName)) { assertEquals("Mismatched sub-file type", SftpConstants.SSH_FILEXFER_TYPE_REGULAR, attrs.getType()); assertTrue("Sub-folder not marked as directory", attrs.isRegularFile()); } } } } } @Test // see SSHD-574 public void testSftpACLEncodeDecode() throws Exception { AclEntryType[] types = AclEntryType.values(); final List<AclEntry> aclExpected = new ArrayList<>(types.length); for (AclEntryType t : types) { aclExpected.add(AclEntry.newBuilder() .setType(t) .setFlags(EnumSet.allOf(AclEntryFlag.class)) .setPermissions(EnumSet.allOf(AclEntryPermission.class)) .setPrincipal(new DefaultGroupPrincipal(getCurrentTestName() + "@" + getClass().getPackage().getName())) .build()); } final AtomicInteger numInvocations = new AtomicInteger(0); SftpSubsystemFactory factory = new SftpSubsystemFactory() { @Override public Command create() { SftpSubsystem subsystem = new SftpSubsystem(getExecutorService(), isShutdownOnExit(), getUnsupportedAttributePolicy(), getFileSystemAccessor()) { @Override protected Map<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options) throws IOException { Map<String, Object> attrs = super.resolveFileAttributes(file, flags, options); if (GenericUtils.isEmpty(attrs)) { attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } @SuppressWarnings("unchecked") List<AclEntry> aclActual = (List<AclEntry>) attrs.put("acl", aclExpected); if (aclActual != null) { log.info("resolveFileAttributes(" + file + ") replaced ACL: " + aclActual); } return attrs; } @Override protected void setFileAccessControl(Path file, List<AclEntry> aclActual, LinkOption... options) throws IOException { if (aclActual != null) { assertListEquals("Mismatched ACL set for file=" + file, aclExpected, aclActual); numInvocations.incrementAndGet(); } } }; Collection<? extends SftpEventListener> listeners = getRegisteredListeners(); if (GenericUtils.size(listeners) > 0) { for (SftpEventListener l : listeners) { subsystem.addSftpEventListener(l); } } return subsystem; } }; factory.addSftpEventListener(new AbstractSftpEventListenerAdapter() { @Override public void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs) { @SuppressWarnings("unchecked") List<AclEntry> aclActual = GenericUtils.isEmpty(attrs) ? null : (List<AclEntry>) attrs.get("acl"); if (getTestedVersion() > SftpConstants.SFTP_V3) { assertListEquals("Mismatched modifying ACL for file=" + path, aclExpected, aclActual); } else { assertNull("Unexpected modifying ACL for file=" + path, aclActual); } } @Override public void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown) { @SuppressWarnings("unchecked") List<AclEntry> aclActual = GenericUtils.isEmpty(attrs) ? null : (List<AclEntry>) attrs.get("acl"); if (getTestedVersion() > SftpConstants.SFTP_V3) { assertListEquals("Mismatched modified ACL for file=" + path, aclExpected, aclActual); } else { assertNull("Unexpected modified ACL for file=" + path, aclActual); } } }); Path targetPath = detectTargetFolder(); Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName()); Files.createDirectories(lclSftp.resolve("sub-folder")); Path lclFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt"); Files.write(lclFile, getClass().getName().getBytes(StandardCharsets.UTF_8)); Path parentPath = targetPath.getParent(); String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclSftp); int numInvoked = 0; List<NamedFactory<Command>> factories = sshd.getSubsystemFactories(); sshd.setSubsystemFactories(Collections.singletonList(factory)); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); try (SftpClient sftp = session.createSftpClient(getTestedVersion())) { for (DirEntry entry : sftp.readDir(remotePath)) { String fileName = entry.getFilename(); if (".".equals(fileName) || "..".equals(fileName)) { continue; } Attributes attrs = validateSftpFileTypeAndPermissions(fileName, getTestedVersion(), entry.getAttributes()); List<AclEntry> aclActual = attrs.getAcl(); if (getTestedVersion() == SftpConstants.SFTP_V3) { assertNull("Unexpected ACL for entry=" + fileName, aclActual); } else { assertListEquals("Mismatched ACL for entry=" + fileName, aclExpected, aclActual); } attrs.getFlags().clear(); attrs.setAcl(aclExpected); sftp.setStat(remotePath + "/" + fileName, attrs); if (getTestedVersion() > SftpConstants.SFTP_V3) { numInvoked++; } } } } finally { sshd.setSubsystemFactories(factories); } assertEquals("Mismatched invocations count", numInvoked, numInvocations.get()); } @Test // see SSHD-575 public void testSftpExtensionsEncodeDecode() throws Exception { final Class<?> anchor = getClass(); final Map<String, String> expExtensions = GenericUtils.<String, String>mapBuilder() .put("class", anchor.getSimpleName()) .put("package", anchor.getPackage().getName()) .put("method", getCurrentTestName()) .build(); final AtomicInteger numInvocations = new AtomicInteger(0); SftpSubsystemFactory factory = new SftpSubsystemFactory() { @Override public Command create() { SftpSubsystem subsystem = new SftpSubsystem(getExecutorService(), isShutdownOnExit(), getUnsupportedAttributePolicy(), getFileSystemAccessor()) { @Override protected Map<String, Object> resolveFileAttributes(Path file, int flags, LinkOption... options) throws IOException { Map<String, Object> attrs = super.resolveFileAttributes(file, flags, options); if (GenericUtils.isEmpty(attrs)) { attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); } @SuppressWarnings("unchecked") Map<String, String> actExtensions = (Map<String, String>) attrs.put("extended", expExtensions); if (actExtensions != null) { log.info("resolveFileAttributes(" + file + ") replaced extensions: " + actExtensions); } return attrs; } @Override protected void setFileExtensions(Path file, Map<String, byte[]> extensions, LinkOption... options) throws IOException { assertExtensionsMapEquals("setFileExtensions(" + file + ")", expExtensions, extensions); numInvocations.incrementAndGet(); int currentVersion = getTestedVersion(); try { super.setFileExtensions(file, extensions, options); assertFalse("Expected exception not generated for version=" + currentVersion, currentVersion >= SftpConstants.SFTP_V6); } catch (UnsupportedOperationException e) { assertTrue("Unexpected exception for version=" + currentVersion, currentVersion >= SftpConstants.SFTP_V6); } } }; Collection<? extends SftpEventListener> listeners = getRegisteredListeners(); if (GenericUtils.size(listeners) > 0) { for (SftpEventListener l : listeners) { subsystem.addSftpEventListener(l); } } return subsystem; } }; factory.addSftpEventListener(new AbstractSftpEventListenerAdapter() { @Override public void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs) { @SuppressWarnings("unchecked") Map<String, byte[]> actExtensions = GenericUtils.isEmpty(attrs) ? null : (Map<String, byte[]>) attrs.get("extended"); assertExtensionsMapEquals("modifying(" + path + ")", expExtensions, actExtensions); } @Override public void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown) { @SuppressWarnings("unchecked") Map<String, byte[]> actExtensions = GenericUtils.isEmpty(attrs) ? null : (Map<String, byte[]>) attrs.get("extended"); assertExtensionsMapEquals("modified(" + path + ")", expExtensions, actExtensions); } }); Path targetPath = detectTargetFolder(); Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName()); Files.createDirectories(lclSftp.resolve("sub-folder")); Path lclFile = assertHierarchyTargetFolderExists(lclSftp).resolve(getCurrentTestName() + "-" + getTestedVersion() + ".txt"); Files.write(lclFile, getClass().getName().getBytes(StandardCharsets.UTF_8)); Path parentPath = targetPath.getParent(); String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclSftp); int numInvoked = 0; List<NamedFactory<Command>> factories = sshd.getSubsystemFactories(); sshd.setSubsystemFactories(Collections.singletonList(factory)); try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { session.addPasswordIdentity(getCurrentTestName()); session.auth().verify(5L, TimeUnit.SECONDS); try (SftpClient sftp = session.createSftpClient(getTestedVersion())) { for (DirEntry entry : sftp.readDir(remotePath)) { String fileName = entry.getFilename(); if (".".equals(fileName) || "..".equals(fileName)) { continue; } Attributes attrs = validateSftpFileTypeAndPermissions(fileName, getTestedVersion(), entry.getAttributes()); Map<String, byte[]> actExtensions = attrs.getExtensions(); assertExtensionsMapEquals("dirEntry=" + fileName, expExtensions, actExtensions); attrs.getFlags().clear(); attrs.setStringExtensions(expExtensions); sftp.setStat(remotePath + "/" + fileName, attrs); numInvoked++; } } } finally { sshd.setSubsystemFactories(factories); } assertEquals("Mismatched invocations count", numInvoked, numInvocations.get()); } @Test // see SSHD-623 public void testEndOfListIndicator() 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); try (SftpClient sftp = session.createSftpClient(getTestedVersion())) { AtomicReference<Boolean> eolIndicator = new AtomicReference<>(); int version = sftp.getVersion(); Path targetPath = detectTargetFolder(); Path parentPath = targetPath.getParent(); String remotePath = Utils.resolveRelativeRemotePath(parentPath, targetPath); try (CloseableHandle handle = sftp.openDir(remotePath)) { List<DirEntry> entries = sftp.readDir(handle, eolIndicator); for (int index = 1; entries != null; entries = sftp.readDir(handle, eolIndicator), index++) { Boolean value = eolIndicator.get(); if (version < SftpConstants.SFTP_V6) { assertNull("Unexpected indicator value at iteration #" + index, value); } else { assertNotNull("No indicator returned at iteration #" + index, value); if (value) { break; } } eolIndicator.set(null); // make sure starting fresh } Boolean value = eolIndicator.get(); if (version < SftpConstants.SFTP_V6) { assertNull("Unexpected end-of-list indication received at end of entries", value); assertNull("Unexpected no last entries indication", entries); } else { assertNotNull("No end-of-list indication received at end of entries", value); assertNotNull("No last received entries", entries); assertTrue("Bad end-of-list value", value); } } } } } @Override public String toString() { return getClass().getSimpleName() + "[" + getTestedVersion() + "]"; } public static void assertExtensionsMapEquals(String message, Map<String, String> expected, Map<String, byte[]> actual) { assertMapEquals(message, expected, SftpHelper.toStringExtensions(actual)); } private static Attributes validateSftpFileTypeAndPermissions(String fileName, int version, Attributes attrs) { int actualPerms = attrs.getPermissions(); if (version == SftpConstants.SFTP_V3) { int expected = SftpHelper.permissionsToFileType(actualPerms); assertEquals(fileName + ": Mismatched file type", expected, attrs.getType()); } else { int expected = SftpHelper.fileTypeToPermission(attrs.getType()); assertTrue(fileName + ": Missing permision=0x" + Integer.toHexString(expected) + " in 0x" + Integer.toHexString(actualPerms), (actualPerms & expected) == expected); } return attrs; } }