GH-198: (S)FTP Server: Use an OS-selected Port

Fixes spring-projects/spring-integration-samples#198

The `SocketUtils.findAvailableServerSocket()` isn't reliable for port selection and its `socket.close()` may cause a port selection by some other process.

Use OS-selected port for SshdServer in SFTP sample

Revert unexpected refactoring after renaming properties

Upgrade to SSHD-1.4, FTP Server 1.1,  fix tests and Boot 2.0 compatibility
This commit is contained in:
Artem Bilan
2017-03-10 14:21:23 -05:00
committed by Gary Russell
parent a198e45239
commit ea919aa4be
11 changed files with 148 additions and 184 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,30 +16,28 @@
package org.springframework.integration.samples.sftp;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.Arrays;
import java.util.Collections;
import org.apache.sshd.SshServer;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
import org.apache.sshd.common.util.Base64;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.sftp.SftpSubsystem;
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.SocketUtils;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.util.Base64Utils;
import org.springframework.util.StreamUtils;
/**
@@ -47,7 +45,10 @@ import org.springframework.util.StreamUtils;
*/
public class EmbeddedSftpServer implements InitializingBean, SmartLifecycle {
public static final int PORT = SocketUtils.findAvailableTcpPort();
/**
* Let OS to obtain the proper port
*/
public static final int PORT = 0;
private final SshServer server = SshServer.setUpDefaultServer();
@@ -55,31 +56,36 @@ public class EmbeddedSftpServer implements InitializingBean, SmartLifecycle {
private volatile boolean running;
private DefaultSftpSessionFactory defaultSftpSessionFactory;
public void setPort(int port) {
this.port = port;
}
public void setDefaultSftpSessionFactory(DefaultSftpSessionFactory defaultSftpSessionFactory) {
this.defaultSftpSessionFactory = defaultSftpSessionFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
final PublicKey allowedKey = decodePublicKey();
this.server.setPublickeyAuthenticator(new PublickeyAuthenticator() {
@Override
public boolean authenticate(String username, PublicKey key, ServerSession session) {
return key.equals(allowedKey);
}
});
this.server.setPublickeyAuthenticator((username, key, session) -> key.equals(allowedKey));
this.server.setPort(this.port);
this.server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider("hostkey.ser"));
this.server.setSubsystemFactories(Collections.<NamedFactory<Command>>singletonList(new SftpSubsystem.Factory()));
final String virtualDir = new FileSystemResource("").getFile().getAbsolutePath();
server.setFileSystemFactory(new VirtualFileSystemFactory(virtualDir));
this.server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser")));
this.server.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
final String pathname = System.getProperty("java.io.tmpdir") + File.separator + "sftptest" + File.separator;
new File(pathname).mkdirs();
server.setFileSystemFactory(new VirtualFileSystemFactory(Paths.get(pathname)));
}
private PublicKey decodePublicKey() throws Exception {
InputStream stream = new ClassPathResource("META-INF/keys/sftp_rsa.pub").getInputStream();
byte[] decodeBuffer = Base64.decodeBase64(StreamUtils.copyToByteArray(stream));
byte[] keyBytes = StreamUtils.copyToByteArray(stream);
// strip any newline chars
while (keyBytes[keyBytes.length - 1] == 0x0a || keyBytes[keyBytes.length - 1] == 0x0d) {
keyBytes = Arrays.copyOf(keyBytes, keyBytes.length - 1);
}
byte[] decodeBuffer = Base64Utils.decode(keyBytes);
ByteBuffer bb = ByteBuffer.wrap(decodeBuffer);
int len = bb.getInt();
byte[] type = new byte[len];
@@ -116,8 +122,9 @@ public class EmbeddedSftpServer implements InitializingBean, SmartLifecycle {
@Override
public void start() {
try {
server.start();
this.running = true;
this.server.start();
this.defaultSftpSessionFactory.setPort(this.server.getPort());
this.running = true;
}
catch (IOException e) {
throw new IllegalStateException(e);
@@ -136,7 +143,7 @@ public class EmbeddedSftpServer implements InitializingBean, SmartLifecycle {
try {
server.stop(true);
}
catch (InterruptedException e) {
catch (Exception e) {
throw new IllegalStateException(e);
}
finally {
@@ -149,5 +156,4 @@ public class EmbeddedSftpServer implements InitializingBean, SmartLifecycle {
public boolean isRunning() {
return this.running;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,19 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.samples.sftp;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.integration.file.remote.RemoteFileTemplate;
import org.springframework.integration.file.remote.SessionCallback;
import org.springframework.integration.file.remote.session.Session;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
@@ -34,6 +32,8 @@ import com.jcraft.jsch.SftpException;
/**
* @author Gary Russell
* @author Artem Bilan
*
* @since 4.1
*
*/
@@ -42,78 +42,57 @@ public class SftpTestUtils {
public static void createTestFiles(RemoteFileTemplate<LsEntry> template, final String... fileNames) {
if (template != null) {
final ByteArrayInputStream stream = new ByteArrayInputStream("foo".getBytes());
template.execute(new SessionCallback<LsEntry, Void>() {
@Override
public Void doInSession(Session<LsEntry> session) throws IOException {
try {
session.mkdir("si.sftp.sample");
}
catch (Exception e) {
assertThat(e.getMessage(), containsString("failed to create"));
}
for (int i = 0; i < fileNames.length; i++) {
stream.reset();
session.write(stream, "si.sftp.sample/" + fileNames[i]);
}
return null;
template.execute((SessionCallback<LsEntry, Void>) session -> {
try {
session.mkdir("si.sftp.sample");
}
catch (Exception e) {
assertThat(e.getMessage(), containsString("failed to create"));
}
for (int i = 0; i < fileNames.length; i++) {
stream.reset();
session.write(stream, "si.sftp.sample/" + fileNames[i]);
}
return null;
});
}
}
public static void cleanUp(RemoteFileTemplate<LsEntry> template, final String... fileNames) {
if (template != null) {
template.execute(new SessionCallback<LsEntry, Void>() {
@Override
public Void doInSession(Session<LsEntry> session) throws IOException {
// TODO: avoid DFAs with Spring 4.1 (INT-3412)
ChannelSftp channel = (ChannelSftp) new DirectFieldAccessor(new DirectFieldAccessor(session)
.getPropertyValue("targetSession")).getPropertyValue("channel");
for (int i = 0; i < fileNames.length; i++) {
try {
session.remove("si.sftp.sample/" + fileNames[i]);
}
catch (IOException e) {}
}
template.execute((SessionCallback<LsEntry, Void>) session -> {
for (int i = 0; i < fileNames.length; i++) {
try {
// should be empty
channel.rmdir("si.sftp.sample");
session.remove("si.sftp.sample/" + fileNames[i]);
}
catch (SftpException e) {
fail("Expected remote directory to be empty " + e.getMessage());
}
return null;
catch (IOException e) {}
}
// should be empty
session.rmdir("si.sftp.sample");
return null;
});
}
}
public static boolean fileExists(RemoteFileTemplate<LsEntry> template, final String... fileNames) {
if (template != null) {
return template.execute(new SessionCallback<LsEntry, Boolean>() {
@Override
public Boolean doInSession(Session<LsEntry> session) throws IOException {
// TODO: avoid DFAs with Spring 4.1 (INT-3412)
ChannelSftp channel = (ChannelSftp) new DirectFieldAccessor(new DirectFieldAccessor(session)
.getPropertyValue("targetSession")).getPropertyValue("channel");
for (int i = 0; i < fileNames.length; i++) {
try {
SftpATTRS stat = channel.stat("si.sftp.sample/" + fileNames[i]);
if (stat == null) {
System.out.println("stat returned null for " + fileNames[i]);
return false;
}
}
catch (SftpException e) {
System.out.println("Remote file not present: " + e.getMessage() + ": " + fileNames[i]);
return template.execute(session -> {
ChannelSftp channel = (ChannelSftp) session.getClientInstance();
for (int i = 0; i < fileNames.length; i++) {
try {
SftpATTRS stat = channel.stat("si.sftp.sample/" + fileNames[i]);
if (stat == null) {
System.out.println("stat returned null for " + fileNames[i]);
return false;
}
}
return true;
catch (SftpException e) {
System.out.println("Remote file not present: " + e.getMessage() + ": " + fileNames[i]);
return false;
}
}
return true;
});
}
else {

View File

@@ -9,20 +9,6 @@
<import resource="SftpSampleCommon.xml"/>
<bean id="sftpSessionFactory" class="org.springframework.integration.file.remote.session.CachingSessionFactory">
<constructor-arg ref="defaultSftpSessionFactory" />
</bean>
<bean id="defaultSftpSessionFactory"
class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
<property name="host" value="${host}"/>
<property name="privateKey" value="${private.keyfile}"/>
<property name="privateKeyPassphrase" value="${passphrase}"/>
<property name="port" value="#{serverPort}"/>
<property name="user" value="${username}"/>
<property name="allowUnknownKeys" value="true"/>
</bean>
<int-sftp:inbound-channel-adapter id="sftpInbondAdapter"
auto-startup="false"
channel="receiveChannel"

View File

@@ -12,20 +12,6 @@
<int:gateway id="gw" service-interface="org.springframework.integration.samples.sftp.ToSftpFlowGateway"
default-request-channel="inbound"/>
<bean id="sftpSessionFactory" class="org.springframework.integration.file.remote.session.CachingSessionFactory">
<constructor-arg ref="defaultSftpSessionFactory" />
</bean>
<bean id="defaultSftpSessionFactory"
class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
<property name="host" value="${host}"/>
<property name="privateKey" value="${private.keyfile}"/>
<property name="privateKeyPassphrase" value="${passphrase}"/>
<property name="port" value="#{serverPort}"/>
<property name="user" value="${username}"/>
<property name="allowUnknownKeys" value="true"/>
</bean>
<int-sftp:outbound-gateway id="gatewayLS"
session-factory="sftpSessionFactory"
request-channel="inbound"

View File

@@ -9,20 +9,6 @@
<import resource="SftpSampleCommon.xml"/>
<bean id="sftpSessionFactory" class="org.springframework.integration.file.remote.session.CachingSessionFactory">
<constructor-arg ref="defaultSftpSessionFactory" />
</bean>
<bean id="defaultSftpSessionFactory"
class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
<property name="host" value="${host}"/>
<property name="privateKey" value="${private.keyfile}"/>
<property name="privateKeyPassphrase" value="${passphrase}"/>
<property name="port" value="#{serverPort}"/>
<property name="user" value="${username}"/>
<property name="allowUnknownKeys" value="true"/>
</bean>
<int:channel id="inputChannel"/>
<int-sftp:outbound-channel-adapter id="sftpOutboundAdapter"

View File

@@ -9,12 +9,23 @@
<context:property-placeholder location="classpath:user.properties"/>
<bean id="serverPort" class="java.lang.String">
<constructor-arg value="#{'${port}' == '-1' ? T(org.springframework.integration.samples.sftp.EmbeddedSftpServer).PORT : '${port}'}"/>
<bean id="sftpSessionFactory" class="org.springframework.integration.file.remote.session.CachingSessionFactory">
<constructor-arg ref="defaultSftpSessionFactory" />
</bean>
<bean id="defaultSftpSessionFactory"
class="org.springframework.integration.sftp.session.DefaultSftpSessionFactory">
<property name="host" value="${sftp.host}"/>
<property name="privateKey" value="${sftp.private.keyfile}"/>
<property name="privateKeyPassphrase" value="${sftp.passphrase}"/>
<property name="port" value="${sftp.port}"/>
<property name="user" value="${sftp.username}"/>
<property name="allowUnknownKeys" value="true"/>
</bean>
<bean class="org.springframework.integration.samples.sftp.EmbeddedSftpServer">
<property name="port" value="#{serverPort}"/>
<property name="port" value="${sftp.port}"/>
<property name="defaultSftpSessionFactory" ref="defaultSftpSessionFactory"/>
</bean>
</beans>

View File

@@ -1,7 +1,7 @@
host=localhost
# -1 means the embedded Apache MINA SshServer. Change it to any real port, if you are going to test sample against real SFTP Server
port=-1
username=user
passphrase=password
sftp.host=localhost
# 0 means the embedded Apache MINA SshServer. Change it to any real port, if you are going to test sample against real SFTP Server
sftp.port=0
sftp.username=user
sftp.passphrase=password
#private.keyfile=file:/home/someuser/.ssh/id_rsa
private.keyfile=classpath:META-INF/keys/sftp_rsa
sftp.private.keyfile=classpath:META-INF/keys/sftp_rsa