/*
* 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;
}
}