You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2017/06/04 09:30:19 UTC
[1/3] mina-sshd git commit: [SSHD-741] Provide seamless replacement
for Spring integation SFTP adapter
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 <ly...@c-b4.com>
Authored: Thu May 25 11:58:30 2017 +0300
Committer: Goldstein Lyor <ly...@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:dev@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:dev@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:dev@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;
+ }
+}
[3/3] mina-sshd git commit: [SSHD-741] Added ValidateUtils#checkState
that throws an IllegalStateException if state flag is FALSE
Posted by lg...@apache.org.
[SSHD-741] Added ValidateUtils#checkState that throws an IllegalStateException if state flag is FALSE
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/7f6563c2
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/7f6563c2
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/7f6563c2
Branch: refs/heads/master
Commit: 7f6563c2a30ceddc5257d431f25a15d73d43dd92
Parents: 02f1d57
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Thu May 25 12:38:15 2017 +0300
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Sun Jun 4 12:30:12 2017 +0300
----------------------------------------------------------------------
.../apache/sshd/common/util/ValidateUtils.java | 40 +++++++++++++++++++-
1 file changed, 38 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/7f6563c2/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java
index f5986ad..a55e5d6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/ValidateUtils.java
@@ -21,6 +21,7 @@ package org.apache.sshd.common.util;
import java.util.Collection;
import java.util.Map;
+import java.util.function.Function;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
@@ -174,7 +175,42 @@ public final class ValidateUtils {
}
}
- public static void throwIllegalArgumentException(String message, Object... args) {
- throw new IllegalArgumentException(String.format(message, args));
+ public static void throwIllegalArgumentException(String format, Object... args) {
+ throw createFormattedException(IllegalArgumentException::new, format, args);
}
+
+ public static void checkState(boolean flag, String message) {
+ if (!flag) {
+ throwIllegalStateException(message, GenericUtils.EMPTY_OBJECT_ARRAY);
+ }
+ }
+
+ public static void checkState(boolean flag, String message, long value) {
+ if (!flag) {
+ throwIllegalStateException(message, value);
+ }
+ }
+
+ public static void checkState(boolean flag, String message, Object arg) {
+ if (!flag) {
+ throwIllegalStateException(message, arg);
+ }
+ }
+
+ public static void checkState(boolean flag, String message, Object... args) {
+ if (!flag) {
+ throwIllegalStateException(message, args);
+ }
+ }
+
+ public static void throwIllegalStateException(String format, Object... args) {
+ throw createFormattedException(IllegalStateException::new, format, args);
+ }
+
+ public static <T extends Throwable> T createFormattedException(
+ Function<? super String, ? extends T> constructor, String format, Object... args) {
+ String message = String.format(format, args);
+ return constructor.apply(message);
+ }
+
}
[2/3] mina-sshd git commit: [SSHD-741] Added
FilePasswordProvider#of(String) convenience method
Posted by lg...@apache.org.
[SSHD-741] Added FilePasswordProvider#of(String) convenience method
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/283857b8
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/283857b8
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/283857b8
Branch: refs/heads/master
Commit: 283857b80209756488c6661be61a666284d3c2a1
Parents: 7f6563c
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Thu May 25 13:08:21 2017 +0300
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Sun Jun 4 12:30:12 2017 +0300
----------------------------------------------------------------------
.../org/apache/sshd/common/config/keys/FilePasswordProvider.java | 4 ++++
1 file changed, 4 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/283857b8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
index 35f7558..064f75c 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
@@ -38,4 +38,8 @@ public interface FilePasswordProvider {
* @throws IOException if cannot resolve password
*/
String getPassword(String resourceKey) throws IOException;
+
+ static FilePasswordProvider of(String password) {
+ return r -> password;
+ }
}