diff --git a/build.gradle b/build.gradle index f8176d2251b..3f077c920c9 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,6 @@ ext { jmsApiVersion = '3.0.0' jpaApiVersion = '3.0.3' jrubyVersion = '9.3.8.0' - jschVersion = '0.1.55' jsonpathVersion = '2.7.0' junit4Version = '4.13.2' junitJupiterVersion = '5.9.0' @@ -900,13 +899,11 @@ project('spring-integration-sftp') { description = 'Spring Integration SFTP Support' dependencies { api project(':spring-integration-file') - api "com.jcraft:jsch:$jschVersion" api 'org.springframework:spring-context-support' - optionalApi ("org.apache.sshd:sshd-sftp:$apacheSshdVersion") { + api ("org.apache.sshd:sshd-sftp:$apacheSshdVersion") { exclude group: 'org.slf4j', module: 'jcl-over-slf4j' } - testImplementation "org.apache.sshd:sshd-core:$apacheSshdVersion" testImplementation project(':spring-integration-event') testImplementation project(':spring-integration-file').sourceSets.test.output } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/dsl/RemoteFileInboundChannelAdapterSpec.java b/spring-integration-file/src/main/java/org/springframework/integration/file/dsl/RemoteFileInboundChannelAdapterSpec.java index a5e315ff4e1..1454660f88e 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/dsl/RemoteFileInboundChannelAdapterSpec.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/dsl/RemoteFileInboundChannelAdapterSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2022 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. @@ -290,7 +290,7 @@ public S scanner(DirectoryScanner scanner) { * @return the spec. * @since 5.2.9 */ - public S remoteComparator(Comparator remoteComparator) { + public S remoteComparator(Comparator remoteComparator) { this.synchronizer.setComparator(remoteComparator); return _this(); } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractRemoteFileStreamingMessageSource.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractRemoteFileStreamingMessageSource.java index 56b01b8501b..10c5a887794 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractRemoteFileStreamingMessageSource.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/AbstractRemoteFileStreamingMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2022 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. @@ -62,11 +62,11 @@ public abstract class AbstractRemoteFileStreamingMessageSource extends AbstractFetchLimitingMessageSource implements ManageableLifecycle { - private final RemoteFileTemplate remoteFileTemplate; + private final RemoteFileTemplate remoteFileTemplate; private final BlockingQueue> toBeReceived = new LinkedBlockingQueue<>(); - private final Comparator comparator; + private final Comparator comparator; private final AtomicBoolean running = new AtomicBoolean(); @@ -86,8 +86,8 @@ public abstract class AbstractRemoteFileStreamingMessageSource */ private FileListFilter filter; - protected AbstractRemoteFileStreamingMessageSource(RemoteFileTemplate template, - @Nullable Comparator comparator) { + protected AbstractRemoteFileStreamingMessageSource(RemoteFileTemplate template, + @Nullable Comparator comparator) { Assert.notNull(template, "'template' must not be null"); this.remoteFileTemplate = template; @@ -143,7 +143,7 @@ public void setFileInfoJson(boolean fileInfoJson) { this.fileInfoJson = fileInfoJson; } - protected RemoteFileTemplate getRemoteFileTemplate() { + protected RemoteFileTemplate getRemoteFileTemplate() { return this.remoteFileTemplate; } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/synchronizer/AbstractInboundFileSynchronizer.java b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/synchronizer/AbstractInboundFileSynchronizer.java index 4d83bed6892..0c285ee1090 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/remote/synchronizer/AbstractInboundFileSynchronizer.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/remote/synchronizer/AbstractInboundFileSynchronizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -123,7 +123,7 @@ public abstract class AbstractInboundFileSynchronizer private BeanFactory beanFactory; @Nullable - private Comparator comparator; + private Comparator comparator; private MetadataStore remoteFileMetadataStore = new SimpleMetadataStore(); @@ -141,7 +141,7 @@ public AbstractInboundFileSynchronizer(SessionFactory sessionFactory) { } @Nullable - protected Comparator getComparator() { + protected Comparator getComparator() { return this.comparator; } @@ -151,7 +151,7 @@ protected Comparator getComparator() { * @param comparator the comparator. * @since 5.1 */ - public void setComparator(@Nullable Comparator comparator) { + public void setComparator(@Nullable Comparator comparator) { this.comparator = comparator; } diff --git a/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileUtils.java b/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileUtils.java index 72e198735da..0e65417a939 100644 --- a/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileUtils.java +++ b/spring-integration-file/src/main/java/org/springframework/integration/file/support/FileUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2022 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. @@ -29,6 +29,7 @@ * Utilities for operations on Files. * * @author Gary Russell + * @author Artem Bilan * * @since 5.0 * @@ -47,8 +48,8 @@ public final class FileUtils { * @since 5.0.7 */ @SuppressWarnings("unchecked") - public static F[] purgeUnwantedElements(F[] fileArray, Predicate predicate, - @Nullable Comparator comparator) { + public static F[] purgeUnwantedElements(F[] fileArray, Predicate predicate, + @Nullable Comparator comparator) { if (ObjectUtils.isEmpty(fileArray)) { return fileArray; @@ -56,13 +57,13 @@ public static F[] purgeUnwantedElements(F[] fileArray, Predicate predicat else { if (comparator == null) { return Arrays.stream(fileArray) - .filter(predicate.negate()) + .filter((Predicate) predicate.negate()) .toArray(size -> (F[]) Array.newInstance(fileArray[0].getClass(), size)); } else { return Arrays.stream(fileArray) - .filter(predicate.negate()) - .sorted(comparator) + .filter((Predicate) predicate.negate()) + .sorted((Comparator) comparator) .toArray(size -> (F[]) Array.newInstance(fileArray[0].getClass(), size)); } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/Sftp.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/Sftp.java index ed871124308..9a2a9540a80 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/Sftp.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/Sftp.java @@ -19,6 +19,8 @@ import java.io.File; import java.util.Comparator; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.remote.MessageSessionCallback; import org.springframework.integration.file.remote.RemoteFileTemplate; import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway; @@ -27,9 +29,6 @@ import org.springframework.integration.sftp.gateway.SftpOutboundGateway; import org.springframework.integration.sftp.session.SftpRemoteFileTemplate; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * The factory for SFTP components. * @@ -46,7 +45,7 @@ public final class Sftp { * @param sessionFactory the session factory. * @return the spec. */ - public static SftpInboundChannelAdapterSpec inboundAdapter(SessionFactory sessionFactory) { + public static SftpInboundChannelAdapterSpec inboundAdapter(SessionFactory sessionFactory) { return inboundAdapter(sessionFactory, null); } @@ -56,7 +55,7 @@ public static SftpInboundChannelAdapterSpec inboundAdapter(SessionFactory sessionFactory, + public static SftpInboundChannelAdapterSpec inboundAdapter(SessionFactory sessionFactory, Comparator receptionOrderComparator) { return new SftpInboundChannelAdapterSpec(sessionFactory, receptionOrderComparator); @@ -69,7 +68,7 @@ public static SftpInboundChannelAdapterSpec inboundAdapter(SessionFactory remoteFileTemplate) { + RemoteFileTemplate remoteFileTemplate) { return inboundStreamingAdapter(remoteFileTemplate, null); } @@ -82,8 +81,8 @@ public static SftpStreamingInboundChannelAdapterSpec inboundStreamingAdapter( * @return the spec. */ public static SftpStreamingInboundChannelAdapterSpec inboundStreamingAdapter( - RemoteFileTemplate remoteFileTemplate, - Comparator receptionOrderComparator) { + RemoteFileTemplate remoteFileTemplate, + Comparator receptionOrderComparator) { return new SftpStreamingInboundChannelAdapterSpec(remoteFileTemplate, receptionOrderComparator); } @@ -93,7 +92,7 @@ public static SftpStreamingInboundChannelAdapterSpec inboundStreamingAdapter( * @param sessionFactory the session factory. * @return the spec. */ - public static SftpMessageHandlerSpec outboundAdapter(SessionFactory sessionFactory) { + public static SftpMessageHandlerSpec outboundAdapter(SessionFactory sessionFactory) { return new SftpMessageHandlerSpec(sessionFactory); } @@ -103,7 +102,7 @@ public static SftpMessageHandlerSpec outboundAdapter(SessionFactory sessionFactory, + public static SftpMessageHandlerSpec outboundAdapter(SessionFactory sessionFactory, FileExistsMode fileExistsMode) { return outboundAdapter(new SftpRemoteFileTemplate(sessionFactory), fileExistsMode); @@ -141,7 +140,7 @@ public static SftpMessageHandlerSpec outboundAdapter(SftpRemoteFileTemplate sftp * @param expression the remoteFilePath SpEL expression. * @return the {@link SftpOutboundGatewaySpec} */ - public static SftpOutboundGatewaySpec outboundGateway(SessionFactory sessionFactory, + public static SftpOutboundGatewaySpec outboundGateway(SessionFactory sessionFactory, AbstractRemoteFileOutboundGateway.Command command, String expression) { return outboundGateway(sessionFactory, command.getCommand(), expression); @@ -157,7 +156,7 @@ public static SftpOutboundGatewaySpec outboundGateway(SessionFactory sessionFactory, + public static SftpOutboundGatewaySpec outboundGateway(SessionFactory sessionFactory, String command, String expression) { return new SftpOutboundGatewaySpec(new SftpOutboundGateway(sessionFactory, command, expression)); @@ -172,7 +171,7 @@ public static SftpOutboundGatewaySpec outboundGateway(SessionFactory remoteFileTemplate, + public static SftpOutboundGatewaySpec outboundGateway(RemoteFileTemplate remoteFileTemplate, AbstractRemoteFileOutboundGateway.Command command, String expression) { return outboundGateway(remoteFileTemplate, command.getCommand(), expression); @@ -187,7 +186,7 @@ public static SftpOutboundGatewaySpec outboundGateway(RemoteFileTemplate remoteFileTemplate, + public static SftpOutboundGatewaySpec outboundGateway(RemoteFileTemplate remoteFileTemplate, String command, String expression) { return new SftpOutboundGatewaySpec(new SftpOutboundGateway(remoteFileTemplate, command, expression)); @@ -201,8 +200,8 @@ public static SftpOutboundGatewaySpec outboundGateway(RemoteFileTemplate sessionFactory, - MessageSessionCallback messageSessionCallback) { + public static SftpOutboundGatewaySpec outboundGateway(SessionFactory sessionFactory, + MessageSessionCallback messageSessionCallback) { return new SftpOutboundGatewaySpec(new SftpOutboundGateway(sessionFactory, messageSessionCallback)); } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpInboundChannelAdapterSpec.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpInboundChannelAdapterSpec.java index 6a72e05fbf7..e3a9ccfb76b 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpInboundChannelAdapterSpec.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpInboundChannelAdapterSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -19,6 +19,8 @@ import java.io.File; import java.util.Comparator; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.dsl.RemoteFileInboundChannelAdapterSpec; import org.springframework.integration.file.filters.CompositeFileListFilter; import org.springframework.integration.file.filters.FileListFilter; @@ -30,8 +32,6 @@ import org.springframework.integration.sftp.inbound.SftpInboundFileSynchronizer; import org.springframework.integration.sftp.inbound.SftpInboundFileSynchronizingMessageSource; -import com.jcraft.jsch.ChannelSftp; - /** * A {@link RemoteFileInboundChannelAdapterSpec} for an {@link SftpInboundFileSynchronizingMessageSource}. * @@ -40,10 +40,10 @@ * @since 5.0 */ public class SftpInboundChannelAdapterSpec - extends RemoteFileInboundChannelAdapterSpec { + extends RemoteFileInboundChannelAdapterSpec { - protected SftpInboundChannelAdapterSpec(SessionFactory sessionFactory, + protected SftpInboundChannelAdapterSpec(SessionFactory sessionFactory, Comparator comparator) { super(new SftpInboundFileSynchronizer(sessionFactory)); @@ -70,9 +70,10 @@ public SftpInboundChannelAdapterSpec regexFilter(String regex) { return filter(composeFilters(new SftpRegexPatternFileListFilter(regex))); } - private CompositeFileListFilter composeFilters(FileListFilter + private CompositeFileListFilter composeFilters(FileListFilter fileListFilter) { - CompositeFileListFilter compositeFileListFilter = new CompositeFileListFilter<>(); + + CompositeFileListFilter compositeFileListFilter = new CompositeFileListFilter<>(); compositeFileListFilter.addFilters(fileListFilter, new SftpPersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "sftpMessageSource")); return compositeFileListFilter; diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpMessageHandlerSpec.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpMessageHandlerSpec.java index c6c9906d732..bfcd4612f38 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpMessageHandlerSpec.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpMessageHandlerSpec.java @@ -16,14 +16,14 @@ package org.springframework.integration.sftp.dsl; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.dsl.FileTransferringMessageHandlerSpec; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.file.support.FileExistsMode; import org.springframework.integration.sftp.outbound.SftpMessageHandler; import org.springframework.integration.sftp.session.SftpRemoteFileTemplate; -import com.jcraft.jsch.ChannelSftp; - /** * @author Artem Bilan * @author Joaquin Santana @@ -32,9 +32,9 @@ * @since 5.0 */ public class SftpMessageHandlerSpec - extends FileTransferringMessageHandlerSpec { + extends FileTransferringMessageHandlerSpec { - protected SftpMessageHandlerSpec(SessionFactory sessionFactory) { + protected SftpMessageHandlerSpec(SessionFactory sessionFactory) { this.target = new SftpMessageHandler(sessionFactory); } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpOutboundGatewaySpec.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpOutboundGatewaySpec.java index 02df28e1f49..3390cc6651b 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpOutboundGatewaySpec.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpOutboundGatewaySpec.java @@ -16,13 +16,13 @@ package org.springframework.integration.sftp.dsl; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.dsl.RemoteFileOutboundGatewaySpec; import org.springframework.integration.file.remote.gateway.AbstractRemoteFileOutboundGateway; import org.springframework.integration.sftp.filters.SftpRegexPatternFileListFilter; import org.springframework.integration.sftp.filters.SftpSimplePatternFileListFilter; -import com.jcraft.jsch.ChannelSftp; - /** * @author Artem Bilan * @author Gary Russell @@ -30,10 +30,10 @@ * @since 5.0 */ public class SftpOutboundGatewaySpec - extends RemoteFileOutboundGatewaySpec { + extends RemoteFileOutboundGatewaySpec { - protected SftpOutboundGatewaySpec(AbstractRemoteFileOutboundGateway outboundGateway) { + protected SftpOutboundGatewaySpec(AbstractRemoteFileOutboundGateway outboundGateway) { super(outboundGateway); } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpStreamingInboundChannelAdapterSpec.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpStreamingInboundChannelAdapterSpec.java index c12c71ca975..99c51d24094 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpStreamingInboundChannelAdapterSpec.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/dsl/SftpStreamingInboundChannelAdapterSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2022 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. @@ -18,6 +18,8 @@ import java.util.Comparator; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.dsl.RemoteFileStreamingInboundChannelAdapterSpec; import org.springframework.integration.file.filters.CompositeFileListFilter; import org.springframework.integration.file.filters.FileListFilter; @@ -28,8 +30,6 @@ import org.springframework.integration.sftp.filters.SftpSimplePatternFileListFilter; import org.springframework.integration.sftp.inbound.SftpStreamingMessageSource; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * @author Gary Russell * @@ -37,11 +37,11 @@ * */ public class SftpStreamingInboundChannelAdapterSpec - extends RemoteFileStreamingInboundChannelAdapterSpec { + extends RemoteFileStreamingInboundChannelAdapterSpec { - protected SftpStreamingInboundChannelAdapterSpec(RemoteFileTemplate remoteFileTemplate, - Comparator comparator) { + protected SftpStreamingInboundChannelAdapterSpec(RemoteFileTemplate remoteFileTemplate, + Comparator comparator) { this.target = new SftpStreamingMessageSource(remoteFileTemplate, comparator); } @@ -68,8 +68,10 @@ public SftpStreamingInboundChannelAdapterSpec regexFilter(String regex) { return filter(composeFilters(new SftpRegexPatternFileListFilter(regex))); } - private CompositeFileListFilter composeFilters(FileListFilter fileListFilter) { - CompositeFileListFilter compositeFileListFilter = new CompositeFileListFilter<>(); + private CompositeFileListFilter composeFilters( + FileListFilter fileListFilter) { + + CompositeFileListFilter compositeFileListFilter = new CompositeFileListFilter<>(); compositeFileListFilter.addFilters(fileListFilter, new SftpPersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "sftpStreamingMessageSource")); return compositeFileListFilter; diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilter.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilter.java index 9c1ccd5cc9c..fa8d55a1626 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilter.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilter.java @@ -16,40 +16,42 @@ package org.springframework.integration.sftp.filters; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.filters.AbstractPersistentAcceptOnceFileListFilter; import org.springframework.integration.metadata.ConcurrentMetadataStore; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * Persistent file list filter using the server's file timestamp to detect if we've already * 'seen' this file. * * @author Gary Russell * @author David Liu + * @author Artem Bilan * * @since 3.0 * */ -public class SftpPersistentAcceptOnceFileListFilter extends AbstractPersistentAcceptOnceFileListFilter { +public class SftpPersistentAcceptOnceFileListFilter + extends AbstractPersistentAcceptOnceFileListFilter { public SftpPersistentAcceptOnceFileListFilter(ConcurrentMetadataStore store, String prefix) { super(store, prefix); } @Override - protected long modified(LsEntry file) { - return ((long) file.getAttrs().getMTime()) * 1000; // NOSONAR magic number + protected long modified(SftpClient.DirEntry file) { + return file.getAttributes().getModifyTime().toMillis(); } @Override - protected String fileName(LsEntry file) { + protected String fileName(SftpClient.DirEntry file) { return file.getFilename(); } @Override - protected boolean isDirectory(LsEntry file) { - return file.getAttrs().isDir(); + protected boolean isDirectory(SftpClient.DirEntry file) { + return file.getAttributes().isDirectory(); } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpRegexPatternFileListFilter.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpRegexPatternFileListFilter.java index 8875be16832..1ce3e783ffc 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpRegexPatternFileListFilter.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpRegexPatternFileListFilter.java @@ -18,19 +18,20 @@ import java.util.regex.Pattern; -import org.springframework.integration.file.filters.AbstractRegexPatternFileListFilter; +import org.apache.sshd.sftp.client.SftpClient; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; +import org.springframework.integration.file.filters.AbstractRegexPatternFileListFilter; /** * Implementation of {@link AbstractRegexPatternFileListFilter} for SFTP. * * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 */ -public class SftpRegexPatternFileListFilter extends AbstractRegexPatternFileListFilter { +public class SftpRegexPatternFileListFilter extends AbstractRegexPatternFileListFilter { public SftpRegexPatternFileListFilter(String pattern) { super(pattern); @@ -42,13 +43,13 @@ public SftpRegexPatternFileListFilter(Pattern pattern) { @Override - protected String getFilename(LsEntry entry) { + protected String getFilename(SftpClient.DirEntry entry) { return (entry != null) ? entry.getFilename() : null; } @Override - protected boolean isDirectory(LsEntry file) { - return file.getAttrs().isDir(); + protected boolean isDirectory(SftpClient.DirEntry file) { + return file.getAttributes().isDirectory(); } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSimplePatternFileListFilter.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSimplePatternFileListFilter.java index 4c95a15ae99..64f5652d568 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSimplePatternFileListFilter.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSimplePatternFileListFilter.java @@ -16,19 +16,20 @@ package org.springframework.integration.sftp.filters; -import org.springframework.integration.file.filters.AbstractSimplePatternFileListFilter; +import org.apache.sshd.sftp.client.SftpClient; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; +import org.springframework.integration.file.filters.AbstractSimplePatternFileListFilter; /** * Implementation of {@link AbstractSimplePatternFileListFilter} for SFTP. * * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 */ -public class SftpSimplePatternFileListFilter extends AbstractSimplePatternFileListFilter { +public class SftpSimplePatternFileListFilter extends AbstractSimplePatternFileListFilter { public SftpSimplePatternFileListFilter(String pattern) { super(pattern); @@ -36,13 +37,13 @@ public SftpSimplePatternFileListFilter(String pattern) { @Override - protected String getFilename(LsEntry entry) { + protected String getFilename(SftpClient.DirEntry entry) { return (entry != null) ? entry.getFilename() : null; } @Override - protected boolean isDirectory(LsEntry file) { - return file.getAttrs().isDir(); + protected boolean isDirectory(SftpClient.DirEntry file) { + return file.getAttributes().isDirectory(); } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.java index 06bd4b15a3c..8f9216a3f86 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.java @@ -19,40 +19,45 @@ import java.util.Map; import java.util.function.Function; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.filters.AbstractMarkerFilePresentFileListFilter; import org.springframework.integration.file.filters.FileListFilter; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * SFTP implementation of {@link AbstractMarkerFilePresentFileListFilter}. * * @author Gary Russell + * @author Artem Bilan + * * @since 5.0 * */ -public class SftpSystemMarkerFilePresentFileListFilter extends AbstractMarkerFilePresentFileListFilter { +public class SftpSystemMarkerFilePresentFileListFilter + extends AbstractMarkerFilePresentFileListFilter { - public SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter) { + public SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter) { super(filter); } - public SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter, String suffix) { + public SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter, String suffix) { super(filter, suffix); } - public SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter, + public SftpSystemMarkerFilePresentFileListFilter(FileListFilter filter, Function function) { + super(filter, function); } public SftpSystemMarkerFilePresentFileListFilter( - Map, Function> filtersAndFunctions) { + Map, Function> filtersAndFunctions) { + super(filtersAndFunctions); } @Override - protected String getFilename(LsEntry file) { + protected String getFilename(SftpClient.DirEntry file) { return file.getFilename(); } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java index 4aed4ae9a7c..6eb308b2221 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/gateway/SftpOutboundGateway.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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,11 +16,14 @@ package org.springframework.integration.sftp.gateway; -import java.lang.reflect.Method; +import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.remote.AbstractFileInfo; import org.springframework.integration.file.remote.ClientCallbackWithoutResult; import org.springframework.integration.file.remote.MessageSessionCallback; @@ -30,12 +33,6 @@ import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.sftp.session.SftpFileInfo; import org.springframework.integration.sftp.session.SftpRemoteFileTemplate; -import org.springframework.integration.sftp.support.GeneralSftpException; -import org.springframework.util.ReflectionUtils; - -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.SftpException; /** * Outbound Gateway for performing remote file operations via SFTP. @@ -45,16 +42,7 @@ * * @since 2.1 */ -public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway { - - private static final Method LS_ENTRY_SET_FILENAME_METHOD; - - static { - LS_ENTRY_SET_FILENAME_METHOD = ReflectionUtils.findMethod(LsEntry.class, "setFilename", String.class); - if (LS_ENTRY_SET_FILENAME_METHOD != null) { - ReflectionUtils.makeAccessible(LS_ENTRY_SET_FILENAME_METHOD); - } - } +public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway { /** * Construct an instance using the provided session factory and callback for @@ -62,8 +50,8 @@ public class SftpOutboundGateway extends AbstractRemoteFileOutboundGateway sessionFactory, - MessageSessionCallback messageSessionCallback) { + public SftpOutboundGateway(SessionFactory sessionFactory, + MessageSessionCallback messageSessionCallback) { this(new SftpRemoteFileTemplate(sessionFactory), messageSessionCallback); remoteFileTemplateExplicitlySet(false); @@ -75,8 +63,8 @@ public SftpOutboundGateway(SessionFactory sessionFactory, * @param remoteFileTemplate the remote file template. * @param messageSessionCallback the callback. */ - public SftpOutboundGateway(RemoteFileTemplate remoteFileTemplate, - MessageSessionCallback messageSessionCallback) { + public SftpOutboundGateway(RemoteFileTemplate remoteFileTemplate, + MessageSessionCallback messageSessionCallback) { super(remoteFileTemplate, messageSessionCallback); } @@ -88,7 +76,7 @@ public SftpOutboundGateway(RemoteFileTemplate remoteFileTemplate, * @param command the command. * @param expression the filename expression. */ - public SftpOutboundGateway(SessionFactory sessionFactory, String command, String expression) { + public SftpOutboundGateway(SessionFactory sessionFactory, String command, String expression) { this(new SftpRemoteFileTemplate(sessionFactory), command, expression); remoteFileTemplateExplicitlySet(false); } @@ -100,46 +88,46 @@ public SftpOutboundGateway(SessionFactory sessionFactory, String comman * @param command the command. * @param expression the filename expression. */ - public SftpOutboundGateway(RemoteFileTemplate remoteFileTemplate, String command, String expression) { + public SftpOutboundGateway(RemoteFileTemplate remoteFileTemplate, String command, String expression) { super(remoteFileTemplate, command, expression); } @Override - protected boolean isDirectory(LsEntry file) { - return file.getAttrs().isDir(); + protected boolean isDirectory(SftpClient.DirEntry file) { + return file.getAttributes().isDirectory(); } @Override - protected boolean isLink(LsEntry file) { - return file.getAttrs().isLink(); + protected boolean isLink(SftpClient.DirEntry file) { + return file.getAttributes().isSymbolicLink(); } @Override - protected String getFilename(LsEntry file) { + protected String getFilename(SftpClient.DirEntry file) { return file.getFilename(); } @Override - protected String getFilename(AbstractFileInfo file) { + protected String getFilename(AbstractFileInfo file) { return file.getFilename(); } @Override - protected List> asFileInfoList(Collection files) { + protected List> asFileInfoList(Collection files) { return files.stream() .map(SftpFileInfo::new) .collect(Collectors.toList()); } @Override - protected long getModified(LsEntry file) { - return ((long) file.getAttrs().getMTime()) * 1000; // NOSONAR magic number + protected long getModified(SftpClient.DirEntry file) { + return file.getAttributes().getModifyTime().toMillis(); } @Override - protected LsEntry enhanceNameWithSubDirectory(LsEntry file, String directory) { - ReflectionUtils.invokeMethod(LS_ENTRY_SET_FILENAME_METHOD, file, directory + file.getFilename()); - return file; + protected SftpClient.DirEntry enhanceNameWithSubDirectory(SftpClient.DirEntry file, String directory) { + return new SftpClient.DirEntry(directory + file.getFilename(), directory + file.getFilename(), + file.getAttributes()); } @Override @@ -153,17 +141,18 @@ public boolean isChmodCapable() { } @Override - protected void doChmod(RemoteFileOperations remoteFileOperations, final String path, final int chmod) { - remoteFileOperations - .executeWithClient((ClientCallbackWithoutResult) client -> { - try { - client.chmod(chmod, path); - } - catch (SftpException e) { - throw new GeneralSftpException( - "Failed to execute 'chmod " + Integer.toOctalString(chmod) + " " + path + "'", e); - } - }); + protected void doChmod(RemoteFileOperations remoteFileOperations, String path, int chmod) { + remoteFileOperations.executeWithClient((ClientCallbackWithoutResult) client -> { + try { + SftpClient.Attributes attributes = client.stat(path); + attributes.setPermissions(chmod); + client.setStat(path, attributes); + } + catch (IOException ex) { + throw new UncheckedIOException( + "Failed to execute 'chmod " + Integer.toOctalString(chmod) + " " + path + "'", ex); + } + }); } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpInboundFileSynchronizer.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpInboundFileSynchronizer.java index 6f833ece3af..fca7fcc2c19 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpInboundFileSynchronizer.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpInboundFileSynchronizer.java @@ -16,13 +16,13 @@ package org.springframework.integration.sftp.inbound; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizer; import org.springframework.integration.metadata.SimpleMetadataStore; import org.springframework.integration.sftp.filters.SftpPersistentAcceptOnceFileListFilter; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * Handles the synchronization between a remote SFTP directory and a local mount. * @@ -33,30 +33,30 @@ * * @since 2.0 */ -public class SftpInboundFileSynchronizer extends AbstractInboundFileSynchronizer { +public class SftpInboundFileSynchronizer extends AbstractInboundFileSynchronizer { /** * Create a synchronizer with the {@code SessionFactory} used to acquire {@code Session} instances. * @param sessionFactory The session factory. */ - public SftpInboundFileSynchronizer(SessionFactory sessionFactory) { + public SftpInboundFileSynchronizer(SessionFactory sessionFactory) { super(sessionFactory); doSetFilter(new SftpPersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "sftpMessageSource")); } @Override - protected boolean isFile(LsEntry file) { - return (file != null && file.getAttrs() != null && !file.getAttrs().isDir() && !file.getAttrs().isLink()); + protected boolean isFile(SftpClient.DirEntry file) { + return file != null && file.getAttributes().isRegularFile(); } @Override - protected String getFilename(LsEntry file) { - return (file != null ? file.getFilename() : null); + protected String getFilename(SftpClient.DirEntry file) { + return file != null ? file.getFilename() : null; } @Override - protected long getModified(LsEntry file) { - return (long) file.getAttrs().getMTime() * 1000; // NOSONAR magic number + protected long getModified(SftpClient.DirEntry file) { + return file.getAttributes().getModifyTime().toMillis(); } @Override diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpInboundFileSynchronizingMessageSource.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpInboundFileSynchronizingMessageSource.java index 08095561f45..0adaf070933 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpInboundFileSynchronizingMessageSource.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpInboundFileSynchronizingMessageSource.java @@ -19,31 +19,34 @@ import java.io.File; import java.util.Comparator; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizer; import org.springframework.integration.file.remote.synchronizer.AbstractInboundFileSynchronizingMessageSource; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * A {@link org.springframework.integration.core.MessageSource} implementation for SFTP * that delegates to an InboundFileSynchronizer. * * @author Josh Long * @author Oleg Zhurakousky + * @author Artem Bilan + * * @since 2.0 */ -public class SftpInboundFileSynchronizingMessageSource extends AbstractInboundFileSynchronizingMessageSource { +public class SftpInboundFileSynchronizingMessageSource + extends AbstractInboundFileSynchronizingMessageSource { - public SftpInboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer synchronizer) { + public SftpInboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer synchronizer) { super(synchronizer); } - public SftpInboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer synchronizer, Comparator comparator) { + public SftpInboundFileSynchronizingMessageSource(AbstractInboundFileSynchronizer synchronizer, + Comparator comparator) { + super(synchronizer, comparator); } - public String getComponentType() { return "sftp:inbound-channel-adapter"; } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpStreamingMessageSource.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpStreamingMessageSource.java index 44df8c4d20b..288abeed756 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpStreamingMessageSource.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/inbound/SftpStreamingMessageSource.java @@ -21,6 +21,8 @@ import java.util.Comparator; import java.util.List; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.remote.AbstractFileInfo; import org.springframework.integration.file.remote.AbstractRemoteFileStreamingMessageSource; import org.springframework.integration.file.remote.RemoteFileTemplate; @@ -28,8 +30,6 @@ import org.springframework.integration.sftp.filters.SftpPersistentAcceptOnceFileListFilter; import org.springframework.integration.sftp.session.SftpFileInfo; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * Message source for streaming SFTP remote file contents. * @@ -39,13 +39,13 @@ * @since 4.3 * */ -public class SftpStreamingMessageSource extends AbstractRemoteFileStreamingMessageSource { +public class SftpStreamingMessageSource extends AbstractRemoteFileStreamingMessageSource { /** * Construct an instance with the supplied template. * @param template the template. */ - public SftpStreamingMessageSource(RemoteFileTemplate template) { + public SftpStreamingMessageSource(RemoteFileTemplate template) { this(template, null); } @@ -56,8 +56,9 @@ public SftpStreamingMessageSource(RemoteFileTemplate template) { * @param template the template. * @param comparator the comparator. */ - public SftpStreamingMessageSource(RemoteFileTemplate template, - Comparator comparator) { + public SftpStreamingMessageSource(RemoteFileTemplate template, + Comparator comparator) { + super(template, comparator); doSetFilter(new SftpPersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "sftpStreamingMessageSource")); } @@ -68,17 +69,17 @@ public String getComponentType() { } @Override - protected List> asFileInfoList(Collection files) { - List> canonicalFiles = new ArrayList>(); - for (LsEntry file : files) { + protected List> asFileInfoList(Collection files) { + List> canonicalFiles = new ArrayList<>(); + for (SftpClient.DirEntry file : files) { canonicalFiles.add(new SftpFileInfo(file)); } return canonicalFiles; } @Override - protected boolean isDirectory(LsEntry file) { - return file != null && file.getAttrs().isDir(); + protected boolean isDirectory(SftpClient.DirEntry file) { + return file != null && file.getAttributes().isDirectory(); } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/outbound/SftpMessageHandler.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/outbound/SftpMessageHandler.java index 32ad2069676..79253ff4b38 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/outbound/SftpMessageHandler.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/outbound/SftpMessageHandler.java @@ -16,26 +16,28 @@ package org.springframework.integration.sftp.outbound; +import java.io.IOException; +import java.io.UncheckedIOException; + +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.remote.ClientCallbackWithoutResult; import org.springframework.integration.file.remote.RemoteFileTemplate; import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.file.support.FileExistsMode; import org.springframework.integration.sftp.session.SftpRemoteFileTemplate; -import org.springframework.integration.sftp.support.GeneralSftpException; - -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.SftpException; /** * Subclass of {@link FileTransferringMessageHandler} for SFTP. * * @author Gary Russell + * @author Artme Bilan + * * @since 4.3 * */ -public class SftpMessageHandler extends FileTransferringMessageHandler { +public class SftpMessageHandler extends FileTransferringMessageHandler { /** * @param remoteFileTemplate the template. @@ -62,7 +64,7 @@ public SftpMessageHandler(SftpRemoteFileTemplate remoteFileTemplate, FileExistsM * @see FileTransferringMessageHandler#FileTransferringMessageHandler * (SessionFactory) */ - public SftpMessageHandler(SessionFactory sessionFactory) { + public SftpMessageHandler(SessionFactory sessionFactory) { this(new SftpRemoteFileTemplate(sessionFactory)); } @@ -72,14 +74,16 @@ public boolean isChmodCapable() { } @Override - protected void doChmod(RemoteFileTemplate remoteFileTemplate, final String path, final int chmod) { - remoteFileTemplate.executeWithClient((ClientCallbackWithoutResult) client -> { + protected void doChmod(RemoteFileTemplate remoteFileTemplate, String path, int chmod) { + remoteFileTemplate.executeWithClient((ClientCallbackWithoutResult) client -> { try { - client.chmod(chmod, path); + SftpClient.Attributes attributes = client.stat(path); + attributes.setPermissions(chmod); + client.setStat(path, attributes); } - catch (SftpException e) { - throw new GeneralSftpException( - "Failed to execute 'chmod " + Integer.toOctalString(chmod) + " " + path + "'", e); + catch (IOException ex) { + throw new UncheckedIOException( + "Failed to execute 'chmod " + Integer.toOctalString(chmod) + " " + path + "'", ex); } }); } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.java index 1038b0dacac..1450d9dd36c 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.java @@ -17,31 +17,38 @@ package org.springframework.integration.sftp.session; import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyPair; import java.time.Duration; -import java.util.Arrays; -import java.util.Properties; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.auth.keyboard.UserInteraction; +import org.apache.sshd.client.auth.password.PasswordIdentityProvider; +import org.apache.sshd.client.config.hosts.HostConfigEntry; +import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier; +import org.apache.sshd.client.keyverifier.RejectAllServerKeyVerifier; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.keyprovider.KeyIdentityProvider; +import org.apache.sshd.common.util.io.resource.AbstractIoResource; +import org.apache.sshd.common.util.io.resource.IoResource; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.apache.sshd.common.util.security.SecurityUtils; +import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.client.SftpClientFactory; +import org.apache.sshd.sftp.client.SftpVersionSelector; -import org.springframework.beans.factory.BeanCreationException; import org.springframework.core.io.Resource; -import org.springframework.integration.JavaUtils; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.file.remote.session.SharedSessionCapable; import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.StringUtils; - -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Proxy; -import com.jcraft.jsch.SocketFactory; -import com.jcraft.jsch.UIKeyboardInteractive; -import com.jcraft.jsch.UserInfo; /** * Factory for creating {@link SftpSession} instances. @@ -57,62 +64,43 @@ * * @since 2.0 */ -public class DefaultSftpSessionFactory implements SessionFactory, SharedSessionCapable { - - private static final Log LOGGER = LogFactory.getLog(DefaultSftpSessionFactory.class); +public class DefaultSftpSessionFactory implements SessionFactory, SharedSessionCapable { - static { - JSch.setLogger(new JschLogger()); - } + private final SshClient sshClient; - private final UserInfo userInfoWrapper = new UserInfoWrapper(); - - private final JSch jsch; + private final AtomicBoolean initialized = new AtomicBoolean(); private final boolean isSharedSession; private final Lock sharedSessionLock; + private boolean isInnerClient = false; + private String host; - private int port = 22; // NOSONAR magic number. The default + private int port = SshConstants.DEFAULT_PORT; private String user; private String password; + private HostConfigEntry hostConfig; + private Resource knownHosts; private Resource privateKey; private String privateKeyPassphrase; - private Properties sessionConfig; - - private Proxy proxy; - - private SocketFactory socketFactory; - - private Integer timeout; - - private String clientVersion; - - private String hostKeyAlias; - - private Integer serverAliveInterval; - - private Integer serverAliveCountMax; - - private Boolean enableDaemonThread; - - private UserInfo userInfo; + private UserInteraction userInteraction; private boolean allowUnknownKeys = false; - private Duration channelConnectTimeout; + private Integer timeout; - private volatile JSchSessionWrapper sharedJschSession; + private SftpVersionSelector sftpVersionSelector = SftpVersionSelector.CURRENT; + private volatile SftpClient sharedSftpClient; public DefaultSftpSessionFactory() { this(false); @@ -122,16 +110,18 @@ public DefaultSftpSessionFactory() { * @param isSharedSession true if the session is to be shared. */ public DefaultSftpSessionFactory(boolean isSharedSession) { - this(new JSch(), isSharedSession); + this(SshClient.setUpDefaultClient(), isSharedSession); + this.isInnerClient = true; } /** * Intended for use in tests so the jsch can be mocked. - * @param jsch The jsch instance. + * @param sshClient The SshClient instance. * @param isSharedSession true if the session is to be shared. */ - public DefaultSftpSessionFactory(JSch jsch, boolean isSharedSession) { - this.jsch = jsch; + public DefaultSftpSessionFactory(SshClient sshClient, boolean isSharedSession) { + Assert.notNull(sshClient, "'sshClient' must not be null"); + this.sshClient = sshClient; this.isSharedSession = isSharedSession; if (this.isSharedSession) { this.sharedSessionLock = new ReentrantLock(); @@ -142,9 +132,9 @@ public DefaultSftpSessionFactory(JSch jsch, boolean isSharedSession) { } /** - * The url of the host you want connect to. This is a mandatory property. + * The url of the host you want to connect to. This is a mandatory property. * @param host The host. - * @see JSch#getSession(String, String, int) + * @see SshClient#connect(String, String, int) */ public void setHost(String host) { this.host = host; @@ -155,7 +145,7 @@ public void setHost(String host) { * this value defaults to 22. If specified, this properties must * be a positive number. * @param port The port. - * @see JSch#getSession(String, String, int) + * @see SshClient#connect(String, String, int) */ public void setPort(int port) { this.port = port; @@ -164,7 +154,7 @@ public void setPort(int port) { /** * The remote user to use. This is a mandatory property. * @param user The user. - * @see JSch#getSession(String, String, int) + * @see SshClient#connect(String, String, int) */ public void setUser(String user) { this.user = user; @@ -174,23 +164,36 @@ public void setUser(String user) { * The password to authenticate against the remote host. If a password is * not provided, then a {@link DefaultSftpSessionFactory#setPrivateKey(Resource) privateKey} is * mandatory. - * Not allowed if {@link #setUserInfo(UserInfo) userInfo} is provided - the password is obtained - * from that object. * @param password The password. - * @see com.jcraft.jsch.Session#setPassword(String) + * @see SshClient#setPasswordIdentityProvider(PasswordIdentityProvider) */ public void setPassword(String password) { + Assert.state(this.isInnerClient, + "A password must be configured on the externally provided SshClient instance"); this.password = password; } /** - * Specifies the filename that will be used for a host key repository. - * The file has the same format as OpenSSH's known_hosts file. + * Provide a {@link HostConfigEntry} as an alternative for the user/host/port options. + * Can be configured with a proxy jump property. + * @param hostConfig the {@link HostConfigEntry} for connection. + * @since 6.0 + * @see SshClient#connect(HostConfigEntry) + */ + public void setHostConfig(HostConfigEntry hostConfig) { + this.hostConfig = hostConfig; + } + + /** + * Specifies a {@link Resource} that will be used for a host key repository. + * The data has to have the same format as OpenSSH's known_hosts file. * @param knownHosts the resource for known hosts. * @since 5.2.5 - * @see JSch#setKnownHosts(java.io.InputStream) + * @see SshClient#setServerKeyVerifier(ServerKeyVerifier) */ public void setKnownHostsResource(Resource knownHosts) { + Assert.state(this.isInnerClient, + "Known hosts must be configured on the externally provided SshClient instance"); this.knownHosts = knownHosts; } @@ -198,152 +201,43 @@ public void setKnownHostsResource(Resource knownHosts) { * 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 DefaultSftpSessionFactory#setPassword(String) password} - * property is mandatory (or {@link #setUserInfo(UserInfo) userInfo} that returns a - * password. + * property is mandatory. * @param privateKey The private key. - * @see JSch#addIdentity(String) - * @see JSch#addIdentity(String, String) + * @see SshClient#setKeyIdentityProvider(KeyIdentityProvider) */ public void setPrivateKey(Resource privateKey) { + Assert.state(this.isInnerClient, + "A private key auth must be configured on the externally provided SshClient instance"); this.privateKey = privateKey; } /** * The password for the private key. Optional. - * Not allowed if {@link #setUserInfo(UserInfo) userInfo} is provided - the passphrase is obtained - * from that object. * @param privateKeyPassphrase The private key passphrase. - * @see JSch#addIdentity(String, String) + * @see SshClient#setKeyIdentityProvider(KeyIdentityProvider) */ public void setPrivateKeyPassphrase(String privateKeyPassphrase) { + Assert.state(this.isInnerClient, + "A private key auth must be configured on the externally provided SshClient instance"); this.privateKeyPassphrase = privateKeyPassphrase; } /** - * Using {@link Properties}, you can set additional configuration settings on - * the underlying JSch {@link com.jcraft.jsch.Session}. - * @param sessionConfig The session configuration properties. - * @see com.jcraft.jsch.Session#setConfig(Properties) - */ - public void setSessionConfig(Properties sessionConfig) { - this.sessionConfig = sessionConfig; - } - - /** - * Allows for specifying a JSch-based {@link Proxy}. If set, then the proxy - * object is used to create the connection to the remote host. - * @param proxy The proxy. - * @see com.jcraft.jsch.Session#setProxy(Proxy) - */ - public void setProxy(Proxy proxy) { - this.proxy = proxy; - } - - /** - * Allows you to pass in a {@link SocketFactory}. The socket factory is used - * to create a socket to the target host. When a {@link Proxy} is used, the - * socket factory is passed to the proxy. By default plain TCP sockets are used. - * @param socketFactory The socket factory. - * @see com.jcraft.jsch.Session#setSocketFactory(SocketFactory) - */ - public void setSocketFactory(SocketFactory socketFactory) { - this.socketFactory = socketFactory; - } - - /** - * The timeout property is used as the socket timeout parameter, as well as - * the default connection timeout. Defaults to 0, which means, - * that no timeout will occur. - * @param timeout The timeout. - * @see com.jcraft.jsch.Session#setTimeout(int) - */ - public void setTimeout(Integer timeout) { - this.timeout = timeout; - } - - /** - * Allows you to set the client version property. It's default depends on the - * underlying JSch version but it will look like SSH-2.0-JSCH-0.1.45 - * @param clientVersion The client version. - * @see com.jcraft.jsch.Session#setClientVersion(String) - */ - public void setClientVersion(String clientVersion) { - this.clientVersion = clientVersion; - } - - /** - * Sets the host key alias, used when comparing the host key to the known - * hosts list. - * @param hostKeyAlias The host key alias. - * @see com.jcraft.jsch.Session#setHostKeyAlias(String) - */ - public void setHostKeyAlias(String hostKeyAlias) { - this.hostKeyAlias = hostKeyAlias; - } - - /** - * Sets the timeout interval (milliseconds) before a server alive message is - * sent, in case no message is received from the server. - * @param serverAliveInterval The server alive interval. - * @see com.jcraft.jsch.Session#setServerAliveInterval(int) - */ - public void setServerAliveInterval(Integer serverAliveInterval) { - this.serverAliveInterval = serverAliveInterval; - } - - /** - * Specifies the number of server-alive messages, which will be sent without - * any reply from the server before disconnecting. If not set, this property - * defaults to 1. - * @param serverAliveCountMax The server alive count max. - * @see com.jcraft.jsch.Session#setServerAliveCountMax(int) - */ - public void setServerAliveCountMax(Integer serverAliveCountMax) { - this.serverAliveCountMax = serverAliveCountMax; - } - - /** - * If true, all threads will be daemon threads. If set to false, - * normal non-daemon threads will be used. This property will be set on the - * underlying {@link com.jcraft.jsch.Session} using - * {@link com.jcraft.jsch.Session#setDaemonThread(boolean)}. There, this - * property will default to false, if not explicitly set. - * @param enableDaemonThread true to enable a daemon thread. - * @see com.jcraft.jsch.Session#setDaemonThread(boolean) - */ - public void setEnableDaemonThread(Boolean enableDaemonThread) { - this.enableDaemonThread = enableDaemonThread; - } - - /** - * Provide a {@link UserInfo} which exposes control over dealing with new keys or key + * Provide a {@link UserInteraction} which exposes control over dealing with new keys or key * changes. As Spring Integration will not normally allow user interaction, the - * implementation must respond to Jsch calls in a suitable way. - *

- * Jsch calls {@link UserInfo#promptYesNo(String)} when connecting to an unknown host, - * or when a known host's key has changed (see {@link #setKnownHostsResource(Resource)} - * knownHosts}). Generally, it should return false as returning true will accept all - * new keys or key changes. - *

- * If no {@link UserInfo} is provided, the behavior is defined by - * {@link #setAllowUnknownKeys(boolean) allowUnknownKeys}. - *

- * If {@link #setPassword(String) setPassword} is invoked with a non-null password, it will - * override any password in the supplied {@link UserInfo}. - *

- * NOTE: When this is provided, the {@link #setPassword(String) password} and - * {@link #setPrivateKeyPassphrase(String) passphrase} are not allowed because those values - * will be obtained from the {@link UserInfo}. - * @param userInfo the UserInfo. + * implementation must respond to SSH protocol calls in a suitable way. + * @param userInteraction the UserInteraction. * @since 4.1.7 - * @see com.jcraft.jsch.Session#setUserInfo(com.jcraft.jsch.UserInfo) + * @see SshClient#setUserInteraction(UserInteraction) */ - public void setUserInfo(UserInfo userInfo) { - this.userInfo = userInfo; + public void setUserInteraction(UserInteraction userInteraction) { + Assert.state(this.isInnerClient, + "A `UserInteraction` must be configured on the externally provided SshClient instance"); + this.userInteraction = userInteraction; } /** - * When no {@link UserInfo} has been provided, set to true to unconditionally allow + * When no {@link #knownHosts} has been provided, set to true to unconditionally allow * connecting to an unknown host or when a host's key has changed (see * {@link #setKnownHostsResource(Resource) knownHosts}). Default false (since 4.2). * Set to true if a knownHosts file is not provided. @@ -351,17 +245,25 @@ public void setUserInfo(UserInfo userInfo) { * @since 4.1.7 */ public void setAllowUnknownKeys(boolean allowUnknownKeys) { + Assert.state(this.isInnerClient, + "An `AcceptAllServerKeyVerifier` must be configured on the externally provided SshClient instance"); this.allowUnknownKeys = allowUnknownKeys; } /** - * Set the connection timeout. - * @param timeout the timeout to set. - * @since 5.2 + * The timeout property is used as the socket timeout parameter, as well as + * the default connection timeout. Defaults to 0, which means, + * that no timeout will occur. + * @param timeout The timeout. + * @see org.apache.sshd.client.future.ConnectFuture#verify(long) */ - public void setChannelConnectTimeout(Duration timeout) { - Assert.notNull(timeout, "'connectTimeout' cannot be null"); - this.channelConnectTimeout = timeout; + public void setTimeout(Integer timeout) { + this.timeout = timeout; + } + + public void setSftpVersionSelector(SftpVersionSelector sftpVersionSelector) { + Assert.notNull(sftpVersionSelector, "'sftpVersionSelector' must noy be null"); + this.sftpVersionSelector = sftpVersionSelector; } @Override @@ -370,18 +272,19 @@ public SftpSession getSession() { if (this.sharedSessionLock != null) { this.sharedSessionLock.lock(); } - JSchSessionWrapper jschSession = this.sharedJschSession; + SftpClient sftpClient = this.sharedSftpClient; try { - boolean freshJschSession = false; - if (jschSession == null || !jschSession.isConnected()) { - jschSession = new JSchSessionWrapper(initJschSession()); - freshJschSession = true; + boolean freshSftpClient = false; + if (sftpClient == null || !sftpClient.isOpen()) { + sftpClient = + SftpClientFactory.instance() + .createSftpClient(initClientSession(), this.sftpVersionSelector); + freshSftpClient = true; } - sftpSession = new SftpSession(jschSession); - JavaUtils.INSTANCE.acceptIfNotNull(this.channelConnectTimeout, sftpSession::setChannelConnectTimeout); + sftpSession = new SftpSession(sftpClient); sftpSession.connect(); - if (this.isSharedSession && freshJschSession) { - this.sharedJschSession = jschSession; + if (this.isSharedSession && freshSftpClient) { + this.sharedSftpClient = sftpClient; } } catch (Exception e) { @@ -392,60 +295,68 @@ public SftpSession getSession() { this.sharedSessionLock.unlock(); } } - jschSession.addChannel(); return sftpSession; } - private com.jcraft.jsch.Session initJschSession() throws JSchException, IOException { + private ClientSession initClientSession() throws IOException { Assert.hasText(this.host, "host must not be empty"); Assert.hasText(this.user, "user must not be empty"); - Assert.isTrue(StringUtils.hasText(this.userInfoWrapper.getPassword()) || this.privateKey != null, - "either a password or a private key is required"); - if (this.port <= 0) { - this.port = 22; // NOSONAR magic number - } - if (this.knownHosts != null) { - this.jsch.setKnownHosts(this.knownHosts.getInputStream()); - } + initClient(); - // private key - if (this.privateKey != null) { - byte[] keyByteArray = FileCopyUtils.copyToByteArray(this.privateKey.getInputStream()); - String passphrase = this.userInfoWrapper.getPassphrase(); - if (StringUtils.hasText(passphrase)) { - this.jsch.addIdentity(this.user, keyByteArray, null, passphrase.getBytes()); - } - else { - this.jsch.addIdentity(this.user, keyByteArray, null, null); - } + Duration verifyTimeout = this.timeout != null ? Duration.ofMillis(this.timeout) : null; + HostConfigEntry hostConfig = this.hostConfig; + if (hostConfig == null) { + hostConfig = + new HostConfigEntry(SshdSocketAddress.isIPv6Address(this.host) ? "" : this.host, this.host, + this.port, this.user); } - com.jcraft.jsch.Session jschSession = this.jsch.getSession(this.user, this.host, this.port); - JavaUtils.INSTANCE - .acceptIfNotNull(this.sessionConfig, jschSession::setConfig) - .acceptIfHasText(this.userInfoWrapper.getPassword(), jschSession::setPassword); - jschSession.setUserInfo(this.userInfoWrapper); + ClientSession clientSession = + this.sshClient.connect(hostConfig) + .verify(verifyTimeout) + .getSession(); - try { - if (this.timeout != null) { - jschSession.setTimeout(this.timeout); + clientSession.auth().verify(verifyTimeout); + + return clientSession; + } + + private void initClient() throws IOException { + if (this.initialized.compareAndSet(false, true)) { + if (this.port <= 0) { + this.port = SshConstants.DEFAULT_PORT; } - if (this.serverAliveInterval != null) { - jschSession.setServerAliveInterval(this.serverAliveInterval); + ServerKeyVerifier serverKeyVerifier = + this.allowUnknownKeys ? AcceptAllServerKeyVerifier.INSTANCE : RejectAllServerKeyVerifier.INSTANCE; + if (this.knownHosts != null) { + serverKeyVerifier = new ResourceKnownHostsServerKeyVerifier(this.knownHosts); } - JavaUtils.INSTANCE - .acceptIfNotNull(this.proxy, jschSession::setProxy) - .acceptIfNotNull(this.socketFactory, jschSession::setSocketFactory) - .acceptIfHasText(this.clientVersion, jschSession::setClientVersion) - .acceptIfHasText(this.hostKeyAlias, jschSession::setHostKeyAlias) - .acceptIfNotNull(this.serverAliveCountMax, jschSession::setServerAliveCountMax) - .acceptIfNotNull(this.enableDaemonThread, jschSession::setDaemonThread); - } - catch (Exception e) { - throw new BeanCreationException("Attempt to set additional properties of " + - "the com.jcraft.jsch.Session resulted in error: " + e.getMessage(), e); + this.sshClient.setServerKeyVerifier(serverKeyVerifier); + + this.sshClient.setPasswordIdentityProvider(PasswordIdentityProvider.wrapPasswords(this.password)); + if (this.privateKey != null) { + IoResource privateKeyResource = + new AbstractIoResource<>(Resource.class, this.privateKey) { + + @Override + public InputStream openInputStream() throws IOException { + return getResourceValue().getInputStream(); + } + }; + try { + Collection keys = + SecurityUtils.getKeyPairResourceParser() + .loadKeyPairs(null, privateKeyResource, + FilePasswordProvider.of(this.privateKeyPassphrase)); + this.sshClient.setKeyIdentityProvider(KeyIdentityProvider.wrapKeyPairs(keys)); + } + catch (GeneralSecurityException ex) { + throw new IOException("Cannot load private key: " + this.privateKey.getFilename(), ex); + } + } + this.sshClient.setUserInteraction(this.userInteraction); + this.sshClient.start(); } - return jschSession; } @Override @@ -456,130 +367,7 @@ public final boolean isSharedSession() { @Override public void resetSharedSession() { Assert.state(this.isSharedSession, "Shared sessions are not being used"); - this.sharedJschSession = null; - } - - /** - * Wrapper class will delegate calls to a configured {@link UserInfo}, providing - * sensible defaults if null. As the password is configured in this Factory, the - * wrapper will return the factory's configured password and only delegate to the - * UserInfo if null. - * - * @since 4.1.7 - */ - private class UserInfoWrapper implements UserInfo, UIKeyboardInteractive { - - UserInfoWrapper() { - } - - /** - * Convenience to check whether enclosing factory's UserInfo is configured. - * @return true if there's a delegate. - */ - private boolean hasDelegate() { - return getDelegate() != null; - } - - /** - * Convenience to retrieve enclosing factory's UserInfo. - * @return the {@link #userInfo} or null if not present. - */ - private UserInfo getDelegate() { - return DefaultSftpSessionFactory.this.userInfo; - } - - @Override - public String getPassphrase() { - if (hasDelegate()) { - Assert.state(!StringUtils.hasText(DefaultSftpSessionFactory.this.privateKeyPassphrase), - "When a 'UserInfo' is provided, 'privateKeyPassphrase' is not allowed"); - return getDelegate().getPassphrase(); - } - else { - return DefaultSftpSessionFactory.this.privateKeyPassphrase; - } - } - - @Override - public String getPassword() { - if (hasDelegate()) { - Assert.state(!StringUtils.hasText(DefaultSftpSessionFactory.this.password), - "When a 'UserInfo' is provided, 'password' is not allowed"); - return getDelegate().getPassword(); - } - else { - return DefaultSftpSessionFactory.this.password; - } - } - - @Override - public boolean promptPassword(String message) { - if (hasDelegate()) { - return getDelegate().promptPassword(message); - } - else { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("No UserInfo provided - " + message + ", returning: true"); - } - return true; - } - } - - @Override - public boolean promptPassphrase(String message) { - if (hasDelegate()) { - return getDelegate().promptPassphrase(message); - } - else { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("No UserInfo provided - " + message + ", returning: true"); - } - return true; - } - } - - @Override - public boolean promptYesNo(String message) { - LOGGER.info(message); - if (hasDelegate()) { - return getDelegate().promptYesNo(message); - } - else { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("No UserInfo provided - " + message + ", returning: " - + DefaultSftpSessionFactory.this.allowUnknownKeys); - } - return DefaultSftpSessionFactory.this.allowUnknownKeys; - } - } - - @Override - public void showMessage(String message) { - if (hasDelegate()) { - getDelegate().showMessage(message); - } - else { - LOGGER.debug(message); - } - } - - @Override - public String[] promptKeyboardInteractive(String destination, String name, String instruction, String[] prompt, - boolean[] echo) { - - if (hasDelegate() && getDelegate() instanceof UIKeyboardInteractive) { - return ((UIKeyboardInteractive) getDelegate()).promptKeyboardInteractive(destination, name, - instruction, prompt, echo); - } - else { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("No UIKeyboardInteractive provided - " + destination + ":" + name + ":" + instruction - + ":" + Arrays.asList(prompt) + ":" + Arrays.toString(echo)); - } - return null; - } - } - + this.sharedSftpClient = null; } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JSchSessionWrapper.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JSchSessionWrapper.java deleted file mode 100644 index c3c05d26173..00000000000 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JSchSessionWrapper.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2014-2019 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. - * You may obtain a copy of the License at - * - * https://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.springframework.integration.sftp.session; - -import java.util.concurrent.atomic.AtomicInteger; - -import com.jcraft.jsch.Session; - -/** - * A wrapper for a JSch session that maintains a channel count and - * physically disconnects when the last channel is closed. - * - * @author Gary Russell - * @since 3.0 - * - */ -class JSchSessionWrapper { - - private final Session session; - - private final AtomicInteger channels = new AtomicInteger(); - - JSchSessionWrapper(Session session) { - this.session = session; - } - - public void addChannel() { - this.channels.incrementAndGet(); - } - - public void close() { - if (this.channels.decrementAndGet() <= 0) { - this.session.disconnect(); - } - } - - public final Session getSession() { - return this.session; - } - - public boolean isConnected() { - return this.session.isConnected(); - } - -} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JschLogger.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JschLogger.java deleted file mode 100644 index 8e8150b0edc..00000000000 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JschLogger.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2002-2019 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. - * You may obtain a copy of the License at - * - * https://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.springframework.integration.sftp.session; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import com.jcraft.jsch.Logger; - -/** - * @author Oleg Zhurakousky - * @author Artem Bilan - * - * @since 2.0.1 - */ -class JschLogger implements Logger { - - private static final Log LOGGER = LogFactory.getLog("com.jcraft.jsch"); - - public boolean isEnabled(int level) { - switch (level) { - case Logger.INFO: - return LOGGER.isInfoEnabled(); - case Logger.WARN: - return LOGGER.isWarnEnabled(); - case Logger.DEBUG: - return LOGGER.isDebugEnabled(); - case Logger.ERROR: - return LOGGER.isErrorEnabled(); - case Logger.FATAL: - return LOGGER.isFatalEnabled(); - default: - return false; - } - } - - public void log(int level, String message) { - switch (level) { - case Logger.INFO: - LOGGER.info(message); - break; - case Logger.WARN: - LOGGER.warn(message); - break; - case Logger.DEBUG: - LOGGER.debug(message); - break; - case Logger.ERROR: - LOGGER.error(message); - break; - case Logger.FATAL: - LOGGER.fatal(message); - break; - default: - break; - } - } - -} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JschProxyFactoryBean.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JschProxyFactoryBean.java deleted file mode 100644 index 04eb4ff673c..00000000000 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/JschProxyFactoryBean.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2016-2019 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. - * You may obtain a copy of the License at - * - * https://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.springframework.integration.sftp.session; - -import org.springframework.beans.factory.config.AbstractFactoryBean; - -import com.jcraft.jsch.Proxy; -import com.jcraft.jsch.ProxyHTTP; -import com.jcraft.jsch.ProxySOCKS4; -import com.jcraft.jsch.ProxySOCKS5; - -/** - * Spring-friendly factory bean to create Jsch {@link Proxy} objects. - * - * @author Gary Russell - * @since 4.3 - * - */ -public class JschProxyFactoryBean extends AbstractFactoryBean { - - public enum Type { - HTTP, SOCKS4, SOCKS5 - } - - private final Type type; - - private final String host; - - private final int port; - - private final String user; - - private final String password; - - public JschProxyFactoryBean(Type type, String host, int port, String user, String password) { - this.type = type; - this.host = host; - this.port = port; - this.user = user; - this.password = password; - } - - @Override - public Class getObjectType() { - switch (this.type) { - case SOCKS5: - return ProxySOCKS5.class; - case SOCKS4: - return ProxySOCKS4.class; - case HTTP: - return ProxyHTTP.class; - default: - throw new IllegalArgumentException("Invalid type:" + this.type); - } - } - - @Override - protected Proxy createInstance() { - switch (this.type) { - case SOCKS5: - ProxySOCKS5 socks5proxy = new ProxySOCKS5(this.host, this.port); - socks5proxy.setUserPasswd(this.user, this.password); - return socks5proxy; - case SOCKS4: - ProxySOCKS4 socks4proxy = new ProxySOCKS4(this.host, this.port); - socks4proxy.setUserPasswd(this.user, this.password); - return socks4proxy; - case HTTP: - ProxyHTTP httpProxy = new ProxyHTTP(this.host, this.port); - httpProxy.setUserPasswd(this.user, this.password); - return httpProxy; - default: - throw new IllegalArgumentException("Invalid type:" + this.type); - } - } - -} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/ResourceKnownHostsServerKeyVerifier.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/ResourceKnownHostsServerKeyVerifier.java new file mode 100644 index 00000000000..2896cf2b0ed --- /dev/null +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/ResourceKnownHostsServerKeyVerifier.java @@ -0,0 +1,144 @@ +/* + * Copyright 2022 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. + * You may obtain a copy of the License at + * + * https://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.springframework.integration.sftp.session; + +import java.io.IOException; +import java.net.SocketAddress; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.TreeSet; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.sshd.client.config.hosts.KnownHostEntry; +import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier; +import org.apache.sshd.client.keyverifier.ServerKeyVerifier; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.net.SshdSocketAddress; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * A {@link ServerKeyVerifier} implementation for a {@link Resource} abstraction. + * The logic is similar to the {@link KnownHostsServerKeyVerifier}, but in read-only mode. + * + * @author Artem Bilan + * + * @since 6.0 + * + * @see KnownHostsServerKeyVerifier + */ +public class ResourceKnownHostsServerKeyVerifier implements ServerKeyVerifier { + + private static final Log logger = LogFactory.getLog(ResourceKnownHostsServerKeyVerifier.class); + + private final Supplier> keysSupplier; + + public ResourceKnownHostsServerKeyVerifier(Resource knownHostsResource) { + Assert.notNull(knownHostsResource, "'knownHostsResource' must not be null"); + this.keysSupplier = GenericUtils.memoizeLock(getKnownHostSupplier(knownHostsResource)); + } + + @Override + public boolean verifyServerKey(ClientSession clientSession, SocketAddress remoteAddress, PublicKey serverKey) { + Collection knownHosts = this.keysSupplier.get(); + KnownHostsServerKeyVerifier.HostEntryPair match = findKnownHostEntry(clientSession, remoteAddress, knownHosts); + if (match == null) { + return false; + } + + KnownHostEntry entry = match.getHostEntry(); + PublicKey expected = match.getServerKey(); + if (KeyUtils.compareKeys(expected, serverKey)) { + return !"revoked".equals(entry.getMarker()); + } + + return false; + } + + private static Supplier> getKnownHostSupplier( + Resource knownHostsResource) { + + return () -> { + try { + Collection entries = + KnownHostEntry.readKnownHostEntries(knownHostsResource.getInputStream(), true); + List keys = new ArrayList<>(entries.size()); + for (KnownHostEntry entry : entries) { + keys.add(new KnownHostsServerKeyVerifier.HostEntryPair(entry, resolveHostKey(entry))); + } + return keys; + } + catch (Exception ex) { + logger.warn("Known hosts cannot be loaded from the: " + knownHostsResource, ex); + return Collections.emptyList(); + } + }; + } + + private static PublicKey resolveHostKey(KnownHostEntry entry) throws IOException, GeneralSecurityException { + AuthorizedKeyEntry authEntry = entry.getKeyEntry(); + Assert.notNull(authEntry, () -> "No key extracted from " + entry); + return authEntry.resolvePublicKey(null, PublicKeyEntryResolver.IGNORING); + } + + private static KnownHostsServerKeyVerifier.HostEntryPair findKnownHostEntry( + ClientSession clientSession, SocketAddress remoteAddress, + Collection knownHosts) { + + Collection candidates = resolveHostNetworkIdentities(clientSession, remoteAddress); + + if (GenericUtils.isEmpty(candidates)) { + return null; + } + + for (KnownHostsServerKeyVerifier.HostEntryPair match : knownHosts) { + KnownHostEntry entry = match.getHostEntry(); + for (SshdSocketAddress host : candidates) { + if (entry.isHostMatch(host.getHostName(), host.getPort())) { + return match; + } + } + } + + return null; // no match found + } + + private static Collection resolveHostNetworkIdentities( + ClientSession clientSession, SocketAddress remoteAddress) { + /* + * NOTE !!! we do not resolve the fully-qualified name to avoid long DNS timeouts. Instead we use the reported + * peer address and the original connection target host + */ + Collection candidates = new TreeSet<>(SshdSocketAddress.BY_HOST_AND_PORT); + candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress)); + SocketAddress connectAddress = clientSession.getConnectAddress(); + candidates.add(SshdSocketAddress.toSshdSocketAddress(connectAddress)); + return candidates; + } + +} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java index 53e9e66b2c4..a93d9f3b4d8 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpFileInfo.java @@ -16,12 +16,14 @@ package org.springframework.integration.sftp.session; +import java.nio.file.attribute.PosixFilePermissions; + +import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.common.SftpHelper; + import org.springframework.integration.file.remote.AbstractFileInfo; import org.springframework.util.Assert; -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.SftpATTRS; - /** * A {@link org.springframework.integration.file.remote.FileInfo} implementation for SFTP. * @@ -30,37 +32,37 @@ * * @since 2.1 */ -public class SftpFileInfo extends AbstractFileInfo { +public class SftpFileInfo extends AbstractFileInfo { - private final LsEntry lsEntry; + private final SftpClient.DirEntry lsEntry; - private final SftpATTRS attrs; + private final SftpClient.Attributes attrs; - public SftpFileInfo(LsEntry lsEntry) { + public SftpFileInfo(SftpClient.DirEntry lsEntry) { Assert.notNull(lsEntry, "'lsEntry' must not be null"); this.lsEntry = lsEntry; - this.attrs = lsEntry.getAttrs(); + this.attrs = lsEntry.getAttributes(); } /** - * @see SftpATTRS#isDir() + * @see SftpClient.Attributes#isDirectory() */ @Override public boolean isDirectory() { - return this.attrs.isDir(); + return this.attrs.isDirectory(); } /** - * @see SftpATTRS#isLink() + * @see SftpClient.Attributes#isSymbolicLink() */ @Override public boolean isLink() { - return this.attrs.isLink(); + return this.attrs.isSymbolicLink(); } /** - * @see SftpATTRS#getSize() + * @see SftpClient.Attributes#getSize() */ @Override public long getSize() { @@ -68,15 +70,15 @@ public long getSize() { } /** - * @see SftpATTRS#getMTime() + * @see SftpClient.Attributes#getModifyTime() */ @Override public long getModified() { - return ((long) this.attrs.getMTime()) * 1000; // NOSONAR magic number + return this.attrs.getModifyTime().toMillis(); } /** - * @see LsEntry#getFilename() + * @see SftpClient.DirEntry#getFilename() */ @Override public String getFilename() { @@ -85,11 +87,11 @@ public String getFilename() { @Override public String getPermissions() { - return this.attrs.getPermissionsString(); + return PosixFilePermissions.toString(SftpHelper.permissionsToAttributes(this.attrs.getPermissions())); } @Override - public LsEntry getFileInfo() { + public SftpClient.DirEntry getFileInfo() { return this.lsEntry; } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplate.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplate.java index ecdf047493d..e809b3c0a32 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplate.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2022 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,35 +16,36 @@ package org.springframework.integration.sftp.session; +import org.apache.sshd.sftp.client.SftpClient; + import org.springframework.integration.file.remote.ClientCallback; import org.springframework.integration.file.remote.RemoteFileTemplate; import org.springframework.integration.file.remote.session.SessionFactory; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * SFTP version of {@code RemoteFileTemplate} providing type-safe access to * the underlying ChannelSftp object. * * @author Gary Russell + * @author Artme Bilan + * * @since 4.1 * */ -public class SftpRemoteFileTemplate extends RemoteFileTemplate { +public class SftpRemoteFileTemplate extends RemoteFileTemplate { - public SftpRemoteFileTemplate(SessionFactory sessionFactory) { + public SftpRemoteFileTemplate(SessionFactory sessionFactory) { super(sessionFactory); } @SuppressWarnings("unchecked") @Override public T executeWithClient(final ClientCallback callback) { - return doExecuteWithClient((ClientCallback) callback); + return doExecuteWithClient((ClientCallback) callback); } - protected T doExecuteWithClient(final ClientCallback callback) { - return execute(session -> callback.doWithClient((ChannelSftp) session.getClientInstance())); + protected T doExecuteWithClient(final ClientCallback callback) { + return execute(session -> callback.doWithClient((SftpClient) session.getClientInstance())); } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java index b7c17ea4a6f..522b7ec17a0 100644 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java +++ b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/session/SftpSession.java @@ -21,22 +21,19 @@ import java.io.OutputStream; import java.io.UncheckedIOException; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Vector; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.apache.sshd.sftp.SftpModuleProperties; +import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.common.SftpConstants; +import org.apache.sshd.sftp.common.SftpException; import org.springframework.integration.file.remote.session.Session; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; - -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.SftpATTRS; -import com.jcraft.jsch.SftpException; +import org.springframework.util.PatternMatchUtils; +import org.springframework.util.StringUtils; /** * Default SFTP {@link Session} implementation. Wraps a JSCH session instance. @@ -50,115 +47,66 @@ * * @since 2.0 */ -public class SftpSession implements Session { - - private static final Log LOGGER = LogFactory.getLog(SftpSession.class); - - private static final String SESSION_IS_NOT_CONNECTED = "session is not connected"; - - private static final Duration DEFAULT_CHANNEL_CONNECT_TIMEOUT = Duration.ofSeconds(5); - - private final com.jcraft.jsch.Session jschSession; - - private final JSchSessionWrapper wrapper; - - private int channelConnectTimeout = (int) DEFAULT_CHANNEL_CONNECT_TIMEOUT.toMillis(); - - private volatile ChannelSftp channel; +public class SftpSession implements Session { - private volatile boolean closed; + private final SftpClient sftpClient; - - public SftpSession(com.jcraft.jsch.Session jschSession) { - Assert.notNull(jschSession, "jschSession must not be null"); - this.jschSession = jschSession; - this.wrapper = null; - } - - public SftpSession(JSchSessionWrapper wrapper) { - Assert.notNull(wrapper, "wrapper must not be null"); - this.jschSession = wrapper.getSession(); - this.wrapper = wrapper; - } - - /** - * Set the connect timeout. - * @param timeout the timeout to set. - * @since 5.2 - */ - public void setChannelConnectTimeout(Duration timeout) { - Assert.notNull(timeout, "'timeout' cannot be null"); - this.channelConnectTimeout = (int) timeout.toMillis(); + public SftpSession(SftpClient sftpClient) { + Assert.notNull(sftpClient, "'sftpClient' must not be null"); + this.sftpClient = sftpClient; } @Override public boolean remove(String path) throws IOException { - Assert.state(this.channel != null, SESSION_IS_NOT_CONNECTED); - try { - this.channel.rm(path); - return true; - } - catch (SftpException ex) { - throw new IOException("Failed to remove file.", ex); - } + this.sftpClient.remove(path); + return true; } @Override - public LsEntry[] list(String path) throws IOException { - Assert.state(this.channel != null, SESSION_IS_NOT_CONNECTED); - try { - Vector lsEntries = this.channel.ls(path); // NOSONAR (Vector) - if (lsEntries != null) { - LsEntry[] entries = new LsEntry[lsEntries.size()]; - for (int i = 0; i < lsEntries.size(); i++) { - Object next = lsEntries.get(i); - Assert.state(next instanceof LsEntry, "expected only LsEntry instances from channel.ls()"); - entries[i] = (LsEntry) next; - } - return entries; - } - } - catch (SftpException ex) { - throw new IOException("Failed to list files", ex); - } - return new LsEntry[0]; + public SftpClient.DirEntry[] list(String path) throws IOException { + return doList(path) + .toArray(SftpClient.DirEntry[]::new); } @Override public String[] listNames(String path) throws IOException { - LsEntry[] entries = this.list(path); - List names = new ArrayList<>(); - for (LsEntry entry : entries) { - String fileName = entry.getFilename(); - SftpATTRS attrs = entry.getAttrs(); - if (!attrs.isDir() && !attrs.isLink()) { - names.add(fileName); + return doList(path) + .map(SftpClient.DirEntry::getFilename) + .toArray(String[]::new); + } + + public Stream doList(String path) throws IOException { + String remotePath = StringUtils.trimTrailingCharacter(StringUtils.trimLeadingCharacter(path, '/'), '/'); + String remoteDir = remotePath; + int lastIndex = remotePath.lastIndexOf('/'); + if (lastIndex > 0) { + remoteDir = remoteDir.substring(0, lastIndex); + } + String remoteFile = lastIndex > 0 ? remotePath.substring(lastIndex + 1) : null; + boolean isPattern = remoteFile != null && remoteFile.contains("*"); + + if (!isPattern && remoteFile != null) { + SftpClient.Attributes attributes = this.sftpClient.lstat(path); + if (!attributes.isDirectory()) { + return Stream.of(new SftpClient.DirEntry(remoteFile, path, attributes)); + } + else { + remoteDir = remotePath; } } - return names.toArray(new String[0]); + return StreamSupport.stream(this.sftpClient.readDir(remoteDir).spliterator(), false) + .filter((entry) -> !isPattern || PatternMatchUtils.simpleMatch(remoteFile, entry.getFilename())); } - @Override public void read(String source, OutputStream os) throws IOException { - Assert.state(this.channel != null, SESSION_IS_NOT_CONNECTED); - try { - InputStream is = this.channel.get(source); - FileCopyUtils.copy(is, os); - } - catch (SftpException ex) { - throw new IOException("failed to read file " + source, ex); - } + InputStream is = this.sftpClient.read(source); + FileCopyUtils.copy(is, os); } @Override public InputStream readRaw(String source) throws IOException { - try { - return this.channel.get(source); - } - catch (SftpException ex) { - throw new IOException("failed to read file " + source, ex); - } + return this.sftpClient.read(source); } @Override @@ -168,149 +116,91 @@ public boolean finalizeRaw() { @Override public void write(InputStream inputStream, String destination) throws IOException { - Assert.state(this.channel != null, SESSION_IS_NOT_CONNECTED); - try { - this.channel.put(inputStream, destination); - } - catch (SftpException ex) { - throw new IOException("failed to write file", ex); + synchronized (this.sftpClient) { + OutputStream outputStream = this.sftpClient.write(destination); + FileCopyUtils.copy(inputStream, outputStream); } } @Override public void append(InputStream inputStream, String destination) throws IOException { - Assert.state(this.channel != null, SESSION_IS_NOT_CONNECTED); - try { - this.channel.put(inputStream, destination, ChannelSftp.APPEND); - } - catch (SftpException ex) { - throw new IOException("failed to write file", ex); + synchronized (this.sftpClient) { + OutputStream outputStream = + this.sftpClient.write(destination, SftpClient.OpenMode.Create, SftpClient.OpenMode.Append); + FileCopyUtils.copy(inputStream, outputStream); } } @Override public void close() { - this.closed = true; - if (this.wrapper != null) { - if (this.channel != null) { - this.channel.disconnect(); - } - this.wrapper.close(); + try { + this.sftpClient.close(); } - else { - if (this.jschSession.isConnected()) { - this.jschSession.disconnect(); - } + catch (IOException ex) { + throw new UncheckedIOException("failed to close an SFTP client", ex); } } @Override public boolean isOpen() { - return !this.closed && this.jschSession.isConnected(); + return this.sftpClient.isOpen(); } @Override public void rename(String pathFrom, String pathTo) throws IOException { - try { - this.channel.rename(pathFrom, pathTo); - } - catch (SftpException sftpex) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Initial File rename failed, possibly because file already exists. " + - "Will attempt to delete file: " + pathTo + " and execute rename again."); - } - try { - remove(pathTo); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Delete file: " + pathTo + " succeeded. Will attempt rename again"); - } - } - catch (IOException ioex) { - IOException exception = new IOException("Failed to delete file " + pathTo, sftpex); - exception.addSuppressed(ioex); - throw exception; - } - try { - // attempt to rename again - this.channel.rename(pathFrom, pathTo); - } - catch (SftpException sftpex2) { - IOException exception = - new IOException("failed to rename from " + pathFrom + " to " + pathTo, sftpex); - exception.addSuppressed(sftpex2); - throw exception; - } - } - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("File: " + pathFrom + " was successfully renamed to " + pathTo); - } + this.sftpClient.rename(pathFrom, pathTo, SftpClient.CopyMode.Overwrite); } @Override public boolean mkdir(String remoteDirectory) throws IOException { - try { - this.channel.mkdir(remoteDirectory); - } - catch (SftpException ex) { - if (ex.id != ChannelSftp.SSH_FX_FAILURE || !exists(remoteDirectory)) { - throw new IOException("failed to create remote directory '" + remoteDirectory + "'.", ex); - } - } + this.sftpClient.mkdir(remoteDirectory); return true; } @Override public boolean rmdir(String remoteDirectory) throws IOException { - try { - this.channel.rmdir(remoteDirectory); - } - catch (SftpException ex) { - throw new IOException("failed to remove remote directory '" + remoteDirectory + "'.", ex); - } + this.sftpClient.rmdir(remoteDirectory); return true; } @Override public boolean exists(String path) { try { - this.channel.lstat(path); + this.sftpClient.lstat(path); return true; } - catch (SftpException ex) { - if (ex.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) { + catch (IOException ex) { + if (ex instanceof SftpException sftpException && + SftpConstants.SSH_FX_NO_SUCH_FILE == sftpException.getStatus()) { + return false; } - else { - throw new UncheckedIOException("Cannot check 'lstat' for path " + path, - new IOException(ex)); - } + throw new UncheckedIOException("Cannot check 'lstat' for path " + path, ex); } } void connect() { try { - if (!this.jschSession.isConnected()) { - this.jschSession.connect(); - } - this.channel = (ChannelSftp) this.jschSession.openChannel("sftp"); - if (this.channel != null && !this.channel.isConnected()) { - this.channel.connect(this.channelConnectTimeout); + if (!this.sftpClient.isOpen()) { + Duration initializationTimeout = + SftpModuleProperties.SFTP_CHANNEL_OPEN_TIMEOUT.getRequired(this.sftpClient.getSession()); + this.sftpClient.getClientChannel().open().verify(initializationTimeout); } } - catch (JSchException e) { - this.close(); - throw new IllegalStateException("failed to connect", e); + catch (IOException ex) { + close(); + throw new UncheckedIOException("failed to connect an SFTP client", ex); } } @Override - public ChannelSftp getClientInstance() { - return this.channel; + public SftpClient getClientInstance() { + return this.sftpClient; } @Override public String getHostPort() { - return this.jschSession.getHost() + ':' + this.jschSession.getPort(); + return this.sftpClient.getSession().getConnectAddress().toString(); } @Override @@ -320,10 +210,10 @@ public boolean test() { private boolean doTest() { try { - this.channel.lstat(this.channel.getHome()); + this.sftpClient.canonicalPath(""); return true; } - catch (@SuppressWarnings("unused") Exception e) { + catch (@SuppressWarnings("unused") Exception ex) { return false; } } diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/support/GeneralSftpException.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/support/GeneralSftpException.java deleted file mode 100644 index c0a14d1cf3f..00000000000 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/support/GeneralSftpException.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016-2019 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. - * You may obtain a copy of the License at - * - * https://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.springframework.integration.sftp.support; - -import org.springframework.core.NestedRuntimeException; - -/** - * Simple runtime exception to wrap an SftpException. - * - * @author Gary Russell - * @since 4.3 - * - */ -@SuppressWarnings("serial") -public class GeneralSftpException extends NestedRuntimeException { - - public GeneralSftpException(String msg, Throwable cause) { - super(msg, cause); - } - -} diff --git a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/support/package-info.java b/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/support/package-info.java deleted file mode 100644 index b78b05597d8..00000000000 --- a/spring-integration-sftp/src/main/java/org/springframework/integration/sftp/support/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Provides general support classes for sftp. - */ -package org.springframework.integration.sftp.support; diff --git a/spring-integration-sftp/src/main/resources/JSCH_LICENSE.txt b/spring-integration-sftp/src/main/resources/JSCH_LICENSE.txt deleted file mode 100644 index aa38f5ac1fd..00000000000 --- a/spring-integration-sftp/src/main/resources/JSCH_LICENSE.txt +++ /dev/null @@ -1,30 +0,0 @@ -JSch 0.0.* was released under the GNU LGPL license. Later, we have switched -over to a BSD-style license. - ------------------------------------------------------------------------------- -Copyright (c) 2002,2003,2004,2005,2006,2007,2008 Atsuhiko Yamanaka, JCraft,Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the distribution. - - 3. The names of the authors may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, -INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, -INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/SftpTestSupport.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/SftpTestSupport.java index ca0dc07dd17..91344b8aa6e 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/SftpTestSupport.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/SftpTestSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2022 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. @@ -22,6 +22,7 @@ import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.sftp.client.SftpClient; import org.apache.sshd.sftp.server.SftpSubsystemFactory; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -32,8 +33,6 @@ import org.springframework.integration.sftp.server.ApacheMinaSftpEventListener; import org.springframework.integration.sftp.session.DefaultSftpSessionFactory; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * Provides an embedded SFTP Server for test cases. * @@ -77,14 +76,16 @@ public static void createServer() throws Exception { port = server.getPort(); } - public static SessionFactory sessionFactory() { - DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true); + public static SessionFactory sessionFactory() { + DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(false); factory.setHost("localhost"); factory.setPort(port); factory.setUser("foo"); factory.setPassword("foo"); factory.setAllowUnknownKeys(true); - return new CachingSessionFactory<>(factory); + CachingSessionFactory cachingSessionFactory = new CachingSessionFactory<>(factory); + cachingSessionFactory.setTestSession(true); + return cachingSessionFactory; } public static ApacheMinaSftpEventListener eventListener() { diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserCachingTests-context.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserCachingTests-context.xml index 8e3f17e7b5e..7e4720f6018 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserCachingTests-context.xml +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserCachingTests-context.xml @@ -22,7 +22,6 @@ - diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserCachingTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserCachingTests.java index f265d8be665..246465eb960 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserCachingTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserCachingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -18,8 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.Properties; - import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -38,22 +36,23 @@ @DirtiesContext public class InboundChannelAdapterParserCachingTests { - @Autowired private Object cachingAdapter; + @Autowired + private Object cachingAdapter; - @Autowired private Object nonCachingAdapter; + @Autowired + private Object nonCachingAdapter; @Test public void cachingAdapter() { - Object sessionFactory = TestUtils.getPropertyValue(cachingAdapter, "source.synchronizer.remoteFileTemplate.sessionFactory"); + Object sessionFactory = + TestUtils.getPropertyValue(cachingAdapter, "source.synchronizer.remoteFileTemplate.sessionFactory"); assertThat(sessionFactory.getClass()).isEqualTo(CachingSessionFactory.class); - Properties sessionConfig = TestUtils.getPropertyValue(sessionFactory, "sessionFactory.sessionConfig", Properties.class); - assertThat(sessionConfig).isNotNull(); - assertThat(sessionConfig.getProperty("StrictHostKeyChecking")).isEqualTo("no"); } @Test public void nonCachingAdapter() { - Object sessionFactory = TestUtils.getPropertyValue(nonCachingAdapter, "source.synchronizer.remoteFileTemplate.sessionFactory"); + Object sessionFactory = + TestUtils.getPropertyValue(nonCachingAdapter, "source.synchronizer.remoteFileTemplate.sessionFactory"); assertThat(sessionFactory.getClass()).isEqualTo(DefaultSftpSessionFactory.class); } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserTests-context-fail.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserTests-context-fail.xml deleted file mode 100644 index 3aedcf3ec58..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserTests-context-fail.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserTests.java index 87b9f1d9b73..785e1613ac6 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/InboundChannelAdapterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -29,7 +29,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.Lifecycle; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -126,16 +125,6 @@ public void testAutoChannel() { context.close(); } - @Test - //exactly one of 'filename-pattern' or 'filter' is allowed on SFTP inbound adapter - public void testFailWithFilePatternAndFilter() { - assertThat(!new File("target/bar").exists()).isTrue(); - assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> - new ClassPathXmlApplicationContext("InboundChannelAdapterParserTests-context-fail.xml", - getClass())); - } - @Test public void testLocalDirAutoCreated() { assertThat(new File("foo").exists()).isFalse(); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/MessageHistory-context.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/MessageHistory-context.xml deleted file mode 100644 index 9d4d2eaf919..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/MessageHistory-context.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests-context-fail-fileFileGen.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests-context-fail-fileFileGen.xml deleted file mode 100644 index 554dd78dd45..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests-context-fail-fileFileGen.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests.java index 9f397db705a..7a14dee03b1 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/OutboundChannelAdapterParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -157,15 +157,6 @@ public void testFailWithRemoteDirAndExpression() { .withMessageContaining("Only one of 'remote-directory'"); } - @Test - public void testFailWithFileExpressionAndFileGenerator() { - assertThatExceptionOfType(BeanDefinitionStoreException.class) - .isThrownBy(() -> - new ClassPathXmlApplicationContext( - "OutboundChannelAdapterParserTests-context-fail-fileFileGen.xml", - getClass())); - } - public static class FooAdvice extends AbstractRequestHandlerAdvice { @Override diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpInboundOutboundSanitySample.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpInboundOutboundSanitySample.java deleted file mode 100644 index e17ecd83e4a..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpInboundOutboundSanitySample.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2002-2020 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. - * You may obtain a copy of the License at - * - * https://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.springframework.integration.sftp.config; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.File; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.messaging.MessageChannel; -import org.springframework.messaging.support.GenericMessage; - -/** - * @author Oleg Zhurakousy - * @author Gunnar Hillert - * - */ -public class SftpInboundOutboundSanitySample { - - - @Test - @Disabled - public void testInbound() throws Exception { - File fileA = new File("local-test-dir/a.test"); - if (fileA.exists()) { - fileA.delete(); - } - File fileB = new File("local-test-dir/b.test"); - if (fileB.exists()) { - fileB.delete(); - } - fileA = new File("remote-target-dir/a.test-foo"); - if (fileA.exists()) { - fileA.delete(); - } - fileB = new File("remote-target-dir/b.test-foo"); - if (fileB.exists()) { - fileB.delete(); - } - - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - "SftpInboundReceiveSample-ignored.xml", this.getClass()); - Thread.sleep(5000); - fileA = new File("local-test-dir/a.test"); - fileB = new File("local-test-dir/b.test"); - assertThat(fileA.exists()).isTrue(); - assertThat(fileB.exists()).isTrue(); - context.close(); - } - - @Test - @Disabled - public void testOutbound() throws Exception { - ClassPathXmlApplicationContext ac = - new ClassPathXmlApplicationContext("SftpOutboundTransferSample-ignored.xml", this.getClass()); - File fileA = new File("local-test-dir/a.test"); - File fileB = new File("local-test-dir/b.test"); - MessageChannel ftpChannel = ac.getBean("ftpChannel", MessageChannel.class); - ftpChannel.send(new GenericMessage(fileA)); - ftpChannel.send(new GenericMessage(fileB)); - Thread.sleep(6000); - fileA = new File("remote-target-dir/a.test-foo"); - fileB = new File("remote-target-dir/b.test-foo"); - assertThat(fileA.exists()).isTrue(); - assertThat(fileB.exists()).isTrue(); - ac.close(); - } - -} diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpInboundReceiveSample-ignored.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpInboundReceiveSample-ignored.xml deleted file mode 100644 index 5fdc8efa0bd..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpInboundReceiveSample-ignored.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpMessageHistoryTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpMessageHistoryTests.java deleted file mode 100644 index 340d4a14bec..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpMessageHistoryTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2002-2020 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. - * You may obtain a copy of the License at - * - * https://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.springframework.integration.sftp.config; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; - -import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.integration.endpoint.SourcePollingChannelAdapter; - -/** - * @author Oleg Zhurakousky - * @author Gunnar Hillert - * @author Gary Russell - */ -public class SftpMessageHistoryTests { - - @Test - public void testMessageHistoryCompliance() { - ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("MessageHistory-context.xml", - SftpMessageHistoryTests.class); - SourcePollingChannelAdapter spca = ac.getBean("sftpAdapter", SourcePollingChannelAdapter.class); - assertThat(spca.getComponentName()).isEqualTo("sftpAdapter"); - assertThat(spca.getComponentType()).isEqualTo("sftp:inbound-channel-adapter"); - ac.close(); - } - -} diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundTransferSample-ignored.xml b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundTransferSample-ignored.xml deleted file mode 100644 index 8fa469908b1..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundTransferSample-ignored.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundTransferSample.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundTransferSample.java deleted file mode 100644 index 2f1b78d3e8a..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/config/SftpOutboundTransferSample.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2002-2020 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. - * You may obtain a copy of the License at - * - * https://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.springframework.integration.sftp.config; - -import java.io.File; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.integration.support.MessageBuilder; -import org.springframework.messaging.Message; -import org.springframework.messaging.MessageChannel; - -/** - * @author Oleg Zhurakousky - * - */ -public class SftpOutboundTransferSample { - - @Test - @Disabled - public void testOutbound() throws Exception { - ClassPathXmlApplicationContext ac = - new ClassPathXmlApplicationContext("SftpOutboundTransferSample-ignored.xml", SftpOutboundTransferSample.class); - ac.start(); - File file = new File("/Users/ozhurakousky/workspace-sts-2.3.3.M2/si/spring-integration/spring-integration-sftp/local-test-dir/foo.txt"); - if (file.exists()) { - Message message = MessageBuilder.withPayload(file).build(); - MessageChannel inputChannel = ac.getBean("inputChannel", MessageChannel.class); - inputChannel.send(message); - Thread.sleep(2000); - } - ac.close(); - } - -} diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/dsl/SftpTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/dsl/SftpTests.java index 6c2688653bf..c6f851acf1a 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/dsl/SftpTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/dsl/SftpTests.java @@ -20,10 +20,14 @@ import java.io.File; import java.io.InputStream; +import java.nio.file.attribute.PosixFilePermission; import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; +import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.common.SftpHelper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; @@ -50,8 +54,6 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.jcraft.jsch.ChannelSftp; - /** * @author Artem Bilan * @author Gary Russell @@ -68,7 +70,6 @@ public class SftpTests extends SftpTestSupport { private IntegrationFlowContext flowContext; @Test - @SuppressWarnings("unchecked") public void testSftpInboundFlow() { QueueChannel out = new QueueChannel(); IntegrationFlow flow = IntegrationFlow @@ -78,7 +79,7 @@ public void testSftpInboundFlow() { .regexFilter(".*\\.txt$") .localFilenameExpression("#this.toUpperCase() + '.a'") .localDirectory(getTargetLocalDirectory()) - .remoteComparator(Comparator.naturalOrder()), + .remoteComparator(Comparator.comparing(SftpClient.DirEntry::getFilename)), e -> e.id("sftpInboundAdapter").poller(Pollers.fixedDelay(100))) .channel(out) .get(); @@ -122,7 +123,7 @@ public void testSftpInboundStreamFlow() throws Exception { assertThat(message).isNotNull(); assertThat(message.getPayload()).isInstanceOf(InputStream.class); assertThat(message.getHeaders().get(FileHeaders.REMOTE_FILE)).isIn(" sftpSource1.txt", "sftpSource2.txt"); - assertThat(message.getHeaders().get(FileHeaders.REMOTE_HOST_PORT, String.class)).contains("localhost:"); + assertThat(message.getHeaders().get(FileHeaders.REMOTE_HOST_PORT, String.class)).contains("localhost"); ((InputStream) message.getPayload()).close(); new IntegrationMessageHeaderAccessor(message).getCloseableResource().close(); @@ -141,11 +142,11 @@ public void testSftpOutboundFlow() { .setHeader(FileHeaders.FILENAME, fileName) .build()); - RemoteFileTemplate template = new RemoteFileTemplate<>(sessionFactory()); - ChannelSftp.LsEntry[] files = template.execute(session -> - session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); + RemoteFileTemplate template = new RemoteFileTemplate<>(sessionFactory()); + SftpClient.DirEntry[] files = + template.execute(session -> session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); assertThat(files.length).isEqualTo(1); - assertThat(files[0].getAttrs().getSize()).isEqualTo(3); + assertThat(files[0].getAttributes().getSize()).isEqualTo(3); registration.destroy(); } @@ -163,10 +164,10 @@ public void testSftpOutboundFlowSftpTemplate() { .setHeader(FileHeaders.FILENAME, fileName) .build()); - ChannelSftp.LsEntry[] files = sftpTemplate.execute(session -> - session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); + SftpClient.DirEntry[] files = + sftpTemplate.execute(session -> session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); assertThat(files.length).isEqualTo(1); - assertThat(files[0].getAttrs().getSize()).isEqualTo(3); + assertThat(files[0].getAttributes().getSize()).isEqualTo(3); registration.destroy(); } @@ -187,10 +188,10 @@ public void testSftpOutboundFlowSftpTemplateAndMode() { .setHeader(FileHeaders.FILENAME, fileName) .build()); - ChannelSftp.LsEntry[] files = sftpTemplate.execute(session -> - session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); + SftpClient.DirEntry[] files = + sftpTemplate.execute(session -> session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); assertThat(files.length).isEqualTo(1); - assertThat(files[0].getAttrs().getSize()).isEqualTo(6); + assertThat(files[0].getAttributes().getSize()).isEqualTo(6); registration.destroy(); } @@ -210,16 +211,17 @@ public void testSftpOutboundFlowWithChmod() { .setHeader(FileHeaders.FILENAME, fileName) .build()); - RemoteFileTemplate template = new RemoteFileTemplate<>(sessionFactory()); - ChannelSftp.LsEntry[] files = template.execute(session -> - session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); + RemoteFileTemplate template = new RemoteFileTemplate<>(sessionFactory()); + SftpClient.DirEntry[] files = + template.execute(session -> session.list(getTargetRemoteDirectory().getName() + "/" + fileName)); assertThat(files.length).isEqualTo(1); - assertThat(files[0].getAttrs().getSize()).isEqualTo(3); - String[] permissions = files[0].getAttrs().getPermissionsString().substring(1).replaceAll("--", "-").split("-"); - assertThat(permissions[0]).isEqualTo("rw"); - assertThat(permissions[1]).isEqualTo("r"); - assertThat(permissions[2]).isEqualTo("r"); - + assertThat(files[0].getAttributes().getSize()).isEqualTo(3); + int permissionFlags = files[0].getAttributes().getPermissions(); + Set posixFilePermissions = SftpHelper.permissionsToAttributes(permissionFlags); + assertThat(posixFilePermissions) + .contains(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, + PosixFilePermission.GROUP_READ, PosixFilePermission.OTHERS_READ) + .doesNotContain(PosixFilePermission.GROUP_WRITE, PosixFilePermission.OTHERS_WRITE); registration.destroy(); } @@ -264,9 +266,9 @@ public void testSftpSessionCallback() { Message receive = out.receive(10_000); assertThat(receive).isNotNull(); Object payload = receive.getPayload(); - assertThat(payload).isInstanceOf(ChannelSftp.LsEntry[].class); + assertThat(payload).isInstanceOf(SftpClient.DirEntry[].class); - assertThat(((ChannelSftp.LsEntry[]) payload).length > 0).isTrue(); + assertThat(((SftpClient.DirEntry[]) payload).length > 0).isTrue(); registration.destroy(); } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpFileListFilterTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpFileListFilterTests.java index 2c030b34dd0..cb4fff802bb 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpFileListFilterTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpFileListFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 the original author or authors. + * Copyright 2017-2022 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. @@ -21,6 +21,7 @@ import java.io.File; import java.util.List; +import org.apache.sshd.sftp.client.SftpClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -32,10 +33,10 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * @author Gary Russell + * @author Artem Bilan + * * @since 5.0 * */ @@ -50,9 +51,9 @@ public class SftpFileListFilterTests extends SftpTestSupport { public void testMarkerFile() throws Exception { SftpSystemMarkerFilePresentFileListFilter filter = new SftpSystemMarkerFilePresentFileListFilter( new SftpSimplePatternFileListFilter("*.txt")); - LsEntry[] files = template.list("sftpSource"); + SftpClient.DirEntry[] files = template.list("sftpSource"); assertThat(files.length).isGreaterThan(0); - List filtered = filter.filterFiles(files); + List filtered = filter.filterFiles(files); assertThat(filtered.size()).isEqualTo(0); File remoteDir = getSourceRemoteDirectory(); File marker = new File(remoteDir, "sftpSource2.txt.complete"); @@ -68,12 +69,12 @@ public void testMarkerFile() throws Exception { public static class Config { @Bean - public SessionFactory sftpSessionFactory() { + public SessionFactory sftpSessionFactory() { return SftpFileListFilterTests.sessionFactory(); } @Bean - public SftpRemoteFileTemplate remoteFileTempalte() { + public SftpRemoteFileTemplate remoteFileTemplate() { return new SftpRemoteFileTemplate(sftpSessionFactory()); } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilterTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilterTests.java index 7354c382fce..d669ea3d2a0 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilterTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/filters/SftpPersistentAcceptOnceFileListFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -17,23 +17,22 @@ package org.springframework.integration.sftp.filters; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import java.lang.reflect.Constructor; +import java.nio.file.attribute.FileTime; +import java.time.Instant; import java.util.Arrays; import java.util.List; +import org.apache.sshd.sftp.client.SftpClient; import org.junit.jupiter.api.Test; import org.springframework.integration.metadata.SimpleMetadataStore; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.SftpATTRS; - /** * @author Gary Russell * @author David Liu + * @author Artem Bilan + * * @since 4.0.4 * */ @@ -43,18 +42,15 @@ public class SftpPersistentAcceptOnceFileListFilterTests { public void testRollback() throws Exception { SftpPersistentAcceptOnceFileListFilter filter = new SftpPersistentAcceptOnceFileListFilter( new SimpleMetadataStore(), "rollback:"); - ChannelSftp channel = new ChannelSftp(); - SftpATTRS attrs = mock(SftpATTRS.class); - @SuppressWarnings("unchecked") - Constructor ctor = (Constructor) LsEntry.class.getDeclaredConstructors()[0]; - ctor.setAccessible(true); - LsEntry sftpFile1 = ctor.newInstance(channel, "foo", "foo", attrs); - LsEntry sftpFile2 = ctor.newInstance(channel, "bar", "bar", attrs); - LsEntry ftpFile3 = ctor.newInstance(channel, "baz", "baz", attrs); - LsEntry[] files = new LsEntry[] {sftpFile1, sftpFile2, ftpFile3}; - List passed = filter.filterFiles(files); + SftpClient.Attributes attrs = new SftpClient.Attributes(); + attrs.setModifyTime(FileTime.from(Instant.now())); + SftpClient.DirEntry sftpFile1 = new SftpClient.DirEntry("foo", "foo", attrs); + SftpClient.DirEntry sftpFile2 = new SftpClient.DirEntry("bar", "bar", attrs); + SftpClient.DirEntry sftpFile3 = new SftpClient.DirEntry("baz", "baz", attrs); + SftpClient.DirEntry[] files = new SftpClient.DirEntry[]{ sftpFile1, sftpFile2, sftpFile3 }; + List passed = filter.filterFiles(files); assertThat(Arrays.equals(files, passed.toArray())).isTrue(); - List now = filter.filterFiles(files); + List now = filter.filterFiles(files); assertThat(now.size()).isEqualTo(0); filter.rollback(passed.get(1), passed); now = filter.filterFiles(files); @@ -70,15 +66,12 @@ public void testRollback() throws Exception { public void testKeyUsingFileName() throws Exception { SftpPersistentAcceptOnceFileListFilter filter = new SftpPersistentAcceptOnceFileListFilter( new SimpleMetadataStore(), "rollback:"); - ChannelSftp channel = new ChannelSftp(); - SftpATTRS attrs = mock(SftpATTRS.class); - @SuppressWarnings("unchecked") - Constructor ctor = (Constructor) LsEntry.class.getDeclaredConstructors()[0]; - ctor.setAccessible(true); - LsEntry sftpFile1 = ctor.newInstance(channel, "foo", "same", attrs); - LsEntry sftpFile2 = ctor.newInstance(channel, "bar", "same", attrs); - LsEntry[] files = new LsEntry[] {sftpFile1, sftpFile2}; - List now = filter.filterFiles(files); + SftpClient.Attributes attrs = new SftpClient.Attributes(); + attrs.setModifyTime(FileTime.from(Instant.now())); + SftpClient.DirEntry sftpFile1 = new SftpClient.DirEntry("foo", "same", attrs); + SftpClient.DirEntry sftpFile2 = new SftpClient.DirEntry("bar", "same", attrs); + SftpClient.DirEntry[] files = new SftpClient.DirEntry[]{ sftpFile1, sftpFile2 }; + List now = filter.filterFiles(files); assertThat(now.size()).isEqualTo(2); assertThat(now.get(0).getFilename()).isEqualTo("foo"); assertThat(now.get(1).getFilename()).isEqualTo("bar"); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/RollbackLocalFilterTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/RollbackLocalFilterTests.java index 63832b4bbc0..936d63a09ed 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/RollbackLocalFilterTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/RollbackLocalFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-2022 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. @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.sshd.sftp.client.SftpClient; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -34,11 +35,10 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * @author Gary Russell * @author Artem Bilan + * * @since 4.1.7 * */ @@ -85,12 +85,13 @@ public void handle(File in) { this.file = in; latch.countDown(); } + } public static class Config { @Bean - public SessionFactory sftpSessionFactory() { + public SessionFactory sftpSessionFactory() { return RollbackLocalFilterTests.sessionFactory(); } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpInboundRemoteFileSystemSynchronizerTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpInboundRemoteFileSystemSynchronizerTests.java index 05da098f4f7..45eb154a1d4 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpInboundRemoteFileSystemSynchronizerTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpInboundRemoteFileSystemSynchronizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -17,7 +17,7 @@ package org.springframework.integration.sftp.inbound; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -27,13 +27,15 @@ import java.io.File; import java.io.FileInputStream; import java.net.URI; +import java.nio.file.attribute.FileTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Vector; +import org.apache.sshd.sftp.client.SftpClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -53,10 +55,6 @@ import org.springframework.integration.test.util.TestUtils; import org.springframework.messaging.Message; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.SftpATTRS; - /** * @author Oleg Zhurakousky * @author Gunnar Hillert @@ -68,14 +66,9 @@ */ public class SftpInboundRemoteFileSystemSynchronizerTests { - private static final com.jcraft.jsch.Session jschSession = mock(com.jcraft.jsch.Session.class); - @BeforeEach @AfterEach public void cleanup() { - willReturn("::1") - .given(jschSession) - .getHost(); File file = new File("test"); if (file.exists()) { String[] files = file.list(); @@ -104,12 +97,11 @@ public void testCopyFileToLocalDir() throws Exception { PropertiesPersistingMetadataStore store = spy(new PropertiesPersistingMetadataStore()); store.setBaseDirectory("test"); store.afterPropertiesSet(); - SftpPersistentAcceptOnceFileListFilter persistFilter = - new SftpPersistentAcceptOnceFileListFilter(store, "foo"); - List> filters = new ArrayList<>(); + SftpPersistentAcceptOnceFileListFilter persistFilter = new SftpPersistentAcceptOnceFileListFilter(store, "foo"); + List> filters = new ArrayList<>(); filters.add(persistFilter); filters.add(patternFilter); - CompositeFileListFilter filter = new CompositeFileListFilter<>(filters); + CompositeFileListFilter filter = new CompositeFileListFilter<>(filters); synchronizer.setFilter(filter); synchronizer.setBeanFactory(mock(BeanFactory.class)); synchronizer.afterPropertiesSet(); @@ -140,7 +132,7 @@ public void testCopyFileToLocalDir() throws Exception { TestUtils.getPropertyValue(synchronizer, "remoteFileMetadataStore.metadata", Map.class); String next = remoteFileMetadataStore.values().iterator().next(); - assertThat(URI.create(next).getHost()).isEqualTo("[::1]"); + assertThat(URI.create(next).getHost()).isEqualTo("mock.sftp.host"); Message btestFile = ms.receive(); assertThat(btestFile).isNotNull(); @@ -172,43 +164,40 @@ public void testCopyFileToLocalDir() throws Exception { public static class TestSftpSessionFactory extends DefaultSftpSessionFactory { - private final Vector sftpEntries = new Vector<>(); + private List sftpEntries; private void init() { String[] files = new File("remote-test-dir").list(); - for (String fileName : files) { - LsEntry lsEntry = mock(LsEntry.class); - SftpATTRS attributes = mock(SftpATTRS.class); - when(lsEntry.getAttrs()).thenReturn(attributes); - - Calendar calendar = Calendar.getInstance(); - calendar.add(Calendar.DATE, 1); - when(lsEntry.getAttrs().getMTime()) - .thenReturn(Long.valueOf(calendar.getTimeInMillis() / 1000).intValue()); - when(lsEntry.getFilename()).thenReturn(fileName); - when(lsEntry.getLongname()).thenReturn(fileName); - sftpEntries.add(lsEntry); - } + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DATE, 1); + this.sftpEntries = + Arrays.stream(files) + .map((file) -> { + SftpClient.Attributes attributes = spy(new SftpClient.Attributes()); + attributes.setModifyTime(FileTime.fromMillis(calendar.getTimeInMillis())); + given(attributes.isRegularFile()).willReturn(true); + return new SftpClient.DirEntry(file, file, attributes); + }) + .toList(); } @Override public SftpSession getSession() { - if (this.sftpEntries.size() == 0) { - this.init(); + if (this.sftpEntries == null) { + init(); } try { - ChannelSftp channel = mock(ChannelSftp.class); + SftpClient sftpClient = mock(SftpClient.class); String[] files = new File("remote-test-dir").list(); for (String fileName : files) { - when(channel.get("remote-test-dir/" + fileName)) + when(sftpClient.read("remote-test-dir/" + fileName)) .thenReturn(new FileInputStream("remote-test-dir/" + fileName)); } - when(channel.ls("remote-test-dir")).thenReturn(sftpEntries); + when(sftpClient.readDir("remote-test-dir")).thenReturn(this.sftpEntries); - when(jschSession.openChannel("sftp")).thenReturn(channel); - return SftpTestSessionFactory.createSftpSession(jschSession); + return SftpTestSessionFactory.createSftpSession(sftpClient); } catch (Exception e) { throw new RuntimeException("Failed to create mock sftp session", e); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpStreamingMessageSourceTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpStreamingMessageSourceTests.java index 8eebc733837..2c2c28b1261 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpStreamingMessageSourceTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/inbound/SftpStreamingMessageSourceTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.apache.sshd.sftp.client.SftpClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -53,8 +54,6 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * @author Gary Russell * @author Artem Bilan @@ -120,7 +119,7 @@ public void testAllContents() { received = (Message) this.data.receive(10000); assertThat(received).isNotNull(); assertThat(received.getHeaders().get(FileHeaders.REMOTE_FILE_INFO)).isInstanceOf(SftpFileInfo.class); - assertThat(received.getHeaders().get(FileHeaders.REMOTE_HOST_PORT, String.class)).contains("localhost:"); + assertThat(received.getHeaders().get(FileHeaders.REMOTE_HOST_PORT, String.class)).contains("localhost"); this.adapter.stop(); } @@ -172,7 +171,7 @@ public void testMaxFetchLambdaFilter() throws Exception { private SftpStreamingMessageSource buildSource() { SftpStreamingMessageSource messageSource = new SftpStreamingMessageSource(this.config.template(), - Comparator.comparing(LsEntry::getFilename)); + Comparator.comparing(SftpClient.DirEntry::getFilename)); messageSource.setRemoteDirectory("sftpSource/"); messageSource.setBeanFactory(this.context); return messageSource; @@ -205,7 +204,7 @@ public ConcurrentMap metadataMap() { @InboundChannelAdapter(channel = "stream", autoStartup = "false") public MessageSource sftpMessageSource() { SftpStreamingMessageSource messageSource = new SftpStreamingMessageSource(template(), - Comparator.comparing(LsEntry::getFilename)); + Comparator.comparing(SftpClient.DirEntry::getFilename)); messageSource.setFilter( new SftpPersistentAcceptOnceFileListFilter( new SimpleMetadataStore(metadataMap()), "testStreaming")); @@ -225,7 +224,7 @@ public SftpRemoteFileTemplate template() { } @Bean - public SessionFactory ftpSessionFactory() { + public SessionFactory ftpSessionFactory() { return SftpStreamingMessageSourceTests.sessionFactory(); } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpOutboundTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpOutboundTests.java index 0f0f2cb4c7c..ef314b443d3 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpOutboundTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpOutboundTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -22,41 +22,42 @@ import static org.mockito.AdditionalMatchers.not; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; -import java.lang.reflect.Constructor; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.Vector; -import java.util.concurrent.atomic.AtomicInteger; +import org.apache.sshd.common.SshConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.sftp.client.SftpClient; +import org.apache.sshd.sftp.common.SftpConstants; +import org.apache.sshd.sftp.common.SftpException; +import org.apache.sshd.sftp.server.SftpSubsystemFactory; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; -import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.expression.common.LiteralExpression; import org.springframework.integration.file.DefaultFileNameGenerator; import org.springframework.integration.file.remote.FileInfo; import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler; -import org.springframework.integration.file.remote.session.CachingSessionFactory; import org.springframework.integration.file.remote.session.Session; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.sftp.session.DefaultSftpSessionFactory; @@ -70,13 +71,6 @@ import org.springframework.messaging.support.GenericMessage; import org.springframework.util.FileCopyUtils; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.SftpATTRS; -import com.jcraft.jsch.SftpException; - /** * @author Oleg Zhurakousky * @author Gary Russell @@ -85,19 +79,18 @@ */ public class SftpOutboundTests { - private static final com.jcraft.jsch.Session jschSession = mock(com.jcraft.jsch.Session.class); - @Test public void testHandleFileMessage() throws Exception { File targetDir = new File("remote-target-dir"); assertThat(targetDir.exists()).as("target directory does not exist: " + targetDir.getName()).isTrue(); - SessionFactory sessionFactory = new TestSftpSessionFactory(); - FileTransferringMessageHandler handler = new FileTransferringMessageHandler<>(sessionFactory); + SessionFactory sessionFactory = new TestSftpSessionFactory(); + FileTransferringMessageHandler handler = + new FileTransferringMessageHandler<>(sessionFactory); handler.setRemoteDirectoryExpression(new LiteralExpression(targetDir.getName())); DefaultFileNameGenerator fGenerator = new DefaultFileNameGenerator(); fGenerator.setBeanFactory(mock(BeanFactory.class)); - fGenerator.setExpression("payload + '.test'"); + fGenerator.setExpression("payload.name + '.test'"); handler.setFileNameGenerator(fGenerator); handler.setBeanFactory(mock(BeanFactory.class)); handler.afterPropertiesSet(); @@ -118,8 +111,9 @@ public void testHandleStringMessage() throws Exception { if (file.exists()) { file.delete(); } - SessionFactory sessionFactory = new TestSftpSessionFactory(); - FileTransferringMessageHandler handler = new FileTransferringMessageHandler<>(sessionFactory); + SessionFactory sessionFactory = new TestSftpSessionFactory(); + FileTransferringMessageHandler handler = + new FileTransferringMessageHandler<>(sessionFactory); DefaultFileNameGenerator fGenerator = new DefaultFileNameGenerator(); fGenerator.setBeanFactory(mock(BeanFactory.class)); fGenerator.setExpression("'foo.txt'"); @@ -128,7 +122,7 @@ public void testHandleStringMessage() throws Exception { handler.setBeanFactory(mock(BeanFactory.class)); handler.afterPropertiesSet(); - handler.handleMessage(new GenericMessage("String data")); + handler.handleMessage(new GenericMessage<>("String data")); assertThat(new File("remote-target-dir", "foo.txt").exists()).isTrue(); byte[] inFile = FileCopyUtils.copyToByteArray(file); assertThat(new String(inFile)).isEqualTo("String data"); @@ -141,8 +135,9 @@ public void testHandleBytesMessage() throws Exception { if (file.exists()) { file.delete(); } - SessionFactory sessionFactory = new TestSftpSessionFactory(); - FileTransferringMessageHandler handler = new FileTransferringMessageHandler<>(sessionFactory); + SessionFactory sessionFactory = new TestSftpSessionFactory(); + FileTransferringMessageHandler handler = + new FileTransferringMessageHandler<>(sessionFactory); DefaultFileNameGenerator fGenerator = new DefaultFileNameGenerator(); fGenerator.setBeanFactory(mock(BeanFactory.class)); fGenerator.setExpression("'foo.txt'"); @@ -174,7 +169,7 @@ public void testSftpOutboundChannelAdapterInsideChain() throws Exception { MessageChannel channel = context.getBean("outboundChannelAdapterInsideChain", MessageChannel.class); - channel.send(new GenericMessage(srcFile)); + channel.send(new GenericMessage<>(srcFile)); assertThat(destFile.exists()).as("destination file was not created").isTrue(); context.close(); } @@ -203,15 +198,15 @@ public void testFtpOutboundGatewayInsideChain() { context.close(); } - @Test //INT-2954 + @Test + @SuppressWarnings("unchecked") public void testMkDir() throws Exception { - @SuppressWarnings("unchecked") - Session session = mock(Session.class); + Session session = mock(Session.class); when(session.exists(anyString())).thenReturn(Boolean.FALSE); - @SuppressWarnings("unchecked") - SessionFactory sessionFactory = mock(SessionFactory.class); + SessionFactory sessionFactory = mock(SessionFactory.class); when(sessionFactory.getSession()).thenReturn(session); - FileTransferringMessageHandler handler = new FileTransferringMessageHandler<>(sessionFactory); + FileTransferringMessageHandler handler = + new FileTransferringMessageHandler<>(sessionFactory); handler.setAutoCreateDirectory(true); handler.setRemoteDirectoryExpression(new LiteralExpression("/foo/bar/baz")); handler.setBeanFactory(mock(BeanFactory.class)); @@ -228,186 +223,56 @@ public void testMkDir() throws Exception { assertThat(madeDirs.get(2)).isEqualTo("/foo/bar/baz"); } - @Test - public void testSharedSession() throws Exception { - JSch jsch = spy(new JSch()); - Constructor ctor = com.jcraft.jsch.Session.class.getDeclaredConstructor(JSch.class, - String.class, String.class, int.class); - ctor.setAccessible(true); - com.jcraft.jsch.Session jschSession1 = spy(ctor.newInstance(jsch, "foo", "host", 22)); - com.jcraft.jsch.Session jschSession2 = spy(ctor.newInstance(jsch, "foo", "host", 22)); - - willAnswer(invocation -> { - new DirectFieldAccessor(jschSession1).setPropertyValue("isConnected", true); - return null; - }) - .given(jschSession1) - .connect(); - - willAnswer(invocation -> { - new DirectFieldAccessor(jschSession2).setPropertyValue("isConnected", true); - return null; - }) - .given(jschSession2) - .connect(); - - when(jsch.getSession("foo", "host", 22)).thenReturn(jschSession1, jschSession2); - final ChannelSftp channel1 = spy(new ChannelSftp()); - doReturn("channel1").when(channel1).toString(); - final ChannelSftp channel2 = spy(new ChannelSftp()); - doReturn("channel2").when(channel2).toString(); - new DirectFieldAccessor(channel1).setPropertyValue("session", jschSession1); - new DirectFieldAccessor(channel2).setPropertyValue("session", jschSession1); - // Can't use when(session.open()) with a spy - final AtomicInteger n = new AtomicInteger(); - doAnswer(invocation -> n.getAndIncrement() == 0 ? channel1 : channel2).when(jschSession1).openChannel("sftp"); - DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(jsch, true); - factory.setHost("host"); - factory.setUser("foo"); - factory.setPassword("bar"); - noopConnect(channel1); - noopConnect(channel2); - Session s1 = factory.getSession(); - Session s2 = factory.getSession(); - assertThat(TestUtils.getPropertyValue(s2, "jschSession")) - .isSameAs(TestUtils.getPropertyValue(s1, "jschSession")); - assertThat(TestUtils.getPropertyValue(s1, "channel")).isSameAs(channel1); - assertThat(TestUtils.getPropertyValue(s2, "channel")).isSameAs(channel2); - } - - @Test - public void testNotSharedSession() throws Exception { - JSch jsch = spy(new JSch()); - Constructor ctor = - com.jcraft.jsch.Session.class.getDeclaredConstructor(JSch.class, String.class, String.class, - int.class); - ctor.setAccessible(true); - com.jcraft.jsch.Session jschSession1 = spy(ctor.newInstance(jsch, "foo", "host", 22)); - com.jcraft.jsch.Session jschSession2 = spy(ctor.newInstance(jsch, "foo", "host", 22)); - new DirectFieldAccessor(jschSession1).setPropertyValue("isConnected", true); - new DirectFieldAccessor(jschSession2).setPropertyValue("isConnected", true); - when(jsch.getSession("foo", "host", 22)).thenReturn(jschSession1, jschSession2); - ChannelSftp channel1 = spy(new ChannelSftp()); - ChannelSftp channel2 = spy(new ChannelSftp()); - new DirectFieldAccessor(channel1).setPropertyValue("session", jschSession1); - new DirectFieldAccessor(channel2).setPropertyValue("session", jschSession1); - doReturn(channel1).when(jschSession1).openChannel("sftp"); - doReturn(channel2).when(jschSession2).openChannel("sftp"); - DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(jsch, false); - factory.setHost("host"); - factory.setUser("foo"); - factory.setPassword("bar"); - noopConnect(channel1); - noopConnect(channel2); - Session s1 = factory.getSession(); - Session s2 = factory.getSession(); - assertThat(TestUtils.getPropertyValue(s2, "jschSession")) - .isNotSameAs(TestUtils.getPropertyValue(s1, "jschSession")); - assertThat(TestUtils.getPropertyValue(s1, "channel")).isSameAs(channel1); - assertThat(TestUtils.getPropertyValue(s2, "channel")).isSameAs(channel2); - } - - @Test - public void testSharedSessionCachedReset() throws Exception { - JSch jsch = spy(new JSch()); - Constructor ctor = - com.jcraft.jsch.Session.class.getDeclaredConstructor(JSch.class, String.class, String.class, - int.class); - ctor.setAccessible(true); - com.jcraft.jsch.Session jschSession1 = spy(ctor.newInstance(jsch, "foo", "host", 22)); - com.jcraft.jsch.Session jschSession2 = spy(ctor.newInstance(jsch, "foo", "host", 22)); - - willAnswer(invocation -> { - new DirectFieldAccessor(jschSession1).setPropertyValue("isConnected", true); - return null; - }) - .given(jschSession1) - .connect(); - - willAnswer(invocation -> { - new DirectFieldAccessor(jschSession2).setPropertyValue("isConnected", true); - return null; - }) - .given(jschSession2) - .connect(); - - when(jsch.getSession("foo", "host", 22)).thenReturn(jschSession1, jschSession2); - final ChannelSftp channel1 = spy(new ChannelSftp()); - doReturn("channel1").when(channel1).toString(); - final ChannelSftp channel2 = spy(new ChannelSftp()); - doReturn("channel2").when(channel2).toString(); - final ChannelSftp channel3 = spy(new ChannelSftp()); - doReturn("channel3").when(channel3).toString(); - final ChannelSftp channel4 = spy(new ChannelSftp()); - doReturn("channel4").when(channel4).toString(); - new DirectFieldAccessor(channel1).setPropertyValue("session", jschSession1); - new DirectFieldAccessor(channel2).setPropertyValue("session", jschSession1); - // Can't use when(session.open()) with a spy - final AtomicInteger n = new AtomicInteger(); - doAnswer(invocation -> n.getAndIncrement() == 0 ? channel1 : channel2).when(jschSession1).openChannel("sftp"); - doAnswer(invocation -> n.getAndIncrement() < 3 ? channel3 : channel4).when(jschSession2).openChannel("sftp"); - DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(jsch, true); - factory.setHost("host"); - factory.setUser("foo"); - factory.setPassword("bar"); - CachingSessionFactory cachedFactory = new CachingSessionFactory(factory); - noopConnect(channel1); - noopConnect(channel2); - noopConnect(channel3); - noopConnect(channel4); - Session s1 = cachedFactory.getSession(); - Session s2 = cachedFactory.getSession(); - assertThat(TestUtils.getPropertyValue(s2, "targetSession.jschSession")).isSameAs(jschSession1); - assertThat(TestUtils.getPropertyValue(s1, "targetSession.channel")).isSameAs(channel1); - assertThat(TestUtils.getPropertyValue(s2, "targetSession.channel")).isSameAs(channel2); - assertThat(TestUtils.getPropertyValue(s2, "targetSession.jschSession")) - .isSameAs(TestUtils.getPropertyValue(s1, "targetSession.jschSession")); - s1.close(); - Session s3 = cachedFactory.getSession(); - assertThat(TestUtils.getPropertyValue(s3, "targetSession")) - .isSameAs(TestUtils.getPropertyValue(s1, "targetSession")); - assertThat(TestUtils.getPropertyValue(s3, "targetSession.channel")).isSameAs(channel1); - s3.close(); - cachedFactory.resetCache(); - verify(jschSession1, never()).disconnect(); - s3 = cachedFactory.getSession(); - assertThat(TestUtils.getPropertyValue(s3, "targetSession.jschSession")).isSameAs(jschSession2); - assertThat(TestUtils.getPropertyValue(s3, "targetSession")) - .isNotSameAs(TestUtils.getPropertyValue(s1, "targetSession")); - assertThat(TestUtils.getPropertyValue(s3, "targetSession.channel")).isSameAs(channel3); - s2.close(); - verify(jschSession1).disconnect(); - s2 = cachedFactory.getSession(); - assertThat(TestUtils.getPropertyValue(s2, "targetSession.jschSession")).isSameAs(jschSession2); - assertThat(TestUtils.getPropertyValue(s2, "targetSession")) - .isNotSameAs(TestUtils.getPropertyValue(s3, "targetSession")); - assertThat(TestUtils.getPropertyValue(s2, "targetSession.channel")).isSameAs(channel4); - s2.close(); - s3.close(); - verify(jschSession2, never()).disconnect(); - cachedFactory.resetCache(); - verify(jschSession2).disconnect(); + @ParameterizedTest + @ValueSource(booleans = { true, false }) + public void testSharedSession(boolean sharedSession) throws IOException { + try (SshServer server = SshServer.setUpDefaultServer()) { + server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); + server.setPort(0); + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); + 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))); + server.start(); + + DefaultSftpSessionFactory f = new DefaultSftpSessionFactory(sharedSession); + f.setHost("localhost"); + f.setPort(server.getPort()); + f.setUser("user"); + f.setPassword("pass"); + f.setAllowUnknownKeys(true); + + Session s1 = f.getSession(); + Session s2 = f.getSession(); + if (sharedSession) { + assertThat(TestUtils.getPropertyValue(s2, "sftpClient")) + .isSameAs(TestUtils.getPropertyValue(s1, "sftpClient")); + } + else { + assertThat(TestUtils.getPropertyValue(s2, "sftpClient")) + .isNotSameAs(TestUtils.getPropertyValue(s1, "sftpClient")); + } + } } @Test - public void testExists() throws SftpException, IOException { - ChannelSftp channelSftp = mock(ChannelSftp.class); + public void testExists() throws IOException { + SftpClient sftpClient = mock(SftpClient.class); - willReturn(mock(SftpATTRS.class)) - .given(channelSftp) + willReturn(new SftpClient.Attributes()) + .given(sftpClient) .lstat(eq("exist")); - willThrow(new SftpException(ChannelSftp.SSH_FX_NO_SUCH_FILE, "Path does not exist.")) - .given(channelSftp) + willThrow(new SftpException(SftpConstants.SSH_FX_NO_SUCH_FILE, "notExist")) + .given(sftpClient) .lstat(eq("notExist")); - willThrow(new SftpException(ChannelSftp.SSH_FX_CONNECTION_LOST, "Connection lost.")) - .given(channelSftp) + willThrow(new SshException(SshConstants.SSH_OPEN_CONNECT_FAILED, "Connection lost.")) + .given(sftpClient) .lstat(and(not(eq("exist")), not(eq("notExist")))); - SftpSession sftpSession = new SftpSession(mock(com.jcraft.jsch.Session.class)); - DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(sftpSession); - fieldAccessor.setPropertyValue("channel", channelSftp); + SftpSession sftpSession = new SftpSession(sftpClient); assertThat(sftpSession.exists("exist")).isTrue(); @@ -417,23 +282,18 @@ public void testExists() throws SftpException, IOException { isThrownBy(() -> sftpSession.exists("foo")); } - private void noopConnect(ChannelSftp channel1) throws JSchException { - doNothing().when(channel1).connect(5000); - } - public static class TestSftpSessionFactory extends DefaultSftpSessionFactory { @Override public SftpSession getSession() { try { - ChannelSftp channel = mock(ChannelSftp.class); + SftpClient sftpClient = mock(SftpClient.class); doAnswer(invocation -> { - File file = new File((String) invocation.getArgument(1)); + File file = new File((String) invocation.getArgument(0)); assertThat(file.getName()).endsWith(".writing"); - FileCopyUtils.copy((InputStream) invocation.getArgument(0), new FileOutputStream(file)); - return null; - }).when(channel).put(Mockito.any(InputStream.class), Mockito.anyString()); + return new FileOutputStream(file); + }).when(sftpClient).write(Mockito.anyString()); doAnswer(invocation -> { File file = new File((String) invocation.getArgument(0)); @@ -441,21 +301,16 @@ public SftpSession getSession() { File renameToFile = new File((String) invocation.getArgument(1)); file.renameTo(renameToFile); return null; - }).when(channel).rename(Mockito.anyString(), Mockito.anyString()); + }).when(sftpClient).rename(Mockito.anyString(), Mockito.anyString(), eq(SftpClient.CopyMode.Overwrite)); String[] files = new File("remote-test-dir").list(); - Vector sftpEntries = new Vector<>(); - for (String fileName : files) { - LsEntry lsEntry = mock(LsEntry.class); - SftpATTRS attributes = mock(SftpATTRS.class); - when(lsEntry.getAttrs()).thenReturn(attributes); - when(lsEntry.getFilename()).thenReturn(fileName); - sftpEntries.add(lsEntry); - } - when(channel.ls("remote-test-dir/")).thenReturn(sftpEntries); - - when(jschSession.openChannel("sftp")).thenReturn(channel); - return SftpTestSessionFactory.createSftpSession(jschSession); + List dirEntries = + Arrays.stream(files) + .map((file) -> new SftpClient.DirEntry(file, file, new SftpClient.Attributes())) + .toList(); + when(sftpClient.readDir("remote-test-dir")).thenReturn(dirEntries); + + return SftpTestSessionFactory.createSftpSession(sftpClient); } catch (Exception e) { throw new RuntimeException("Failed to create mock sftp session", e); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java index 1c67c3067f5..169b7e07e4f 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/outbound/SftpServerOutboundTests.java @@ -19,6 +19,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -40,6 +44,7 @@ import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; +import org.apache.sshd.sftp.client.SftpClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -78,9 +83,6 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.FileCopyUtils; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * @author Artem Bilan * @author Gary Russell @@ -128,7 +130,7 @@ public class SftpServerOutboundTests extends SftpTestSupport { private DirectChannel inboundMPutRecursiveFiltered; @Autowired - private SessionFactory sessionFactory; + private SessionFactory sessionFactory; @Autowired private DirectChannel appending; @@ -180,8 +182,6 @@ public void testInt2866LocalDirectoryExpressionGET() { assertThat(localFile.getPath().replaceAll(Matcher.quoteReplacement(File.separator), "/")) .contains(dir.toUpperCase()); Session session2 = this.sessionFactory.getSession(); - assertThat(TestUtils.getPropertyValue(session2, "targetSession.jschSession")) - .isSameAs(TestUtils.getPropertyValue(session, "targetSession.jschSession")); } @Test @@ -284,10 +284,10 @@ void testLSRecursive() { assertThat(files.stream() .map(fi -> fi.getFilename()) .collect(Collectors.toList())).contains( - " sftpSource1.txt", - "sftpSource2.txt", - "subSftpSource", - "subSftpSource/subSftpSource1.txt"); + " sftpSource1.txt", + "sftpSource2.txt", + "subSftpSource", + "subSftpSource/subSftpSource1.txt"); } @Test @@ -302,14 +302,14 @@ void testLSRecursiveALL() { assertThat(files.stream() .map(fi -> fi.getFilename()) .collect(Collectors.toList())).contains( - " sftpSource1.txt", - "sftpSource2.txt", - "subSftpSource", - "subSftpSource/subSftpSource1.txt", - ".", - "..", - "subSftpSource/.", - "subSftpSource/.."); + " sftpSource1.txt", + "sftpSource2.txt", + "subSftpSource", + "subSftpSource/subSftpSource1.txt", + ".", + "..", + "subSftpSource/.", + "subSftpSource/.."); } @Test @@ -324,9 +324,9 @@ void testLSRecursiveNoDirs() throws IOException { assertThat(files.stream() .map(fi -> fi.getFilename()) .collect(Collectors.toList())).contains( - " sftpSource1.txt", - "sftpSource2.txt", - "subSftpSource/subSftpSource1.txt"); + " sftpSource1.txt", + "sftpSource2.txt", + "subSftpSource/subSftpSource1.txt"); File newDeepFile = new File(this.sourceRemoteDirectory + "/subSftpSource/subSftpSource2.txt"); OutputStream fos = new FileOutputStream(newDeepFile); fos.write("test".getBytes()); @@ -474,8 +474,9 @@ public void testInt3088MPutNotRecursive() throws Exception { Session session = sessionFactory.getSession(); session.close(); session = TestUtils.getPropertyValue(session, "targetSession", Session.class); - ChannelSftp channel = spy(TestUtils.getPropertyValue(session, "channel", ChannelSftp.class)); - new DirectFieldAccessor(session).setPropertyValue("channel", channel); + SftpClient sftpClient = spy(TestUtils.getPropertyValue(session, "sftpClient", SftpClient.class)); + doNothing().when(sftpClient).setStat(anyString(), any(SftpClient.Attributes.class)); + new DirectFieldAccessor(session).setPropertyValue("sftpClient", sftpClient); String dir = "sftpSource/"; this.inboundMGetRecursive.send(new GenericMessage(dir + "*")); @@ -492,8 +493,8 @@ public void testInt3088MPutNotRecursive() throws Exception { .isIn("sftpTarget/localSource1.txt", "sftpTarget/localSource2.txt"); assertThat(out.getPayload().get(1)) .isIn("sftpTarget/localSource1.txt", "sftpTarget/localSource2.txt"); - verify(channel).chmod(0600, "sftpTarget/localSource1.txt"); - verify(channel).chmod(0600, "sftpTarget/localSource2.txt"); + verify(sftpClient).setStat(eq("sftpTarget/localSource1.txt"), any(SftpClient.Attributes.class)); + verify(sftpClient).setStat(eq("sftpTarget/localSource2.txt"), any(SftpClient.Attributes.class)); resetSessionCache(); assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.config.events).hasSize(6); @@ -676,9 +677,10 @@ public void testMessageSessionCallback() { } private void assertLength6(SftpRemoteFileTemplate template) { - LsEntry[] files = template.execute(session -> session.list("sftpTarget/appending.txt")); - assertThat(files.length).isEqualTo(1); - assertThat(files[0].getAttrs().getSize()).isEqualTo(6); + SftpClient.DirEntry[] files = template.execute(session -> session.list("sftpTarget")); + assertThat(files.length).isEqualTo(3); + assertThat(files[2].getFilename()).isEqualTo("appending.txt"); + assertThat(files[2].getAttributes().getSize()).isEqualTo(6); } @Test @@ -689,7 +691,7 @@ public void testSessionExists() throws IOException { sessionFactory.setUser("foo"); sessionFactory.setPassword("foo"); sessionFactory.setAllowUnknownKeys(true); - Session session = sessionFactory.getSession(); + Session session = sessionFactory.getSession(); assertThat(session.exists("sftpSource")).isTrue(); assertThat(session.exists("notExist")).isFalse(); @@ -699,15 +701,15 @@ public void testSessionExists() throws IOException { assertThatExceptionOfType(UncheckedIOException.class) .isThrownBy(() -> session.exists("any")) .withRootCauseInstanceOf(IOException.class) - .withStackTraceContaining("Pipe closed"); + .withStackTraceContaining("lstat(any) client is closed"); } @SuppressWarnings("unused") private static final class TestMessageSessionCallback - implements MessageSessionCallback { + implements MessageSessionCallback { @Override - public Object doInSession(Session session, Message requestMessage) { + public Object doInSession(Session session, Message requestMessage) { return ((String) requestMessage.getPayload()).toUpperCase(); } @@ -722,13 +724,13 @@ public static class Config { private volatile CountDownLatch latch; @Bean - public SessionFactory sftpSessionFactory(ApplicationContext context) { + public SessionFactory sftpSessionFactory(ApplicationContext context) { SftpServerOutboundTests.eventListener().setApplicationEventPublisher(context); return SftpServerOutboundTests.sessionFactory(); } @Bean - public SftpRemoteFileTemplate template(SessionFactory sf) { + public SftpRemoteFileTemplate template(SessionFactory sf) { return new SftpRemoteFileTemplate(sf); } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/ProxyTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/ProxyTests.java deleted file mode 100644 index 9f143c2babf..00000000000 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/ProxyTests.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2016-2020 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. - * You may obtain a copy of the License at - * - * https://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.springframework.integration.sftp.session; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.integration.test.util.TestUtils; - -import com.jcraft.jsch.Proxy; -import com.jcraft.jsch.ProxyHTTP; -import com.jcraft.jsch.ProxySOCKS4; -import com.jcraft.jsch.ProxySOCKS5; - -/** - * @author Gary Russell - * @since 4.3 - * - */ -public class ProxyTests { - - @Test - @Disabled // TODO Use SftpTestSupport - /* - * Needs host and account - */ - public void testSimpleConnect() { - DefaultSftpSessionFactory sf = new DefaultSftpSessionFactory(); - sf.setHost("10.0.0.3"); - sf.setPort(22); - sf.setUser("ftptest"); - sf.setPassword("ftptest"); - sf.setAllowUnknownKeys(true); - sf.getSession().close(); - } - - @Test - @Disabled - /* - * Needs host and account and... - * $ ssh -D 1080 -f -N gpr@10.0.0.3 - */ - public void testProxyConnect() throws Exception { - DefaultSftpSessionFactory sf = new DefaultSftpSessionFactory(); - JschProxyFactoryBean proxyFactoryBean = new JschProxyFactoryBean(JschProxyFactoryBean.Type.SOCKS5, "localhost", - 1080, "ftptest", "ftptest"); - proxyFactoryBean.afterPropertiesSet(); - sf.setHost("10.0.0.3"); - sf.setPort(22); - sf.setUser("ftptest"); - sf.setPassword("ftptest"); - sf.setProxy(proxyFactoryBean.getObject()); - sf.setAllowUnknownKeys(true); - sf.getSession().close(); - } - - @Test - public void testFactoryBean() throws Exception { - JschProxyFactoryBean proxyFactoryBean = new JschProxyFactoryBean(JschProxyFactoryBean.Type.SOCKS5, "localhost", - 1080, "ftptest", "pass"); - proxyFactoryBean.afterPropertiesSet(); - Proxy proxy = proxyFactoryBean.getObject(); - assertProxy(proxy, ProxySOCKS5.class); - - proxyFactoryBean = new JschProxyFactoryBean(JschProxyFactoryBean.Type.SOCKS4, "localhost", - 1080, "ftptest", "pass"); - proxyFactoryBean.afterPropertiesSet(); - proxy = proxyFactoryBean.getObject(); - assertProxy(proxy, ProxySOCKS4.class); - - proxyFactoryBean = new JschProxyFactoryBean(JschProxyFactoryBean.Type.HTTP, "localhost", - 1080, "ftptest", "pass"); - proxyFactoryBean.afterPropertiesSet(); - proxy = proxyFactoryBean.getObject(); - assertProxy(proxy, ProxyHTTP.class); - } - - private void assertProxy(Proxy proxy, Class clazz) { - assertThat(proxy).isInstanceOf(clazz); - assertThat(TestUtils.getPropertyValue(proxy, "user")).isEqualTo("ftptest"); - assertThat(TestUtils.getPropertyValue(proxy, "passwd")).isEqualTo("pass"); - } - -} diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/ResourceKnownHostsServerKeyVerifierTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/ResourceKnownHostsServerKeyVerifierTests.java new file mode 100644 index 00000000000..34a80feb98d --- /dev/null +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/ResourceKnownHostsServerKeyVerifierTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2022 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. + * You may obtain a copy of the License at + * + * https://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.springframework.integration.sftp.session; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.sshd.client.ClientFactoryManager; +import org.apache.sshd.client.config.hosts.HostPatternsHolder; +import org.apache.sshd.client.config.hosts.KnownHostEntry; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.Factory; +import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; +import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; +import org.apache.sshd.common.random.JceRandomFactory; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import org.mockito.Mockito; + +import org.springframework.core.io.FileSystemResource; + +/** + * + * @author Artem Bilan + * + * @since 6.0 + */ +@EnabledIf("isDefaultKnownHostsFilePresent") +public class ResourceKnownHostsServerKeyVerifierTests { + + private static final Map HOST_KEYS = new TreeMap<>(SshdSocketAddress.BY_HOST_AND_PORT); + + @BeforeAll + static void loadHostKeys() throws GeneralSecurityException, IOException { + Map hostsEntries = loadEntries(KnownHostEntry.getDefaultKnownHostsFile()); + for (Map.Entry ke : hostsEntries.entrySet()) { + SshdSocketAddress hostIdentity = ke.getKey(); + KnownHostEntry entry = ke.getValue(); + AuthorizedKeyEntry authEntry = entry.getKeyEntry(); + PublicKey key = authEntry.resolvePublicKey(null, Collections.emptyMap(), PublicKeyEntryResolver.FAILING); + HOST_KEYS.put(hostIdentity, key); + } + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + void testServerKeys() { + ResourceKnownHostsServerKeyVerifier verifier + = new ResourceKnownHostsServerKeyVerifier( + new FileSystemResource(KnownHostEntry.getDefaultKnownHostsFile())); + + ClientFactoryManager manager = Mockito.mock(ClientFactoryManager.class); + Mockito.when(manager.getRandomFactory()).thenReturn((Factory) JceRandomFactory.INSTANCE); + + HOST_KEYS.forEach((key, value) -> { + ClientSession session = Mockito.mock(ClientSession.class); + Mockito.when(session.getFactoryManager()).thenReturn(manager); + + Mockito.when(session.getConnectAddress()).thenReturn(key); + assertThat(verifier.verifyServerKey(session, key, value)).isTrue(); + }); + } + + private static Map loadEntries(Path file) throws IOException { + Collection entries = KnownHostEntry.readKnownHostEntries(file); + if (GenericUtils.isEmpty(entries)) { + return Collections.emptyMap(); + } + + Map hostsMap = new TreeMap<>(SshdSocketAddress.BY_HOST_AND_PORT); + for (KnownHostEntry entry : entries) { + String line = entry.getConfigLine(); + // extract hosts + int pos = line.indexOf(' '); + String patterns = line.substring(0, pos); + if (entry.getHashedEntry() != null) { + hostsMap.put(new SshdSocketAddress("localhost", 0), entry); + } + else { + String[] addrs = GenericUtils.split(patterns, ','); + for (String a : addrs) { + int port = 0; + if (a.charAt(0) == HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM) { + pos = a.indexOf(HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM, 1); + + port = Integer.parseInt(a.substring(pos + 2)); + a = a.substring(1, pos); + } + hostsMap.put(new SshdSocketAddress(a, port), entry); + } + } + } + + return hostsMap; + } + + static boolean isDefaultKnownHostsFilePresent() { + return KnownHostEntry.getDefaultKnownHostsFile().toFile().exists(); + } + +} diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java index c96c773a338..fee997e7f46 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -20,10 +20,11 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.mock; +import java.io.IOException; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; +import org.apache.sshd.sftp.client.SftpClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; @@ -44,11 +45,6 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.ChannelSftp.LsEntry; -import com.jcraft.jsch.SftpATTRS; -import com.jcraft.jsch.SftpException; - /** * @author Gary Russell * @author Artem Bilan @@ -61,7 +57,7 @@ public class SftpRemoteFileTemplateTests extends SftpTestSupport { @Autowired - private CachingSessionFactory sessionFactory; + private CachingSessionFactory sessionFactory; @Test public void testINT3412AppendStatRmdir() { @@ -82,24 +78,24 @@ public void testINT3412AppendStatRmdir() { template.append(new GenericMessage<>("foo")); template.append(new GenericMessage<>("bar")); assertThat(template.exists("foo/foobar.txt")).isTrue(); - template.executeWithClient((ClientCallbackWithoutResult) client -> { + template.executeWithClient((ClientCallbackWithoutResult) client -> { try { - SftpATTRS file = client.lstat("foo/foobar.txt"); + SftpClient.Attributes file = client.lstat("foo/foobar.txt"); assertThat(file.getSize()).isEqualTo(6); } - catch (SftpException e) { + catch (IOException e) { throw new RuntimeException(e); } }); - template.execute((SessionCallbackWithoutResult) session -> { - LsEntry[] files = session.list("foo/"); + template.execute((SessionCallbackWithoutResult) session -> { + SftpClient.DirEntry[] files = session.list("foo/"); assertThat(files.length).isEqualTo(4); assertThat(session.remove("foo/foobar.txt")).isTrue(); assertThat(session.rmdir("foo/bar/")).isTrue(); files = session.list("foo/"); assertThat(files.length).isEqualTo(2); - List list = Arrays.asList(files); - assertThat(list.stream().map(l -> l.getFilename()).collect(Collectors.toList())).contains(".", ".."); + List fileNames = Arrays.stream(files).map(SftpClient.DirEntry::getFilename).toList(); + assertThat(fileNames).contains(".", ".."); assertThat(session.rmdir("foo/")).isTrue(); }); assertThat(template.exists("foo")).isFalse(); @@ -107,8 +103,8 @@ public void testINT3412AppendStatRmdir() { @Test public void testNoDeadLockOnSend() { - CachingSessionFactory cachingSessionFactory = new CachingSessionFactory<>(sessionFactory(), 1); - SftpRemoteFileTemplate template = new SftpRemoteFileTemplate(cachingSessionFactory); + CachingSessionFactory sessionFactory = new CachingSessionFactory<>(sessionFactory(), 1); + SftpRemoteFileTemplate template = new SftpRemoteFileTemplate(sessionFactory); template.setRemoteDirectoryExpression(new LiteralExpression("")); template.setBeanFactory(mock(BeanFactory.class)); template.setUseTemporaryFileName(false); @@ -125,14 +121,14 @@ public void testNoDeadLockOnSend() { .withCauseInstanceOf(MessagingException.class) .withStackTraceContaining("he destination file already exists at 'test.file'."); - cachingSessionFactory.destroy(); + sessionFactory.destroy(); } @Configuration public static class Config { @Bean - public SessionFactory ftpSessionFactory() { + public SessionFactory ftpSessionFactory() { return SftpRemoteFileTemplateTests.sessionFactory(); } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpServerTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpServerTests.java index a3ff03e05b9..98a7d54c78d 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpServerTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2022 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. @@ -35,6 +35,7 @@ import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.sftp.client.SftpClient; import org.apache.sshd.sftp.server.SftpSubsystemFactory; import org.junit.jupiter.api.Test; @@ -44,8 +45,6 @@ import org.springframework.util.Base64Utils; import org.springframework.util.FileCopyUtils; -import com.jcraft.jsch.ChannelSftp.LsEntry; - /** * * * @author Gary Russell @@ -59,8 +58,7 @@ public class SftpServerTests { @Test public void testUcPw() throws Exception { - SshServer server = SshServer.setUpDefaultServer(); - try { + try (SshServer server = SshServer.setUpDefaultServer()) { server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); server.setPort(0); server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); @@ -76,12 +74,9 @@ public void testUcPw() throws Exception { f.setUser("user"); f.setPassword("pass"); f.setAllowUnknownKeys(true); - Session session = f.getSession(); + Session session = f.getSession(); doTest(server, session); } - finally { - server.stop(true); - } } @Test @@ -94,11 +89,9 @@ public void testPubPrivKeyPassphrase() throws Exception { testKeyExchange("id_rsa_pp.pub", "id_rsa_pp", "secret"); } - private void testKeyExchange(String pubKey, String privKey, String passphrase) - throws Exception { - SshServer server = SshServer.setUpDefaultServer(); + private void testKeyExchange(String pubKey, String privKey, String passphrase) throws Exception { final PublicKey allowedKey = decodePublicKey(pubKey); - try { + try (SshServer server = SshServer.setUpDefaultServer()) { server.setPublickeyAuthenticator((username, key, session) -> key.equals(allowedKey)); server.setPort(0); server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); @@ -116,12 +109,9 @@ private void testKeyExchange(String pubKey, String privKey, String passphrase) InputStream stream = new ClassPathResource(privKey).getInputStream(); f.setPrivateKey(new ByteArrayResource(FileCopyUtils.copyToByteArray(stream))); f.setPrivateKeyPassphrase(passphrase); - Session session = f.getSession(); + Session session = f.getSession(); doTest(server, session); } - finally { - server.stop(true); - } } private PublicKey decodePublicKey(String key) throws Exception { @@ -155,12 +145,15 @@ private BigInteger decodeBigInt(ByteBuffer bb) { return new BigInteger(bytes); } - protected void doTest(SshServer server, Session session) throws IOException { + protected void doTest(SshServer server, Session session) throws IOException { assertThat(server.getActiveSessions().size()).isEqualTo(1); - LsEntry[] list = session.list("."); - if (list.length > 0) { - session.remove("*"); + SftpClient.DirEntry[] list = session.list("."); + for (SftpClient.DirEntry entry : list) { + if (entry.getAttributes().isRegularFile()) { + session.remove(entry.getFilename()); + } } + session.write(new ByteArrayInputStream("foo".getBytes()), "bar"); list = session.list("."); assertThat(list[1].getFilename()).isEqualTo("bar"); diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpSessionFactoryTests.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpSessionFactoryTests.java index bf55210e2c0..a048af3bec8 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpSessionFactoryTests.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpSessionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2022 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. @@ -18,28 +18,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.io.File; -import java.io.IOException; import java.net.ConnectException; -import java.time.Duration; -import java.util.Collections; +import org.apache.sshd.common.SshException; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; -import org.apache.sshd.sftp.server.SftpSubsystemFactory; import org.junit.jupiter.api.Test; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.integration.test.util.TestUtils; - -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.UserInfo; - /** * @author Gary Russell * @author Artem Bilan @@ -54,8 +41,7 @@ public class SftpSessionFactoryTests { */ @Test public void testConnectFailSocketOpen() throws Exception { - SshServer server = SshServer.setUpDefaultServer(); - try { + try (SshServer server = SshServer.setUpDefaultServer()) { server.setPasswordAuthenticator((arg0, arg1, arg2) -> true); server.setPort(0); server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); @@ -69,24 +55,22 @@ public void testConnectFailSocketOpen() throws Exception { int n = 0; while (true) { try { - f.getSession(); + f.getSession().connect(); fail("Expected Exception"); } catch (Exception e) { if (e instanceof IllegalStateException && "failed to create SFTP Session".equals(e.getMessage())) { if (e.getCause() instanceof IllegalStateException) { - if (e.getCause().getCause() instanceof JSchException) { - if (e.getCause().getCause().getCause() instanceof ConnectException) { - assertThat(n++ < 100).as("Server failed to start in 10 seconds").isTrue(); - Thread.sleep(100); - continue; - } + if (e.getCause().getCause() instanceof ConnectException) { + assertThat(n++ < 100).as("Server failed to start in 10 seconds").isTrue(); + Thread.sleep(100); + continue; } } } assertThat(e).isInstanceOf(IllegalStateException.class); - assertThat(e.getCause()).isInstanceOf(IllegalStateException.class); - assertThat(e.getCause().getMessage()).isEqualTo("failed to connect"); + assertThat(e.getCause()).isInstanceOf(SshException.class); + assertThat(e.getCause().getMessage()).isEqualTo("Server key did not validate"); break; } } @@ -98,130 +82,6 @@ public void testConnectFailSocketOpen() throws Exception { assertThat(server.getActiveSessions().size()).isEqualTo(0); } - finally { - server.stop(true); - } - } - - @Test - public void testPasswordPassPhraseViaUserInfo() { - DefaultSftpSessionFactory f = new DefaultSftpSessionFactory(); - f.setUser("user"); - f.setAllowUnknownKeys(true); - UserInfo ui = mock(UserInfo.class); - when(ui.getPassword()).thenReturn("pass"); - when(ui.getPassphrase()).thenReturn("pp").thenReturn(null); - f.setUserInfo(ui); - UserInfo userInfo = TestUtils.getPropertyValue(f, "userInfoWrapper", UserInfo.class); - assertThat(userInfo.getPassword()).isEqualTo("pass"); - f.setPassword("foo"); - try { - userInfo.getPassword(); - fail("expected Exception"); - } - catch (IllegalStateException e) { - assertThat(e.getMessage()).startsWith("When a 'UserInfo' is provided, 'password' is not allowed"); - } - assertThat(userInfo.getPassphrase()).isEqualTo("pp"); - f.setPrivateKeyPassphrase("bar"); - try { - userInfo.getPassphrase(); - fail("expected Exception"); - } - catch (IllegalStateException e) { - assertThat(e - .getMessage()).startsWith("When a 'UserInfo' is provided, 'privateKeyPassphrase' is not allowed"); - } - f.setUserInfo(null); - assertThat(userInfo.getPassword()).isEqualTo("foo"); - assertThat(userInfo.getPassphrase()).isEqualTo("bar"); - } - - @Test - public void testDefaultUserInfoFalse() throws Exception { - SshServer server = SshServer.setUpDefaultServer(); - try { - DefaultSftpSessionFactory f = createServerAndClient(server); - expectReject(f); - } - finally { - server.stop(true); - } - } - - @Test - public void testDefaultUserInfoTrue() throws Exception { - SshServer server = SshServer.setUpDefaultServer(); - try { - DefaultSftpSessionFactory f = createServerAndClient(server); - f.setChannelConnectTimeout(Duration.ofSeconds(6)); - f.setAllowUnknownKeys(true); - SftpSession session = f.getSession(); - assertThat(TestUtils.getPropertyValue(session, "channelConnectTimeout", Integer.class)).isEqualTo(6_000); - session.close(); - } - finally { - server.stop(true); - } - } - - @Test - public void testCustomUserInfoFalse() throws Exception { - SshServer server = SshServer.setUpDefaultServer(); - try { - DefaultSftpSessionFactory f = createServerAndClient(server); - UserInfo userInfo = mock(UserInfo.class); - when(userInfo.promptYesNo(anyString())).thenReturn(false); - f.setUserInfo(userInfo); - expectReject(f); - } - finally { - server.stop(true); - } - } - - private void expectReject(DefaultSftpSessionFactory f) { - try { - f.getSession().close(); - fail("Expected Exception"); - } - catch (Exception e) { - assertThat(e).isInstanceOf(IllegalStateException.class); - assertThat(e.getCause()).isInstanceOf(IllegalStateException.class); - assertThat(e.getCause().getCause()).isInstanceOf(JSchException.class); - assertThat(e.getCause().getCause().getMessage()).contains("reject HostKey"); - } - } - - @Test - public void testCustomUserInfoTrue() throws Exception { - SshServer server = SshServer.setUpDefaultServer(); - try { - DefaultSftpSessionFactory f = createServerAndClient(server); - UserInfo userInfo = mock(UserInfo.class); - when(userInfo.promptYesNo(anyString())).thenReturn(true); - f.setUserInfo(userInfo); - f.getSession().close(); - } - finally { - server.stop(true); - } - } - - private DefaultSftpSessionFactory createServerAndClient(SshServer server) throws IOException { - server.setPublickeyAuthenticator((username, key, session) -> true); - server.setPort(0); - server.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); - server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); - server.start(); - - DefaultSftpSessionFactory f = new DefaultSftpSessionFactory(); - f.setHost("localhost"); - f.setPort(server.getPort()); - f.setUser("user"); - Resource privateKey = new ClassPathResource("id_rsa"); - f.setPrivateKey(privateKey); - return f; } } diff --git a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpTestSessionFactory.java b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpTestSessionFactory.java index e8e500da757..7eb66d7dd7b 100644 --- a/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpTestSessionFactory.java +++ b/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpTestSessionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -17,6 +17,11 @@ package org.springframework.integration.sftp.session; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.spy; + +import org.apache.sshd.sftp.client.SftpClient; + /** * @author Oleg Zhurakousky * @author Gary Russell @@ -28,9 +33,12 @@ private SftpTestSessionFactory() { super(); } - public static SftpSession createSftpSession(com.jcraft.jsch.Session jschSession) { - SftpSession sftpSession = new SftpSession(jschSession); - sftpSession.connect(); + public static SftpSession createSftpSession(SftpClient sftpClient) { + SftpSession sftpSession = spy(new SftpSession(sftpClient)); + willReturn("mock.sftp.host:22") + .given(sftpSession) + .getHostPort(); return sftpSession; } + } diff --git a/spring-integration-sftp/src/test/resources/log4j2-test.xml b/spring-integration-sftp/src/test/resources/log4j2-test.xml index 57ea6a43a36..19a9b9b5c17 100644 --- a/spring-integration-sftp/src/test/resources/log4j2-test.xml +++ b/spring-integration-sftp/src/test/resources/log4j2-test.xml @@ -6,7 +6,7 @@ - + diff --git a/src/reference/asciidoc/changes-4.2-4.3.adoc b/src/reference/asciidoc/changes-4.2-4.3.adoc index 631413631c4..78b7e71d2e3 100644 --- a/src/reference/asciidoc/changes-4.2-4.3.adoc +++ b/src/reference/asciidoc/changes-4.2-4.3.adoc @@ -232,7 +232,7 @@ This section describes general changes to the Spring Integration SFTP functional ====== Factory Bean We added a new factory bean to simplify the configuration of Jsch proxies for SFTP. -See <<./sftp.adoc#sftp-proxy-factory-bean,Proxy Factory Bean>> for more information. +See `JschProxyFactoryBean` for more information. ====== `chmod` Changes diff --git a/src/reference/asciidoc/sftp.adoc b/src/reference/asciidoc/sftp.adoc index 38ff8da4c19..c397d7a3113 100644 --- a/src/reference/asciidoc/sftp.adoc +++ b/src/reference/asciidoc/sftp.adoc @@ -10,6 +10,12 @@ The SFTP protocol requires a secure channel, such as SSH, and visibility to a cl Spring Integration supports sending and receiving files over SFTP by providing three client side endpoints: inbound channel adapter, outbound channel adapter, and outbound gateway. It also provides convenient namespace configuration to define these client components. +NOTE: Starting with version 6.0, an outdated JCraft JSch client has been replaced with modern https://mina.apache.org/sshd-project/index.html[Apache MINA SSHD] framework. +This caused a lot of breaking changes in the framework components. +However, in most cases, such a migration is hidden behind Spring Integration API. +The most drastic changed has happened with a `DefaultSftpSessionFactory` which is based now on the `org.apache.sshd.client.SshClient` and exposes some if its configuration properties. + + You need to include this dependency into your project: ==== @@ -64,16 +70,16 @@ You can configure the SFTP session factory with a regular bean definition, as th ==== Every time an adapter requests a session object from its `SessionFactory`, a new SFTP session is created. -Under the covers, the SFTP Session Factory relies on the http://www.jcraft.com/jsch[JSch] library to provide the SFTP capabilities. +Under the covers, the SFTP Session Factory relies on the https://mina.apache.org/sshd-project/index.html[Apache MINA SSHD] library to provide the SFTP capabilities. However, Spring Integration also supports the caching of SFTP sessions. See <> for more information. [IMPORTANT] ===== -JSch supports multiple channels (operations) over a connection to the server. +The `SshClient` supports multiple channels (operations) over a connection to the server. By default, the Spring Integration session factory uses a separate physical connection for each channel. -Since Spring Integration 3.0, you can configure the session factory (using a boolean constructor arg - default `false`) to use a single connection to the server and create multiple `JSch` channels on that single connection. +Since Spring Integration 3.0, you can configure the session factory (using a boolean constructor arg - default `false`) to use a single connection to the server and create multiple `SftpClient` instances on that single connection. When using this feature, you must wrap the session factory in a caching session factory, as <>, so that the connection is not physically closed when an operation completes. @@ -82,9 +88,6 @@ If the cache is reset, the session is disconnected only when the last channel is The connection is refreshed if it is found to be disconnected when a new operation obtains a session. ===== -NOTE: If you experience connectivity problems and would like to trace session creation and see which sessions are polled, you may enable tracing by setting the logger to `TRACE` level (for example, `log4j.category.org.springframework.integration.sftp=TRACE`). -See <>. - Now all you need to do is inject this SFTP session factory into your adapters. NOTE: A more practical way to provide values for the SFTP session factory is to use Spring's https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-placeholderconfigurer[property placeholder support]. @@ -94,35 +97,32 @@ NOTE: A more practical way to provide values for the SFTP session factory is to The following list describes all the properties that are exposed by the https://docs.spring.io/spring-integration/api/org/springframework/integration/sftp/session/DefaultSftpSessionFactory.html[`DefaultSftpSessionFactory`]. -`isSharedSession` (constructor argument)::When `true`, a single connection is used, and `JSch Channels` are multiplexed. +`isSharedSession` (constructor argument)::When `true`, a single `SftpClient` is used for all the requested `SftpSession` instances. It defaults to `false`. -`clientVersion`::Lets you set the client version property. -It's default depends on the underlying JSch version but it will look like: _SSH-2.0-JSCH-0.1.45_ +`sftpVersionSelector`::An `SftpVersionSelector` instance for SFTP protocol selection. +The default one is `SftpVersionSelector.CURRENT`. -`enableDaemonThread`::If `true`, all threads are daemon threads. -If set to `false`, normal non-daemon threads are used instead. -This property is set on the underlying https://epaul.github.io/jsch-documentation/javadoc/com/jcraft/jsch/Session.html[session]. -There, this property defaults to `false`. - -`host`::The URL of the host to which you want to connect. +`host`::The URL of the host to which to connect. Required. -`hostKeyAlias`::Sets the host key alias, which is used when comparing the host key to the known hosts list. - -`knownHostsResource`::Specifies the file resource that used for a host key repository. -The file has the same format as OpenSSH's `known_hosts` file and is required and must be pre-populated if `allowUnknownKeys` is false. - -`password`::The password to authenticate against the remote host. -If a password is not provided, then the `privateKey` property is required. -It is not allowed if you set `userInfo`. -The password is obtained from that object. +`hostConfig`::An `org.apache.sshd.client.config.hosts.HostConfigEntry` instance as an alternative for the user/host/port options. +Can be configured with a proxy jump property. `port`::The port over which the SFTP connection shall be established. If not specified, this value defaults to `22`. If specified, this properties must be a positive number. -`privateKey`::Lets you set a https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/io/Resource.html[resource] that represents the location of the private key used for authenticating against the remote host. +`user`::The remote user to use. +Required. + +`knownHostsResource`::An `org.springframework.core.io.Resource` that used for a host key repository. +The content of the resource has to be the same format as OpenSSH `known_hosts` file and is required and must be pre-populated if `allowUnknownKeys` is false. + +`password`::The password to authenticate against the remote host. +If a password is not provided, then the `privateKey` property is required. + +`privateKey`::An `org.springframework.core.io.Resource` that represents the location of the private key used for authenticating against the remote host. If the `privateKey` is not provided, then the `password` property is required. `privateKeyPassphrase`::The password for the private key. @@ -130,66 +130,15 @@ If you set `userInfo`, `privateKeyPassphrase` is not allowed . The passphrase is obtained from that object. Optional. -`proxy`::Allows for specifying a JSch-based https://epaul.github.com/jsch-documentation/javadoc/com/jcraft/jsch/Proxy.html[proxy]. -If set, the proxy object is used to create the connection to the remote host through the proxy. -See <> for a convenient way to configure the proxy. - -`serverAliveCountMax`::Specifies the number of server-alive messages, which are sent without any reply from the server before disconnecting. -If not set, this property defaults to `1`. - -`serverAliveInterval`::Sets the timeout interval (in milliseconds) before a server-alive message is sent, in case no message is received from the server. - -`sessionConfig`::By using `Properties`, you can set additional configuration setting on the underlying JSch Session. - -`socketFactory`::Lets you pass in a https://epaul.github.com/jsch-documentation/javadoc/com/jcraft/jsch/SocketFactory.html[`SocketFactory`]. -The socket factory is used to create a socket to the target host. -When a proxy is used, the socket factory is passed to the proxy. -By default, plain TCP sockets are used. - `timeout`::The timeout property is used as the socket timeout parameter, as well as the default connection timeout. Defaults to `0`, which means, that no timeout will occur. -`user`::The remote user to use. -Required. - [[sftp-unk-keys]] `allowUnknownKeys`::Set to `true` to allow connections to hosts with unknown (or changed) keys. Its default is 'false'. -It is applied only if no `userInfo` is provided. If `false`, a pre-populated `knownHosts` file is required. -`userInfo`::Set a custom `UserInfo` to be used during authentication. -In particular, `promptYesNo()` is invoked when an unknown (or changed) host key is received. -See also <>. -When you provide a `UserInfo`, the `password` and private key `passphrase` are obtained from it, and you cannot set discrete `password` and `privateKeyPassphrase` properties. - -[[sftp-proxy-factory-bean]] -=== Proxy Factory Bean - -`Jsch` provides a mechanism to connect to the server over an HTTP or SOCKS proxy. -To use this feature, configure the `Proxy` and provide a reference to the `DefaultSftpSessionFactory`, as discussed earlier. -Three implementations are provided by `Jsch`: `HTTP`, `SOCKS4`, and `SOCKS5`. -Spring Integration 4.3 introduced a `FactoryBean`, easing configuration of these proxies by allowing property injection, as the following example shows: - -==== -[source, xml] ----- - - - - - - - - - - ... - - ... - ----- -==== +`userInteraction`::A custom `org.apache.sshd.client.auth.keyboard.UserInteraction` to be used during authentication. [[sftp-dsf]] === Delegating Session Factory @@ -269,7 +218,7 @@ When using `isSharedSession=true`, the channel is closed and the shared session New requests for sessions establish new sessions as necessary. Starting with version 5.1, the `CachingSessionFactory` has a new property `testSession`. -When true, the session will be tested by performing a `stat(getHome())` command to ensure it is still active; if not, it will be removed from the cache; a new session is created if no active sessions are in the cache. +When true, the session will be tested by performing a `REALPATH` command for an empty path to ensure it is still active; if not, it will be removed from the cache; a new session is created if no active sessions are in the cache. [[sftp-rft]] === Using `RemoteFileTemplate` @@ -485,7 +434,7 @@ public class SftpJavaApplication { } @Bean - public SessionFactory sftpSessionFactory() { + public SessionFactory sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true); factory.setHost("localhost"); factory.setPort(port); @@ -493,7 +442,7 @@ public class SftpJavaApplication { factory.setPassword("foo"); factory.setAllowUnknownKeys(true); factory.setTestSession(true); - return new CachingSessionFactory(factory); + return new CachingSessionFactory<>(factory); } @Bean @@ -624,14 +573,10 @@ See <> for more information. The adapter puts the remote directory and the file name in headers (`FileHeaders.REMOTE_DIRECTORY` and `FileHeaders.REMOTE_FILE`, respectively). Starting with version 5.0, the `FileHeaders.REMOTE_FILE_INFO` header provides additional remote file information (in JSON). If you set the `fileInfoJson` property on the `SftpStreamingMessageSource` to `false`, the header contains an `SftpFileInfo` object. -You can access the `LsEntry` object provided by the underlying Jsch library by using the `SftpFileInfo.getFileInfo()` method. +You can access the `SftpClient.DirEntry` object provided by the underlying `SftpClient` by using the `SftpFileInfo.getFileInfo()` method. The `fileInfoJson` property is not available when you use XML configuration, but you can set it by injecting the `SftpStreamingMessageSource` into one of your configuration classes. See also <>. -Starting with version 5.1, the generic type of the `comparator` is `LsEntry`. -Previously, it was `AbstractFileInfo`. -This is because the sort is now performed earlier in the processing, before filtering and applying `maxFetch`. - [[sftp-streaming-java-config]] ==== Configuring with Java Configuration @@ -689,7 +634,7 @@ public class SftpJavaApplication { ---- ==== -Notice that, in this example, the message handler downstream of the transformer has an advice that removes the remote file after processing. +Notice that, in this example, the message handler downstream of the transformer has an `advice` that removes the remote file after processing. [[sftp-rotating-server-advice]] === Inbound Channel Adapters: Polling Multiple Servers and Directories @@ -800,7 +745,7 @@ Another use for `max-fetch-size` is when you want to stop fetching remote files Setting the `maxFetchSize` property on the `MessageSource` (programmatically, via JMX, or via a <<./control-bus.adoc#control-bus, control bus>>) effectively stops the adapter from fetching more files but lets the poller continue to emit messages for files that have previously been fetched. If the poller is active when the property is changed, the change takes effect on the next poll. -Starting with version 5.1, the synchronizer can be provided with a `Comparator`. +Starting with version 5.1, the synchronizer can be provided with a `Comparator`. This is useful when restricting the number of files fetched with `maxFetchSize`. [[sftp-outbound]] @@ -896,7 +841,7 @@ public class SftpJavaApplication { } @Bean - public SessionFactory sftpSessionFactory() { + public SessionFactory sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true); factory.setHost("localhost"); factory.setPort(port); @@ -904,7 +849,7 @@ public class SftpJavaApplication { factory.setPassword("foo"); factory.setAllowUnknownKeys(true); factory.setTestSession(true); - return new CachingSessionFactory(factory); + return new CachingSessionFactory(factory); } @Bean @@ -1090,7 +1035,7 @@ The message payload resulting from an `mget` operation is a `List` object IMPORTANT: Starting with version 5.0, if the `FileExistsMode` is `IGNORE`, the payload of the output message no longer contain files that were not fetched due to the file already existing. Previously, the array contained all files, including those that already existed. -The expression you use determine the remote path should produce a result that ends with `*` for example `myfiles/*` fetches the complete tree under `myfiles`. +The expression you use determine the remote path should produce a result that ends with `\*` for example `myfiles/*` fetches the complete tree under `myfiles`. Starting with version 5.0, you can use a recursive `MGET`, combined with the `FileExistsMode.REPLACE_IF_MODIFIED` mode, to periodically synchronize an entire remote directory tree locally. This mode sets the local file's last modified timestamp to the remote file's timestamp, regardless of the `-P` (preserve timestamp) option. @@ -1223,7 +1168,7 @@ It is particularly useful for mget (for example: `local-directory-expression="'/ This attribute is mutually exclusive with the `local-directory` attribute. For all commands, the 'expression' property of the gateway holds the path on which the command acts. -For the `mget` command, the expression might evaluate to `*`, meaning to retrieve all files, `somedirectory/*`, and other values that end with `*`. +For the `mget` command, the expression might evaluate to `\*`, meaning to retrieve all files, `somedirectory/*`, and other values that end with `*`. The following example shows a gateway configured for an `ls` command: @@ -1293,14 +1238,14 @@ public class SftpJavaApplication { } @Bean - public SessionFactory sftpSessionFactory() { + public SessionFactory sftpSessionFactory() { DefaultSftpSessionFactory sf = new DefaultSftpSessionFactory(); sf.setHost("localhost"); sf.setPort(port); sf.setUsername("foo"); sf.setPassword("foo"); factory.setTestSession(true); - return new CachingSessionFactory(sf); + return new CachingSessionFactory<>(sf); } @Bean @@ -1356,26 +1301,10 @@ root/ If the exception occurs on `file3.txt`, the `PartialSuccessException` thrown by the gateway has `derivedInput` of `file1.txt`, `subdir`, and `zoo.txt` and `partialResults` of `file1.txt`. Its `cause` is another `PartialSuccessException` with `derivedInput` of `file2.txt` and `file3.txt` and `partialResults` of `file2.txt`. -[[sftp-jsch-logging]] -=== SFTP/JSCH Logging - -Since we use JSch libraries to provide SFTP support, you may at times require more information from the JSch API itself, especially if something is not working properly (such as authentication exceptions). -Unfortunately JSch does not use `commons-logging` but instead relies on custom implementations of their `com.jcraft.jsch.Logger` interface. -As of Spring Integration 2.0.1, we have implemented this interface. -So now, to enable JSch logging, you can configure your logger the way you usually do. -For example, the following example is valid configuration of a logger that uses Log4J: - -==== -[source,java] ----- -log4j.category.com.jcraft.jsch=DEBUG ----- -==== - [[sftp-session-callback]] === MessageSessionCallback -Starting with Spring Integration version 4.2, you can use a `MessageSessionCallback` implementation with the `` (`SftpOutboundGateway`) to perform any operation on the `Session` with the `requestMessage` context. +Starting with Spring Integration version 4.2, you can use a `MessageSessionCallback` implementation with the `` (`SftpOutboundGateway`) to perform any operation on the `Session` with the `requestMessage` context. You can use it for any non-standard or low-level SFTP operation (or several), such as allowing access from an integration flow definition, or functional interface (lambda) implementation injection. The following example uses a lambda: @@ -1384,7 +1313,7 @@ The following example uses a lambda: ---- @Bean @ServiceActivator(inputChannel = "sftpChannel") -public MessageHandler sftpOutboundGateway(SessionFactory sessionFactory) { +public MessageHandler sftpOutboundGateway(SessionFactory sessionFactory) { return new SftpOutboundGateway(sessionFactory, (session, requestMessage) -> session.list(requestMessage.getPayload())); } diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 18ddc403b43..8b061d1b51b 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -50,6 +50,12 @@ See <<./jdbc.adoc#postgresql-push,PostgreSQL: Receiving Push Notifications>> for The AMQP module has been enhanced to provide support for inbound and outbound channel adapters using RabbitMQ Stream Queues. See <<./amqp.adoc#rmq-streams,RabbitMQ Stream Queue Support>> for more information. +[[x6.0-sftp]] +==== Apache MINA SFTP + +The SFTP modules has been fully reworked from outdated JCraft JSch library to more robust and modern `org.apache.sshd:sshd-sftp` module of the Apache MINA project. + +See <<./sftp.adoc#sftp,SFTP Adapters>> for more information. [[x6.0-general]] === General Changes