Repository: mina-sshd Updated Branches: refs/heads/master 02f1d57be -> c9f8995c4
[SSHD-741] Provide seamless replacement for Spring integation SFTP adapter Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/c9f8995c Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/c9f8995c Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/c9f8995c Branch: refs/heads/master Commit: c9f8995c41411d0f108341b4ec5600f737d1c1a7 Parents: 283857b Author: Goldstein Lyor <l...@c-b4.com> Authored: Thu May 25 11:58:30 2017 +0300 Committer: Goldstein Lyor <l...@c-b4.com> Committed: Sun Jun 4 12:30:12 2017 +0300 ---------------------------------------------------------------------- pom.xml | 8 +- .../subsystem/AbstractSubsystemClient.java | 8 + .../sshd/client/subsystem/sftp/SftpClient.java | 33 ++ .../subsystem/sftp/SftpVersionSelector.java | 23 +- .../apache/sshd/common/util/GenericUtils.java | 44 +- .../apache/sshd/client/ClientDeadlockTest.java | 7 +- sshd-spring-sftp/pom.xml | 133 ++++++ .../sftp/ApacheSshdSftpSessionFactory.java | 473 +++++++++++++++++++ .../integration/sftp/SpringSftpSession.java | 262 ++++++++++ .../sftp/ApacheSshdSftpSessionFactoryTest.java | 400 ++++++++++++++++ 10 files changed, 1358 insertions(+), 33 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/pom.xml ---------------------------------------------------------------------- diff --git a/pom.xml b/pom.xml index 06be0ad..470115e 100644 --- a/pom.xml +++ b/pom.xml @@ -110,7 +110,7 @@ <bouncycastle.version>1.57</bouncycastle.version> <slf4j.version>1.7.25</slf4j.version> - <spring.version>3.2.17.RELEASE</spring.version> + <spring.version>4.3.8.RELEASE</spring.version> <jgit.version>4.7.0.201704051617-r</jgit.version> <junit.version>4.12</junit.version> <surefire.plugin.version>2.20</surefire.plugin.version> @@ -296,11 +296,6 @@ </exclusions> </dependency> <dependency> - <groupId>commons-logging</groupId> - <artifactId>commons-logging</artifactId> - <version>1.1.1</version> - </dependency> - <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> @@ -1007,6 +1002,7 @@ <module>sshd-ldap</module> <module>sshd-git</module> <module>sshd-contrib</module> + <module>sshd-spring-sftp</module> <module>assembly</module> </modules> </project> http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java index 6bd32c1..32e8d26 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/AbstractSubsystemClient.java @@ -34,4 +34,12 @@ public abstract class AbstractSubsystemClient extends AbstractLoggingBean implem public final ClientSession getSession() { return getClientSession(); } + + @Override + public String toString() { + return getClass().getSimpleName() + + "[name=" + getName() + + ", session=" + getSession() + + "]"; + } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java index bda565d..eb22451 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java @@ -30,6 +30,7 @@ import java.nio.file.attribute.FileTime; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -443,6 +444,36 @@ public interface SftpClient extends SubsystemClient { } class DirEntry { + public static final Comparator<DirEntry> BY_CASE_SENSITIVE_FILENAME = new Comparator<DirEntry>() { + @Override + public int compare(DirEntry o1, DirEntry o2) { + if (o1 == o2) { + return 0; + } else if (o1 == null) { + return 1; + } else if (o2 == null) { + return -1; + } else { + return GenericUtils.safeCompare(o1.getFilename(), o2.getFilename(), true); + } + } + }; + + public static final Comparator<DirEntry> BY_CASE_INSENSITIVE_FILENAME = new Comparator<DirEntry>() { + @Override + public int compare(DirEntry o1, DirEntry o2) { + if (o1 == o2) { + return 0; + } else if (o1 == null) { + return 1; + } else if (o2 == null) { + return -1; + } else { + return GenericUtils.safeCompare(o1.getFilename(), o2.getFilename(), false); + } + } + }; + private final String filename; private final String longFilename; private final Attributes attributes; @@ -471,6 +502,8 @@ public interface SftpClient extends SubsystemClient { } } + DirEntry[] EMPTY_DIR_ENTRIES = new DirEntry[0]; + // default values used if none specified int MIN_BUFFER_SIZE = Byte.MAX_VALUE; int MIN_READ_BUFFER_SIZE = MIN_BUFFER_SIZE; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java index 83a7964..3f0de71 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java @@ -33,7 +33,6 @@ import org.apache.sshd.common.util.ValidateUtils; */ @FunctionalInterface public interface SftpVersionSelector { - /** * An {@link SftpVersionSelector} that returns the current version */ @@ -54,8 +53,7 @@ public interface SftpVersionSelector { /** * @param session The {@link ClientSession} through which the SFTP connection is made * @param current The current version negotiated with the server - * @param available Extra versions available - may be empty and/or contain - * only the current one + * @param available Extra versions available - may be empty and/or contain only the current one * @return The new requested version - if same as current, then nothing is done */ int selectVersion(ClientSession session, int current, List<Integer> available); @@ -69,8 +67,8 @@ public interface SftpVersionSelector { * @param version The requested version * @return The {@link SftpVersionSelector} */ - static SftpVersionSelector fixedVersionSelector(final int version) { - return (session, current, available) -> version; + static SftpVersionSelector fixedVersionSelector(int version) { + return new NamedVersionSelector(Integer.toString(version), (session, current, available) -> version); } /** @@ -83,7 +81,7 @@ public interface SftpVersionSelector { * @return A {@link SftpVersionSelector} that attempts to select * the most preferred version that is also listed as available. */ - static SftpVersionSelector preferredVersionSelector(final int... preferred) { + static SftpVersionSelector preferredVersionSelector(int... preferred) { return preferredVersionSelector(NumberUtils.asList(preferred)); } @@ -98,13 +96,13 @@ public interface SftpVersionSelector { * @return A {@link SftpVersionSelector} that attempts to select * the most preferred version that is also listed as available. */ - static SftpVersionSelector preferredVersionSelector(final Iterable<? extends Number> preferred) { + static SftpVersionSelector preferredVersionSelector(Iterable<? extends Number> preferred) { ValidateUtils.checkNotNullAndNotEmpty((Collection<?>) preferred, "Empty preferred versions"); - return (session, current, available) -> StreamSupport.stream(preferred.spliterator(), false) - .mapToInt(Number::intValue) - .filter(v -> v == current || available.contains(v)) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Preferred versions (" + preferred + ") not available: " + available)); + return new NamedVersionSelector(GenericUtils.join(preferred, ','), (session, current, available) -> StreamSupport.stream(preferred.spliterator(), false) + .mapToInt(Number::intValue) + .filter(v -> v == current || available.contains(v)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Preferred versions (" + preferred + ") not available: " + available))); } class NamedVersionSelector implements SftpVersionSelector { @@ -126,5 +124,4 @@ public interface SftpVersionSelector { return name; } } - } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java index 1f56fec..bd8a7a3 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/util/GenericUtils.java @@ -39,6 +39,7 @@ import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; +import java.util.concurrent.ExecutionException; import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; @@ -411,24 +412,18 @@ public final class GenericUtils { return stream(values).map(mapper).collect(Collectors.toList()); } - public static <T, U> SortedSet<U> mapSort(Collection<T> values, - Function<? super T, ? extends U> mapper, - Comparator<U> comparator) { + public static <T, U> SortedSet<U> mapSort( + Collection<T> values, Function<? super T, ? extends U> mapper, Comparator<U> comparator) { return stream(values).map(mapper).collect(toSortedSet(comparator)); } public static <T, K, U> SortedMap<K, U> toSortedMap( - Iterable<T> values, - Function<? super T, ? extends K> keyMapper, - Function<? super T, ? extends U> valueMapper, - Comparator<K> comparator) { + Iterable<T> values, Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Comparator<K> comparator) { return stream(values).collect(toSortedMap(keyMapper, valueMapper, comparator)); } public static <T, K, U> Collector<T, ?, SortedMap<K, U>> toSortedMap( - Function<? super T, ? extends K> keyMapper, - Function<? super T, ? extends U> valueMapper, - Comparator<K> comparator) { + Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Comparator<K> comparator) { return Collectors.toMap(keyMapper, valueMapper, throwingMerger(), () -> new TreeMap<>(comparator)); } @@ -657,6 +652,29 @@ public final class GenericUtils { } } + public static RuntimeException toRuntimeException(Throwable t) { + return toRuntimeException(t, true); + } + + /** + * Converts a thrown generic exception to a {@link RuntimeException} + * + * @param t The original thrown exception + * @param peelThrowable Whether to determine the root cause by "peeling" + * any enclosing exceptions + * @return The thrown cause if already a runtime exception, otherwise a + * runtime exception of the resolved exception as its cause + * @see #peelException(Throwable) + */ + public static RuntimeException toRuntimeException(Throwable t, boolean peelThrowable) { + Throwable e = peelThrowable ? peelException(t) : t; + if (e instanceof RuntimeException) { + return (RuntimeException) e; + } + + return new RuntimeException(e); + } + /** * Attempts to get to the "effective" exception being thrown, * by taking care of some known exceptions that wrap the original thrown @@ -666,6 +684,7 @@ public final class GenericUtils { * @return The effective exception - same as input if not a wrapper */ public static Throwable peelException(Throwable t) { + // NOTE: check order is important - e.g., InvocationTargetException extends ReflectiveOperationException if (t == null) { return t; } else if (t instanceof UndeclaredThrowableException) { @@ -690,6 +709,11 @@ public final class GenericUtils { if (target != null) { return peelException(target); } + } else if (t instanceof ExecutionException) { + Throwable wrapped = resolveExceptionCause(t); + if (wrapped != null) { + return peelException(wrapped); + } } else if (t instanceof MBeanException) { Throwable target = ((MBeanException) t).getTargetException(); if (target != null) { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java b/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java index bff2420..9c37ba4 100644 --- a/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/client/ClientDeadlockTest.java @@ -78,9 +78,8 @@ public class ClientDeadlockTest extends BaseTestSupport { client.start(); ConnectFuture future = client.connect(getCurrentTestName(), TEST_LOCALHOST, port); - ClientSession session = future.verify(5, TimeUnit.SECONDS).getSession(); - session.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), TimeUnit.SECONDS.toMillis(3L)); - assertTrue(session.isClosed()); + ClientSession session = future.verify(5L, TimeUnit.SECONDS).getSession(); + session.waitFor(EnumSet.of(ClientSession.ClientSessionEvent.CLOSED), TimeUnit.SECONDS.toMillis(7L)); + assertFalse(session.isOpen()); } - } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-spring-sftp/pom.xml ---------------------------------------------------------------------- diff --git a/sshd-spring-sftp/pom.xml b/sshd-spring-sftp/pom.xml new file mode 100644 index 0000000..e3d00f8 --- /dev/null +++ b/sshd-spring-sftp/pom.xml @@ -0,0 +1,133 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + + <!-- + + 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. + --> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.sshd</groupId> + <artifactId>sshd</artifactId> + <version>1.6.0-SNAPSHOT</version> + </parent> + + <artifactId>sshd-spring-sftp</artifactId> + <name>Apache Mina SSHD :: Spring integration SFTP adapter</name> + <packaging>jar</packaging> + <inceptionYear>2017</inceptionYear> + + <properties> + <projectRoot>${basedir}/..</projectRoot> + <spring.integration.version>4.3.9.RELEASE</spring.integration.version> + </properties> + + <dependencyManagement> + <dependencies> + <dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-stream</artifactId> + <version>${spring.integration.version}</version> + </dependency> + </dependencies> + </dependencyManagement> + + <dependencies> + <dependency> + <groupId>org.apache.sshd</groupId> + <artifactId>sshd-core</artifactId> + <version>${project.version}</version> + </dependency> + <!-- Replacement of commons-logging for Spring parts that still use it --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>jcl-over-slf4j</artifactId> + <optional>true</optional> + </dependency> + + <dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-core</artifactId> + <version>${spring.integration.version}</version> + </dependency> + <dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-file</artifactId> + <version>${spring.integration.version}</version> + </dependency> + + <!-- Test dependencies --> + <dependency> + <groupId>org.springframework.integration</groupId> + <artifactId>spring-integration-sftp</artifactId> + <version>${spring.integration.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.jcraft</groupId> + <artifactId>jsch</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.jcraft</groupId> + <artifactId>jzlib</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.sshd</groupId> + <artifactId>sshd-core</artifactId> + <version>${project.version}</version> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <redirectTestOutputToFile>true</redirectTestOutputToFile> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <configuration> + <additionalparam>-Xdoclint:none</additionalparam> + </configuration> + </plugin> + </plugins> + </build> +</project> http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java ---------------------------------------------------------------------- diff --git a/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java new file mode 100644 index 0000000..cb455cd --- /dev/null +++ b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java @@ -0,0 +1,473 @@ +/* + * 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.spring.integration.sftp; + +import java.io.InputStream; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Collection; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.simple.SimpleClientConfigurator; +import org.apache.sshd.client.subsystem.sftp.SftpClient; +import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry; +import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector; +import org.apache.sshd.common.PropertyResolverUtils; +import org.apache.sshd.common.config.SshConfigFileReader; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.loader.pem.PEMResourceParserUtils; +import org.apache.sshd.common.future.CloseFuture; +import org.apache.sshd.common.future.SshFutureListener; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; +import org.apache.sshd.server.subsystem.sftp.SftpSubsystem; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.integration.file.remote.session.Session; +import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.file.remote.session.SharedSessionCapable; + +/** + * A proper replacement for the {@link org.springframework.integration.sftp.session.DefaultSftpSessionFactory} + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class ApacheSshdSftpSessionFactory + extends AbstractLoggingBean + implements SessionFactory<DirEntry>, SharedSessionCapable, + SimpleClientConfigurator, + InitializingBean, DisposableBean { + + // TODO add support for loading multiple private keys + protected volatile KeyPair privateKeyPair; + + private final boolean sharedSession; + private final AtomicReference<ClientSession> sharedSessionHolder = new AtomicReference<>(); + + private volatile String hostValue; + private volatile int portValue = SshConfigFileReader.DEFAULT_PORT; + private volatile String userValue; + private volatile String passwordValue; + private volatile Resource privateKey; + private volatile String privateKeyPassphrase; + private volatile Properties sessionConfig; + private volatile long connTimeout = DEFAULT_CONNECT_TIMEOUT; + private volatile long authTimeout = DEFAULT_AUTHENTICATION_TIMEOUT; + private volatile SftpVersionSelector versionSelector = SftpVersionSelector.CURRENT; + + private SshClient sshClient; + + public ApacheSshdSftpSessionFactory() { + this(false); + } + + public ApacheSshdSftpSessionFactory(boolean sharedSession) { + this.sharedSession = sharedSession; + } + + public String getHost() { + return hostValue; + } + + /** + * @param host The host to connect to - this is a mandatory property. + */ + public void setHost(String host) { + this.hostValue = ValidateUtils.checkNotNullAndNotEmpty(host, "No host name provided"); + } + + public int getPort() { + return portValue; + } + + /** + * The port over which the SFTP connection shall be established. If not specified, + * this value defaults to <code>22</code>. If specified, this property must + * be a positive number. + * + * @param port The port value + */ + public void setPort(int port) { + ValidateUtils.checkTrue(port > 0, "Non-positive port value specified: %d", port); + this.portValue = port; + } + + public String getUser() { + return userValue; + } + + /** + * The remote user to use. This is a mandatory property. + * + * @param user The username + */ + public void setUser(String user) { + this.userValue = ValidateUtils.checkNotNullAndNotEmpty(user, "No user specified: %s", user); + } + + public String getPassword() { + return passwordValue; + } + + /** + * The password to authenticate against the remote host. If a password is + * not provided, then a {@link #setPrivateKey(Resource)} call is mandatory. + * + * @param password The password to use - if {@code null} then no password + * is set - in which case the {@link #getPrivateKey()} resource is used + */ + public void setPassword(String password) { + this.passwordValue = password; + } + + public Resource getPrivateKey() { + return privateKey; + } + + /** + * Allows you to set a {@link Resource}, which represents the location of the + * private key used for authenticating against the remote host. If the privateKey + * is not provided, then the {@link #setPassword(String)} call is mandatory + * + * @param privateKey The private key {@link Resource} + */ + public void setPrivateKey(Resource privateKey) { + this.privateKey = privateKey; + } + + public String getPrivateKeyPassphrase() { + return privateKeyPassphrase; + } + + /** + * @param privateKeyPassphrase The password for the private key - required if + * the private key resource is encrypted + */ + public void setPrivateKeyPassphrase(String privateKeyPassphrase) { + this.privateKeyPassphrase = privateKeyPassphrase; + } + + public KeyPair getPrivateKeyPair() { + return privateKeyPair; + } + + public void setPrivateKeyPair(KeyPair privateKeyPair) { + this.privateKeyPair = privateKeyPair; + } + + @Override // In seconds + public long getConnectTimeout() { + return connTimeout; + } + + @Override + public void setConnectTimeout(long timeout) { + connTimeout = timeout; + } + + @Override // In seconds + public long getAuthenticationTimeout() { + return authTimeout; + } + + @Override + public void setAuthenticationTimeout(long timeout) { + authTimeout = timeout; + } + + public Properties getSessionConfig() { + return sessionConfig; + } + + /** + * @param sessionConfig Extra {@link Properties} that can be used to set specific + * SSHD session properties + */ + public void setSessionConfig(Properties sessionConfig) { + this.sessionConfig = sessionConfig; + } + + public SshClient getSshClient() { + return sshClient; + } + + public void setSshClient(SshClient sshClient) { + this.sshClient = sshClient; + } + + @Override + public boolean isSharedSession() { + return sharedSession; + } + + public SftpVersionSelector getSftpVersionSelector() { + return versionSelector; + } + + public void setSftpVersion(String version) { + if ("CURRENT".equalsIgnoreCase(version)) { + setSftpVersionSelector(SftpVersionSelector.CURRENT); + } else if ("MAXIMUM".equalsIgnoreCase(version)) { + setSftpVersionSelector(SftpVersionSelector.MAXIMUM); + } else if ("MINIMUM".equalsIgnoreCase(version)) { + setSftpVersionSelector(SftpVersionSelector.MINIMUM); + } else { + int fixedVersion = Integer.parseInt(version); + ValidateUtils.checkTrue((fixedVersion >= SftpSubsystem.LOWER_SFTP_IMPL) && (fixedVersion <= SftpSubsystem.HIGHER_SFTP_IMPL), + "Unsupported SFTP version: %s", version); + setSftpVersionSelector(SftpVersionSelector.fixedVersionSelector(fixedVersion)); + } + } + + public void setSftpVersionSelector(SftpVersionSelector selector) { + versionSelector = Objects.requireNonNull(selector, "No version selector provided"); + } + + protected ClientSession getSharedClientSession() { + synchronized (sharedSessionHolder) { + return sharedSessionHolder.get(); + } + } + + @Override + public void resetSharedSession() { + ClientSession sharedSession; + synchronized (sharedSessionHolder) { + sharedSession = sharedSessionHolder.getAndSet(null); + } + if (sharedSession != null) { + log.info("resetSharedSession - session={}", sharedSession); + sharedSession.close(false).addListener(new SshFutureListener<CloseFuture>() { + @SuppressWarnings("synthetic-access") + @Override + public void operationComplete(CloseFuture future) { + log.info("resetSharedSession - session closed: {}", sharedSession); + } + }); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + KeyPair kp = getPrivateKeyPair(); + if (kp == null) { + Resource privateKeyLocation = getPrivateKey(); + if (privateKeyLocation != null) { + kp = loadPrivateKey(privateKeyLocation, getPrivateKeyPassphrase()); + log.info("afterPropertiesSet() - loaded private key={}", privateKeyLocation); + setPrivateKeyPair(kp); + } + } + ValidateUtils.checkState(GenericUtils.isNotEmpty(getPassword()) || (kp != null), "Either password or private key must be set"); + + SshClient client = getSshClient(); + if (client == null) { + client = createSshClientInstance(); + setSshClient(client); + } + + if (!client.isOpen()) { + log.info("afterPropertiesSet() - starting client"); + client.start(); + log.info("afterPropertiesSet() - client started"); + } + } + + protected SshClient createSshClientInstance() throws Exception { + return SshClient.setUpDefaultClient(); + } + + @Override + public void destroy() throws Exception { + SshClient client = getSshClient(); + if ((client != null) && client.isOpen()) { + log.info("destroy() - stopping client"); + client.close(false); // do not wait for the close to complete + log.info("destroy() - client stopped"); + } + } + + protected KeyPair loadPrivateKey(Resource keyResource, String keyPassword) throws Exception { + FilePasswordProvider passwordProvider = GenericUtils.isEmpty(keyPassword) + ? FilePasswordProvider.EMPTY + : FilePasswordProvider.of(keyPassword); + Collection<KeyPair> keyPairs; + try (InputStream inputStream = keyResource.getInputStream()) { + keyPairs = PEMResourceParserUtils.PROXY.loadKeyPairs(keyResource.toString(), passwordProvider, inputStream); + } + + int numLoaded = GenericUtils.size(keyPairs); + ValidateUtils.checkState(numLoaded > 0, "No keys loaded from %s", keyResource); + ValidateUtils.checkState(numLoaded == 1, "Multiple keys loaded from %s", keyResource); + return keyPairs.iterator().next(); + } + + @Override + public Session<DirEntry> getSession() { + boolean sharedInstance = isSharedSession(); + try { + ClientSession session = null; + try { + session = resolveClientSession(sharedInstance); + + SftpVersionSelector selector = getSftpVersionSelector(); + SftpClient sftpClient = session.createSftpClient(selector); + try { + ClientSession sessionInstance = session; + Session<DirEntry> result = sharedInstance + ? new SpringSftpSession(sftpClient) + : new SpringSftpSession(sftpClient, () -> { + try { + sessionInstance.close(); + return null; + } catch (Exception e) { + return e; + } + }); + // avoid auto-close at finally clause + sftpClient = null; + session = null; + return result; + } finally { + if (sftpClient != null) { + sftpClient.close(); + } + } + } finally { + if (session != null) { + try { + session.close(); + } finally { + if (sharedInstance) { + resetSharedSession(); + } + } + } + } + } catch (Exception e) { + throw GenericUtils.toRuntimeException(e); + } + } + + protected ClientSession resolveClientSession(boolean sharedInstance) throws Exception { + ClientSession session; + if (sharedInstance) { + synchronized (sharedSessionHolder) { + session = sharedSessionHolder.get(); + if (session == null) { + session = createClientSession(); + } + sharedSessionHolder.set(session); + } + } else { + session = createClientSession(); + } + + return session; + } + + protected ClientSession createClientSession() throws Exception { + String hostname = ValidateUtils.checkNotNullAndNotEmpty(getHost(), "Host must not be empty"); + String username = ValidateUtils.checkNotNullAndNotEmpty(getUser(), "User must not be empty"); + String passwordIdentity = getPassword(); + KeyPair kp = getPrivateKeyPair(); + ValidateUtils.checkState(GenericUtils.isNotEmpty(passwordIdentity) || (kp != null), + "Either password or private key must be set"); + ClientSession session = createClientSession(hostname, username, getPort(), getEffectiveTimeoutValue(getConnectTimeout())); + try { + session = configureClientSessionProperties(session, getSessionConfig()); + session = authenticateClientSession(session, passwordIdentity, kp, getEffectiveTimeoutValue(getAuthenticationTimeout())); + + ClientSession newSession = session; + if (log.isDebugEnabled()) { + log.debug("createClientSession - session={}", session); + } + session = null; // avoid auto-close at finally clause + return newSession; + } finally { + if (session != null) { + session.close(); + } + } + } + + protected ClientSession createClientSession(String hostname, String username, int port, long timeout) throws Exception { + SshClient client = getSshClient(); + if (log.isDebugEnabled()) { + log.debug("createClientSession({}@{}:{}) waitTimeout={}", username, hostname, port, timeout); + } + ConnectFuture connectFuture = client.connect(username, hostname, port); + return connectFuture.verify(timeout).getSession(); + } + + protected ClientSession configureClientSessionProperties(ClientSession session, Properties props) throws Exception { + if (GenericUtils.isEmpty(props)) { + return session; + } + + boolean debugEnabled = log.isDebugEnabled(); + for (String propName : props.stringPropertyNames()) { + String propValue = props.getProperty(propName); + if (debugEnabled) { + log.debug("configureClientSessionProperties({}) set {}={}", session, propName, propValue); + } + PropertyResolverUtils.updateProperty(session, propName, propValue); + } + + return session; + } + + protected ClientSession authenticateClientSession( + ClientSession session, String passwordIdentity, KeyPair privateKeyIdentity, long timeout) throws Exception { + if (log.isDebugEnabled()) { + PublicKey key = (privateKeyIdentity == null) ? null : privateKeyIdentity.getPublic(); + log.debug("authenticateClientSession({}) password?={}, key={}/{}", + session, GenericUtils.isNotEmpty(passwordIdentity), KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key)); + } + + if (GenericUtils.isNotEmpty(passwordIdentity)) { + session.addPasswordIdentity(passwordIdentity); + } + + if (privateKeyIdentity != null) { + session.addPublicKeyIdentity(privateKeyIdentity); + } + + session.auth().verify(timeout); + return session; + } + + protected long getEffectiveTimeoutValue(long timeoutSeconds) { + if (timeoutSeconds < (Long.MAX_VALUE / 61L)) { + return TimeUnit.SECONDS.toMillis(timeoutSeconds); + } else { + return timeoutSeconds; + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/SpringSftpSession.java ---------------------------------------------------------------------- diff --git a/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/SpringSftpSession.java b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/SpringSftpSession.java new file mode 100644 index 0000000..63c0883 --- /dev/null +++ b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/SpringSftpSession.java @@ -0,0 +1,262 @@ +/* + * 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.spring.integration.sftp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import org.apache.sshd.client.subsystem.sftp.SftpClient; +import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes; +import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry; +import org.apache.sshd.client.subsystem.sftp.SftpClient.OpenMode; +import org.apache.sshd.common.subsystem.sftp.SftpException; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; +import org.springframework.integration.file.remote.session.Session; +import org.springframework.util.FileCopyUtils; + +/** + * Implements the <I>Spring</I> session for the SFTP client + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class SpringSftpSession extends AbstractLoggingBean implements Session<DirEntry> { + private final SftpClient sftpClient; + private final Callable<Exception> sessionCloser; + + public SpringSftpSession(SftpClient clientInstance) { + this(clientInstance, () -> null); + } + + public SpringSftpSession(SftpClient clientInstance, Callable<Exception> sessionCloser) { + this.sftpClient = Objects.requireNonNull(clientInstance, "No SFTP client instance"); + this.sessionCloser = sessionCloser; + } + + @Override + public boolean isOpen() { + SftpClient client = getClientInstance(); + return client.isOpen(); + } + + @Override + public SftpClient getClientInstance() { + return sftpClient; + } + + public Callable<Exception> getSessionCloser() { + return sessionCloser; + } + + @Override + public void close() { + Exception err = null; + try { + SftpClient client = getClientInstance(); + closeClientInstance(client); + } catch (Exception e) { + err = GenericUtils.accumulateException(err, e); + } + + try { + closeSessionInstance(getSessionCloser()); + } catch (Exception e) { + err = GenericUtils.accumulateException(err, e); + } + + if (err != null) { + throw GenericUtils.toRuntimeException(err); + } + } + + protected void closeClientInstance(SftpClient client) throws Exception { + if (client.isOpen()) { + client.close(); + } + } + + protected void closeSessionInstance(Callable<Exception> closer) throws Exception { + if (closer == null) { + return; + } + + Exception err; + try { + err = closer.call(); + } catch (Exception e) { + err = e; + } + + if (err != null) { + throw err; + } + } + + @Override + public boolean remove(String path) throws IOException { + SftpClient client = getClientInstance(); + if (log.isDebugEnabled()) { + log.debug("remove({})[{}]", client, path); + } + client.remove(path); + return true; + } + + @Override + public boolean mkdir(String directory) throws IOException { + SftpClient client = getClientInstance(); + if (log.isDebugEnabled()) { + log.debug("mkdir({})[{}]", client, directory); + } + client.mkdir(directory); + return true; + } + + @Override + public boolean rmdir(String directory) throws IOException { + SftpClient client = getClientInstance(); + if (log.isDebugEnabled()) { + log.debug("rmdir({})[{}]", client, directory); + } + client.rmdir(directory); + return true; + } + + @Override + public void rename(String pathFrom, String pathTo) throws IOException { + SftpClient client = getClientInstance(); + boolean debugEnabled = log.isDebugEnabled(); + if (exists(pathTo)) { + if (debugEnabled) { + log.debug("rename({})[{} => {}] target exists - attempting to remove", client, pathFrom, pathTo); + } + remove(pathTo); + } + + if (debugEnabled) { + log.debug("rename({})[{} => {}] renaming", client, pathFrom, pathTo); + } + client.rename(pathFrom, pathTo); + } + + @Override + public boolean exists(String path) throws IOException { + SftpClient client = getClientInstance(); + try { + Attributes attrs = client.lstat(path); + return attrs != null; + } catch (SftpException e) { + if (log.isDebugEnabled()) { + log.debug("exists({})[{}]: {} - {}", client, path, e.getStatus(), e.getMessage()); + } + return false; + } + } + + @Override + public String[] listNames(String path) throws IOException { + DirEntry[] entries = list(path); + if (GenericUtils.isEmpty(entries)) { + return GenericUtils.EMPTY_STRING_ARRAY; + } + + Collection<String> names = new LinkedList<>(); + for (int index = 0; index < entries.length; index++) { + DirEntry de = entries[index]; + Attributes attrs = de.getAttributes(); + if (!attrs.isRegularFile()) { + continue; + } + if (attrs.isSymbolicLink()) { + continue; + } + + String n = de.getFilename(); + if (".".equals(n) || "..".equals(n)) { + continue; + } + + names.add(n); + } + + if (GenericUtils.isEmpty(names)) { + return GenericUtils.EMPTY_STRING_ARRAY; + } + + return names.toArray(new String[names.size()]); + } + + @Override + public DirEntry[] list(String path) throws IOException { + SftpClient client = getClientInstance(); + Iterable<DirEntry> entries = client.readDir(path); + Collection<DirEntry> result = GenericUtils.stream(entries).collect(Collectors.toCollection(LinkedList::new)); + if (GenericUtils.isEmpty(result)) { + return SftpClient.EMPTY_DIR_ENTRIES; + } + + return result.toArray(new DirEntry[result.size()]); + } + + @Override + public void read(String source, OutputStream outputStream) throws IOException { + SftpClient client = getClientInstance(); + try (InputStream inputStream = client.read(source)) { + FileCopyUtils.copy(inputStream, outputStream); + } + } + + @Override + public void write(InputStream inputStream, String destination) throws IOException { + SftpClient client = getClientInstance(); + try (OutputStream outputStream = client.write(destination)) { + FileCopyUtils.copy(inputStream, outputStream); + } + } + + @Override + public void append(InputStream inputStream, String destination) throws IOException { + SftpClient client = getClientInstance(); + try (OutputStream outputStream = client.write(destination, OpenMode.Append)) { + FileCopyUtils.copy(inputStream, outputStream); + } + } + + @Override + public InputStream readRaw(String source) throws IOException { + SftpClient client = getClientInstance(); + return client.read(source); + } + + @Override + public boolean finalizeRaw() throws IOException { + return true; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + getClientInstance() + "]"; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c9f8995c/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java ---------------------------------------------------------------------- diff --git a/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java new file mode 100644 index 0000000..d6dafee --- /dev/null +++ b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java @@ -0,0 +1,400 @@ +/* + * 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.spring.integration.sftp; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.jcraft.jsch.ChannelSftp.LsEntry; +import com.jcraft.jsch.SftpATTRS; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.subsystem.sftp.SftpClient; +import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes; +import org.apache.sshd.common.file.FileSystemFactory; +import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; +import org.apache.sshd.common.subsystem.sftp.SftpConstants; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.scp.ScpCommandFactory; +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.JSchLogger; +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 org.springframework.integration.file.remote.session.Session; +import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.sftp.session.DefaultSftpSessionFactory; + +/** + * TODO Add javadoc + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class ApacheSshdSftpSessionFactoryTest extends BaseTestSupport { + private static final Comparator<LsEntry> BY_CASE_INSENSITIVE_FILENAME = new Comparator<LsEntry>() { + @Override + public int compare(LsEntry o1, LsEntry o2) { + if (o1 == o2) { + return 0; + } else if (o1 == null) { + return 1; + } else if (o2 == null) { + return -1; + } else { + return GenericUtils.safeCompare(o1.getFilename(), o2.getFilename(), false); + } + } + }; + + private static final Comparator<Path> BY_CASE_INSENSITIVE_FILE_PART = new Comparator<Path>() { + @Override + public int compare(Path o1, Path o2) { + String n1 = (o1 == null) ? null : Objects.toString(o1.getFileName(), null); + String n2 = (o2 == null) ? null : Objects.toString(o2.getFileName(), null); + return GenericUtils.safeCompare(n1, n2, false); + } + }; + + private static final Predicate<String> SYNTHETIC_DIR_ENTRY_NAME = n -> ".".equals(n) || "..".equals(n); + + private static SshServer sshd; + private static int port; + private static SshClient client; + + private final FileSystemFactory fileSystemFactory; + + public ApacheSshdSftpSessionFactoryTest() { + Path targetPath = detectTargetFolder(); + Path parentPath = targetPath.getParent(); + fileSystemFactory = new VirtualFileSystemFactory(parentPath); + } + + @BeforeClass + public static void setupClientAndServer() throws Exception { + JSchLogger.init(); + sshd = Utils.setupTestServer(ApacheSshdSftpSessionFactoryTest.class); + sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + sshd.setCommandFactory(new ScpCommandFactory()); + sshd.start(); + port = sshd.getPort(); + + client = Utils.setupTestClient(ApacheSshdSftpSessionFactoryTest.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); // just making sure + } + + @Test + public void testOpenCloseStateReport() throws Exception { + SessionFactory<SftpClient.DirEntry> sshdFactory = getSshdSessionFactory(); + try (Session<SftpClient.DirEntry> sshdSession = sshdFactory.getSession()) { + assertTrue("Session not reported as open", sshdSession.isOpen()); + sshdSession.close(); + assertFalse("Session not reported as closed", sshdSession.isOpen()); + } + } + + @Test + public void testSharedSessionInstance() throws Exception { + ApacheSshdSftpSessionFactory sshdFactory = getSshdSessionFactory(true); + ClientSession sessionInstance; + try (Session<SftpClient.DirEntry> sshdSession = sshdFactory.getSession()) { + SftpClient client = (SftpClient) sshdSession.getClientInstance(); + sessionInstance = client.getClientSession(); + assertSame("Mismatched factory session instance", sshdFactory.getSharedClientSession(), sessionInstance); + } + + for (int index = 1; index <= Byte.SIZE; index++) { + try (Session<SftpClient.DirEntry> sshdSession = sshdFactory.getSession()) { + SftpClient client = (SftpClient) sshdSession.getClientInstance(); + assertSame("Mismatched session #" + index + " session instance", sessionInstance, client.getClientSession()); + } + } + } + + @Test + public void testWriteRemoteFileContents() throws Exception { + Path targetPath = detectTargetFolder(); + Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName()); + Path srcFile = Files.createDirectories(lclSftp).resolve("source.txt"); + List<String> expectedLines = Arrays.asList(getClass().getPackage().getName(), getClass().getSimpleName(), getCurrentTestName()); + Files.deleteIfExists(srcFile); + Files.write(srcFile, expectedLines, StandardCharsets.UTF_8); + + Path dstFile = srcFile.getParent().resolve("destination.txt"); + Path parentPath = targetPath.getParent(); + String remoteFile = Utils.resolveRelativeRemotePath(parentPath, dstFile); + SessionFactory<LsEntry> legacyFactory = getLegacySessionFactory(); + SessionFactory<SftpClient.DirEntry> sshdFactory = getSshdSessionFactory(); + try (Session<LsEntry> legacySession = legacyFactory.getSession(); + Session<SftpClient.DirEntry> sshdSession = sshdFactory.getSession()) { + testWriteRemoteFileContents("Legacy", legacySession, srcFile, dstFile, remoteFile, expectedLines); + } + } + + private static void testWriteRemoteFileContents( + String type, Session<?> session, Path srcFile, Path dstFile, String remotePath, List<String> expectedLines) + throws Exception { + Files.deleteIfExists(dstFile); + + try (InputStream inputStream = Files.newInputStream(srcFile)) { + session.write(inputStream, remotePath); + } + assertTrue(type + ": destination file not created", Files.exists(dstFile)); + + List<String> actualLines = Files.readAllLines(dstFile, StandardCharsets.UTF_8); + assertListEquals(type, expectedLines, actualLines); + } + + @Test + public void testRetrieveRemoteFileContents() throws Exception { + Path targetPath = detectTargetFolder(); + Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName()); + Path lclFile = Files.createDirectories(lclSftp).resolve("source.txt"); + List<String> expectedLines = Arrays.asList(getClass().getPackage().getName(), getClass().getSimpleName(), getCurrentTestName()); + Files.deleteIfExists(lclFile); + Files.write(lclFile, expectedLines, StandardCharsets.UTF_8); + + Path parentPath = targetPath.getParent(); + String remoteFile = Utils.resolveRelativeRemotePath(parentPath, lclFile); + SessionFactory<LsEntry> legacyFactory = getLegacySessionFactory(); + SessionFactory<SftpClient.DirEntry> sshdFactory = getSshdSessionFactory(); + try (Session<LsEntry> legacySession = legacyFactory.getSession(); + Session<SftpClient.DirEntry> sshdSession = sshdFactory.getSession()) { + for (boolean directStream : new boolean[]{true, false}) { + List<String> legacyLines = readRemoteFileLines(legacySession, remoteFile, directStream); + assertListEquals("Pure legacy lines - direct=" + directStream, expectedLines, legacyLines); + + List<String> sshdLines = readRemoteFileLines(sshdSession, remoteFile, directStream); + assertListEquals("Legacy vs. SSHD lines - direct=" + directStream, legacyLines, sshdLines); + } + } + } + + private static List<String> readRemoteFileLines(Session<?> session, String remoteFile, boolean directStream) throws Exception { + if (directStream) { + try (InputStream rawStream = session.readRaw(remoteFile)) { + try { + return IoUtils.readAllLines(rawStream); + } finally { + session.finalizeRaw(); + } + } + } else { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Byte.MAX_VALUE)) { + session.read(remoteFile, baos); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray())) { + return IoUtils.readAllLines(bais); + } + } + } + } + + @Test + public void testListContents() throws Exception { + Path targetPath = detectTargetFolder(); + Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName()); + Utils.deleteRecursive(lclSftp); // start clean + + List<Path> subFolders = new ArrayList<>(); + for (int index = 1; index <= Byte.SIZE; index++) { + Path dir = Files.createDirectories(lclSftp.resolve("dir" + index)); + subFolders.add(dir); + } + Collections.sort(subFolders, BY_CASE_INSENSITIVE_FILE_PART); + + List<Path> subFiles = new ArrayList<>(); + for (int index = 1; index <= Byte.SIZE; index++) { + Path file = Files.write(lclSftp.resolve("file" + index + ".txt"), + (getClass().getSimpleName() + "#" + getCurrentTestName() + "-" + index).getBytes(StandardCharsets.UTF_8)); + subFiles.add(file); + } + Collections.sort(subFiles, BY_CASE_INSENSITIVE_FILE_PART); + + Path parentPath = targetPath.getParent(); + String remotePath = Utils.resolveRelativeRemotePath(parentPath, lclSftp); + SessionFactory<LsEntry> legacyFactory = getLegacySessionFactory(); + SessionFactory<SftpClient.DirEntry> sshdFactory = getSshdSessionFactory(); + try (Session<LsEntry> legacySession = legacyFactory.getSession(); + Session<SftpClient.DirEntry> sshdSession = sshdFactory.getSession()) { + SftpClient.DirEntry[] sshdEntries = sshdSession.list(remotePath); + List<SftpClient.DirEntry> sshdFiles = new ArrayList<>(); + List<SftpClient.DirEntry> sshdDirs = new ArrayList<>(); + for (SftpClient.DirEntry de : sshdEntries) { + String filename = de.getFilename(); + if (SYNTHETIC_DIR_ENTRY_NAME.test(filename)) { + continue; + } + + Attributes attrs = de.getAttributes(); + if (attrs.isDirectory()) { + sshdDirs.add(de); + } else if (attrs.isRegularFile()) { + sshdFiles.add(de); + } + } + + Collections.sort(sshdDirs, SftpClient.DirEntry.BY_CASE_INSENSITIVE_FILENAME); + assertLocalEntriesEqual(subFolders, sshdDirs, true); + Collections.sort(sshdFiles, SftpClient.DirEntry.BY_CASE_INSENSITIVE_FILENAME); + assertLocalEntriesEqual(subFiles, sshdFiles, false); + + LsEntry[] legacyEntries = legacySession.list(remotePath); + List<LsEntry> legacyFiles = new ArrayList<>(); + List<LsEntry> legacyDirs = new ArrayList<>(); + for (LsEntry lse : legacyEntries) { + String filename = lse.getFilename(); + if (SYNTHETIC_DIR_ENTRY_NAME.test(filename)) { + continue; + } + + SftpATTRS attrs = lse.getAttrs(); + if (attrs.isDir()) { + legacyDirs.add(lse); + } else if (attrs.isReg()) { + legacyFiles.add(lse); + } + } + + Collections.sort(legacyDirs, BY_CASE_INSENSITIVE_FILENAME); + assertRemoteEntriesEqual(legacyDirs, sshdDirs, true); + Collections.sort(legacyFiles, BY_CASE_INSENSITIVE_FILENAME); + assertRemoteEntriesEqual(legacyFiles, sshdFiles, false); + + List<String> sshdNames = Stream.of(sshdSession.listNames(remotePath)) + .filter(SYNTHETIC_DIR_ENTRY_NAME.negate()) + .collect(Collectors.toList()); + Collections.sort(sshdNames, String.CASE_INSENSITIVE_ORDER); + + List<String> localNames = subFiles.stream() + .map(Path::getFileName) + .map(Objects::toString) + .collect(Collectors.toList()); + Collections.sort(localNames, String.CASE_INSENSITIVE_ORDER); + assertListEquals("Local names mismatch", localNames, sshdNames); + + List<String> legacyNames = Stream.of(legacySession.listNames(remotePath)) + .filter(SYNTHETIC_DIR_ENTRY_NAME.negate()) + .collect(Collectors.toList()); + Collections.sort(legacyNames, String.CASE_INSENSITIVE_ORDER); + assertListEquals("Remote names mismatch", legacyNames, sshdNames); + } + } + + private static void assertLocalEntriesEqual(List<Path> expected, List<SftpClient.DirEntry> actual, boolean dirs) { + assertEquals("Mismatched dir=" + dirs + " entries count", expected.size(), actual.size()); + for (int index = 0; index < expected.size(); index++) { + Path path = expected.get(index); + SftpClient.DirEntry de = actual.get(index); + assertEquals("Mismatched filename at dirs=" + dirs + " index=" + index, Objects.toString(path.getFileName(), null), de.getFilename()); + + Attributes deAttrs = de.getAttributes(); + assertEquals("Mismatched SSHD directory indicator for " + path, dirs, deAttrs.isDirectory()); + } + } + + private static void assertRemoteEntriesEqual(List<LsEntry> expected, List<SftpClient.DirEntry> actual, boolean dirs) { + assertEquals("Mismatched dir=" + dirs + " entries count", expected.size(), actual.size()); + for (int index = 0; index < expected.size(); index++) { + LsEntry lse = expected.get(index); + SftpClient.DirEntry de = actual.get(index); + assertEquals("Mismatched filename at dirs=" + dirs + " index=" + index, lse.getFilename(), de.getFilename()); + + SftpATTRS lsAttrs = lse.getAttrs(); + Attributes deAttrs = de.getAttributes(); + assertEquals("Mismatched legacy directory indicator for " + lse.getFilename(), dirs, lsAttrs.isDir()); + assertEquals("Mismatched SSHD directory indicator for " + de.getFilename(), dirs, deAttrs.isDirectory()); + } + } + + protected SessionFactory<LsEntry> getLegacySessionFactory() { + DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(); + factory.setHost(TEST_LOCALHOST); + factory.setPort(port); + factory.setUser(getCurrentTestName()); + factory.setPassword(getCurrentTestName()); + factory.setTimeout(30 * 1000); + factory.setEnableDaemonThread(true); + factory.setAllowUnknownKeys(true); + return factory; + } + + protected ApacheSshdSftpSessionFactory getSshdSessionFactory() throws Exception { + return getSshdSessionFactory(false); + } + + protected ApacheSshdSftpSessionFactory getSshdSessionFactory(boolean sharedSession) throws Exception { + ApacheSshdSftpSessionFactory factory = new ApacheSshdSftpSessionFactory(sharedSession); + factory.setHost(TEST_LOCALHOST); + factory.setPort(port); + factory.setUser(getCurrentTestName()); + factory.setPassword(getCurrentTestName()); + factory.setSshClient(client); + factory.setConnectTimeout(TimeUnit.SECONDS.toMillis(7L)); + factory.setAuthenticationTimeout(TimeUnit.SECONDS.toMillis(11L)); + factory.afterPropertiesSet(); + return factory; + } +}