From dd6398bff3a0ed3c83e917fe155f36ce7ebe8ba7 Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Wed, 7 Jun 2023 15:47:46 +0300 Subject: [PATCH 1/9] Bug fixes and new features --- Frends.SFTP.DownloadFiles/CHANGELOG.md | 7 + .../ConnectivityTests.cs | 71 +- .../ErrorTests.cs | 1 - .../Lib/Helpers.cs | 18 +- .../TransferTests.cs | 754 +++++++++--------- .../Definitions/Connection.cs | 279 +++---- .../Definitions/FileTransporter.cs | 223 ++++-- .../Definitions/PromptResponse.cs | 25 + .../Definitions/SFTPLogger.cs | 1 + .../Definitions/SingleFileTransfer.cs | 6 +- .../Definitions/TransferLogSink.cs | 45 +- .../Frends.SFTP.DownloadFiles.cs | 4 +- .../Frends.SFTP.DownloadFiles.csproj | 2 +- 13 files changed, 816 insertions(+), 620 deletions(-) create mode 100644 Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/PromptResponse.cs diff --git a/Frends.SFTP.DownloadFiles/CHANGELOG.md b/Frends.SFTP.DownloadFiles/CHANGELOG.md index 3274d0b..2155ea5 100644 --- a/Frends.SFTP.DownloadFiles/CHANGELOG.md +++ b/Frends.SFTP.DownloadFiles/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.7.0] - 2023-06-07 +### Fixed +- Modified private key passphrase to be visible when all private key authentication options were enabled. +- Added new parameter for keyboard-interactive auhentication were users can add prompts and responses. +- Modified operations log to list current system and sftp server information. +- Fixed operations log to show case exceptions more precise. + ## [2.6.1] - 2023-05-17 ### Fixed - Fixed issue with TransferredFileNames was incorrect when FilePaths parameter was used. diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs index cea7a82..adc22e6 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Threading; using NUnit.Framework; using Frends.SFTP.DownloadFiles.Definitions; @@ -47,7 +48,7 @@ public void DownloadFiles_TestPrivateKeyFileRsa() { var connection = Helpers.GetSftpConnection(); connection.Authentication = AuthenticationType.UsernamePasswordPrivateKeyFile; - connection.PrivateKeyFilePassphrase = "passphrase"; + connection.PrivateKeyPassphrase = "passphrase"; connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); @@ -61,8 +62,9 @@ public void DownloadFiles_TestPrivateKeyFileRsaFromString() var key = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); var connection = Helpers.GetSftpConnection(); + connection.HostKeyAlgorithm = HostKeyAlgorithms.RSA; connection.Authentication = AuthenticationType.UsernamePasswordPrivateKeyString; - connection.PrivateKeyFilePassphrase = "passphrase"; + connection.PrivateKeyPassphrase = "passphrase"; connection.PrivateKeyString = key; var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); @@ -76,9 +78,74 @@ public void DownloadFiles_TestWithInteractiveKeyboardAuthentication() var connection = Helpers.GetSftpConnection(); connection.UseKeyboardInteractiveAuthentication = true; + var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + } + + [Test] + public void DownloadFiles_TestWithInteractiveKeyboardAuthenticationAndPrivateKey() + { + var connection = Helpers.GetSftpConnection(); + connection.Authentication = AuthenticationType.UsernamePrivateKeyFile; + connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); + connection.Password = null; + connection.PrivateKeyPassphrase = "passphrase"; + connection.UseKeyboardInteractiveAuthentication = true; + connection.PromptAndResponse = new PromptResponse[] { new PromptResponse { Prompt = "Password", Response = "pass" } }; + var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); Assert.IsTrue(result.Success); Assert.AreEqual(1, result.SuccessfulTransferCount); + + connection.Authentication = AuthenticationType.UsernamePrivateKeyString; + connection.PrivateKeyFile = null; + connection.PrivateKeyString = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); + connection.PrivateKeyPassphrase = "passphrase"; + + var destination = new Destination + { + Directory = Path.Combine(_workDir, "destination"), + Action = DestinationAction.Overwrite, + FileNameEncoding = FileEncoding.UTF8, + EnableBomForFileName = true + }; + + result = SFTP.DownloadFiles(_source, destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + } + + [Test] + public void DownloadFiles_TestShouldThrowWithoutPromptAndResponse() + { + var connection = Helpers.GetSftpConnection(); + connection.Authentication = AuthenticationType.UsernamePrivateKeyFile; + connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); + connection.Password = null; + connection.PrivateKeyPassphrase = "passphrase"; + connection.UseKeyboardInteractiveAuthentication = true; + connection.HostKeyAlgorithm = HostKeyAlgorithms.RSA; + connection.ServerFingerPrint = "NUfXVu2omU2k3ELtmCzhkcERRLHAEbNakrpBgEXn8JM"; + + var ex = Assert.Throws(() => SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken())); + Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); + + connection.Authentication = AuthenticationType.UsernamePrivateKeyString; + connection.PrivateKeyFile = null; + connection.PrivateKeyString = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); + connection.PrivateKeyPassphrase = "passphrase"; + + var destination = new Destination + { + Directory = Path.Combine(_workDir, "destination"), + Action = DestinationAction.Overwrite, + FileNameEncoding = FileEncoding.UTF8, + EnableBomForFileName = true + }; + + ex = Assert.Throws(() => SFTP.DownloadFiles(_source, destination, connection, _options, _info, new CancellationToken())); + Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); } [Test] diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ErrorTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ErrorTests.cs index 0619584..bcb5268 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ErrorTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ErrorTests.cs @@ -59,7 +59,6 @@ public void DownloadFiles_TestWithSubDirNameAsFileMask() var ex = Assert.Throws(() => SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken())); Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: 1 Errors: No source files found from directory")); - Helpers.DeleteSubDirectory(path); } diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/Lib/Helpers.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/Lib/Helpers.cs index c0ca2d5..9afb52c 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/Lib/Helpers.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/Lib/Helpers.cs @@ -115,23 +115,6 @@ internal static string[] UploadLargeTestFiles(string destination, int count = 3, return filePaths.ToArray(); } - internal static void DeleteRemoteFiles(int count, string directory) - { - using (var client = new SftpClient(_dockerAddress, 2222, _dockerUsername, _dockerPassword)) - { - client.Connect(); - var files = client.ListDirectory(directory); - var i = 0; - foreach (var file in files) - { - if (i == count) - break; - client.DeleteFile(file.FullName); - i++; - } - } - } - internal static List CreateDummyFiles(int count, List filenames = null) { Directory.CreateDirectory(_workDir); @@ -184,6 +167,7 @@ internal static void DeleteDummyFiles() { if (Directory.Exists(file)) Directory.Delete(file, true); + File.Delete(file); } } diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/TransferTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/TransferTests.cs index fcf7621..4acc8d9 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/TransferTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/TransferTests.cs @@ -1,380 +1,380 @@ -using NUnit.Framework; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Collections.Generic; -using Frends.SFTP.DownloadFiles.Definitions; - -namespace Frends.SFTP.DownloadFiles.Tests -{ - [TestFixture] - class TransferTests : DownloadFilesTestBase - { - [Test] - public void DownloadFiles_TestSimpleTransfer() - { - var result = SFTP.DownloadFiles(_source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - Assert.AreEqual(Path.Combine(_destination.Directory, _source.FileName), result.TransferredDestinationFilePaths.ToList().FirstOrDefault()); - } - - [Test] - public void DownloadFiles_TestTransferWithoutSourceDirectoryShouldThrow() - { - var source = new Source - { - Directory = "", - FileName = _source.FileName, - Action = _source.Action, - Operation = _source.Operation, - }; - - var ex = Assert.Throws(() => SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken())); - Assert.IsTrue(ex.Message.Contains("No source")); - } - - [Test] - public void DownloadFiles_TestDownloadWithFileMask() - { - var source = new Source - { - Directory = _source.Directory, - FileName = "*", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing - }; - - var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(3, result.SuccessfulTransferCount); - } - - [Test] - public void DownloadFiles_TestWithOperationLogDisabled() - { - var options = new Options - { - ThrowErrorOnFail = true, - RenameSourceFileBeforeTransfer = true, - RenameDestinationFileDuringTransfer = true, - CreateDestinationDirectories = true, - PreserveLastModified = false, - OperationLog = false - }; - - var result = SFTP.DownloadFiles(_source, _destination, _connection, options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(0, result.OperationsLog.Count); - } - - [Test] - public void DownloadFiles_TestWithMultipleSubdirectoriesInDestination() - { - var destination = new Destination - { - Directory = Path.Combine(_destWorkDir, "another\\folder"), - Action = DestinationAction.Error, - }; - - var result = SFTP.DownloadFiles(_source, destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - Assert.IsTrue(File.Exists(Path.Combine(destination.Directory, _source.FileName))); - } - - [Test] - public void DownloadFiles_TestOneErrorInTransferWithMultipleFiles() - { - Directory.CreateDirectory(_destWorkDir); - File.Copy(Path.Combine(_workDir, _source.FileName), Path.Combine(_destWorkDir, _source.FileName)); - - var destination = new Destination - { - Directory = _destWorkDir, - FileNameEncoding = FileEncoding.UTF8, - EnableBomForFileName = true, - Action = DestinationAction.Error, - }; - - var options = new Options - { - ThrowErrorOnFail = false, - RenameSourceFileBeforeTransfer = true, - RenameDestinationFileDuringTransfer = true, - CreateDestinationDirectories = true, - PreserveLastModified = false, - OperationLog = true - }; - - var source = new Source - { - Directory = _source.Directory, - FileName = "*.txt", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing - }; - - var result = SFTP.DownloadFiles(source, destination, _connection, options, _info, new CancellationToken()); - Assert.IsFalse(result.Success); - Assert.AreEqual(2, result.SuccessfulTransferCount); - Assert.AreEqual(1, result.FailedTransferCount); - Assert.IsTrue(result.UserResultMessage.Contains("2 files transferred:")); - } - - [Test] - public void DownloadFiles_TestSingleFileTransferWithError() - { - var options = new Options - { - ThrowErrorOnFail = false, - RenameSourceFileBeforeTransfer = true, - RenameDestinationFileDuringTransfer = true, - CreateDestinationDirectories = true, - PreserveLastModified = false, - OperationLog = true - }; - - Directory.CreateDirectory(_destWorkDir); - File.Copy(Path.Combine(_workDir, _source.FileName), Path.Combine(_destWorkDir, _source.FileName)); - - var result = SFTP.DownloadFiles(_source, _destination, _connection, options, _info, new CancellationToken()); - Assert.IsFalse(result.Success); - Assert.AreEqual(1, result.FailedTransferCount); - } - - [Test] - public void DownloadFiles_TestWithFileMaskWithFileAlreadyInDestination() - { - var source = new Source - { - Directory = _source.Directory, - FileName = "*File1.txt", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing - }; - - var options = new Options - { - ThrowErrorOnFail = false, - RenameSourceFileBeforeTransfer = true, - RenameDestinationFileDuringTransfer = true, - CreateDestinationDirectories = true, - PreserveLastModified = false, - OperationLog = true - }; - - var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - - source.FileName = "*.txt"; - result = SFTP.DownloadFiles(source, _destination, _connection, options, _info, new CancellationToken()); - Assert.IsFalse(result.Success); - Assert.AreEqual(1, result.FailedTransferCount); - Assert.AreEqual(2, result.SuccessfulTransferCount); - } - - [Test] - public void DownloadFiles_TestDownloadWithOverwrite() - { - var destination = new Destination - { - Directory = Path.Combine(_workDir, "destination"), - Action = DestinationAction.Overwrite, - }; - - var result = SFTP.DownloadFiles(_source, destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - - Assert.IsTrue(File.Exists(Path.Combine(destination.Directory, _source.FileName))); - } - - [Test] - public void DownloadFiles_TestTransferWithRenameSourceEnabledRenameDestinationDisabled() - { - var destination = new Destination - { - Directory = Path.Combine(_workDir, "destination"), - Action = DestinationAction.Overwrite, - }; - - var options = new Options - { - ThrowErrorOnFail = true, - RenameSourceFileBeforeTransfer = true, - RenameDestinationFileDuringTransfer = false, - CreateDestinationDirectories = true, - PreserveLastModified = false, - OperationLog = true - }; - - var result = SFTP.DownloadFiles(_source, destination, _connection, options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - - Assert.IsTrue(File.Exists(Path.Combine(destination.Directory, _source.FileName))); - } - - [Test] - public void DownloadFiles_NoSourceFilesAndIgnoreShouldNotThrowException() - { - Helpers.CreateSubDirectory("/upload/Upload"); - var options = new Options - { - ThrowErrorOnFail = true, - RenameSourceFileBeforeTransfer = true, - RenameDestinationFileDuringTransfer = true, - CreateDestinationDirectories = true, - PreserveLastModified = true, - OperationLog = true - }; - - var source = new Source - { - Directory = _source.Directory, - FileName = "*.csv", - Action = SourceAction.Ignore, - Operation = SourceOperation.Delete - }; - - var result = SFTP.DownloadFiles(source, _destination, _connection, options, _info, new CancellationToken()); - Assert.IsTrue(result.ActionSkipped); - Assert.IsFalse(result.UserResultMessage.Contains("1 files transferred")); - Assert.IsTrue(result.UserResultMessage.Contains("No files transferred")); - } - - [Test] - public void DownloadFiles_NoSourceFilesAndInfoShouldNotThrowException() - { - Helpers.CreateSubDirectory("/upload/Upload"); - var options = new Options - { - ThrowErrorOnFail = true, - RenameSourceFileBeforeTransfer = true, - RenameDestinationFileDuringTransfer = true, - CreateDestinationDirectories = true, - PreserveLastModified = true, - OperationLog = true - }; - - var source = new Source - { - Directory = _source.Directory, - FileName = "*.csv", - Action = SourceAction.Info, - Operation = SourceOperation.Delete - }; - - var result = SFTP.DownloadFiles(source, _destination, _connection, options, _info, new CancellationToken()); - Assert.IsTrue(result.ActionSkipped); - Assert.IsFalse(result.UserResultMessage.Contains("1 files transferred")); - Assert.IsTrue(result.UserResultMessage.Contains("No files transferred")); - } - - [Test] - public void DownloadFiles_TestNoSourceShouldNotCreateOperationsLogWhenSourceActionIsIgnore() - { - Directory.CreateDirectory(_destWorkDir); - - var source = new Source - { - Directory = "/upload/", - FileName = _source.FileName, - Action = SourceAction.Ignore, - Operation = SourceOperation.Nothing, - }; - - var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(0, result.OperationsLog.Count); - } - - [Test] - public void DownloadFiles_TestWithFilePaths() - { - var filePaths = Helpers.UploadTestFiles(_source.Directory, 3); - - var source = new Source - { - Action = SourceAction.Error, - Operation = SourceOperation.Nothing, - FilePaths = filePaths - }; - - var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.AreEqual(3, result.SuccessfulTransferCount); +using NUnit.Framework; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Collections.Generic; +using Frends.SFTP.DownloadFiles.Definitions; + +namespace Frends.SFTP.DownloadFiles.Tests +{ + [TestFixture] + class TransferTests : DownloadFilesTestBase + { + [Test] + public void DownloadFiles_TestSimpleTransfer() + { + var result = SFTP.DownloadFiles(_source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + Assert.AreEqual(Path.Combine(_destination.Directory, _source.FileName), result.TransferredDestinationFilePaths.ToList().FirstOrDefault()); + } + + [Test] + public void DownloadFiles_TestTransferWithoutSourceDirectoryShouldThrow() + { + var source = new Source + { + Directory = "", + FileName = _source.FileName, + Action = _source.Action, + Operation = _source.Operation, + }; + + var ex = Assert.Throws(() => SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken())); + Assert.IsTrue(ex.Message.Contains("No source")); + } + + [Test] + public void DownloadFiles_TestDownloadWithFileMask() + { + var source = new Source + { + Directory = _source.Directory, + FileName = "*", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing + }; + + var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(3, result.SuccessfulTransferCount); + } + + [Test] + public void DownloadFiles_TestWithOperationLogDisabled() + { + var options = new Options + { + ThrowErrorOnFail = true, + RenameSourceFileBeforeTransfer = true, + RenameDestinationFileDuringTransfer = true, + CreateDestinationDirectories = true, + PreserveLastModified = false, + OperationLog = false + }; + + var result = SFTP.DownloadFiles(_source, _destination, _connection, options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(0, result.OperationsLog.Count); + } + + [Test] + public void DownloadFiles_TestWithMultipleSubdirectoriesInDestination() + { + var destination = new Destination + { + Directory = Path.Combine(_destWorkDir, "another\\folder"), + Action = DestinationAction.Error, + }; + + var result = SFTP.DownloadFiles(_source, destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + Assert.IsTrue(File.Exists(Path.Combine(destination.Directory, _source.FileName))); + } + + [Test] + public void DownloadFiles_TestOneErrorInTransferWithMultipleFiles() + { + Directory.CreateDirectory(_destWorkDir); + File.Copy(Path.Combine(_workDir, _source.FileName), Path.Combine(_destWorkDir, _source.FileName)); + + var destination = new Destination + { + Directory = _destWorkDir, + FileNameEncoding = FileEncoding.UTF8, + EnableBomForFileName = true, + Action = DestinationAction.Error, + }; + + var options = new Options + { + ThrowErrorOnFail = false, + RenameSourceFileBeforeTransfer = true, + RenameDestinationFileDuringTransfer = true, + CreateDestinationDirectories = true, + PreserveLastModified = false, + OperationLog = true + }; + + var source = new Source + { + Directory = _source.Directory, + FileName = "*.txt", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing + }; + + var result = SFTP.DownloadFiles(source, destination, _connection, options, _info, new CancellationToken()); + Assert.IsFalse(result.Success); + Assert.AreEqual(2, result.SuccessfulTransferCount); + Assert.AreEqual(1, result.FailedTransferCount); + Assert.IsTrue(result.UserResultMessage.Contains("2 files transferred:")); + } + + [Test] + public void DownloadFiles_TestSingleFileTransferWithError() + { + var options = new Options + { + ThrowErrorOnFail = false, + RenameSourceFileBeforeTransfer = true, + RenameDestinationFileDuringTransfer = true, + CreateDestinationDirectories = true, + PreserveLastModified = false, + OperationLog = true + }; + + Directory.CreateDirectory(_destWorkDir); + File.Copy(Path.Combine(_workDir, _source.FileName), Path.Combine(_destWorkDir, _source.FileName)); + + var result = SFTP.DownloadFiles(_source, _destination, _connection, options, _info, new CancellationToken()); + Assert.IsFalse(result.Success); + Assert.AreEqual(1, result.FailedTransferCount); + } + + [Test] + public void DownloadFiles_TestWithFileMaskWithFileAlreadyInDestination() + { + var source = new Source + { + Directory = _source.Directory, + FileName = "*File1.txt", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing + }; + + var options = new Options + { + ThrowErrorOnFail = false, + RenameSourceFileBeforeTransfer = true, + RenameDestinationFileDuringTransfer = true, + CreateDestinationDirectories = true, + PreserveLastModified = false, + OperationLog = true + }; + + var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + + source.FileName = "*.txt"; + result = SFTP.DownloadFiles(source, _destination, _connection, options, _info, new CancellationToken()); + Assert.IsFalse(result.Success); + Assert.AreEqual(1, result.FailedTransferCount); + Assert.AreEqual(2, result.SuccessfulTransferCount); + } + + [Test] + public void DownloadFiles_TestDownloadWithOverwrite() + { + var destination = new Destination + { + Directory = Path.Combine(_workDir, "destination"), + Action = DestinationAction.Overwrite, + }; + + var result = SFTP.DownloadFiles(_source, destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + + Assert.IsTrue(File.Exists(Path.Combine(destination.Directory, _source.FileName))); + } + + [Test] + public void DownloadFiles_TestTransferWithRenameSourceEnabledRenameDestinationDisabled() + { + var destination = new Destination + { + Directory = Path.Combine(_workDir, "destination"), + Action = DestinationAction.Overwrite, + }; + + var options = new Options + { + ThrowErrorOnFail = true, + RenameSourceFileBeforeTransfer = true, + RenameDestinationFileDuringTransfer = false, + CreateDestinationDirectories = true, + PreserveLastModified = false, + OperationLog = true + }; + + var result = SFTP.DownloadFiles(_source, destination, _connection, options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + + Assert.IsTrue(File.Exists(Path.Combine(destination.Directory, _source.FileName))); + } + + [Test] + public void DownloadFiles_NoSourceFilesAndIgnoreShouldNotThrowException() + { + Helpers.CreateSubDirectory("/upload/Upload"); + var options = new Options + { + ThrowErrorOnFail = true, + RenameSourceFileBeforeTransfer = true, + RenameDestinationFileDuringTransfer = true, + CreateDestinationDirectories = true, + PreserveLastModified = true, + OperationLog = true + }; + + var source = new Source + { + Directory = _source.Directory, + FileName = "*.csv", + Action = SourceAction.Ignore, + Operation = SourceOperation.Delete + }; + + var result = SFTP.DownloadFiles(source, _destination, _connection, options, _info, new CancellationToken()); + Assert.IsTrue(result.ActionSkipped); + Assert.IsFalse(result.UserResultMessage.Contains("1 files transferred")); + Assert.IsTrue(result.UserResultMessage.Contains("No files transferred")); + } + + [Test] + public void DownloadFiles_NoSourceFilesAndInfoShouldNotThrowException() + { + Helpers.CreateSubDirectory("/upload/Upload"); + var options = new Options + { + ThrowErrorOnFail = true, + RenameSourceFileBeforeTransfer = true, + RenameDestinationFileDuringTransfer = true, + CreateDestinationDirectories = true, + PreserveLastModified = true, + OperationLog = true + }; + + var source = new Source + { + Directory = _source.Directory, + FileName = "*.csv", + Action = SourceAction.Info, + Operation = SourceOperation.Delete + }; + + var result = SFTP.DownloadFiles(source, _destination, _connection, options, _info, new CancellationToken()); + Assert.IsTrue(result.ActionSkipped); + Assert.IsFalse(result.UserResultMessage.Contains("1 files transferred")); + Assert.IsTrue(result.UserResultMessage.Contains("No files transferred")); + } + + [Test] + public void DownloadFiles_TestNoSourceShouldNotCreateOperationsLogWhenSourceActionIsIgnore() + { + Directory.CreateDirectory(_destWorkDir); + + var source = new Source + { + Directory = "/upload/", + FileName = _source.FileName, + Action = SourceAction.Ignore, + Operation = SourceOperation.Nothing, + }; + + var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(0, result.OperationsLog.Count); + } + + [Test] + public void DownloadFiles_TestWithFilePaths() + { + var filePaths = Helpers.UploadTestFiles(_source.Directory, 3); + + var source = new Source + { + Action = SourceAction.Error, + Operation = SourceOperation.Nothing, + FilePaths = filePaths + }; + + var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.AreEqual(3, result.SuccessfulTransferCount); Assert.IsTrue(result.Success); Assert.AreNotEqual(filePaths[0], result.TransferredFileNames.ToList()[0]); Assert.AreEqual(Path.GetFileName(filePaths[0]), result.TransferredFileNames.ToList()[0]); - } - - [Test] - public void DownloadFiles_TestWitFilePathsEvenIfSourceFileIsAssigned() - { - var filePaths = Helpers.UploadTestFiles(_source.Directory, 3); - - var source = new Source - { - Directory = _source.Directory, - FileName = _source.FileName, - Action = SourceAction.Error, - Operation = SourceOperation.Nothing, - FilePaths = filePaths - }; - - var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.AreEqual(3, result.SuccessfulTransferCount); - Assert.IsTrue(result.Success); - } - - [Test] - public void DownloadFiles_TestTransferWithSpecialCharactersInFileNames() - { - // upload test files - var files = new List { "this is a test file.txt", "This_is(a test file).txt", "this is { a test} file.txt" }; - Helpers.UploadTestFiles(_source.Directory, 0, null, files); - - var source = new Source - { - Directory = _source.Directory, - FileName = "this is a test file.txt", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing, - }; - - var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - - source = new Source - { - Directory = _source.Directory, - FileName = "This_is(a test file).txt", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing, - }; - - result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - - source = new Source - { - Directory = _source.Directory, - FileName = "this is { a test} file.txt", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing, - }; - - result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - } - } -} - - - + } + + [Test] + public void DownloadFiles_TestWitFilePathsEvenIfSourceFileIsAssigned() + { + var filePaths = Helpers.UploadTestFiles(_source.Directory, 3); + + var source = new Source + { + Directory = _source.Directory, + FileName = _source.FileName, + Action = SourceAction.Error, + Operation = SourceOperation.Nothing, + FilePaths = filePaths + }; + + var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.AreEqual(3, result.SuccessfulTransferCount); + Assert.IsTrue(result.Success); + } + + [Test] + public void DownloadFiles_TestTransferWithSpecialCharactersInFileNames() + { + // upload test files + var files = new List { "this is a test file.txt", "This_is(a test file).txt", "this is { a test} file.txt" }; + Helpers.UploadTestFiles(_source.Directory, 0, null, files); + + var source = new Source + { + Directory = _source.Directory, + FileName = "this is a test file.txt", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing, + }; + + var result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + + source = new Source + { + Directory = _source.Directory, + FileName = "This_is(a test file).txt", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing, + }; + + result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + + source = new Source + { + Directory = _source.Directory, + FileName = "this is { a test} file.txt", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing, + }; + + result = SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + } + } +} + + + diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/Connection.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/Connection.cs index be62ecb..286079a 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/Connection.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/Connection.cs @@ -1,135 +1,144 @@ -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; - -namespace Frends.SFTP.DownloadFiles.Definitions; - -/// -/// Parameters class usually contains parameters that are required. -/// -public class Connection -{ - /// - /// The lenght of time, in seconds, until the connection times out. - /// You can use value -1 to indicate that the connection does not time out. - /// Default value is 60 seconds. - /// - /// 60 - [DefaultValue(60)] - public int ConnectionTimeout { get; set; } - - /// - /// The keep-alive interval in milliseconds. Interval the client send keep-alive packages to the host. - /// You can use value -1 to disable the keep-alive. - /// - /// -1 - [DefaultValue(-1)] - public int KeepAliveInterval { get; set; } - - /// - /// SFTP host address - /// - /// localhost - [DisplayFormat(DataFormatString = "Text")] - public string Address { get; set; } - - /// - /// Port number to use in the connection to the server. - /// - /// 22 - [DefaultValue(22)] - public int Port { get; set; } = 22; - - /// - /// Selection for authentication type - /// - /// AuthenticationType.UsernamePassword - public AuthenticationType Authentication { get; set; } = AuthenticationType.UsernamePassword; - - /// - /// Username to use for authentication to the server. Note that the file endpoint only supports - /// username for remote shares and the username must be in the format DOMAIN\Username. - /// - /// foo - [DisplayFormat(DataFormatString = "Text")] - public string UserName { get; set; } - - /// - /// Password to use in the authentication to the server. - /// - /// pass - [UIHint(nameof(Authentication), "", AuthenticationType.UsernamePassword, AuthenticationType.UsernamePasswordPrivateKeyFile, AuthenticationType.UsernamePasswordPrivateKeyString)] - [PasswordPropertyText] - public string Password { get; set; } - - /// - /// Full path to private key file. - /// - /// C:\path\to\private\key - [UIHint(nameof(Authentication), "", AuthenticationType.UsernamePrivateKeyFile, AuthenticationType.UsernamePasswordPrivateKeyFile)] - [DisplayFormat(DataFormatString = "Text")] - public string PrivateKeyFile { get; set; } - - /// - /// Private key as a string, supported private key formats: OpenSSH and ssh.com. - /// PuTTY keys can be converted with puttygen.exe application. - /// 1. Load your key file into puttygen.exe - /// 2. Conversion > Export OpenSSH key (not the "force new file format" option) - /// - /// - /// -----BEGIN RSA PRIVATE KEY----- - /// Fqxq2jbSKyb0a+oW96Tjoif3Kcb5zZ0FiQyiHgQozLXrecjdUwjWuedkDoZMxwG5 - /// bxpOnxZ/88tDzYCtCPcYCPRF8BNueUsZO8/tztTra+4NgVd/omXHG5bqb7iMB4dc - /// ... - /// OX7Q/wO4lqOlFhLtRnSL0cfuhRmt59pM75Zd+euX5tv9jmCj+AQT/kiBoMhNrDGk - /// N2gTujnH7HCr/afSBeL3xnYcEmeCQTxTPZofBjPC+TPd9g7MntSGBeU/Fstv0jbg - /// -----END RSA PRIVATE KEY----- - /// - [UIHint(nameof(Authentication), "", AuthenticationType.UsernamePrivateKeyString, AuthenticationType.UsernamePasswordPrivateKeyString)] - [PasswordPropertyText] - public string PrivateKeyString { get; set; } - - /// - /// Passphrase for the private key file. - /// - /// passphrase - [UIHint(nameof(Authentication), "", AuthenticationType.UsernamePrivateKeyFile, AuthenticationType.UsernamePasswordPrivateKeyFile)] - [PasswordPropertyText] - public string PrivateKeyFilePassphrase { get; set; } - - /// - /// Fingerprint of the SFTP server. When using "Username-Password" - /// authentication it is recommended to use server fingerprint in - /// order to be sure of the server you are connecting. Supported - /// formats for server fingerprints: MD5 and SHA256. - /// - /// - /// MD5: '41:76:EA:65:62:6E:D3:68:DC:41:9A:F2:F2:20:69:9D' - /// SHA256: 'FBQn5eyoxpAl33Ly0gyScCGAqZeMVsfY7qss3KOM/hY=' - /// - [DefaultValue("")] - public string ServerFingerPrint { get; set; } - - /// - /// Host key algorithm to use when connecting to server. - /// Default value is Any which doesn't force the task to use - /// specific algorithm. - /// - /// HostKeyAlgorithm.RSA - [DefaultValue(HostKeyAlgorithms.Any)] - public HostKeyAlgorithms HostKeyAlgorithm { get; set; } - - /// - /// Integer value of used buffer size as KB. - /// Default value is 32 KB. - /// - /// 32 - [DefaultValue(32)] - public uint BufferSize { get; set; } - - /// - /// Enable if the server uses keyboard-interactive authentication method. - /// - /// false - [DefaultValue(false)] - public bool UseKeyboardInteractiveAuthentication { get; set; } -} +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.SFTP.DownloadFiles.Definitions; + +/// +/// Parameters class usually contains parameters that are required. +/// +public class Connection +{ + /// + /// The lenght of time, in seconds, until the connection times out. + /// You can use value -1 to indicate that the connection does not time out. + /// Default value is 60 seconds. + /// + /// 60 + [DefaultValue(60)] + public int ConnectionTimeout { get; set; } + + /// + /// The keep-alive interval in milliseconds. Interval the client send keep-alive packages to the host. + /// You can use value -1 to disable the keep-alive. + /// + /// -1 + [DefaultValue(-1)] + public int KeepAliveInterval { get; set; } + + /// + /// SFTP host address + /// + /// localhost + [DisplayFormat(DataFormatString = "Text")] + public string Address { get; set; } + + /// + /// Port number to use in the connection to the server. + /// + /// 22 + [DefaultValue(22)] + public int Port { get; set; } = 22; + + /// + /// Selection for authentication type + /// + /// AuthenticationType.UsernamePassword + public AuthenticationType Authentication { get; set; } = AuthenticationType.UsernamePassword; + + /// + /// Username to use for authentication to the server. Note that the file endpoint only supports + /// username for remote shares and the username must be in the format DOMAIN\Username. + /// + /// foo + [DisplayFormat(DataFormatString = "Text")] + public string UserName { get; set; } + + /// + /// Password to use in the authentication to the server. + /// + /// pass + [UIHint(nameof(Authentication), "", AuthenticationType.UsernamePassword, AuthenticationType.UsernamePasswordPrivateKeyFile, AuthenticationType.UsernamePasswordPrivateKeyString)] + [PasswordPropertyText] + [DisplayFormat(DataFormatString = "Text")] + public string Password { get; set; } + + /// + /// Full path to private key file. + /// + /// C:\path\to\private\key + [UIHint(nameof(Authentication), "", AuthenticationType.UsernamePrivateKeyFile, AuthenticationType.UsernamePasswordPrivateKeyFile)] + [DisplayFormat(DataFormatString = "Text")] + public string PrivateKeyFile { get; set; } + + /// + /// Private key as a string, supported private key formats: OpenSSH and ssh.com. + /// PuTTY keys can be converted with puttygen.exe application. + /// 1. Load your key file into puttygen.exe + /// 2. Conversion > Export OpenSSH key (not the "force new file format" option) + /// + /// + /// -----BEGIN RSA PRIVATE KEY----- + /// Fqxq2jbSKyb0a+oW96Tjoif3Kcb5zZ0FiQyiHgQozLXrecjdUwjWuedkDoZMxwG5 + /// bxpOnxZ/88tDzYCtCPcYCPRF8BNueUsZO8/tztTra+4NgVd/omXHG5bqb7iMB4dc + /// ... + /// OX7Q/wO4lqOlFhLtRnSL0cfuhRmt59pM75Zd+euX5tv9jmCj+AQT/kiBoMhNrDGk + /// N2gTujnH7HCr/afSBeL3xnYcEmeCQTxTPZofBjPC+TPd9g7MntSGBeU/Fstv0jbg + /// -----END RSA PRIVATE KEY----- + /// + [UIHint(nameof(Authentication), "", AuthenticationType.UsernamePrivateKeyString, AuthenticationType.UsernamePasswordPrivateKeyString)] + [PasswordPropertyText] + [DisplayFormat(DataFormatString = "Text")] + public string PrivateKeyString { get; set; } + + /// + /// Passphrase for the private key file. + /// + /// passphrase + [UIHint(nameof(Authentication), "", AuthenticationType.UsernamePrivateKeyFile, AuthenticationType.UsernamePasswordPrivateKeyFile, AuthenticationType.UsernamePasswordPrivateKeyString, AuthenticationType.UsernamePrivateKeyString)] + [PasswordPropertyText] + [DisplayFormat(DataFormatString = "Text")] + public string PrivateKeyPassphrase { get; set; } + + /// + /// Fingerprint of the SFTP server. When using "Username-Password" + /// authentication it is recommended to use server fingerprint in + /// order to be sure of the server you are connecting. Supported + /// formats for server fingerprints: MD5 and SHA256. + /// + /// + /// MD5: '41:76:EA:65:62:6E:D3:68:DC:41:9A:F2:F2:20:69:9D' + /// SHA256: 'FBQn5eyoxpAl33Ly0gyScCGAqZeMVsfY7qss3KOM/hY=' + /// + [DefaultValue("")] + public string ServerFingerPrint { get; set; } + + /// + /// Host key algorithm to use when connecting to server. + /// Default value is Any which doesn't force the task to use + /// specific algorithm. + /// + /// HostKeyAlgorithm.RSA + [DefaultValue(HostKeyAlgorithms.Any)] + public HostKeyAlgorithms HostKeyAlgorithm { get; set; } + + /// + /// Integer value of used buffer size as KB. + /// Default value is 32 KB. + /// + /// 32 + [DefaultValue(32)] + public uint BufferSize { get; set; } + + /// + /// Enable if the server uses keyboard-interactive authentication method. + /// + /// false + [DefaultValue(false)] + public bool UseKeyboardInteractiveAuthentication { get; set; } + + /// + /// Responses for the server prompts when using Keyboard Interactive authentication method. + /// + [UIHint(nameof(UseKeyboardInteractiveAuthentication), "", true)] + public PromptResponse[] PromptAndResponse { get; set; } = new PromptResponse[0]; +} diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs index 83e9d93..c218117 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs @@ -1,6 +1,9 @@ using Renci.SshNet; using Renci.SshNet.Common; +using System.Globalization; using System.Net.Sockets; +using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Security.Cryptography; using Renci.SshNet.Security; @@ -17,13 +20,15 @@ internal class FileTransporter private readonly BatchContext _batchContext; private readonly string[] _filePaths; private readonly RenamingPolicy _renamingPolicy; + private readonly CancellationToken _cancellationToken; - internal FileTransporter(ISFTPLogger logger, BatchContext context, Guid instanceId) + internal FileTransporter(ISFTPLogger logger, BatchContext context, Guid instanceId, CancellationToken cancellationToken) { _logger = logger; _batchContext = context; _instanceId = instanceId; _renamingPolicy = new RenamingPolicy(_batchContext.Info.TransferName, _instanceId); + _cancellationToken = cancellationToken; _result = new List(); _filePaths = ConvertObjectToStringArray(context.Source.FilePaths); @@ -48,13 +53,14 @@ internal FileTransporter(ISFTPLogger logger, BatchContext context, Guid instance /// /// Executes file transfers. /// - /// /// /// /// /// - public FileTransferResult Run(CancellationToken cancellationToken) + public FileTransferResult Run() { + _logger.NotifyInformation(_batchContext, $"Connecting to {_batchContext.Connection.Address}:{_batchContext.Connection.Port} using SFTP."); + var userResultMessage = ""; try { @@ -71,6 +77,10 @@ public FileTransferResult Run(CancellationToken cancellationToken) return FormFailedFileTransferResult(userResultMessage); } + LogSourceSystemInfo(_batchContext, connectionInfo, _logger); + + _logger.NotifyInformation(_batchContext, "Negotiation started."); + using (var client = new SftpClient(connectionInfo)) { if (_batchContext.Connection.HostKeyAlgorithm != HostKeyAlgorithms.Any) @@ -78,20 +88,14 @@ public FileTransferResult Run(CancellationToken cancellationToken) var expectedServerFingerprint = _batchContext.Connection.ServerFingerPrint; - // Check the fingerprint of the server if given. - if (!string.IsNullOrEmpty(expectedServerFingerprint)) + try { - SetCurrentState(TransferState.Connection, "Checking server fingerprint."); - try - { - CheckServerFingerprint(client, expectedServerFingerprint); - } - catch (Exception e) - { - _logger.NotifyError(null, $"Error when checking the server fingerprint", e); - return FormFailedFileTransferResult(userResultMessage); - } - + CheckServerFingerprint(client, expectedServerFingerprint); + } + catch (Exception e) + { + _logger.NotifyError(null, $"Error when checking the server fingerprint", e); + return FormFailedFileTransferResult(userResultMessage); } client.ConnectionInfo.Timeout = TimeSpan.FromSeconds(_batchContext.Connection.ConnectionTimeout); @@ -99,8 +103,6 @@ public FileTransferResult Run(CancellationToken cancellationToken) client.BufferSize = _batchContext.Connection.BufferSize * 1024; - SetCurrentState(TransferState.Connection, $"Connecting to {_batchContext.Connection.Address}:{_batchContext.Connection.Port} using SFTP."); - client.Connect(); if (!client.IsConnected) @@ -108,10 +110,11 @@ public FileTransferResult Run(CancellationToken cancellationToken) _logger.NotifyError(null, "Error while connecting to destination: ", new SshConnectionException(userResultMessage)); return FormFailedFileTransferResult(userResultMessage); } - _logger.NotifyInformation(_batchContext, $"Connection has been stablished to target {_batchContext.Connection.Address}:{_batchContext.Connection.Port} using SFTP."); + + _logger.NotifyInformation(_batchContext, "Negotiation finished."); // Fetch source file info and check if files were returned. - var (files, success) = ListSourceFiles(client, _batchContext.Source, cancellationToken); + var (files, success) = ListSourceFiles(client, _batchContext.Source, _cancellationToken); // If source directory doesn't exist, modify userResultMessage accordingly. if (!success) @@ -155,11 +158,11 @@ public FileTransferResult Run(CancellationToken cancellationToken) } } - _batchContext.DestinationFiles = GetDestinationFiles(DestinationDirectoryWithMacrosExtended); + _batchContext.DestinationFiles = GetDestinationFiles(DestinationDirectoryWithMacrosExtended, _cancellationToken); foreach (var file in files) { - cancellationToken.ThrowIfCancellationRequested(); + _cancellationToken.ThrowIfCancellationRequested(); // Check that the connection is alive and if not try to connect again if (!client.IsConnected) @@ -203,9 +206,15 @@ public FileTransferResult Run(CancellationToken cancellationToken) _logger.NotifyError(_batchContext, userResultMessage, ex); return FormFailedFileTransferResult(userResultMessage); } + catch (ArgumentException ex) + { + userResultMessage = $"{ex.Message} {userResultMessage}"; + _logger.NotifyError(_batchContext, userResultMessage, ex); + return FormFailedFileTransferResult(userResultMessage); + } catch (Exception ex) { - userResultMessage = $"Error when establishing connection to the Server: {ex.Message}, {userResultMessage}"; + userResultMessage = $"Error when executing file transfer: {ex.Message}, {userResultMessage}"; _logger.NotifyError(_batchContext, userResultMessage, ex); return FormFailedFileTransferResult(userResultMessage); } @@ -225,10 +234,17 @@ private ConnectionInfo GetConnectionInfo(Destination destination, Connection con if (connect.UseKeyboardInteractiveAuthentication) { - // Construct keyboard-interactive authentication method - var kauth = new KeyboardInteractiveAuthenticationMethod(connect.UserName); - kauth.AuthenticationPrompt += new EventHandler(HandleKeyEvent); - methods.Add(kauth); + try + { + // Construct keyboard-interactive authentication method + var kauth = new KeyboardInteractiveAuthenticationMethod(connect.UserName); + kauth.AuthenticationPrompt += new EventHandler(HandleKeyEvent); + methods.Add(kauth); + } catch (Exception ex) + { + _logger.NotifyError(_batchContext, "Failure in Keyboard-Interactive authentication: ", ex); + } + } PrivateKeyFile privateKey = null; @@ -236,8 +252,8 @@ private ConnectionInfo GetConnectionInfo(Destination destination, Connection con { if (string.IsNullOrEmpty(connect.PrivateKeyFile)) throw new ArgumentException("Private key file path was not given."); - privateKey = (connect.PrivateKeyFilePassphrase != null) - ? new PrivateKeyFile(connect.PrivateKeyFile, connect.PrivateKeyFilePassphrase) + privateKey = (connect.PrivateKeyPassphrase != null) + ? new PrivateKeyFile(connect.PrivateKeyFile, connect.PrivateKeyPassphrase) : new PrivateKeyFile(connect.PrivateKeyFile); } if (connect.Authentication == AuthenticationType.UsernamePrivateKeyString || connect.Authentication == AuthenticationType.UsernamePasswordPrivateKeyString) @@ -245,9 +261,9 @@ private ConnectionInfo GetConnectionInfo(Destination destination, Connection con if (string.IsNullOrEmpty(connect.PrivateKeyString)) throw new ArgumentException("Private key string was not given."); var stream = new MemoryStream(Encoding.UTF8.GetBytes(connect.PrivateKeyString)); - privateKey = (connect.PrivateKeyFilePassphrase != null) - ? new PrivateKeyFile(stream, connect.PrivateKeyFilePassphrase) - : new PrivateKeyFile(stream, ""); + privateKey = (connect.PrivateKeyPassphrase != null) + ? new PrivateKeyFile(stream, connect.PrivateKeyPassphrase) + : new PrivateKeyFile(stream); } switch (connect.Authentication) { @@ -272,18 +288,43 @@ private ConnectionInfo GetConnectionInfo(Destination destination, Connection con throw new ArgumentException($"Unknown Authentication type: '{connect.Authentication}'."); } - connectionInfo = new ConnectionInfo(connect.Address, connect.Port, connect.UserName, methods.ToArray()); - - connectionInfo.Encoding = Util.GetEncoding(destination.FileNameEncoding, destination.FileNameEncodingInString, destination.EnableBomForFileName); + connectionInfo = new ConnectionInfo(connect.Address, connect.Port, connect.UserName, methods.ToArray()) + { + Encoding = Util.GetEncoding(destination.FileNameEncoding, destination.FileNameEncodingInString, destination.EnableBomForFileName) + }; return connectionInfo; } private void HandleKeyEvent(object sender, AuthenticationPromptEventArgs e) { - foreach (var prompt in e.Prompts) - if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) - prompt.Response = _batchContext.Connection.Password; + _logger.NotifyInformation(_batchContext, $"Keyboard-Interactive negotiation started with the server {_batchContext.Connection.Address}."); + foreach (var serverPrompt in e.Prompts) + { + _cancellationToken.ThrowIfCancellationRequested(); + + _logger.NotifyInformation(_batchContext, $"Prompt: {serverPrompt.Request.Replace(":", "")}"); + + if (!string.IsNullOrEmpty(_batchContext.Connection.Password) && serverPrompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) + serverPrompt.Response = _batchContext.Connection.Password; + else + { + if (!_batchContext.Connection.PromptAndResponse.Any() || !_batchContext.Connection.PromptAndResponse.Select(p => p.Prompt.ToLower()).ToList().Contains(serverPrompt.Request.Replace(":", "").Trim().ToLower())) + { + var errorMsg = $"Failure in Keyboard-interactive authentication: No response given for server prompt request --> {serverPrompt.Request.Replace(":", "").Trim()}"; + throw new ArgumentException(errorMsg); + } + + foreach (var prompt in _batchContext.Connection.PromptAndResponse) + { + _cancellationToken.ThrowIfCancellationRequested(); + if (serverPrompt.Request.IndexOf(prompt.Prompt, StringComparison.InvariantCultureIgnoreCase) != -1) + serverPrompt.Response = prompt.Response; + } + } + } + _logger.NotifyInformation(_batchContext, $"Keyboard-Interactive negotiation finished."); + } private static void ForceHostKeyAlgorithm(SftpClient client, HostKeyAlgorithms algorithm) @@ -317,70 +358,80 @@ private static void ForceHostKeyAlgorithm(SftpClient client, HostKeyAlgorithms a private void CheckServerFingerprint(SftpClient client, string expectedServerFingerprint) { - var userResultMessage = ""; + var userResultMessage = string.Empty; + var MD5serverFingerprint = string.Empty; + var SHAServerFingerprint = string.Empty; client.HostKeyReceived += delegate (object sender, HostKeyEventArgs e) { - if (Util.IsMD5(expectedServerFingerprint.Replace(":", "").Replace("-", ""))) + MD5serverFingerprint = BitConverter.ToString(e.FingerPrint).Replace('-', ':'); + + using (SHA256 mySHA256 = SHA256.Create()) { - if (!expectedServerFingerprint.Contains(':')) - { - var serverFingerprint = BitConverter.ToString(e.FingerPrint).Replace("-", "").Replace(":", ""); - e.CanTrust = expectedServerFingerprint.ToLower() == serverFingerprint.ToLower(); - if (!e.CanTrust) - userResultMessage = $"Can't trust SFTP server. The server fingerprint does not match. " + - $"Expected fingerprint: '{expectedServerFingerprint}', but was: '{serverFingerprint}'."; - } - else - { - var serverFingerprint = BitConverter.ToString(e.FingerPrint).Replace('-', ':'); - e.CanTrust = e.FingerPrint.SequenceEqual(Util.ConvertFingerprintToByteArray(expectedServerFingerprint)); - if (!e.CanTrust) - userResultMessage = $"Can't trust SFTP server. The server fingerprint does not match. " + - $"Expected fingerprint: '{expectedServerFingerprint}', but was: '{serverFingerprint}'."; - } - + SHAServerFingerprint = Convert.ToBase64String(mySHA256.ComputeHash(e.HostKey)); } - else if (Util.IsSha256(expectedServerFingerprint)) + + if (!string.IsNullOrEmpty(expectedServerFingerprint)) { - if (Util.TryConvertHexStringToHex(expectedServerFingerprint)) + if (Util.IsMD5(expectedServerFingerprint.Replace(":", "").Replace("-", ""))) { - using (SHA256 mySHA256 = SHA256.Create()) + if (!expectedServerFingerprint.Contains(':')) { - var sha256Fingerprint = Util.ToHex(mySHA256.ComputeHash(e.HostKey)); - - e.CanTrust = (sha256Fingerprint == expectedServerFingerprint); + e.CanTrust = expectedServerFingerprint.ToLower() == MD5serverFingerprint.Replace(":", "").ToLower(); + if (!e.CanTrust) + userResultMessage = $"Can't trust SFTP server. The server fingerprint does not match. " + + $"Expected fingerprint: '{expectedServerFingerprint}', but was: '{MD5serverFingerprint}'."; + } + else + { + e.CanTrust = e.FingerPrint.SequenceEqual(Util.ConvertFingerprintToByteArray(expectedServerFingerprint)); if (!e.CanTrust) userResultMessage = $"Can't trust SFTP server. The server fingerprint does not match. " + - $"Expected fingerprint: '{expectedServerFingerprint}', but was: '{sha256Fingerprint}'."; + $"Expected fingerprint: '{expectedServerFingerprint}', but was: '{MD5serverFingerprint}'."; } + } - else + else if (Util.IsSha256(expectedServerFingerprint)) { - using (SHA256 mySHA256 = SHA256.Create()) + if (Util.TryConvertHexStringToHex(expectedServerFingerprint)) { - var sha256Fingerprint = Convert.ToBase64String(mySHA256.ComputeHash(e.HostKey)); - e.CanTrust = (sha256Fingerprint == expectedServerFingerprint || sha256Fingerprint.Replace("=", "") == expectedServerFingerprint); + using (SHA256 mySHA256 = SHA256.Create()) + { + SHAServerFingerprint = Util.ToHex(mySHA256.ComputeHash(e.HostKey)); + } + e.CanTrust = (SHAServerFingerprint == expectedServerFingerprint); if (!e.CanTrust) userResultMessage = $"Can't trust SFTP server. The server fingerprint does not match. " + - $"Expected fingerprint: '{expectedServerFingerprint}', but was: '{sha256Fingerprint}'."; + $"Expected fingerprint: '{expectedServerFingerprint}', but was: '{SHAServerFingerprint}'."; + } + else + { + e.CanTrust = (SHAServerFingerprint == expectedServerFingerprint || SHAServerFingerprint.Replace("=", "") == expectedServerFingerprint); + if (!e.CanTrust) + userResultMessage = $"Can't trust SFTP server. The server fingerprint does not match. " + + $"Expected fingerprint: '{expectedServerFingerprint}', but was: '{SHAServerFingerprint}'."; } } - } - else - { - userResultMessage = "Expected server fingerprint was given in unsupported format."; - e.CanTrust = false; + else + { + userResultMessage = "Expected server fingerprint was given in unsupported format."; + e.CanTrust = false; + } + + if (!e.CanTrust) + _logger.NotifyError(_batchContext, userResultMessage, new SshConnectionException()); } - if (!e.CanTrust) - _logger.NotifyError(_batchContext, userResultMessage, new SshConnectionException()); + _logger.NotifyInformation(_batchContext, $"Server: {client.ConnectionInfo.ServerVersion}"); + _logger.NotifyInformation(_batchContext, $"Fingerprint (MD5): {MD5serverFingerprint.ToLower()}"); + _logger.NotifyInformation(_batchContext, $"Fingerprint (SHA-256): {SHAServerFingerprint.Replace("=", "")}"); + _logger.NotifyInformation(_batchContext, $"Cipher info: {client.ConnectionInfo.CurrentKeyExchangeAlgorithm}, {client.ConnectionInfo.CurrentHostKeyAlgorithm}, {client.ConnectionInfo.CurrentServerEncryption}"); }; } private Tuple, bool> ListSourceFiles(SftpClient client, Source source, CancellationToken cancellationToken) { - SetCurrentState(TransferState.CheckSourceFiles, "Checking source files."); + //SetCurrentState(TransferState.CheckSourceFiles, "Checking source files."); var fileItems = new List(); @@ -391,7 +442,7 @@ private Tuple, bool> ListSourceFiles(SftpClient client, Source so cancellationToken.ThrowIfCancellationRequested(); if (!client.Exists(file)) - _logger.NotifyError(_batchContext, $"File does not exist: '{file}", new FileNotFoundException()); + _logger.NotifyError(_batchContext, $"File does not exist: '{file}", new SftpPathNotFoundException()); else fileItems.Add(new FileItem(client.Get(file))); } @@ -415,7 +466,6 @@ private Tuple, bool> ListSourceFiles(SftpClient client, Source so { cancellationToken.ThrowIfCancellationRequested(); - var name = file.Name; if (file.Name.Equals(".") || file.Name.Equals("..")) continue; if (file.IsDirectory) continue; @@ -423,19 +473,20 @@ private Tuple, bool> ListSourceFiles(SftpClient client, Source so if (file.Name.Equals(source.FileName) || Util.FileMatchesMask(Path.GetFileName(file.FullName), source.FileName)) { FileItem item = new FileItem(file); - _logger.NotifyInformation(_batchContext, $"FILE LIST {item.FullPath}."); + _logger.NotifyInformation(_batchContext, $"FILE LIST {item.FullPath}"); fileItems.Add(item); } } return new Tuple, bool>(fileItems, true); } - private static List GetDestinationFiles(string directory) + private static List GetDestinationFiles(string directory, CancellationToken cancellationToken) { var destFiles = Directory.GetFiles(directory); var result = new List(); foreach (var file in destFiles) { + cancellationToken.ThrowIfCancellationRequested(); result.Add(new FileItem(file)); } @@ -545,7 +596,7 @@ private SingleFileTransferResult NoSourceOperation(BatchContext context, Source switch (_batchContext.Source.Action) { case SourceAction.Error: - _logger.NotifyError(context, msg, new FileNotFoundException()); + _logger.NotifyError(context, msg, new ArgumentException(msg)); return new SingleFileTransferResult { Success = false, ErrorMessages = { msg } }; case SourceAction.Info: _logger.NotifyInformation(context, msg); @@ -576,6 +627,14 @@ private void SetCurrentState(TransferState state, string msg) _logger.NotifyTrace($"{state}: {msg}"); } + private static void LogSourceSystemInfo(BatchContext context, ConnectionInfo connectionInfo, ISFTPLogger logger) + { + logger.NotifyInformation(context, $"Assembly: {Assembly.GetAssembly(typeof(SftpClient)).GetName().Name} {Assembly.GetAssembly(typeof(SftpClient)).GetName().Version}"); + var bit = Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit"; + logger.NotifyInformation(context, $"Platform: {RuntimeInformation.OSDescription} {bit}; CLR: {RuntimeInformation.FrameworkDescription}"); + logger.NotifyInformation(context, $"Culture: {CultureInfo.CurrentCulture.TwoLetterISOLanguageName}; {Encoding.Default.WebName}"); + } + #endregion } diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/PromptResponse.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/PromptResponse.cs new file mode 100644 index 0000000..12dbcec --- /dev/null +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/PromptResponse.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.SFTP.DownloadFiles.Definitions +{ + /// + /// Prompt response class for Keyboard-interactive authentication. + /// + public class PromptResponse + { + /// + /// Prompt from the server what is to be expected. + /// + /// Verification code + public string Prompt { get; set; } + + /// + /// Response for the Prompt from the server. + /// + /// 123456789 + [PasswordPropertyText] + [DisplayFormat(DataFormatString = "Text")] + public string Response { get; set; } + } +} diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/SFTPLogger.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/SFTPLogger.cs index 0246f68..dc51695 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/SFTPLogger.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/SFTPLogger.cs @@ -43,6 +43,7 @@ public void NotifyError(BatchContext context, string msg, Exception e) { try { + NotifyInformation(context, e.ToString()); if (context == null) context = new BatchContext(); var sourceEndPointName = GetEndPointName(context, EndPoint.Source, "unknown source end point"); diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/SingleFileTransfer.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/SingleFileTransfer.cs index 5b626f4..9318929 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/SingleFileTransfer.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/SingleFileTransfer.cs @@ -319,7 +319,7 @@ private void ExecuteSourceOperationMoveOrRename() if (!Client.Exists(destFileName)) throw new Exception($"Failure in source operation: Source file couldn't be moved to move to directory."); _logger.NotifyInformation(BatchContext, $"Source file {SourceFileDuringTransfer} moved to target {destFileName}."); - SourceFile = new FileItem(file); + WorkFile = new FileItem(file); if (SourceFile.FullPath == null) _logger.NotifyInformation(BatchContext, "Source end point returned null as the moved file. It should return the name of the moved file."); @@ -453,7 +453,9 @@ private string RestoreSourceFileAfterErrorIfItWasRenamed() { if (BatchContext.Source.Operation == SourceOperation.Move) RestoreSourceFileIfItWasMoved(); - if (!Path.GetFileName(WorkFile.Name).Equals(SourceFile.Name) || !SourceFileDuringTransfer.Equals(SourceFile.FullPath)) + if (BatchContext.Source.Operation == SourceOperation.Rename && !Client.Exists(SourceFile.FullPath)) + Client.RenameFile(SourceFileDuringTransfer, SourceFile.FullPath); + if (BatchContext.Options.RenameSourceFileBeforeTransfer && !Client.Exists(SourceFile.FullPath)) Client.RenameFile(SourceFileDuringTransfer, SourceFile.FullPath); return "[Source file restored.]"; } diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/TransferLogSink.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/TransferLogSink.cs index 014eb77..cabb2dd 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/TransferLogSink.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/TransferLogSink.cs @@ -1,4 +1,5 @@ -using Serilog.Core; +using System.Collections.Concurrent; +using Serilog.Core; using Serilog.Events; namespace Frends.SFTP.DownloadFiles.Definitions; @@ -64,5 +65,47 @@ public IList> GetBufferedLogMessages() return _initialLogMessages; } + + /// + /// Circular buffer impl, original from https://codereview.stackexchange.com/a/134147 + /// + /// + public class CircularBuffer + { + private readonly ConcurrentQueue _data; + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + private readonly int _size; + + public CircularBuffer(int size) + { + if (size < 1) throw new ArgumentException($"{nameof(size)} cannot be negative or zero"); + _data = new ConcurrentQueue(); + _size = size; + } + + public IReadOnlyList Latest() + { + return _data.ToArray(); + } + + public void Add(T t) + { + _lock.EnterWriteLock(); + try + { + if (_data.Count == _size) + { + T value; + _data.TryDequeue(out value); + } + + _data.Enqueue(t); + } + finally + { + _lock.ExitWriteLock(); + } + } + } } diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.cs index 08e1c09..139286d 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.cs @@ -140,8 +140,8 @@ public static Result DownloadFiles( Connection = connection }; - var fileTransporter = new FileTransporter(logger, _batchContext, executionId); - var result = fileTransporter.Run(cancellationToken); + var fileTransporter = new FileTransporter(logger, _batchContext, executionId, cancellationToken); + var result = fileTransporter.Run(); if (options.ThrowErrorOnFail && !result.Success) throw new Exception($"SFTP transfer failed: {result.UserResultMessage}. " + diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.csproj b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.csproj index 5a2aeaa..e28c21a 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.csproj +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.csproj @@ -8,7 +8,7 @@ Frends.SFTP.DownloadFiles Frends.SFTP.DownloadFiles - 2.6.1 + 2.7.0 Frends Frends Frends From e7b3f0b5e2fb5847cfc6160b4fcee7914b5671c1 Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Wed, 7 Jun 2023 15:50:03 +0300 Subject: [PATCH 2/9] Fixed typos in Changelog --- Frends.SFTP.DownloadFiles/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Frends.SFTP.DownloadFiles/CHANGELOG.md b/Frends.SFTP.DownloadFiles/CHANGELOG.md index 2155ea5..91c9a06 100644 --- a/Frends.SFTP.DownloadFiles/CHANGELOG.md +++ b/Frends.SFTP.DownloadFiles/CHANGELOG.md @@ -3,9 +3,9 @@ ## [2.7.0] - 2023-06-07 ### Fixed - Modified private key passphrase to be visible when all private key authentication options were enabled. -- Added new parameter for keyboard-interactive auhentication were users can add prompts and responses. +- Added new parameter for keyboard-interactive authentication where users can add prompts and responses. - Modified operations log to list current system and sftp server information. -- Fixed operations log to show case exceptions more precise. +- Fixed operations log to show case exceptions more precisely. ## [2.6.1] - 2023-05-17 ### Fixed From a622ae5e86b4964d9cf5d0a95c76eb37ac3e771c Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Wed, 7 Jun 2023 15:50:38 +0300 Subject: [PATCH 3/9] Added breaking remark to changelog --- Frends.SFTP.DownloadFiles/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frends.SFTP.DownloadFiles/CHANGELOG.md b/Frends.SFTP.DownloadFiles/CHANGELOG.md index 91c9a06..abdc6d0 100644 --- a/Frends.SFTP.DownloadFiles/CHANGELOG.md +++ b/Frends.SFTP.DownloadFiles/CHANGELOG.md @@ -3,7 +3,7 @@ ## [2.7.0] - 2023-06-07 ### Fixed - Modified private key passphrase to be visible when all private key authentication options were enabled. -- Added new parameter for keyboard-interactive authentication where users can add prompts and responses. +- [Breaking] Added new parameter for keyboard-interactive authentication where users can add prompts and responses. - Modified operations log to list current system and sftp server information. - Fixed operations log to show case exceptions more precisely. From 89fe719008847c419b8df652de23e16ca7dca8ae Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Wed, 7 Jun 2023 15:57:45 +0300 Subject: [PATCH 4/9] Debugging failing test --- .../Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs index adc22e6..1c7e28e 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs @@ -129,8 +129,9 @@ public void DownloadFiles_TestShouldThrowWithoutPromptAndResponse() connection.ServerFingerPrint = "NUfXVu2omU2k3ELtmCzhkcERRLHAEbNakrpBgEXn8JM"; var ex = Assert.Throws(() => SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken())); + Console.WriteLine(ex.Message); Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); - + connection.Authentication = AuthenticationType.UsernamePrivateKeyString; connection.PrivateKeyFile = null; connection.PrivateKeyString = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); @@ -145,6 +146,7 @@ public void DownloadFiles_TestShouldThrowWithoutPromptAndResponse() }; ex = Assert.Throws(() => SFTP.DownloadFiles(_source, destination, connection, _options, _info, new CancellationToken())); + Console.WriteLine(ex.Message); Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); } From 9b52cc3f94fc6c7978d52b7cf0b7fb555edd4780 Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Wed, 7 Jun 2023 16:08:52 +0300 Subject: [PATCH 5/9] Linting fixes and more debugging --- .../ConnectivityTests.cs | 34 ------------------ .../ServerFingerprintTests.cs | 35 +++++++++++++++++++ .../Definitions/FileTransporter.cs | 11 +++--- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs index 1c7e28e..3a4aa30 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs @@ -116,40 +116,6 @@ public void DownloadFiles_TestWithInteractiveKeyboardAuthenticationAndPrivateKey Assert.AreEqual(1, result.SuccessfulTransferCount); } - [Test] - public void DownloadFiles_TestShouldThrowWithoutPromptAndResponse() - { - var connection = Helpers.GetSftpConnection(); - connection.Authentication = AuthenticationType.UsernamePrivateKeyFile; - connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); - connection.Password = null; - connection.PrivateKeyPassphrase = "passphrase"; - connection.UseKeyboardInteractiveAuthentication = true; - connection.HostKeyAlgorithm = HostKeyAlgorithms.RSA; - connection.ServerFingerPrint = "NUfXVu2omU2k3ELtmCzhkcERRLHAEbNakrpBgEXn8JM"; - - var ex = Assert.Throws(() => SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken())); - Console.WriteLine(ex.Message); - Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); - - connection.Authentication = AuthenticationType.UsernamePrivateKeyString; - connection.PrivateKeyFile = null; - connection.PrivateKeyString = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); - connection.PrivateKeyPassphrase = "passphrase"; - - var destination = new Destination - { - Directory = Path.Combine(_workDir, "destination"), - Action = DestinationAction.Overwrite, - FileNameEncoding = FileEncoding.UTF8, - EnableBomForFileName = true - }; - - ex = Assert.Throws(() => SFTP.DownloadFiles(_source, destination, connection, _options, _info, new CancellationToken())); - Console.WriteLine(ex.Message); - Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); - } - [Test] public void DownloadFiles_TestKeepAliveIntervalWithDefault() { diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs index cfd0f78..b420d5e 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading; using NUnit.Framework; using Frends.SFTP.DownloadFiles.Definitions; @@ -152,6 +153,40 @@ public void DownloadFiles_TestThrowsTransferWithInvalidExpectedServerFingerprint Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Error when establishing connection to the Server: Key exchange negotiation failed..")); Assert.IsTrue(ex.Message.Contains("Expected server fingerprint was given in unsupported format.")); } + + [Test] + public void DownloadFiles_TestShouldThrowWithoutPromptAndResponse() + { + var connection = Helpers.GetSftpConnection(); + connection.Authentication = AuthenticationType.UsernamePrivateKeyFile; + connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); + connection.Password = null; + connection.PrivateKeyPassphrase = "passphrase"; + connection.UseKeyboardInteractiveAuthentication = true; + connection.HostKeyAlgorithm = HostKeyAlgorithms.RSA; + connection.ServerFingerPrint = _Sha256Hash.Replace("=", ""); + + var ex = Assert.Throws(() => SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken())); + Console.WriteLine(ex.Message); + Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); + + connection.Authentication = AuthenticationType.UsernamePrivateKeyString; + connection.PrivateKeyFile = null; + connection.PrivateKeyString = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); + connection.PrivateKeyPassphrase = "passphrase"; + + var destination = new Destination + { + Directory = Path.Combine(_workDir, "destination"), + Action = DestinationAction.Overwrite, + FileNameEncoding = FileEncoding.UTF8, + EnableBomForFileName = true + }; + + ex = Assert.Throws(() => SFTP.DownloadFiles(_source, destination, connection, _options, _info, new CancellationToken())); + Console.WriteLine(ex.Message); + Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); + } } } diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs index c218117..3529f4f 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs @@ -240,11 +240,12 @@ private ConnectionInfo GetConnectionInfo(Destination destination, Connection con var kauth = new KeyboardInteractiveAuthenticationMethod(connect.UserName); kauth.AuthenticationPrompt += new EventHandler(HandleKeyEvent); methods.Add(kauth); - } catch (Exception ex) + } + catch (Exception ex) { _logger.NotifyError(_batchContext, "Failure in Keyboard-Interactive authentication: ", ex); } - + } PrivateKeyFile privateKey = null; @@ -314,13 +315,13 @@ private void HandleKeyEvent(object sender, AuthenticationPromptEventArgs e) var errorMsg = $"Failure in Keyboard-interactive authentication: No response given for server prompt request --> {serverPrompt.Request.Replace(":", "").Trim()}"; throw new ArgumentException(errorMsg); } - + foreach (var prompt in _batchContext.Connection.PromptAndResponse) { _cancellationToken.ThrowIfCancellationRequested(); if (serverPrompt.Request.IndexOf(prompt.Prompt, StringComparison.InvariantCultureIgnoreCase) != -1) serverPrompt.Response = prompt.Response; - } + } } } _logger.NotifyInformation(_batchContext, $"Keyboard-Interactive negotiation finished."); @@ -365,7 +366,7 @@ private void CheckServerFingerprint(SftpClient client, string expectedServerFing client.HostKeyReceived += delegate (object sender, HostKeyEventArgs e) { MD5serverFingerprint = BitConverter.ToString(e.FingerPrint).Replace('-', ':'); - + using (SHA256 mySHA256 = SHA256.Create()) { SHAServerFingerprint = Convert.ToBase64String(mySHA256.ComputeHash(e.HostKey)); From a0e73d691be5cd63616fb2b5c1ba464a356f479b Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Wed, 7 Jun 2023 16:14:52 +0300 Subject: [PATCH 6/9] Removed debug lines --- .../Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs index b420d5e..f8fca21 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ServerFingerprintTests.cs @@ -167,7 +167,6 @@ public void DownloadFiles_TestShouldThrowWithoutPromptAndResponse() connection.ServerFingerPrint = _Sha256Hash.Replace("=", ""); var ex = Assert.Throws(() => SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken())); - Console.WriteLine(ex.Message); Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); connection.Authentication = AuthenticationType.UsernamePrivateKeyString; @@ -184,7 +183,6 @@ public void DownloadFiles_TestShouldThrowWithoutPromptAndResponse() }; ex = Assert.Throws(() => SFTP.DownloadFiles(_source, destination, connection, _options, _info, new CancellationToken())); - Console.WriteLine(ex.Message); Assert.IsTrue(ex.Message.StartsWith("SFTP transfer failed: Failure in Keyboard-interactive authentication: No response given for server prompt request --> Password")); } } From 267e98ab232f82b7a9f5fa1cee119a75ec7687d5 Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Thu, 8 Jun 2023 14:03:38 +0300 Subject: [PATCH 7/9] Fixed keyboard-interactive logging --- .../Definitions/FileTransporter.cs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs index 3529f4f..367f463 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs @@ -299,33 +299,35 @@ private ConnectionInfo GetConnectionInfo(Destination destination, Connection con private void HandleKeyEvent(object sender, AuthenticationPromptEventArgs e) { - _logger.NotifyInformation(_batchContext, $"Keyboard-Interactive negotiation started with the server {_batchContext.Connection.Address}."); - foreach (var serverPrompt in e.Prompts) + if (e.Prompts.Any()) { - _cancellationToken.ThrowIfCancellationRequested(); + _logger.NotifyInformation(_batchContext, $"Keyboard-Interactive negotiation started with the server {_batchContext.Connection.Address}."); + foreach (var serverPrompt in e.Prompts) + { + _cancellationToken.ThrowIfCancellationRequested(); - _logger.NotifyInformation(_batchContext, $"Prompt: {serverPrompt.Request.Replace(":", "")}"); + _logger.NotifyInformation(_batchContext, $"Prompt: {serverPrompt.Request.Replace(":", "")}"); - if (!string.IsNullOrEmpty(_batchContext.Connection.Password) && serverPrompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) - serverPrompt.Response = _batchContext.Connection.Password; - else - { - if (!_batchContext.Connection.PromptAndResponse.Any() || !_batchContext.Connection.PromptAndResponse.Select(p => p.Prompt.ToLower()).ToList().Contains(serverPrompt.Request.Replace(":", "").Trim().ToLower())) + if (!string.IsNullOrEmpty(_batchContext.Connection.Password) && serverPrompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) + serverPrompt.Response = _batchContext.Connection.Password; + else { - var errorMsg = $"Failure in Keyboard-interactive authentication: No response given for server prompt request --> {serverPrompt.Request.Replace(":", "").Trim()}"; - throw new ArgumentException(errorMsg); - } + if (!_batchContext.Connection.PromptAndResponse.Any() || !_batchContext.Connection.PromptAndResponse.Select(p => p.Prompt.ToLower()).ToList().Contains(serverPrompt.Request.Replace(":", "").Trim().ToLower())) + { + var errorMsg = $"Failure in Keyboard-interactive authentication: No response given for server prompt request --> {serverPrompt.Request.Replace(":", "").Trim()}"; + throw new ArgumentException(errorMsg); + } - foreach (var prompt in _batchContext.Connection.PromptAndResponse) - { - _cancellationToken.ThrowIfCancellationRequested(); - if (serverPrompt.Request.IndexOf(prompt.Prompt, StringComparison.InvariantCultureIgnoreCase) != -1) - serverPrompt.Response = prompt.Response; + foreach (var prompt in _batchContext.Connection.PromptAndResponse) + { + _cancellationToken.ThrowIfCancellationRequested(); + if (serverPrompt.Request.IndexOf(prompt.Prompt, StringComparison.InvariantCultureIgnoreCase) != -1) + serverPrompt.Response = prompt.Response; + } } } + _logger.NotifyInformation(_batchContext, $"Keyboard-Interactive negotiation finished."); } - _logger.NotifyInformation(_batchContext, $"Keyboard-Interactive negotiation finished."); - } private static void ForceHostKeyAlgorithm(SftpClient client, HostKeyAlgorithms algorithm) From 29adcb25923403bc4ec0ccd337ffa6d50f72fd37 Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Fri, 9 Jun 2023 08:52:44 +0300 Subject: [PATCH 8/9] PR review changes --- Frends.SFTP.DownloadFiles/CHANGELOG.md | 5 +- .../ConnectivityTests.cs | 321 +++++++++--------- .../ErrorTests.cs | 6 +- .../Definitions/FileTransporter.cs | 10 +- .../Definitions/Result.cs | 246 +++++++------- .../Frends.SFTP.DownloadFiles.cs | 2 +- 6 files changed, 292 insertions(+), 298 deletions(-) diff --git a/Frends.SFTP.DownloadFiles/CHANGELOG.md b/Frends.SFTP.DownloadFiles/CHANGELOG.md index abdc6d0..5f24be1 100644 --- a/Frends.SFTP.DownloadFiles/CHANGELOG.md +++ b/Frends.SFTP.DownloadFiles/CHANGELOG.md @@ -1,10 +1,11 @@ # Changelog ## [2.7.0] - 2023-06-07 -### Fixed -- Modified private key passphrase to be visible when all private key authentication options were enabled. +### Added - [Breaking] Added new parameter for keyboard-interactive authentication where users can add prompts and responses. - Modified operations log to list current system and sftp server information. +### Fixed +- Modified private key passphrase to be visible when all private key authentication options were enabled. - Fixed operations log to show case exceptions more precisely. ## [2.6.1] - 2023-05-17 diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs index 3a4aa30..49940fa 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ConnectivityTests.cs @@ -1,163 +1,162 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading; -using NUnit.Framework; -using Frends.SFTP.DownloadFiles.Definitions; - -namespace Frends.SFTP.DownloadFiles.Tests -{ - [TestFixture] - public class ConnectivityTests : DownloadFilesTestBase - { - [Test] - public void DownloadFiles_TestWithLargerBuffer() - { - Helpers.UploadLargeTestFiles(_source.Directory, 1); - - var connection = Helpers.GetSftpConnection(); - connection.BufferSize = 256; - - var source = new Source - { - Directory = _source.Directory, - FileName = "LargeTestFile1.bin", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing, - }; - - var result = SFTP.DownloadFiles(source, _destination, connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - } - - [Test] - public void DownloadFiles_TestTransferThatThrowsWithIncorrectCredentials() - { - var connection = Helpers.GetSftpConnection(); - connection.ConnectionTimeout = 10; - connection.UserName = "demo"; - connection.Password = "demo"; - - var result = Assert.Throws(() => SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken())); - Assert.That(result.Message.StartsWith("SFTP transfer failed: Authentication of SSH session failed: Permission denied (password)")); - } - - [Test] - public void DownloadFiles_TestPrivateKeyFileRsa() - { - var connection = Helpers.GetSftpConnection(); - connection.Authentication = AuthenticationType.UsernamePasswordPrivateKeyFile; - connection.PrivateKeyPassphrase = "passphrase"; - connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); - - var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - } - - [Test] - public void DownloadFiles_TestPrivateKeyFileRsaFromString() - { - var key = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); - - var connection = Helpers.GetSftpConnection(); - connection.HostKeyAlgorithm = HostKeyAlgorithms.RSA; - connection.Authentication = AuthenticationType.UsernamePasswordPrivateKeyString; - connection.PrivateKeyPassphrase = "passphrase"; - connection.PrivateKeyString = key; - - var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - } - - [Test] - public void DownloadFiles_TestWithInteractiveKeyboardAuthentication() - { - var connection = Helpers.GetSftpConnection(); - connection.UseKeyboardInteractiveAuthentication = true; - - var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); +using System; +using System.IO; +using System.Threading; +using NUnit.Framework; +using Frends.SFTP.DownloadFiles.Definitions; + +namespace Frends.SFTP.DownloadFiles.Tests +{ + [TestFixture] + public class ConnectivityTests : DownloadFilesTestBase + { + [Test] + public void DownloadFiles_TestWithLargerBuffer() + { + Helpers.UploadLargeTestFiles(_source.Directory, 1); + + var connection = Helpers.GetSftpConnection(); + connection.BufferSize = 256; + + var source = new Source + { + Directory = _source.Directory, + FileName = "LargeTestFile1.bin", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing, + }; + + var result = SFTP.DownloadFiles(source, _destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); Assert.AreEqual(1, result.SuccessfulTransferCount); - } - - [Test] - public void DownloadFiles_TestWithInteractiveKeyboardAuthenticationAndPrivateKey() - { - var connection = Helpers.GetSftpConnection(); - connection.Authentication = AuthenticationType.UsernamePrivateKeyFile; - connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); - connection.Password = null; - connection.PrivateKeyPassphrase = "passphrase"; - connection.UseKeyboardInteractiveAuthentication = true; - connection.PromptAndResponse = new PromptResponse[] { new PromptResponse { Prompt = "Password", Response = "pass" } }; - - var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - - connection.Authentication = AuthenticationType.UsernamePrivateKeyString; - connection.PrivateKeyFile = null; - connection.PrivateKeyString = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); - connection.PrivateKeyPassphrase = "passphrase"; - - var destination = new Destination - { - Directory = Path.Combine(_workDir, "destination"), - Action = DestinationAction.Overwrite, - FileNameEncoding = FileEncoding.UTF8, - EnableBomForFileName = true - }; - - result = SFTP.DownloadFiles(_source, destination, connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); + } + + [Test] + public void DownloadFiles_TestTransferThatThrowsWithIncorrectCredentials() + { + var connection = Helpers.GetSftpConnection(); + connection.ConnectionTimeout = 10; + connection.UserName = "demo"; + connection.Password = "demo"; + + var result = Assert.Throws(() => SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken())); + Assert.IsTrue(result.Message.StartsWith("SFTP transfer failed: Authentication of SSH session failed: Permission denied (password)")); + } + + [Test] + public void DownloadFiles_TestPrivateKeyFileRsa() + { + var connection = Helpers.GetSftpConnection(); + connection.Authentication = AuthenticationType.UsernamePasswordPrivateKeyFile; + connection.PrivateKeyPassphrase = "passphrase"; + connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); + + var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); Assert.AreEqual(1, result.SuccessfulTransferCount); - } - - [Test] - public void DownloadFiles_TestKeepAliveIntervalWithDefault() - { - Helpers.UploadLargeTestFiles(_source.Directory, 1); - - var connection = Helpers.GetSftpConnection(); - - var source = new Source - { - Directory = _source.Directory, - FileName = "*.bin", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing, - }; - - var result = SFTP.DownloadFiles(source, _destination, connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - } - - [Test] - public void DownloadFiles_TestKeepAliveIntervalWith1ms() - { - Helpers.UploadLargeTestFiles(_source.Directory, 1); - - var connection = Helpers.GetSftpConnection(); - connection.KeepAliveInterval = 1; - connection.BufferSize = 256; - - var source = new Source - { - Directory = _source.Directory, - FileName = "*.bin", - Action = SourceAction.Error, - Operation = SourceOperation.Nothing, - }; - - var result = SFTP.DownloadFiles(source, _destination, connection, _options, _info, new CancellationToken()); - Assert.IsTrue(result.Success); - Assert.AreEqual(1, result.SuccessfulTransferCount); - } - } -} - - + } + + [Test] + public void DownloadFiles_TestPrivateKeyFileRsaFromString() + { + var key = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); + + var connection = Helpers.GetSftpConnection(); + connection.HostKeyAlgorithm = HostKeyAlgorithms.RSA; + connection.Authentication = AuthenticationType.UsernamePasswordPrivateKeyString; + connection.PrivateKeyPassphrase = "passphrase"; + connection.PrivateKeyString = key; + + var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + } + + [Test] + public void DownloadFiles_TestWithInteractiveKeyboardAuthentication() + { + var connection = Helpers.GetSftpConnection(); + connection.UseKeyboardInteractiveAuthentication = true; + + var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + } + + [Test] + public void DownloadFiles_TestWithInteractiveKeyboardAuthenticationAndPrivateKey() + { + var connection = Helpers.GetSftpConnection(); + connection.Authentication = AuthenticationType.UsernamePrivateKeyFile; + connection.PrivateKeyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key"); + connection.Password = null; + connection.PrivateKeyPassphrase = "passphrase"; + connection.UseKeyboardInteractiveAuthentication = true; + connection.PromptAndResponse = new PromptResponse[] { new PromptResponse { Prompt = "Password", Response = "pass" } }; + + var result = SFTP.DownloadFiles(_source, _destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + + connection.Authentication = AuthenticationType.UsernamePrivateKeyString; + connection.PrivateKeyFile = null; + connection.PrivateKeyString = File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "../../../Volumes/ssh_host_rsa_key")); + connection.PrivateKeyPassphrase = "passphrase"; + + var destination = new Destination + { + Directory = Path.Combine(_workDir, "destination"), + Action = DestinationAction.Overwrite, + FileNameEncoding = FileEncoding.UTF8, + EnableBomForFileName = true + }; + + result = SFTP.DownloadFiles(_source, destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + } + + [Test] + public void DownloadFiles_TestKeepAliveIntervalWithDefault() + { + Helpers.UploadLargeTestFiles(_source.Directory, 1); + + var connection = Helpers.GetSftpConnection(); + + var source = new Source + { + Directory = _source.Directory, + FileName = "*.bin", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing, + }; + + var result = SFTP.DownloadFiles(source, _destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + } + + [Test] + public void DownloadFiles_TestKeepAliveIntervalWith1ms() + { + Helpers.UploadLargeTestFiles(_source.Directory, 1); + + var connection = Helpers.GetSftpConnection(); + connection.KeepAliveInterval = 1; + connection.BufferSize = 256; + + var source = new Source + { + Directory = _source.Directory, + FileName = "*.bin", + Action = SourceAction.Error, + Operation = SourceOperation.Nothing, + }; + + var result = SFTP.DownloadFiles(source, _destination, connection, _options, _info, new CancellationToken()); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.SuccessfulTransferCount); + } + } +} + + diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ErrorTests.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ErrorTests.cs index bcb5268..9ca50cc 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ErrorTests.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.Tests/ErrorTests.cs @@ -77,7 +77,7 @@ public void DownloadFiles_TestThrowsWithSourceMoveToNonExistingDirectoryShouldRe }; var ex = Assert.Throws(() => SFTP.DownloadFiles(source, _destination, _connection, _options, _info, new CancellationToken())); - Assert.That(ex.Message.Contains($"Operation failed: Source file {_source.FileName} couldn't be moved to given directory {source.DirectoryToMoveAfterTransfer} because the directory didn't exist.")); + Assert.IsTrue(ex.Message.Contains($"Operation failed: Source file {_source.FileName} couldn't be moved to given directory {source.DirectoryToMoveAfterTransfer} because the directory didn't exist.")); Assert.IsTrue(Helpers.SourceFileExists(_source.Directory + "/" + _source.FileName)); } @@ -116,7 +116,7 @@ public void DownloadFiles_TestThrowsSourceMoveToDestinationFileExists() }; var ex = Assert.Throws(() => SFTP.DownloadFiles(source, destination, _connection, _options, _info, new CancellationToken())); - Assert.That(ex.Message.StartsWith($"SFTP transfer failed: 1 Errors: Failure in CheckIfDestination")); + Assert.IsTrue(ex.Message.StartsWith($"SFTP transfer failed: 1 Errors: Failure in CheckIfDestination")); Assert.IsTrue(Helpers.SourceFileExists(Path.Combine(_source.Directory, _source.FileName).Replace("\\", "/"))); } @@ -145,7 +145,7 @@ public void DownloadFiles_TestThrowsSourceMoveToDestinationFileExistsWithRenameS }; var ex = Assert.Throws(() => SFTP.DownloadFiles(source, destination, _connection, _options, _info, new CancellationToken())); - Assert.That(ex.Message.StartsWith($"SFTP transfer failed: 1 Errors: Failure in CheckIfDestination")); + Assert.IsTrue(ex.Message.StartsWith($"SFTP transfer failed: 1 Errors: Failure in CheckIfDestination")); Assert.IsTrue(Helpers.SourceFileExists(Path.Combine(_source.Directory, _source.FileName).Replace("\\", "/"))); } diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs index 367f463..c5bb0c0 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs @@ -77,7 +77,7 @@ public FileTransferResult Run() return FormFailedFileTransferResult(userResultMessage); } - LogSourceSystemInfo(_batchContext, connectionInfo, _logger); + LogSourceSystemInfo(_batchContext, _logger); _logger.NotifyInformation(_batchContext, "Negotiation started."); @@ -624,13 +624,7 @@ private void CleanTempFiles(BatchContext context) } } - private void SetCurrentState(TransferState state, string msg) - { - State = state; - _logger.NotifyTrace($"{state}: {msg}"); - } - - private static void LogSourceSystemInfo(BatchContext context, ConnectionInfo connectionInfo, ISFTPLogger logger) + private static void LogSourceSystemInfo(BatchContext context, ISFTPLogger logger) { logger.NotifyInformation(context, $"Assembly: {Assembly.GetAssembly(typeof(SftpClient)).GetName().Name} {Assembly.GetAssembly(typeof(SftpClient)).GetName().Version}"); var bit = Environment.Is64BitOperatingSystem ? "64-bit" : "32-bit"; diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/Result.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/Result.cs index fa526a1..de1df89 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/Result.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/Result.cs @@ -1,125 +1,125 @@ -namespace Frends.SFTP.DownloadFiles.Definitions; - -/// -/// Return object with private setters. -/// -public class Result -{ - /// - /// Boolean value of the skipped Action. - /// - /// false - public bool ActionSkipped { get; private set; } - - /// - /// Boolean value of the successful transfer. - /// - /// true - public bool Success { get; private set; } - - /// - /// Message of the transfer operations. - /// - /// 1 files transferred: test.txt" - public string UserResultMessage { get; private set; } - - /// - /// Count of files that has been successfully transferred. - /// - /// 1 - public int SuccessfulTransferCount { get; private set; } - - /// - /// Count of files that have not been transferred. - /// - /// 0 - public int FailedTransferCount { get; private set; } - - /// - /// List of transferred file names. - /// - /// - /// - /// [ - /// "test.txt", - /// "test2.txt" - /// ] - /// - /// - public IEnumerable TransferredFileNames { get; private set; } - - /// - /// Dictionary of file names and errors messages of the failed transfers. - /// - /// - /// - /// { - /// test.txt : - /// [ - /// Failure in CheckIfDestinationFileExists: File 'test.txt' could not be - /// transferred to '/upload/Upload'. Error: Unable to transfer file. Destination - /// file already exists: test.txt [Source file restored.], - /// ] - /// text2.txt : - /// [ - /// Failure in CheckIfDestinationFileExists: File 'test2.txt' could not be - /// transferred to '/upload/Upload'. Error: Unable to transfer file. Destination - /// file already exists: test2.txt [Source file restored.], - /// ] - /// } - /// - /// - public Dictionary> TransferErrors { get; private set; } - - /// - /// List of transferred file paths. - /// - /// - /// - /// [ - /// "/Upload/upload/test.txt", - /// "/Upload/upload/test2.txt" - /// ] - /// - /// - public IEnumerable TransferredFilePaths { get; private set; } - +namespace Frends.SFTP.DownloadFiles.Definitions; + +/// +/// Return object with private setters. +/// +public class Result +{ + /// + /// Boolean value of the skipped Action. + /// + /// false + public bool ActionSkipped { get; private set; } + + /// + /// Boolean value of the successful transfer. + /// + /// true + public bool Success { get; private set; } + + /// + /// Message of the transfer operations. + /// + /// 1 files transferred: test.txt" + public string UserResultMessage { get; private set; } + + /// + /// Count of files that has been successfully transferred. + /// + /// 1 + public int SuccessfulTransferCount { get; private set; } + + /// + /// Count of files that have not been transferred. + /// + /// 0 + public int FailedTransferCount { get; private set; } + + /// + /// List of transferred file names. + /// + /// + /// + /// [ + /// "test.txt", + /// "test2.txt" + /// ] + /// + /// + public IEnumerable TransferredFileNames { get; private set; } + + /// + /// Dictionary of file names and errors messages of the failed transfers. + /// + /// + /// + /// { + /// test.txt : + /// [ + /// Failure in CheckIfDestinationFileExists: File 'test.txt' could not be + /// transferred to '/upload/Upload'. Error: Unable to transfer file. Destination + /// file already exists: test.txt [Source file restored.], + /// ] + /// text2.txt : + /// [ + /// Failure in CheckIfDestinationFileExists: File 'test2.txt' could not be + /// transferred to '/upload/Upload'. Error: Unable to transfer file. Destination + /// file already exists: test2.txt [Source file restored.], + /// ] + /// } + /// + /// + public Dictionary> TransferErrors { get; private set; } + + /// + /// List of transferred file paths. + /// + /// + /// + /// [ + /// "/Upload/upload/test.txt", + /// "/Upload/upload/test2.txt" + /// ] + /// + /// + public IEnumerable TransferredFilePaths { get; private set; } + /// /// List of destination file paths of the transferred files. - /// - /// - /// [ - /// "C:\\test\\test.txt", - /// "C:\\test\\test2.txt" - /// ] - /// - public IEnumerable TransferredDestinationFilePaths { get; private set; } - - /// - /// Operations logs for the transfer. - /// - /// - /// - /// { - /// "2022-05-30 12:27:35.00Z": "FILE LIST C:\\test\\test.txt" - /// "2022-06-01 11:01:50.40Z": "RenameSourceFileBeforeTransfer: Renaming source file test.txt to temporary file name frends_637896781104694806az33q4kf.8CO before transfer" - /// } - /// - /// - public IDictionary OperationsLog { get; set; } - - internal Result(FileTransferResult result) - { - ActionSkipped = result.ActionSkipped; - Success = result.Success; - UserResultMessage = result.UserResultMessage; - SuccessfulTransferCount = result.SuccessfulTransferCount; - FailedTransferCount = result.FailedTransferCount; - TransferredFileNames = result.TransferredFileNames; - TransferErrors = result.TransferErrors; - TransferredFilePaths = result.TransferredFilePaths; - TransferredDestinationFilePaths = result.TransferredDestinationFilePaths; - OperationsLog = result.OperationsLog; - } -} - + /// + /// + /// [ + /// "C:\\test\\test.txt", + /// "C:\\test\\test2.txt" + /// ] + /// + public IEnumerable TransferredDestinationFilePaths { get; private set; } + + /// + /// Operations logs for the transfer. + /// + /// + /// + /// { + /// "2022-05-30 12:27:35.00Z": "FILE LIST C:\\test\\test.txt" + /// "2022-06-01 11:01:50.40Z": "RenameSourceFileBeforeTransfer: Renaming source file test.txt to temporary file name frends_637896781104694806az33q4kf.8CO before transfer" + /// } + /// + /// + public IDictionary OperationsLog { get; set; } + + internal Result(FileTransferResult result) + { + ActionSkipped = result.ActionSkipped; + Success = result.Success; + UserResultMessage = result.UserResultMessage; + SuccessfulTransferCount = result.SuccessfulTransferCount; + FailedTransferCount = result.FailedTransferCount; + TransferredFileNames = result.TransferredFileNames; + TransferErrors = result.TransferErrors; + TransferredFilePaths = result.TransferredFilePaths; + TransferredDestinationFilePaths = result.TransferredDestinationFilePaths; + OperationsLog = result.OperationsLog; + } +} + diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.cs index 139286d..742ea86 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles.cs @@ -91,7 +91,7 @@ public class SFTP /// Destination directory location /// Transfer options /// CancellationToken is given by Frends - /// Result object {bool ActionSkiped, bool Success, string UserResultMessage, int SuccessfulTransferCount, int Failedstring FileName, string SourcePath, string DestinationPath, bool Success} + /// Result object {bool ActionSkiped, bool Success, string UserResultMessage, int SuccessfulTransferCount, int FailedTransferCount, IEnumrable TransferredFileNames [ string TransferredFileName ], Dictionary TransferErrors { string FileName: [ string TransferError ] }, IEnumerable TransferredFilePaths [ string FilePath ], IEnumerable TransferredDestinationFilePaths [ string FilePath ], IDictionary Operationslog { string TimeStamp, string Operation }} public static Result DownloadFiles( [PropertyTab] Source source, [PropertyTab] Destination destination, From 3bf774432a8fba6978f65296f4597a40e5922c0e Mon Sep 17 00:00:00 2001 From: Riku Virtanen Date: Mon, 12 Jun 2023 09:58:48 +0300 Subject: [PATCH 9/9] PR review fixes --- .../Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs index c5bb0c0..3bdc3f6 100644 --- a/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs +++ b/Frends.SFTP.DownloadFiles/Frends.SFTP.DownloadFiles/Definitions/FileTransporter.cs @@ -45,11 +45,6 @@ internal FileTransporter(ISFTPLogger logger, BatchContext context, Guid instance private string DestinationDirectoryWithMacrosExtended { get; set; } - /// - /// Transfer state for SFTP Logger - /// - public TransferState State { get; set; } - /// /// Executes file transfers. ///