From 95b89332da0580b20819a52c32c094bba302d3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Wed, 6 Sep 2023 05:39:59 +0200 Subject: [PATCH 01/15] Renci.SshNet.IntegrationTests --- ...ionTests.csproj => Renci.SshNet.IntegrationTests.csproj} | 0 src/Renci.SshNet.IntegrationTests/ScpClientTests.cs | 4 +--- src/Renci.SshNet.IntegrationTests/SftpClientTests.cs | 3 +-- src/Renci.SshNet.IntegrationTests/SshClientTests.cs | 4 +--- src/Renci.SshNet.IntegrationTests/TestInitializer.cs | 2 +- .../TestsFixtures/InfrastructureFixture.cs | 6 +++--- .../TestsFixtures/IntegrationTestBase.cs | 2 +- src/Renci.SshNet.IntegrationTests/TestsFixtures/SshUser.cs | 2 +- src/Renci.SshNet.IntegrationTests/Usings.cs | 6 +----- src/Renci.SshNet.sln | 2 +- 10 files changed, 11 insertions(+), 20 deletions(-) rename src/Renci.SshNet.IntegrationTests/{IntegrationTests.csproj => Renci.SshNet.IntegrationTests.csproj} (100%) diff --git a/src/Renci.SshNet.IntegrationTests/IntegrationTests.csproj b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj similarity index 100% rename from src/Renci.SshNet.IntegrationTests/IntegrationTests.csproj rename to src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj diff --git a/src/Renci.SshNet.IntegrationTests/ScpClientTests.cs b/src/Renci.SshNet.IntegrationTests/ScpClientTests.cs index 5db1fe51c..fc90554a5 100644 --- a/src/Renci.SshNet.IntegrationTests/ScpClientTests.cs +++ b/src/Renci.SshNet.IntegrationTests/ScpClientTests.cs @@ -1,6 +1,4 @@ -using Renci.SshNet; - -namespace IntegrationTests +namespace Renci.SshNet.IntegrationTests { /// /// The SCP client integration tests diff --git a/src/Renci.SshNet.IntegrationTests/SftpClientTests.cs b/src/Renci.SshNet.IntegrationTests/SftpClientTests.cs index 9e91ad70d..ee0258cdc 100644 --- a/src/Renci.SshNet.IntegrationTests/SftpClientTests.cs +++ b/src/Renci.SshNet.IntegrationTests/SftpClientTests.cs @@ -1,7 +1,6 @@ -using Renci.SshNet; using Renci.SshNet.Common; -namespace IntegrationTests +namespace Renci.SshNet.IntegrationTests { /// /// The SFTP client integration tests diff --git a/src/Renci.SshNet.IntegrationTests/SshClientTests.cs b/src/Renci.SshNet.IntegrationTests/SshClientTests.cs index 867514441..b737b343f 100644 --- a/src/Renci.SshNet.IntegrationTests/SshClientTests.cs +++ b/src/Renci.SshNet.IntegrationTests/SshClientTests.cs @@ -1,6 +1,4 @@ -using Renci.SshNet; - -namespace IntegrationTests +namespace Renci.SshNet.IntegrationTests { /// /// The SSH client integration tests diff --git a/src/Renci.SshNet.IntegrationTests/TestInitializer.cs b/src/Renci.SshNet.IntegrationTests/TestInitializer.cs index 16b0a3eca..0c058781e 100644 --- a/src/Renci.SshNet.IntegrationTests/TestInitializer.cs +++ b/src/Renci.SshNet.IntegrationTests/TestInitializer.cs @@ -1,4 +1,4 @@ -namespace IntegrationTests +namespace Renci.SshNet.IntegrationTests { [TestClass] public class TestInitializer diff --git a/src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs b/src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs index 63e6c70ce..6c3ff2093 100644 --- a/src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs +++ b/src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs @@ -1,8 +1,8 @@ -using DotNet.Testcontainers.Images; -using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Images; -namespace IntegrationTests.TestsFixtures +namespace Renci.SshNet.IntegrationTests.TestsFixtures { public sealed class InfrastructureFixture : IDisposable { diff --git a/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs b/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs index 521f947cc..ba9865d74 100644 --- a/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs +++ b/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs @@ -1,4 +1,4 @@ -namespace IntegrationTests.TestsFixtures +namespace Renci.SshNet.IntegrationTests.TestsFixtures { /// /// The base class for integration tests diff --git a/src/Renci.SshNet.IntegrationTests/TestsFixtures/SshUser.cs b/src/Renci.SshNet.IntegrationTests/TestsFixtures/SshUser.cs index 5f2ee4d56..9a67f65c3 100644 --- a/src/Renci.SshNet.IntegrationTests/TestsFixtures/SshUser.cs +++ b/src/Renci.SshNet.IntegrationTests/TestsFixtures/SshUser.cs @@ -1,4 +1,4 @@ -namespace IntegrationTests.TestsFixtures +namespace Renci.SshNet.IntegrationTests.TestsFixtures { public class SshUser { diff --git a/src/Renci.SshNet.IntegrationTests/Usings.cs b/src/Renci.SshNet.IntegrationTests/Usings.cs index e6180a739..8eba0e510 100644 --- a/src/Renci.SshNet.IntegrationTests/Usings.cs +++ b/src/Renci.SshNet.IntegrationTests/Usings.cs @@ -1,9 +1,5 @@ -#pragma warning disable IDE0005 - global using System.Text; global using Microsoft.VisualStudio.TestTools.UnitTesting; -global using IntegrationTests.TestsFixtures; - - +global using Renci.SshNet.IntegrationTests.TestsFixtures; diff --git a/src/Renci.SshNet.sln b/src/Renci.SshNet.sln index be2686bee..c73ed0faa 100644 --- a/src/Renci.SshNet.sln +++ b/src/Renci.SshNet.sln @@ -42,7 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D21A4D03-0 ..\test\Directory.Build.props = ..\test\Directory.Build.props EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "Renci.SshNet.IntegrationTests\IntegrationTests.csproj", "{EEF98046-729C-419E-932D-4E569073C8CC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Renci.SshNet.IntegrationTests", "Renci.SshNet.IntegrationTests\Renci.SshNet.IntegrationTests.csproj", "{EEF98046-729C-419E-932D-4E569073C8CC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From bc985016c4649251cdd74f6e17c2ee1c9aea8b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Wed, 6 Sep 2023 06:05:12 +0200 Subject: [PATCH 02/15] Renci.SshNet.TestTools.OpenSSH --- .editorconfig | 6 + src/Renci.SshNet.TestTools.OpenSSH/Cipher.cs | 54 ++ .../Formatters/BooleanFormatter.cs | 20 + .../Formatters/Int32Formatter.cs | 12 + .../Formatters/LogLevelFormatter.cs | 10 + .../Formatters/MatchFormatter.cs | 54 ++ .../Formatters/SubsystemFormatter.cs | 10 + .../HostKeyAlgorithm.cs | 53 ++ .../KeyExchangeAlgorithm.cs | 52 ++ .../LogLevel.cs | 15 + src/Renci.SshNet.TestTools.OpenSSH/Match.cs | 57 ++ .../MessageAuthenticationCodeAlgorithm.cs | 56 ++ .../PublicKeyAlgorithm.cs | 59 +++ .../Renci.SshNet.TestTools.OpenSSH.csproj | 21 + .../SshdConfig.cs | 501 ++++++++++++++++++ .../Subsystem.cs | 41 ++ src/Renci.SshNet.sln | 22 + 17 files changed, 1043 insertions(+) create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Cipher.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Formatters/BooleanFormatter.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Formatters/Int32Formatter.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Formatters/LogLevelFormatter.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Formatters/MatchFormatter.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Formatters/SubsystemFormatter.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/HostKeyAlgorithm.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/KeyExchangeAlgorithm.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/LogLevel.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Match.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/MessageAuthenticationCodeAlgorithm.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/PublicKeyAlgorithm.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Renci.SshNet.TestTools.OpenSSH.csproj create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs create mode 100644 src/Renci.SshNet.TestTools.OpenSSH/Subsystem.cs diff --git a/.editorconfig b/.editorconfig index 62aefca2e..f1d44ba6c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -571,6 +571,12 @@ dotnet_diagnostic.IDE0130.severity = none # var inputPath = originalDossierPathList.Find(x => x.id == updatedPath.id) ?? throw new PcsException($"Path id ({updatedPath.id}) unknown in PCS for dossier id {dossierFromTs.dossier.id}", updatedPath.id); dotnet_diagnostic.IDE0270.severity = none +#### Source-generator diagnostics #### + +# SYSLIB1045 Use GeneratedRegexAttribute to generate the regular expression implementation at compile time. +# https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib1040-1049 +dotnet_diagnostic.SYSLIB1045.severity = none + #### .NET Compiler Platform code style rules #### ### Language rules ### diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Cipher.cs b/src/Renci.SshNet.TestTools.OpenSSH/Cipher.cs new file mode 100644 index 000000000..f7d8d8842 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Cipher.cs @@ -0,0 +1,54 @@ +namespace Renci.SshNet.TestTools.OpenSSH +{ + public class Cipher + { + public static readonly Cipher TripledesCbc = new Cipher("3des-cbc"); + public static readonly Cipher Aes128Cbc = new Cipher("aes128-cbc"); + public static readonly Cipher Aes192Cbc = new Cipher("aes192-cbc"); + public static readonly Cipher Aes256Cbc = new Cipher("aes256-cbc"); + public static readonly Cipher RijndaelCbc = new Cipher("rijndael-cbc@lysator.liu.se"); + public static readonly Cipher Aes128Ctr = new Cipher("aes128-ctr"); + public static readonly Cipher Aes192Ctr = new Cipher("aes192-ctr"); + public static readonly Cipher Aes256Ctr = new Cipher("aes256-ctr"); + public static readonly Cipher Aes128Gcm = new Cipher("aes128-gcm@openssh.com"); + public static readonly Cipher Aes256Gcm = new Cipher("aes256-gcm@openssh.com"); + public static readonly Cipher Arcfour = new Cipher("arcfour"); + public static readonly Cipher Arcfour128 = new Cipher("arcfour128"); + public static readonly Cipher Arcfour256 = new Cipher("arcfour256"); + public static readonly Cipher BlowfishCbc = new Cipher("blowfish-cbc"); + public static readonly Cipher Cast128Cbc = new Cipher("cast128-cbc"); + public static readonly Cipher Chacha20Poly1305 = new Cipher("chacha20-poly1305@openssh.com"); + + public Cipher(string name) + { + Name = name; + } + + public string Name { get; } + + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is Cipher otherCipher) + { + return otherCipher.Name == Name; + } + + return false; + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Formatters/BooleanFormatter.cs b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/BooleanFormatter.cs new file mode 100644 index 000000000..3eab2b199 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/BooleanFormatter.cs @@ -0,0 +1,20 @@ +namespace Renci.SshNet.TestTools.OpenSSH.Formatters +{ + internal class BooleanFormatter + { + public string Format(bool value) + { + return value ? "yes" : "no"; + } + + public string Format(bool? value, bool defaultValue) + { + if (value.HasValue) + { + return Format(value.Value); + } + + return Format(defaultValue); + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Formatters/Int32Formatter.cs b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/Int32Formatter.cs new file mode 100644 index 000000000..2b8231111 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/Int32Formatter.cs @@ -0,0 +1,12 @@ +using System.Globalization; + +namespace Renci.SshNet.TestTools.OpenSSH.Formatters +{ + internal class Int32Formatter + { + public string Format(int value) + { + return value.ToString(NumberFormatInfo.InvariantInfo); + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Formatters/LogLevelFormatter.cs b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/LogLevelFormatter.cs new file mode 100644 index 000000000..f9f4bbf6f --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/LogLevelFormatter.cs @@ -0,0 +1,10 @@ +namespace Renci.SshNet.TestTools.OpenSSH.Formatters +{ + internal class LogLevelFormatter + { + public string Format(LogLevel logLevel) + { + return logLevel.ToString("G").ToUpperInvariant(); + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Formatters/MatchFormatter.cs b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/MatchFormatter.cs new file mode 100644 index 000000000..df2968be0 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/MatchFormatter.cs @@ -0,0 +1,54 @@ +namespace Renci.SshNet.TestTools.OpenSSH.Formatters +{ + internal class MatchFormatter + { + public string Format(Match match) + { + using (var writer = new StringWriter()) + { + Format(match, writer); + return writer.ToString(); + } + } + + public void Format(Match match, TextWriter writer) + { + writer.Write("Match "); + + if (match.Users.Length > 0) + { + writer.Write("User "); + for (var i = 0; i < match.Users.Length; i++) + { + if (i > 0) + { + writer.Write(','); + } + + writer.Write(match.Users[i]); + } + } + + if (match.Addresses.Length > 0) + { + writer.Write("Address "); + for (var i = 0; i < match.Addresses.Length; i++) + { + if (i > 0) + { + writer.Write(','); + } + + writer.Write(match.Addresses[i]); + } + } + + writer.WriteLine(); + + if (match.AuthenticationMethods != null) + { + writer.WriteLine(" AuthenticationMethods " + match.AuthenticationMethods); + } + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Formatters/SubsystemFormatter.cs b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/SubsystemFormatter.cs new file mode 100644 index 000000000..fcb74e266 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Formatters/SubsystemFormatter.cs @@ -0,0 +1,10 @@ +namespace Renci.SshNet.TestTools.OpenSSH.Formatters +{ + internal class SubsystemFormatter + { + public string Format(Subsystem subsystem) + { + return subsystem.Name + " " + subsystem.Command; + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/HostKeyAlgorithm.cs b/src/Renci.SshNet.TestTools.OpenSSH/HostKeyAlgorithm.cs new file mode 100644 index 000000000..0c79f7792 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/HostKeyAlgorithm.cs @@ -0,0 +1,53 @@ +namespace Renci.SshNet.TestTools.OpenSSH +{ + public class HostKeyAlgorithm + { + public static readonly HostKeyAlgorithm EcdsaSha2Nistp256CertV01OpenSSH = new HostKeyAlgorithm("ecdsa-sha2-nistp256-cert-v01@openssh.com"); + public static readonly HostKeyAlgorithm EcdsaSha2Nistp384CertV01OpenSSH = new HostKeyAlgorithm("ecdsa-sha2-nistp384-cert-v01@openssh.com"); + public static readonly HostKeyAlgorithm EcdsaSha2Nistp521CertV01OpenSSH = new HostKeyAlgorithm("ecdsa-sha2-nistp521-cert-v01@openssh.com"); + public static readonly HostKeyAlgorithm SshEd25519CertV01OpenSSH = new HostKeyAlgorithm("ssh-ed25519-cert-v01@openssh.com"); + public static readonly HostKeyAlgorithm RsaSha2256CertV01OpenSSH = new HostKeyAlgorithm("rsa-sha2-256-cert-v01@openssh.com"); + public static readonly HostKeyAlgorithm RsaSha2512CertV01OpenSSH = new HostKeyAlgorithm("rsa-sha2-512-cert-v01@openssh.com"); + public static readonly HostKeyAlgorithm SshRsaCertV01OpenSSH = new HostKeyAlgorithm("ssh-rsa-cert-v01@openssh.com"); + public static readonly HostKeyAlgorithm EcdsaSha2Nistp256 = new HostKeyAlgorithm("ecdsa-sha2-nistp256"); + public static readonly HostKeyAlgorithm EcdsaSha2Nistp384 = new HostKeyAlgorithm("ecdsa-sha2-nistp384"); + public static readonly HostKeyAlgorithm EcdsaSha2Nistp521 = new HostKeyAlgorithm("ecdsa-sha2-nistp521"); + public static readonly HostKeyAlgorithm SshEd25519 = new HostKeyAlgorithm("ssh-ed25519"); + public static readonly HostKeyAlgorithm RsaSha2512 = new HostKeyAlgorithm("rsa-sha2-512"); + public static readonly HostKeyAlgorithm RsaSha2256 = new HostKeyAlgorithm("rsa-sha2-256"); + public static readonly HostKeyAlgorithm SshRsa = new HostKeyAlgorithm("ssh-rsa"); + public static readonly HostKeyAlgorithm SshDsa = new HostKeyAlgorithm("ssh-dsa"); + + public HostKeyAlgorithm(string name) + { + Name = name; + } + + public string Name { get; } + + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is HostKeyAlgorithm otherHka) + { + return otherHka.Name == Name; + } + + return false; + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/KeyExchangeAlgorithm.cs b/src/Renci.SshNet.TestTools.OpenSSH/KeyExchangeAlgorithm.cs new file mode 100644 index 000000000..4701103f0 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/KeyExchangeAlgorithm.cs @@ -0,0 +1,52 @@ +namespace Renci.SshNet.TestTools.OpenSSH +{ + public class KeyExchangeAlgorithm + { + public static readonly KeyExchangeAlgorithm DiffieHellmanGroup1Sha1 = new KeyExchangeAlgorithm("diffie-hellman-group1-sha1"); + public static readonly KeyExchangeAlgorithm DiffieHellmanGroup14Sha1 = new KeyExchangeAlgorithm("diffie-hellman-group14-sha1"); + public static readonly KeyExchangeAlgorithm DiffieHellmanGroup14Sha256 = new KeyExchangeAlgorithm("diffie-hellman-group14-sha256"); + public static readonly KeyExchangeAlgorithm DiffieHellmanGroup16Sha512 = new KeyExchangeAlgorithm("diffie-hellman-group16-sha512"); + public static readonly KeyExchangeAlgorithm DiffieHellmanGroup18Sha512 = new KeyExchangeAlgorithm("diffie-hellman-group18-sha512"); + public static readonly KeyExchangeAlgorithm DiffieHellmanGroupExchangeSha1 = new KeyExchangeAlgorithm("diffie-hellman-group-exchange-sha1"); + public static readonly KeyExchangeAlgorithm DiffieHellmanGroupExchangeSha256 = new KeyExchangeAlgorithm("diffie-hellman-group-exchange-sha256"); + public static readonly KeyExchangeAlgorithm EcdhSha2Nistp256 = new KeyExchangeAlgorithm("ecdh-sha2-nistp256"); + public static readonly KeyExchangeAlgorithm EcdhSha2Nistp384 = new KeyExchangeAlgorithm("ecdh-sha2-nistp384"); + public static readonly KeyExchangeAlgorithm EcdhSha2Nistp521 = new KeyExchangeAlgorithm("ecdh-sha2-nistp521"); + public static readonly KeyExchangeAlgorithm Curve25519Sha256 = new KeyExchangeAlgorithm("curve25519-sha256"); + public static readonly KeyExchangeAlgorithm Curve25519Sha256Libssh = new KeyExchangeAlgorithm("curve25519-sha256@libssh.org"); + public static readonly KeyExchangeAlgorithm Sntrup4591761x25519Sha512 = new KeyExchangeAlgorithm("sntrup4591761x25519-sha512@tinyssh.org"); + + + public KeyExchangeAlgorithm(string name) + { + Name = name; + } + + public string Name { get; } + + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is KeyExchangeAlgorithm otherKex) + { + return otherKex.Name == Name; + } + + return false; + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/LogLevel.cs b/src/Renci.SshNet.TestTools.OpenSSH/LogLevel.cs new file mode 100644 index 000000000..8724ae068 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/LogLevel.cs @@ -0,0 +1,15 @@ +namespace Renci.SshNet.TestTools.OpenSSH +{ + public enum LogLevel + { + Quiet = 1, + Fatal = 2, + Error = 3, + Info = 4, + Verbose = 5, + Debug = 6, + Debug1 = 7, + Debug2 = 8, + Debug3 = 9 + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Match.cs b/src/Renci.SshNet.TestTools.OpenSSH/Match.cs new file mode 100644 index 000000000..16cd5073d --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Match.cs @@ -0,0 +1,57 @@ +namespace Renci.SshNet.TestTools.OpenSSH +{ + public class Match + { + public Match(string[] users, string[] addresses) + { + Users = users; + Addresses = addresses; + } + + public string[] Users { get; } + + public string[] Addresses { get; } + + public string? AuthenticationMethods { get; set; } + + public void WriteTo(TextWriter writer) + { + writer.Write("Match "); + + if (Users.Length > 0) + { + writer.Write("User "); + for (var i = 0; i < Users.Length; i++) + { + if (i > 0) + { + writer.Write(','); + } + + writer.Write(Users[i]); + } + } + + if (Addresses.Length > 0) + { + writer.Write("Address "); + for (var i = 0; i < Addresses.Length; i++) + { + if (i > 0) + { + writer.Write(','); + } + + writer.Write(Addresses[i]); + } + } + + writer.WriteLine(); + + if (AuthenticationMethods != null) + { + writer.WriteLine(" AuthenticationMethods " + AuthenticationMethods); + } + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/MessageAuthenticationCodeAlgorithm.cs b/src/Renci.SshNet.TestTools.OpenSSH/MessageAuthenticationCodeAlgorithm.cs new file mode 100644 index 000000000..17bf0cf91 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/MessageAuthenticationCodeAlgorithm.cs @@ -0,0 +1,56 @@ +namespace Renci.SshNet.TestTools.OpenSSH +{ + public class MessageAuthenticationCodeAlgorithm + { + public static readonly MessageAuthenticationCodeAlgorithm HmacMd5 = new MessageAuthenticationCodeAlgorithm("hmac-md5"); + public static readonly MessageAuthenticationCodeAlgorithm HmacMd5_96 = new MessageAuthenticationCodeAlgorithm("hmac-md5-96"); + public static readonly MessageAuthenticationCodeAlgorithm HmacRipemd160 = new MessageAuthenticationCodeAlgorithm("hmac-ripemd160"); + public static readonly MessageAuthenticationCodeAlgorithm HmacSha1 = new MessageAuthenticationCodeAlgorithm("hmac-sha1"); + public static readonly MessageAuthenticationCodeAlgorithm HmacSha1_96 = new MessageAuthenticationCodeAlgorithm("hmac-sha1-96"); + public static readonly MessageAuthenticationCodeAlgorithm HmacSha2_256 = new MessageAuthenticationCodeAlgorithm("hmac-sha2-256"); + public static readonly MessageAuthenticationCodeAlgorithm HmacSha2_512 = new MessageAuthenticationCodeAlgorithm("hmac-sha2-512"); + public static readonly MessageAuthenticationCodeAlgorithm Umac64 = new MessageAuthenticationCodeAlgorithm("umac-64@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm Umac128 = new MessageAuthenticationCodeAlgorithm("umac-128@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm HmacMd5Etm = new MessageAuthenticationCodeAlgorithm("hmac-md5-etm@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm HmacMd5_96_Etm = new MessageAuthenticationCodeAlgorithm("hmac-md5-96-etm@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm HmacRipemd160Etm = new MessageAuthenticationCodeAlgorithm("hmac-ripemd160-etm@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm HmacSha1Etm = new MessageAuthenticationCodeAlgorithm("hmac-sha1-etm@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm HmacSha1_96_Etm = new MessageAuthenticationCodeAlgorithm("hmac-sha1-96-etm@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm HmacSha2_256_Etm = new MessageAuthenticationCodeAlgorithm("hmac-sha2-256-etm@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm HmacSha2_512_Etm = new MessageAuthenticationCodeAlgorithm("hmac-sha2-512-etm@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm Umac64_Etm = new MessageAuthenticationCodeAlgorithm("umac-64-etm@openssh.com"); + public static readonly MessageAuthenticationCodeAlgorithm Umac128_Etm = new MessageAuthenticationCodeAlgorithm("umac-128-etm@openssh.com"); + + public MessageAuthenticationCodeAlgorithm(string name) + { + Name = name; + } + + public string Name { get; } + + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is MessageAuthenticationCodeAlgorithm otherMac) + { + return otherMac.Name == Name; + } + + return false; + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/PublicKeyAlgorithm.cs b/src/Renci.SshNet.TestTools.OpenSSH/PublicKeyAlgorithm.cs new file mode 100644 index 000000000..292ace7f9 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/PublicKeyAlgorithm.cs @@ -0,0 +1,59 @@ +namespace Renci.SshNet.TestTools.OpenSSH +{ + public class PublicKeyAlgorithm + { + public static readonly PublicKeyAlgorithm SshEd25519 = new PublicKeyAlgorithm("ssh-ed25519"); + public static readonly PublicKeyAlgorithm SshEd25519CertV01OpenSSH = new PublicKeyAlgorithm("ssh-ed25519-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm SkSshEd25519OpenSSH = new PublicKeyAlgorithm("sk-ssh-ed25519@openssh.com"); + public static readonly PublicKeyAlgorithm SkSshEd25519CertV01OpenSSH = new PublicKeyAlgorithm("sk-ssh-ed25519-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm SshRsa = new PublicKeyAlgorithm("ssh-rsa"); + public static readonly PublicKeyAlgorithm RsaSha2256 = new PublicKeyAlgorithm("rsa-sha2-256"); + public static readonly PublicKeyAlgorithm RsaSha2512 = new PublicKeyAlgorithm("rsa-sha2-512"); + public static readonly PublicKeyAlgorithm SshDss = new PublicKeyAlgorithm("ssh-dss"); + public static readonly PublicKeyAlgorithm EcdsaSha2Nistp256 = new PublicKeyAlgorithm("ecdsa-sha2-nistp256"); + public static readonly PublicKeyAlgorithm EcdsaSha2Nistp384 = new PublicKeyAlgorithm("ecdsa-sha2-nistp384"); + public static readonly PublicKeyAlgorithm EcdsaSha2Nistp521 = new PublicKeyAlgorithm("ecdsa-sha2-nistp521"); + public static readonly PublicKeyAlgorithm SkEcdsaSha2Nistp256OpenSSH = new PublicKeyAlgorithm("sk-ecdsa-sha2-nistp256@openssh.com"); + public static readonly PublicKeyAlgorithm WebAuthnSkEcdsaSha2Nistp256OpenSSH = new PublicKeyAlgorithm("webauthn-sk-ecdsa-sha2-nistp256@openssh.com"); + public static readonly PublicKeyAlgorithm SshRsaCertV01OpenSSH = new PublicKeyAlgorithm("ssh-rsa-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm RsaSha2256CertV01OpenSSH = new PublicKeyAlgorithm("rsa-sha2-256-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm RsaSha2512CertV01OpenSSH = new PublicKeyAlgorithm("rsa-sha2-512-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm SshDssCertV01OpenSSH = new PublicKeyAlgorithm("ssh-dss-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm EcdsaSha2Nistp256CertV01OpenSSH = new PublicKeyAlgorithm("ecdsa-sha2-nistp256-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm EcdsaSha2Nistp384CertV01OpenSSH = new PublicKeyAlgorithm("ecdsa-sha2-nistp384-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm EcdsaSha2Nistp521CertV01OpenSSH = new PublicKeyAlgorithm("ecdsa-sha2-nistp521-cert-v01@openssh.com"); + public static readonly PublicKeyAlgorithm SkEcdsaSha2Nistp256CertV01OpenSSH = new PublicKeyAlgorithm("sk-ecdsa-sha2-nistp256-cert-v01@openssh.com"); + + public PublicKeyAlgorithm(string name) + { + Name = name; + } + + public string Name { get; } + + public override bool Equals(object? obj) + { + if (obj == null) + { + return false; + } + + if (obj is HostKeyAlgorithm otherHka) + { + return otherHka.Name == Name; + } + + return false; + } + + public override int GetHashCode() + { + return Name.GetHashCode(); + } + + public override string ToString() + { + return Name; + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Renci.SshNet.TestTools.OpenSSH.csproj b/src/Renci.SshNet.TestTools.OpenSSH/Renci.SshNet.TestTools.OpenSSH.csproj new file mode 100644 index 000000000..3eb6ff527 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Renci.SshNet.TestTools.OpenSSH.csproj @@ -0,0 +1,21 @@ + + + net7.0 + enable + enable + + + $(NoWarn);CS1591 + + + + + \ No newline at end of file diff --git a/src/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs b/src/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs new file mode 100644 index 000000000..b80967113 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/SshdConfig.cs @@ -0,0 +1,501 @@ +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +using Renci.SshNet.TestTools.OpenSSH.Formatters; + +namespace Renci.SshNet.TestTools.OpenSSH +{ + public class SshdConfig + { + private static readonly Regex MatchRegex = new Regex($@"\s*Match\s+(User\s+(?[\S]+))?\s*(Address\s+(?[\S]+))?\s*", RegexOptions.Compiled); + + private readonly SubsystemFormatter _subsystemFormatter; + private readonly Int32Formatter _int32Formatter; + private readonly BooleanFormatter _booleanFormatter; + private readonly MatchFormatter _matchFormatter; + + private SshdConfig() + { + AcceptedEnvironmentVariables = new List(); + Ciphers = new List(); + HostKeyFiles = new List(); + HostKeyAlgorithms = new List(); + KeyExchangeAlgorithms = new List(); + PublicKeyAcceptedAlgorithms = new List(); + MessageAuthenticationCodeAlgorithms = new List(); + Subsystems = new List(); + Matches = new List(); + LogLevel = LogLevel.Info; + Port = 22; + Protocol = "2,1"; + + _booleanFormatter = new BooleanFormatter(); + _int32Formatter = new Int32Formatter(); + _matchFormatter = new MatchFormatter(); + _subsystemFormatter = new SubsystemFormatter(); + } + + /// + /// Gets or sets the port number that sshd listens on. + /// + /// + /// The port number that sshd listens on. The default is 22. + /// + public int Port { get; set; } + + /// + /// Gets or sets the list of private host key files used by sshd. + /// + /// + /// A list of private host key files used by sshd. + /// + public List HostKeyFiles { get; set; } + + /// + /// Gets or sets a value specifying whether challenge-response authentication is allowed. + /// + /// + /// A value specifying whether challenge-response authentication is allowed, or + /// if this option is not configured. + /// + public bool? ChallengeResponseAuthentication { get; set; } + + /// + /// Gets or sets a value indicating whether to allow keyboard-interactive authentication. + /// + /// + /// to allow and to disallow keyboard-interactive + /// authentication, or if this option is not configured. + /// + public bool? KeyboardInteractiveAuthentication { get; set; } + + /// + /// Gets or sets the verbosity when logging messages from sshd. + /// + /// + /// The verbosity when logging messages from sshd. The default is . + /// + public LogLevel LogLevel { get; set; } + + /// + /// Gets a sets a value indicating whether the Pluggable Authentication Module interface is enabled. + /// + /// + /// A value indicating whether the Pluggable Authentication Module interface is enabled. + /// + public bool? UsePAM { get; set; } + + public List Subsystems { get; } + + /// + /// Gets a list of conditional blocks. + /// + public List Matches { get; } + + public bool X11Forwarding { get; private set; } + public List AcceptedEnvironmentVariables { get; private set; } + public List Ciphers { get; private set; } + + /// + /// Gets the host key signature algorithms that the server offers. + /// + public List HostKeyAlgorithms { get; private set; } + + /// + /// Gets the available KEX (Key Exchange) algorithms. + /// + public List KeyExchangeAlgorithms { get; private set; } + + /// + /// Gets the signature algorithms that will be accepted for public key authentication. + /// + public List PublicKeyAcceptedAlgorithms { get; private set; } + + /// + /// Gets the available MAC (message authentication code) algorithms. + /// + public List MessageAuthenticationCodeAlgorithms { get; private set; } + + /// + /// Gets a value indicating whether sshd should print /etc/motd when a user logs in interactively. + /// + /// + /// if sshd should print /etc/motd when a user logs in interactively + /// and if it should not; if this option is not configured. + /// + public bool? PrintMotd { get; set; } + + /// + /// Gets or sets the protocol versions sshd supported. + /// + /// + /// The protocol versions sshd supported. The default is 2,1. + /// + public string Protocol { get; set; } + + /// + /// Gets or sets a value indicating whether TCP forwarding is allowed. + /// + /// + /// to allow and to disallow TCP forwarding, + /// or if this option is not configured. + /// + public bool? AllowTcpForwarding { get; set; } + + public void SaveTo(TextWriter writer) + { + writer.WriteLine("Protocol " + Protocol); + writer.WriteLine("Port " + _int32Formatter.Format(Port)); + if (HostKeyFiles.Count > 0) + { + writer.WriteLine("HostKey " + string.Join(",", HostKeyFiles.ToArray())); + } + + if (ChallengeResponseAuthentication is not null) + { + writer.WriteLine("ChallengeResponseAuthentication " + _booleanFormatter.Format(ChallengeResponseAuthentication.Value)); + } + + if (KeyboardInteractiveAuthentication is not null) + { + writer.WriteLine("KbdInteractiveAuthentication " + _booleanFormatter.Format(KeyboardInteractiveAuthentication.Value)); + } + + if (AllowTcpForwarding is not null) + { + writer.WriteLine("AllowTcpForwarding " + _booleanFormatter.Format(AllowTcpForwarding.Value)); + } + + if (PrintMotd is not null) + { + writer.WriteLine("PrintMotd " + _booleanFormatter.Format(PrintMotd.Value)); + } + + writer.WriteLine("LogLevel " + new LogLevelFormatter().Format(LogLevel)); + + foreach (var subsystem in Subsystems) + { + writer.WriteLine("Subsystem " + _subsystemFormatter.Format(subsystem)); + } + + if (UsePAM is not null) + { + writer.WriteLine("UsePAM " + _booleanFormatter.Format(UsePAM.Value)); + } + + writer.WriteLine("X11Forwarding " + _booleanFormatter.Format(X11Forwarding)); + + foreach (var acceptedEnvVar in AcceptedEnvironmentVariables) + { + writer.WriteLine("AcceptEnv " + acceptedEnvVar); + } + + if (Ciphers.Count > 0) + { + writer.WriteLine("Ciphers " + string.Join(",", Ciphers.Select(c => c.Name).ToArray())); + } + + if (HostKeyAlgorithms.Count > 0) + { + writer.WriteLine("HostKeyAlgorithms " + string.Join(",", HostKeyAlgorithms.Select(c => c.Name).ToArray())); + } + + if (KeyExchangeAlgorithms.Count > 0) + { + writer.WriteLine("KexAlgorithms " + string.Join(",", KeyExchangeAlgorithms.Select(c => c.Name).ToArray())); + } + + if (MessageAuthenticationCodeAlgorithms.Count > 0) + { + writer.WriteLine("MACs " + string.Join(",", MessageAuthenticationCodeAlgorithms.Select(c => c.Name).ToArray())); + } + + writer.WriteLine("PubkeyAcceptedAlgorithms " + string.Join(",", PublicKeyAcceptedAlgorithms.Select(c => c.Name).ToArray())); + + foreach (var match in Matches) + { + _matchFormatter.Format(match, writer); + } + } + + public static SshdConfig LoadFrom(Stream stream, Encoding encoding) + { + using (var sr = new StreamReader(stream, encoding)) + { + var sshdConfig = new SshdConfig(); + + Match? currentMatchConfiguration = null; + + string? line; + while ((line = sr.ReadLine()) != null) + { + // Skip empty lines + if (line.Length == 0) + { + continue; + } + + // Skip comments + if (line[0] == '#') + { + continue; + } + + var match = MatchRegex.Match(line); + if (match.Success) + { + var usersGroup = match.Groups["users"]; + var addressesGroup = match.Groups["addresses"]; + var users = usersGroup.Success ? usersGroup.Value.Split(',') : Array.Empty(); + var addresses = addressesGroup.Success ? addressesGroup.Value.Split(',') : Array.Empty(); + + currentMatchConfiguration = new Match(users, addresses); + sshdConfig.Matches.Add(currentMatchConfiguration); + continue; + } + + if (currentMatchConfiguration != null) + { + ProcessMatchOption(currentMatchConfiguration, line); + } + else + { + ProcessGlobalOption(sshdConfig, line); + } + } + + if (sshdConfig.Ciphers == null) + { + // Obtain supported ciphers using ssh -Q cipher + } + + if (sshdConfig.KeyExchangeAlgorithms == null) + { + // Obtain supports key exchange algorithms using ssh -Q kex + } + + if (sshdConfig.HostKeyAlgorithms == null) + { + // Obtain supports host key algorithms using ssh -Q key + } + + if (sshdConfig.MessageAuthenticationCodeAlgorithms == null) + { + // Obtain supported MACs using ssh -Q mac + } + + + return sshdConfig; + } + } + + private static void ProcessGlobalOption(SshdConfig sshdConfig, string line) + { + var matchOptionRegex = new Regex(@"^\s*(?[\S]+)\s+(?.+?){1}\s*$"); + + var optionsMatch = matchOptionRegex.Match(line); + if (!optionsMatch.Success) + { + return; + } + + var nameGroup = optionsMatch.Groups["name"]; + var valueGroup = optionsMatch.Groups["value"]; + + var name = nameGroup.Value; + var value = valueGroup.Value; + + switch (name) + { + case "Port": + sshdConfig.Port = ToInt(value); + break; + case "HostKey": + sshdConfig.HostKeyFiles = ParseCommaSeparatedValue(value); + break; + case "ChallengeResponseAuthentication": + sshdConfig.ChallengeResponseAuthentication = ToBool(value); + break; + case "KbdInteractiveAuthentication": + sshdConfig.KeyboardInteractiveAuthentication = ToBool(value); + break; + case "LogLevel": + sshdConfig.LogLevel = (LogLevel) Enum.Parse(typeof(LogLevel), value, true); + break; + case "Subsystem": + sshdConfig.Subsystems.Add(Subsystem.FromConfig(value)); + break; + case "UsePAM": + sshdConfig.UsePAM = ToBool(value); + break; + case "X11Forwarding": + sshdConfig.X11Forwarding = ToBool(value); + break; + case "Ciphers": + sshdConfig.Ciphers = ParseCiphers(value); + break; + case "KexAlgorithms": + sshdConfig.KeyExchangeAlgorithms = ParseKeyExchangeAlgorithms(value); + break; + case "PubkeyAcceptedAlgorithms": + sshdConfig.PublicKeyAcceptedAlgorithms = ParsePublicKeyAcceptedAlgorithms(value); + break; + case "HostKeyAlgorithms": + sshdConfig.HostKeyAlgorithms = ParseHostKeyAlgorithms(value); + break; + case "MACs": + sshdConfig.MessageAuthenticationCodeAlgorithms = ParseMacs(value); + break; + case "PrintMotd": + sshdConfig.PrintMotd = ToBool(value); + break; + case "AcceptEnv": + ParseAcceptedEnvironmentVariable(sshdConfig, value); + break; + case "Protocol": + sshdConfig.Protocol = value; + break; + case "AllowTcpForwarding": + sshdConfig.AllowTcpForwarding = ToBool(value); + break; + case "KeyRegenerationInterval": + case "HostbasedAuthentication": + case "ServerKeyBits": + case "SyslogFacility": + case "LoginGraceTime": + case "PermitRootLogin": + case "StrictModes": + case "RSAAuthentication": + case "PubkeyAuthentication": + case "IgnoreRhosts": + case "RhostsRSAAuthentication": + case "PermitEmptyPasswords": + case "X11DisplayOffset": + case "PrintLastLog": + case "TCPKeepAlive": + case "AuthorizedKeysFile": + case "PasswordAuthentication": + case "GatewayPorts": + break; + default: + throw new Exception($"Global option '{name}' is not implemented."); + } + } + + private static void ParseAcceptedEnvironmentVariable(SshdConfig sshdConfig, string value) + { + var acceptedEnvironmentVariables = value.Split(' '); + foreach (var acceptedEnvironmentVariable in acceptedEnvironmentVariables) + { + sshdConfig.AcceptedEnvironmentVariables.Add(acceptedEnvironmentVariable); + } + } + + private static List ParseCiphers(string value) + { + var cipherNames = value.Split(','); + var ciphers = new List(cipherNames.Length); + foreach (var cipherName in cipherNames) + { + ciphers.Add(new Cipher(cipherName.Trim())); + } + return ciphers; + } + + private static List ParseKeyExchangeAlgorithms(string value) + { + var kexNames = value.Split(','); + var keyExchangeAlgorithms = new List(kexNames.Length); + foreach (var kexName in kexNames) + { + keyExchangeAlgorithms.Add(new KeyExchangeAlgorithm(kexName.Trim())); + } + return keyExchangeAlgorithms; + } + + public static List ParsePublicKeyAcceptedAlgorithms(string value) + { + var publicKeyAlgorithmNames = value.Split(','); + var publicKeyAlgorithms = new List(publicKeyAlgorithmNames.Length); + foreach (var publicKeyAlgorithmName in publicKeyAlgorithmNames) + { + publicKeyAlgorithms.Add(new PublicKeyAlgorithm(publicKeyAlgorithmName.Trim())); + } + return publicKeyAlgorithms; + } + + private static List ParseHostKeyAlgorithms(string value) + { + var algorithmNames = value.Split(','); + var hostKeyAlgorithms = new List(algorithmNames.Length); + foreach (var algorithmName in algorithmNames) + { + hostKeyAlgorithms.Add(new HostKeyAlgorithm(algorithmName.Trim())); + } + return hostKeyAlgorithms; + } + + private static List ParseMacs(string value) + { + var macNames = value.Split(','); + var macAlgorithms = new List(macNames.Length); + foreach (var algorithmName in macNames) + { + macAlgorithms.Add(new MessageAuthenticationCodeAlgorithm(algorithmName.Trim())); + } + return macAlgorithms; + } + + private static void ProcessMatchOption(Match matchConfiguration, string line) + { + var matchOptionRegex = new Regex(@"^\s+(?[\S]+)\s+(?.+?){1}\s*$"); + + var optionsMatch = matchOptionRegex.Match(line); + if (!optionsMatch.Success) + { + return; + } + + var nameGroup = optionsMatch.Groups["name"]; + var valueGroup = optionsMatch.Groups["value"]; + + var name = nameGroup.Value; + var value = valueGroup.Value; + + switch (name) + { + case "AuthenticationMethods": + matchConfiguration.AuthenticationMethods = value; + break; + default: + throw new Exception($"Match option '{name}' is not implemented."); + } + } + + + private static List ParseCommaSeparatedValue(string value) + { + var values = value.Split(','); + return new List(values); + } + + private static bool ToBool(string value) + { + switch (value) + { + case "yes": + return true; + case "no": + return false; + default: + throw new Exception($"Value '{value}' cannot be mapped to a boolean."); + } + } + + private static int ToInt(string value) + { + return int.Parse(value, NumberFormatInfo.InvariantInfo); + } + } +} diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Subsystem.cs b/src/Renci.SshNet.TestTools.OpenSSH/Subsystem.cs new file mode 100644 index 000000000..4d1155105 --- /dev/null +++ b/src/Renci.SshNet.TestTools.OpenSSH/Subsystem.cs @@ -0,0 +1,41 @@ +using System.Text.RegularExpressions; + +namespace Renci.SshNet.TestTools.OpenSSH +{ + public class Subsystem + { + public Subsystem(string name, string command) + { + Name = name; + Command = command; + } + + public string Name { get; } + + public string Command { get; set; } + + public void WriteTo(TextWriter writer) + { + writer.WriteLine(Name + "=" + Command); + } + + public static Subsystem FromConfig(string value) + { + var subSystemValueRegex = new Regex(@"^\s*(?[\S]+)\s+(?.+?){1}\s*$"); + + var match = subSystemValueRegex.Match(value); + if (match.Success) + { + var nameGroup = match.Groups["name"]; + var commandGroup = match.Groups["command"]; + + var name = nameGroup.Value; + var command = commandGroup.Value; + + return new Subsystem(name, command); + } + + throw new Exception($"'{value}' not recognized as value for Subsystem."); + } + } +} diff --git a/src/Renci.SshNet.sln b/src/Renci.SshNet.sln index c73ed0faa..34cc0f483 100644 --- a/src/Renci.SshNet.sln +++ b/src/Renci.SshNet.sln @@ -44,6 +44,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D21A4D03-0 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Renci.SshNet.IntegrationTests", "Renci.SshNet.IntegrationTests\Renci.SshNet.IntegrationTests.csproj", "{EEF98046-729C-419E-932D-4E569073C8CC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Renci.SshNet.TestTools.OpenSSH", "Renci.SshNet.TestTools.OpenSSH\Renci.SshNet.TestTools.OpenSSH.csproj", "{78239046-2019-494E-B6EC-240AF787E4D0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -106,6 +108,26 @@ Global {EEF98046-729C-419E-932D-4E569073C8CC}.Release|x64.Build.0 = Release|Any CPU {EEF98046-729C-419E-932D-4E569073C8CC}.Release|x86.ActiveCfg = Release|Any CPU {EEF98046-729C-419E-932D-4E569073C8CC}.Release|x86.Build.0 = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|ARM.ActiveCfg = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|ARM.Build.0 = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|x64.Build.0 = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Debug|x86.Build.0 = Debug|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|Any CPU.Build.0 = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|ARM.ActiveCfg = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|ARM.Build.0 = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|x64.ActiveCfg = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|x64.Build.0 = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|x86.ActiveCfg = Release|Any CPU + {78239046-2019-494E-B6EC-240AF787E4D0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From f677cef3b398ace3b54063b6f7e8aa223cad51c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Wed, 6 Sep 2023 20:59:34 +0200 Subject: [PATCH 03/15] Move integration tests to main repo --- .editorconfig | 6 - src/Renci.SshNet.IntegrationTests/.gitignore | 1 + src/Renci.SshNet.IntegrationTests/App.config | 23 + .../AuthenticationMethodFactory.cs | 66 + .../AuthenticationTests.cs | 292 + .../Common/ArrayBuilder.cs | 32 + .../Common/AsyncSocketListener.cs | 164 + .../Common/ByteExtensions.cs | 81 + .../Common/Extensions.cs | 41 + .../Common/LinkedListStream.cs | 118 + .../Common/PosixPath.cs | 38 + .../Common/RemoteSshdConfigExtensions.cs | 31 + .../Common/SocketAbstraction.cs | 565 ++ .../Common/Socks5Handler.cs | 254 + .../ConnectivityTests.cs | 586 ++ .../Credential.cs | 14 + .../HostConfig.cs | 115 + .../HostKeyAlgorithmTests.cs | 105 + .../HostKeyFile.cs | 23 + .../IConnectionInfoFactory.cs | 10 + .../Issue67/ISshStream.cs | 11 + .../Issue67/Issue67Program.cs | 64 + .../Issue67/MySshClient.cs | 163 + .../Issue67/SharpSshStream.cs | 68 + .../Issue67/SshNetStream.cs | 63 + .../Issue67/SshStreamFactory.cs | 20 + .../Issue67/UnblockStreamReader.cs | 168 + .../Issue67/UnblockStreamUtility.cs | 103 + .../Issue67/UntilInfo.cs | 37 + .../KeyExchangeAlgorithmTests.cs | 206 + .../LinuxAdminConnectionFactory.cs | 36 + .../LinuxVMConnectionFactory.cs | 62 + .../PrivateKeyAuthenticationTests.cs | 84 + src/Renci.SshNet.IntegrationTests/Program.cs | 11 + .../RemoteSshd.cs | 246 + .../Renci.SshNet.IntegrationTests.csproj | 22 +- src/Renci.SshNet.IntegrationTests/ScpTests.cs | 2380 +++++++ .../SftpTests.cs | 6234 +++++++++++++++++ src/Renci.SshNet.IntegrationTests/SshTests.cs | 971 +++ src/Renci.SshNet.IntegrationTests/TestBase.cs | 80 + .../TestsFixtures/InfrastructureFixture.cs | 6 +- .../TestsFixtures/IntegrationTestBase.cs | 2 +- src/Renci.SshNet.IntegrationTests/Users.cs | 8 + .../resources/client/id_dsa | 12 + .../resources/client/id_dsa.ppk | 17 + .../resources/client/id_noaccess.rsa | 27 + .../resources/client/id_rsa | 27 + .../resources/client/id_rsa.pub | 1 + .../resources/client/key_ecdsa_256_openssh | 9 + .../client/key_ecdsa_256_openssh.pub | 1 + .../resources/client/key_ecdsa_384_openssh | 10 + .../client/key_ecdsa_384_openssh.pub | 1 + .../resources/client/key_ecdsa_521_openssh | 12 + .../client/key_ecdsa_521_openssh.pub | 1 + .../resources/client/key_ed25519_openssh | 7 + .../resources/issue #70.png | Bin 0 -> 312036 bytes .../Renci.SshNet.TestTools.OpenSSH.csproj | 2 +- 57 files changed, 13722 insertions(+), 15 deletions(-) create mode 100644 src/Renci.SshNet.IntegrationTests/.gitignore create mode 100644 src/Renci.SshNet.IntegrationTests/App.config create mode 100644 src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs create mode 100644 src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/ArrayBuilder.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/AsyncSocketListener.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/ByteExtensions.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/Extensions.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/LinkedListStream.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/PosixPath.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/SocketAbstraction.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs create mode 100644 src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Credential.cs create mode 100644 src/Renci.SshNet.IntegrationTests/HostConfig.cs create mode 100644 src/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/HostKeyFile.cs create mode 100644 src/Renci.SshNet.IntegrationTests/IConnectionInfoFactory.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/ISshStream.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/Issue67Program.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/MySshClient.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/SharpSshStream.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/SshNetStream.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/SshStreamFactory.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamReader.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamUtility.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/UntilInfo.cs create mode 100644 src/Renci.SshNet.IntegrationTests/KeyExchangeAlgorithmTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/LinuxAdminConnectionFactory.cs create mode 100644 src/Renci.SshNet.IntegrationTests/LinuxVMConnectionFactory.cs create mode 100644 src/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Program.cs create mode 100644 src/Renci.SshNet.IntegrationTests/RemoteSshd.cs create mode 100644 src/Renci.SshNet.IntegrationTests/ScpTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/SftpTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/SshTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/TestBase.cs create mode 100644 src/Renci.SshNet.IntegrationTests/Users.cs create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/id_dsa create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/id_dsa.ppk create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/id_noaccess.rsa create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/id_rsa create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/id_rsa.pub create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_256_openssh create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_256_openssh.pub create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_384_openssh create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_384_openssh.pub create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_521_openssh create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_521_openssh.pub create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/key_ed25519_openssh create mode 100644 src/Renci.SshNet.IntegrationTests/resources/issue #70.png diff --git a/.editorconfig b/.editorconfig index f1d44ba6c..62aefca2e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -571,12 +571,6 @@ dotnet_diagnostic.IDE0130.severity = none # var inputPath = originalDossierPathList.Find(x => x.id == updatedPath.id) ?? throw new PcsException($"Path id ({updatedPath.id}) unknown in PCS for dossier id {dossierFromTs.dossier.id}", updatedPath.id); dotnet_diagnostic.IDE0270.severity = none -#### Source-generator diagnostics #### - -# SYSLIB1045 Use GeneratedRegexAttribute to generate the regular expression implementation at compile time. -# https://learn.microsoft.com/en-us/dotnet/fundamentals/syslib-diagnostics/syslib1040-1049 -dotnet_diagnostic.SYSLIB1045.severity = none - #### .NET Compiler Platform code style rules #### ### Language rules ### diff --git a/src/Renci.SshNet.IntegrationTests/.gitignore b/src/Renci.SshNet.IntegrationTests/.gitignore new file mode 100644 index 000000000..0819dad73 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/.gitignore @@ -0,0 +1 @@ +TestResults/ \ No newline at end of file diff --git a/src/Renci.SshNet.IntegrationTests/App.config b/src/Renci.SshNet.IntegrationTests/App.config new file mode 100644 index 000000000..c9794e84d --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/App.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs b/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs new file mode 100644 index 000000000..7c52e1a56 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs @@ -0,0 +1,66 @@ +namespace Renci.SshNet.IntegrationTests +{ + public class AuthenticationMethodFactory + { + public PasswordAuthenticationMethod CreatePowerUserPasswordAuthenticationMethod() + { + var user = Users.Admin; + return new PasswordAuthenticationMethod(user.UserName, user.Password); + } + + public PrivateKeyAuthenticationMethod CreateRegularUserPrivateKeyAuthenticationMethod() + { + var privateKeyFile = GetPrivateKey("Renci.SshNet.IntegrationTests.resources.client.id_rsa"); + return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKeyFile); + } + + public PrivateKeyAuthenticationMethod CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey() + { + var privateKeyFile = GetPrivateKey("Renci.SshNet.IntegrationTests.resources.client.id_noaccess.rsa"); + return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKeyFile); + } + + public PasswordAuthenticationMethod CreateRegulatUserPasswordAuthenticationMethod() + { + return new PasswordAuthenticationMethod(Users.Regular.UserName, Users.Regular.Password); + } + + public PasswordAuthenticationMethod CreateRegularUserPasswordAuthenticationMethodWithBadPassword() + { + return new PasswordAuthenticationMethod(Users.Regular.UserName, "xxx"); + } + + public KeyboardInteractiveAuthenticationMethod CreateRegularUserKeyboardInteractiveAuthenticationMethod() + { + var keyboardInteractive = new KeyboardInteractiveAuthenticationMethod(Users.Regular.UserName); + keyboardInteractive.AuthenticationPrompt += (sender, args) => + { + foreach (var authenticationPrompt in args.Prompts) + { + authenticationPrompt.Response = Users.Regular.Password; + } + }; + return keyboardInteractive; + } + + + private PrivateKeyFile GetPrivateKey(string resourceName) + { + using (var stream = GetResourceStream(resourceName)) + { + return new PrivateKeyFile(stream); + } + } + + private Stream GetResourceStream(string resourceName) + { + var type = GetType(); + var resourceStream = type.Assembly.GetManifestResourceStream(resourceName); + if (resourceStream == null) + { + throw new ArgumentException($"Resource '{resourceName}' not found in assembly '{type.Assembly.FullName}'.", nameof(resourceName)); + } + return resourceStream; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs b/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs new file mode 100644 index 000000000..5d36bc148 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs @@ -0,0 +1,292 @@ +using Renci.SshNet.Common; +using Renci.SshNet.IntegrationTests.Common; + +namespace Renci.SshNet.IntegrationTests +{ + [TestClass] + public class AuthenticationTests : IntegrationTestBase + { + private AuthenticationMethodFactory _authenticationMethodFactory; + private IConnectionInfoFactory _connectionInfoFactory; + private IConnectionInfoFactory _adminConnectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _authenticationMethodFactory = new AuthenticationMethodFactory(); + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort, _authenticationMethodFactory); + _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); + _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + + using (var client = new SshClient(_adminConnectionInfoFactory.Create())) + { + client.Connect(); + + // Reset the password back to the "regular" password. + using (var cmd = client.RunCommand($"echo \"{Users.Regular.Password}\n{Users.Regular.Password}\" | sudo passwd " + Users.Regular.UserName)) + { + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } + + // Remove password expiration + using (var cmd = client.RunCommand($"sudo chage --expiredate -1 " + Users.Regular.UserName)) + { + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } + } + } + + [TestMethod] + public void Multifactor_KeyboardInteractiveAndPublicKey() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive,publickey") + .WithChallengeResponseAuthentication(true) + .WithKeyboardInteractiveAuthentication(true) + .WithUsePAM(true) + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethodWithBadPassword(), + _authenticationMethodFactory.CreateRegularUserKeyboardInteractiveAuthenticationMethod(), + _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void Multifactor_Password_ExceedsPartialSuccessLimit() + { + // configure server to require more successfull authentications from a given method than our partial + // success limit (5) allows + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,password,password,password,password,password") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + try + { + client.Connect(); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("Reached authentication attempt limit for method (password).", ex.Message); + } + } + } + + [TestMethod] + public void Multifactor_Password_MatchPartialSuccessLimit() + { + // configure server to require a number of successfull authentications from a given method that exactly + // matches our partial success limit (5) + + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,password,password,password,password") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void Multifactor_Password_Or_PublicKeyAndKeyboardInteractive() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password publickey,keyboard-interactive") + .WithChallengeResponseAuthentication(true) + .WithKeyboardInteractiveAuthentication(true) + .WithUsePAM(true) + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod(), + _authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void Multifactor_Password_Or_PublicKeyAndPassword_BadPassword() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password publickey,password") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethodWithBadPassword(), + _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + try + { + client.Connect(); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("Permission denied (password).", ex.Message); + } + } + } + + [TestMethod] + public void Multifactor_PasswordAndPublicKey_Or_PasswordAndPassword() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,publickey password,password") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod(), + _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + + connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethodWithBadPassword(), + _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + try + { + client.Connect(); + Assert.Fail(); + } + catch (SshAuthenticationException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("Permission denied (password).", ex.Message); + } + } + + } + + [TestMethod] + public void Multifactor_PasswordAndPassword_Or_PublicKey() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,password publickey") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod(), + _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + + connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + + } + + [TestMethod] + public void Multifactor_Password_Or_Password() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password password") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + + connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegulatUserPasswordAuthenticationMethod(), + _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void KeyboardInteractive_PasswordExpired() + { + var temporaryPassword = new Random().Next().ToString(); + + using (var client = new SshClient(_adminConnectionInfoFactory.Create())) + { + client.Connect(); + + // Temporarity modify password so that when we expire this password, we change reset the password back to + // the "regular" password. + using (var cmd = client.RunCommand($"echo \"{temporaryPassword}\n{temporaryPassword}\" | sudo passwd " + Users.Regular.UserName)) + { + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } + + // Force the password to expire immediately + using (var cmd = client.RunCommand($"sudo chage -d 0 " + Users.Regular.UserName)) + { + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } + } + + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive") + .WithChallengeResponseAuthentication(true) + .WithKeyboardInteractiveAuthentication(true) + .WithUsePAM(true) + .Update() + .Restart(); + + var keyboardInteractive = new KeyboardInteractiveAuthenticationMethod(Users.Regular.UserName); + int authenticationPromptCount = 0; + keyboardInteractive.AuthenticationPrompt += (sender, args) => + { + foreach (var authenticationPrompt in args.Prompts) + { + switch (authenticationPromptCount) + { + case 0: + // Regular password prompt + authenticationPrompt.Response = temporaryPassword; + break; + case 1: + // Password expired, provide current password + authenticationPrompt.Response = temporaryPassword; + break; + case 2: + // Password expired, provide new password + authenticationPrompt.Response = Users.Regular.Password; + break; + case 3: + // Password expired, retype new password + authenticationPrompt.Response = Users.Regular.Password; + break; + } + + authenticationPromptCount++; + } + }; + + var connectionInfo = _connectionInfoFactory.Create(keyboardInteractive); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + Assert.AreEqual(4, authenticationPromptCount); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/ArrayBuilder.cs b/src/Renci.SshNet.IntegrationTests/Common/ArrayBuilder.cs new file mode 100644 index 000000000..1720c19c9 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/ArrayBuilder.cs @@ -0,0 +1,32 @@ +namespace Renci.SshNet.IntegrationTests.Common +{ + public class ArrayBuilder + { + private readonly List _buffer; + + public ArrayBuilder() + { + _buffer = new List(); + } + + public ArrayBuilder Add(T[] array) + { + return Add(array, 0, array.Length); + } + + public ArrayBuilder Add(T[] array, int index, int length) + { + for (var i = 0; i < length; i++) + { + _buffer.Add(array[index + i]); + } + + return this; + } + + public T[] Build() + { + return _buffer.ToArray(); + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/AsyncSocketListener.cs b/src/Renci.SshNet.IntegrationTests/Common/AsyncSocketListener.cs new file mode 100644 index 000000000..4a455af4c --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/AsyncSocketListener.cs @@ -0,0 +1,164 @@ +using System.Net; +using System.Net.Sockets; +#if !FEATURE_SOCKET_DISPOSE +#endif // !FEATURE_SOCKET_DISPOSE + +namespace Renci.SshNet.IntegrationTests.Common +{ + public class AsyncSocketListener : IDisposable + { + private readonly IPEndPoint _endPoint; + private readonly ManualResetEvent _acceptCallbackDone; + private Socket _listener; + private Thread _receiveThread; + private bool _started; + + public delegate void BytesReceivedHandler(byte[] bytesReceived, Socket socket); + public delegate void ConnectedHandler(Socket socket); + + public event BytesReceivedHandler BytesReceived; + public event ConnectedHandler Connected; + public event ConnectedHandler Disconnected; + + public AsyncSocketListener(IPEndPoint endPoint) + { + _endPoint = endPoint; + _acceptCallbackDone = new ManualResetEvent(false); + } + + public void Start() + { + _listener = new Socket(_endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + _listener.Bind(_endPoint); + _listener.Listen(1); + + _started = true; + + _receiveThread = new Thread(StartListener); + _receiveThread.Start(_listener); + } + + public void Stop() + { + _started = false; + if (_listener != null) + { + _listener.Dispose(); + _listener = null; + } + if (_receiveThread != null) + { + _receiveThread.Join(); + _receiveThread = null; + } + } + + public void Dispose() + { + Stop(); + GC.SuppressFinalize(this); + } + + private void StartListener(object state) + { + var listener = (Socket)state; + while (_started) + { + _acceptCallbackDone.Reset(); + listener.BeginAccept(AcceptCallback, listener); + _acceptCallbackDone.WaitOne(); + } + } + + private void AcceptCallback(IAsyncResult ar) + { + // Signal the main thread to continue. + _acceptCallbackDone.Set(); + + // Get the socket that handles the client request. + var listener = (Socket)ar.AsyncState; + try + { + var handler = listener.EndAccept(ar); + SignalConnected(handler); + var state = new SocketStateObject(handler); + handler.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReadCallback, state); + } + catch (SocketException) + { + // when the socket is closed, an SocketException is thrown since .NET 5 + // by Socket.EndAccept(IAsyncResult) + } + catch (ObjectDisposedException) + { + // when the socket is closed, an ObjectDisposedException is thrown on old .NET Framework + // by Socket.EndAccept(IAsyncResult) + } + } + + private void ReadCallback(IAsyncResult ar) + { + // Retrieve the state object and the handler socket + // from the asynchronous state object. + var state = (SocketStateObject) ar.AsyncState; + var handler = state.Socket; + + int bytesRead; + try + { + // Read data from the client socket. + bytesRead = handler.EndReceive(ar); + } + catch (ObjectDisposedException) + { + // when the socket is closed, the callback will be invoked for any pending BeginReceive + // we could use the Socket.Connected property to detect this here, but the proper thing + // to do is invoke EndReceive knowing that it will throw an ObjectDisposedException + return; + } + + if (bytesRead > 0) + { + var bytesReceived = new byte[bytesRead]; + Array.Copy(state.Buffer, bytesReceived, bytesRead); + SignalBytesReceived(bytesReceived, handler); + + // prepare to receive more bytes + handler.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReadCallback, state); + } + else + { + SignalDisconnected(handler); + handler.Shutdown(SocketShutdown.Both); + handler.Close(); + } + } + + private void SignalBytesReceived(byte[] bytesReceived, Socket client) + { + BytesReceived?.Invoke(bytesReceived, client); + } + + private void SignalConnected(Socket client) + { + Connected?.Invoke(client); + } + + private void SignalDisconnected(Socket client) + { + Disconnected?.Invoke(client); + } + + private class SocketStateObject + { + public Socket Socket { get; private set; } + + public readonly byte[] Buffer = new byte[1024]; + + public SocketStateObject(Socket handler) + { + Socket = handler; + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/ByteExtensions.cs b/src/Renci.SshNet.IntegrationTests/Common/ByteExtensions.cs new file mode 100644 index 000000000..720a8edf0 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/ByteExtensions.cs @@ -0,0 +1,81 @@ +using System.Globalization; + +namespace Renci.SshNet.IntegrationTests.Common +{ + public static class ByteExtensions + { + public static byte[] HexToByteArray(string hexString) + { + var bytes = new byte[hexString.Length / 2]; + + for (var i = 0; i < hexString.Length; i += 2) + { + var s = hexString.Substring(i, 2); + bytes[i / 2] = byte.Parse(s, NumberStyles.HexNumber, null); + } + + return bytes; + } + + public static string ToHex(byte[] bytes) + { + var builder = new StringBuilder(bytes.Length * 2); + + foreach (byte b in bytes) + { + builder.Append(b.ToString("X2")); + } + + return builder.ToString(); + } + + public static byte[] Repeat(byte b, int count) + { + var value = new byte[count]; + + for (var i = 0; i < count; i++) + { + value[i] = b; + } + + return value; + } + + /// + /// Returns a specified number of contiguous bytes from a given offset. + /// + /// The array to return a number of bytes from. + /// The zero-based offset in at which to begin taking bytes. + /// The number of bytes to take from . + /// + /// A array that contains the specified number of bytes at the specified offset + /// of the input array. + /// + /// is null. + /// + /// When is zero and equals the length of , + /// then is returned. + /// + public static byte[] Take(byte[] value, int offset, int count) + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + if (count == 0) + { + return new byte[0]; + } + + if (offset == 0 && value.Length == count) + { + return value; + } + + var taken = new byte[count]; + Buffer.BlockCopy(value, offset, taken, 0, count); + return taken; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/Extensions.cs b/src/Renci.SshNet.IntegrationTests/Common/Extensions.cs new file mode 100644 index 000000000..a3ac0bdd4 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/Extensions.cs @@ -0,0 +1,41 @@ +namespace Renci.SshNet.IntegrationTests.Common +{ + /// + /// Collection of different extension method + /// + internal static partial class Extensions + { + public static bool IsEqualTo(this byte[] left, byte[] right) + { + if (left == null) + { + throw new ArgumentNullException("left"); + } + + if (right == null) + { + throw new ArgumentNullException("right"); + } + + if (left == right) + { + return true; + } + + if (left.Length != right.Length) + { + return false; + } + + for (var i = 0; i < left.Length; i++) + { + if (left[i] != right[i]) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/LinkedListStream.cs b/src/Renci.SshNet.IntegrationTests/Common/LinkedListStream.cs new file mode 100644 index 000000000..9e6878a4f --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/LinkedListStream.cs @@ -0,0 +1,118 @@ +namespace Renci.SshNet.IntegrationTests.Common +{ + internal class LinkedListStream : Stream + { + private PipeEntry _first; + private PipeEntry _last; + + public override void Flush() + { + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var totalBytesRead = 0; + + while (count > 0 && _first != null) + { + var bytesRead = _first.Read(buffer, offset, count); + if (_first.IsEmpty) + { + _first = _first.Next; + } + + count -= bytesRead; + totalBytesRead += bytesRead; + offset += bytesRead; + } + + return totalBytesRead; + } + + public override void Write(byte[] buffer, int offset, int count) + { + var last = new PipeEntry(buffer, offset, count); + if (_last == null) + { + _last = last; + } + else + { + _last = _last.Next = last; + } + + _first ??= _last; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + + public override long Position { get; set; } + } + + internal class PipeEntry + { + private readonly byte[] _data; + public int Position; + public int Length; + + public PipeEntry(byte[] data, int offset, int count) + { + _data = data; + Position = offset; + Length = count; + } + + public int Read(byte[] dst, int offset, int count) + { + var bytesToCopy = count; + var bytesAvailable = Length - Position; + + if (count > bytesAvailable) + { + bytesToCopy = bytesAvailable; + } + + Buffer.BlockCopy(_data, Position, dst, offset, bytesToCopy); + Position += bytesToCopy; + return bytesToCopy; + } + + public bool IsEmpty + { + get { return Position == Length; } + } + + public PipeEntry Next { get; set; } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/PosixPath.cs b/src/Renci.SshNet.IntegrationTests/Common/PosixPath.cs new file mode 100644 index 000000000..6fd468dcc --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/PosixPath.cs @@ -0,0 +1,38 @@ +namespace Renci.SshNet.IntegrationTests.Common +{ + internal class PosixPath + { + /// + /// Gets the file name part of a given POSIX path. + /// + /// The POSIX path to get the file name for. + /// + /// The file name part of . + /// + /// is null. + /// + /// + /// If contains no forward slash, then + /// is returned. + /// + /// + /// If path has a trailing slash, but return a zero-length string. + /// + /// + public static string GetFileName(string path) + { + var pathEnd = path.LastIndexOf('/'); + if (pathEnd == -1) + { + return path; + } + + if (pathEnd == path.Length - 1) + { + return string.Empty; + } + + return path.Substring(pathEnd + 1); + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs b/src/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs new file mode 100644 index 000000000..64676a0d2 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs @@ -0,0 +1,31 @@ +using Renci.SshNet.TestTools.OpenSSH; + +namespace Renci.SshNet.IntegrationTests.Common +{ + internal static class RemoteSshdConfigExtensions + { + private const string DefaultAuthenticationMethods = "password publickey"; + + public static void Reset(this RemoteSshdConfig remoteSshdConfig) + { + remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, DefaultAuthenticationMethods) + .WithChallengeResponseAuthentication(false) + .WithKeyboardInteractiveAuthentication(false) + .PrintMotd() + .WithLogLevel(LogLevel.Debug3) + .ClearHostKeyFiles() + .AddHostKeyFile(HostKeyFile.Rsa.FilePath) + .ClearSubsystems() + .AddSubsystem(new Subsystem("sftp", "/usr/lib/ssh/sftp-server")) + .ClearCiphers() + .ClearKeyExchangeAlgorithms() + .ClearHostKeyAlgorithms() + .AddHostKeyAlgorithm(HostKeyAlgorithm.SshRsa) + .ClearPublicKeyAcceptedAlgorithms() + .AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm.SshRsa) + .WithUsePAM(true) + .Update() + .Restart(); + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/SocketAbstraction.cs b/src/Renci.SshNet.IntegrationTests/Common/SocketAbstraction.cs new file mode 100644 index 000000000..1f96f7af3 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/SocketAbstraction.cs @@ -0,0 +1,565 @@ +using System.Globalization; +using System.Net; +using System.Net.Sockets; + +using Renci.SshNet.Common; +using Renci.SshNet.Messages.Transport; + +namespace Renci.SshNet.IntegrationTests.Common +{ + internal static class SocketAbstraction + { + public static bool CanRead(Socket socket) + { + if (socket.Connected) + { +#if FEATURE_SOCKET_POLL + return socket.Poll(-1, SelectMode.SelectRead) && socket.Available > 0; +#else + return true; +#endif // FEATURE_SOCKET_POLL + } + + return false; + + } + + /// + /// Returns a value indicating whether the specified can be used + /// to send data. + /// + /// The to check. + /// + /// true if can be written to; otherwise, false. + /// + public static bool CanWrite(Socket socket) + { + if (socket != null && socket.Connected) + { +#if FEATURE_SOCKET_POLL + return socket.Poll(-1, SelectMode.SelectWrite); +#else + return true; +#endif // FEATURE_SOCKET_POLL + } + + return false; + } + + public static Socket Connect(IPEndPoint remoteEndpoint, TimeSpan connectTimeout) + { + var socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {NoDelay = true}; + + var connectCompleted = new ManualResetEvent(false); + var args = new SocketAsyncEventArgs + { + UserToken = connectCompleted, + RemoteEndPoint = remoteEndpoint + }; + args.Completed += ConnectCompleted; + + if (socket.ConnectAsync(args)) + { + if (!connectCompleted.WaitOne(connectTimeout)) + { + // avoid ObjectDisposedException in ConnectCompleted + args.Completed -= ConnectCompleted; + // dispose Socket + socket.Dispose(); + // dispose ManualResetEvent + connectCompleted.Dispose(); + // dispose SocketAsyncEventArgs + args.Dispose(); + + throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture, + "Connection failed to establish within {0:F0} milliseconds.", + connectTimeout.TotalMilliseconds)); + } + } + + // dispose ManualResetEvent + connectCompleted.Dispose(); + + if (args.SocketError != SocketError.Success) + { + var socketError = (int) args.SocketError; + + // dispose Socket + socket.Dispose(); + // dispose SocketAsyncEventArgs + args.Dispose(); + + throw new SocketException(socketError); + } + + // dispose SocketAsyncEventArgs + args.Dispose(); + + return socket; + } + + public static void ClearReadBuffer(Socket socket) + { + var timeout = TimeSpan.FromMilliseconds(500); + var buffer = new byte[256]; + int bytesReceived; + + do + { + bytesReceived = ReadPartial(socket, buffer, 0, buffer.Length, timeout); + } + while (bytesReceived > 0); + } + + public static int ReadPartial(Socket socket, byte[] buffer, int offset, int size, TimeSpan timeout) + { + var receiveCompleted = new ManualResetEvent(false); + var sendReceiveToken = new PartialSendReceiveToken(socket, receiveCompleted); + var args = new SocketAsyncEventArgs + { + RemoteEndPoint = socket.RemoteEndPoint, + UserToken = sendReceiveToken + }; + args.Completed += ReceiveCompleted; + args.SetBuffer(buffer, offset, size); + + try + { + if (socket.ReceiveAsync(args)) + { + if (!receiveCompleted.WaitOne(timeout)) + { + throw new SshOperationTimeoutException( + string.Format( + CultureInfo.InvariantCulture, + "Socket read operation has timed out after {0:F0} milliseconds.", + timeout.TotalMilliseconds)); + } + } + else + { + sendReceiveToken.Process(args); + } + + if (args.SocketError != SocketError.Success) + { + throw new SocketException((int) args.SocketError); + } + + return args.BytesTransferred; + } + finally + { + // initialize token to avoid the waithandle getting used after it's disposed + args.UserToken = null; + args.Dispose(); + receiveCompleted.Dispose(); + } + } + + public static void ReadContinuous(Socket socket, byte[] buffer, int offset, int size, Action processReceivedBytesAction) + { + var completionWaitHandle = new ManualResetEvent(false); + var readToken = new ContinuousReceiveToken(socket, processReceivedBytesAction, completionWaitHandle); + var args = new SocketAsyncEventArgs + { + RemoteEndPoint = socket.RemoteEndPoint, + UserToken = readToken + }; + args.Completed += ReceiveCompleted; + args.SetBuffer(buffer, offset, size); + + if (!socket.ReceiveAsync(args)) + { + ReceiveCompleted(null, args); + } + + completionWaitHandle.WaitOne(); + completionWaitHandle.Dispose(); + + if (readToken.Exception != null) + { + throw readToken.Exception; + } + } + + /// + /// Reads a byte from the specified . + /// + /// The to read from. + /// Specifies the amount of time after which the call will time out. + /// + /// The byte read, or -1 if the socket was closed. + /// + /// The read operation timed out. + /// The read failed. + public static int ReadByte(Socket socket, TimeSpan timeout) + { + var buffer = new byte[1]; + if (Read(socket, buffer, 0, 1, timeout) == 0) + { + return -1; + } + + return buffer[0]; + } + + /// + /// Sends a byte using the specified . + /// + /// The to write to. + /// The value to send. + /// The write failed. + public static void SendByte(Socket socket, byte value) + { + var buffer = new[] {value}; + Send(socket, buffer, 0, 1); + } + + /// + /// Receives data from a bound . + /// + /// + /// The number of bytes to receive. + /// Specifies the amount of time after which the call will time out. + /// + /// The bytes received. + /// + /// + /// If no data is available for reading, the method will + /// block until data is available or the time-out value is exceeded. If the time-out value is exceeded, the + /// call will throw a . + /// If you are in non-blocking mode, and there is no data available in the in the protocol stack buffer, the + /// method will complete immediately and throw a . + /// + public static byte[] Read(Socket socket, int size, TimeSpan timeout) + { + var buffer = new byte[size]; + Read(socket, buffer, 0, size, timeout); + return buffer; + } + + /// + /// Receives data from a bound into a receive buffer. + /// + /// + /// An array of type that is the storage location for the received data. + /// The position in parameter to store the received data. + /// The number of bytes to receive. + /// Specifies the amount of time after which the call will time out. + /// + /// The number of bytes received. + /// + /// + /// If no data is available for reading, the method will + /// block until data is available or the time-out value is exceeded. If the time-out value is exceeded, the + /// call will throw a . + /// If you are in non-blocking mode, and there is no data available in the in the protocol stack buffer, the + /// method will complete immediately and throw a . + /// + public static int Read(Socket socket, byte[] buffer, int offset, int size, TimeSpan timeout) + { + var receiveCompleted = new ManualResetEvent(false); + var sendReceiveToken = new BlockingSendReceiveToken(socket, buffer, offset, size, receiveCompleted); + + var args = new SocketAsyncEventArgs + { + UserToken = sendReceiveToken, + RemoteEndPoint = socket.RemoteEndPoint + }; + args.Completed += ReceiveCompleted; + args.SetBuffer(buffer, offset, size); + + try + { + if (socket.ReceiveAsync(args)) + { + if (!receiveCompleted.WaitOne(timeout)) + { + throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture, + "Socket read operation has timed out after {0:F0} milliseconds.", timeout.TotalMilliseconds)); + } + } + else + { + sendReceiveToken.Process(args); + } + + if (args.SocketError != SocketError.Success) + { + throw new SocketException((int) args.SocketError); + } + + return sendReceiveToken.TotalBytesTransferred; + } + finally + { + // initialize token to avoid the waithandle getting used after it's disposed + args.UserToken = null; + args.Dispose(); + receiveCompleted.Dispose(); + } + } + + public static void Send(Socket socket, byte[] data) + { + Send(socket, data, 0, data.Length); + } + + public static void Send(Socket socket, byte[] data, int offset, int size) + { + var sendCompleted = new ManualResetEvent(false); + var sendReceiveToken = new BlockingSendReceiveToken(socket, data, offset, size, sendCompleted); + var socketAsyncSendArgs = new SocketAsyncEventArgs + { + RemoteEndPoint = socket.RemoteEndPoint, + UserToken = sendReceiveToken + }; + socketAsyncSendArgs.SetBuffer(data, offset, size); + socketAsyncSendArgs.Completed += SendCompleted; + + try + { + if (socket.SendAsync(socketAsyncSendArgs)) + { + if (!sendCompleted.WaitOne()) + { + throw new SocketException((int) SocketError.TimedOut); + } + } + else + { + sendReceiveToken.Process(socketAsyncSendArgs); + } + + if (socketAsyncSendArgs.SocketError != SocketError.Success) + { + throw new SocketException((int) socketAsyncSendArgs.SocketError); + } + + if (sendReceiveToken.TotalBytesTransferred == 0) + { + throw new SshConnectionException("An established connection was aborted by the server.", + DisconnectReason.ConnectionLost); + } + } + finally + { + // initialize token to avoid the completion waithandle getting used after it's disposed + socketAsyncSendArgs.UserToken = null; + socketAsyncSendArgs.Dispose(); + sendCompleted.Dispose(); + } + } + + public static bool IsErrorResumable(SocketError socketError) + { + switch (socketError) + { + case SocketError.WouldBlock: + case SocketError.IOPending: + case SocketError.NoBufferSpaceAvailable: + return true; + default: + return false; + } + } + + private static void ConnectCompleted(object sender, SocketAsyncEventArgs e) + { + var eventWaitHandle = (ManualResetEvent) e.UserToken; + eventWaitHandle?.Set(); + } + + private static void ReceiveCompleted(object sender, SocketAsyncEventArgs e) + { + var sendReceiveToken = (IToken) e.UserToken; + sendReceiveToken?.Process(e); + } + + private static void SendCompleted(object sender, SocketAsyncEventArgs e) + { + var sendReceiveToken = (IToken) e.UserToken; + sendReceiveToken?.Process(e); + } + + private interface IToken + { + void Process(SocketAsyncEventArgs args); + } + + private class BlockingSendReceiveToken : IToken + { + public BlockingSendReceiveToken(Socket socket, byte[] buffer, int offset, int size, EventWaitHandle completionWaitHandle) + { + _socket = socket; + _buffer = buffer; + _offset = offset; + _bytesToTransfer = size; + _completionWaitHandle = completionWaitHandle; + } + + public void Process(SocketAsyncEventArgs args) + { + if (args.SocketError == SocketError.Success) + { + TotalBytesTransferred += args.BytesTransferred; + + if (TotalBytesTransferred == _bytesToTransfer) + { + // finished transferring specified bytes + _completionWaitHandle.Set(); + return; + } + + if (args.BytesTransferred == 0) + { + // remote server closed the connection + _completionWaitHandle.Set(); + return; + } + + _offset += args.BytesTransferred; + args.SetBuffer(_buffer, _offset, _bytesToTransfer - TotalBytesTransferred); + ResumeOperation(args); + return; + } + + if (IsErrorResumable(args.SocketError)) + { + Thread.Sleep(30); + ResumeOperation(args); + return; + } + + // we're dealing with a (fatal) error + _completionWaitHandle.Set(); + } + + private void ResumeOperation(SocketAsyncEventArgs args) + { + switch (args.LastOperation) + { + case SocketAsyncOperation.Receive: + _socket.ReceiveAsync(args); + break; + case SocketAsyncOperation.Send: + _socket.SendAsync(args); + break; + } + } + + private readonly int _bytesToTransfer; + public int TotalBytesTransferred { get; private set; } + private readonly EventWaitHandle _completionWaitHandle; + private readonly Socket _socket; + private readonly byte[] _buffer; + private int _offset; + } + + private class PartialSendReceiveToken : IToken + { + public PartialSendReceiveToken(Socket socket, EventWaitHandle completionWaitHandle) + { + _socket = socket; + _completionWaitHandle = completionWaitHandle; + } + + public void Process(SocketAsyncEventArgs args) + { + if (args.SocketError == SocketError.Success) + { + _completionWaitHandle.Set(); + return; + } + + if (IsErrorResumable(args.SocketError)) + { + Thread.Sleep(30); + ResumeOperation(args); + return; + } + + // we're dealing with a (fatal) error + _completionWaitHandle.Set(); + } + + private void ResumeOperation(SocketAsyncEventArgs args) + { + switch (args.LastOperation) + { + case SocketAsyncOperation.Receive: + _socket.ReceiveAsync(args); + break; + case SocketAsyncOperation.Send: + _socket.SendAsync(args); + break; + } + } + + private readonly EventWaitHandle _completionWaitHandle; + private readonly Socket _socket; + } + + private class ContinuousReceiveToken : IToken + { + public ContinuousReceiveToken(Socket socket, Action processReceivedBytesAction, EventWaitHandle completionWaitHandle) + { + _socket = socket; + _processReceivedBytesAction = processReceivedBytesAction; + _completionWaitHandle = completionWaitHandle; + } + + public Exception Exception { get; private set; } + + public void Process(SocketAsyncEventArgs args) + { + if (args.SocketError == SocketError.Success) + { + if (args.BytesTransferred == 0) + { + // remote socket was closed + _completionWaitHandle.Set(); + return; + } + + _processReceivedBytesAction(args.Buffer, args.Offset, args.BytesTransferred); + ResumeOperation(args); + return; + } + + if (IsErrorResumable(args.SocketError)) + { + Thread.Sleep(30); + ResumeOperation(args); + return; + } + + if (args.SocketError != SocketError.OperationAborted) + { + Exception = new SocketException((int) args.SocketError); + } + + // we're dealing with a (fatal) error + _completionWaitHandle.Set(); + } + + private void ResumeOperation(SocketAsyncEventArgs args) + { + switch (args.LastOperation) + { + case SocketAsyncOperation.Receive: + _socket.ReceiveAsync(args); + break; + case SocketAsyncOperation.Send: + _socket.SendAsync(args); + break; + } + } + + private readonly EventWaitHandle _completionWaitHandle; + private readonly Socket _socket; + private readonly Action _processReceivedBytesAction; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs b/src/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs new file mode 100644 index 000000000..7a29d58a3 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs @@ -0,0 +1,254 @@ +using System.Net; +using System.Net.Sockets; + +using Renci.SshNet.Common; +using Renci.SshNet.Messages.Transport; + +namespace Renci.SshNet.IntegrationTests.Common +{ + class Socks5Handler + { + private readonly IPEndPoint _proxyEndPoint; + private readonly string _userName; + private readonly string _password; + + public Socks5Handler(IPEndPoint proxyEndPoint, string userName, string password) + { + _proxyEndPoint = proxyEndPoint; + _userName = userName; + _password = password; + } + + public Socket Connect(IPEndPoint endPoint) + { + if (endPoint == null) + { + throw new ArgumentNullException("endPoint"); + } + + var addressBytes = GetAddressBytes(endPoint); + return Connect(addressBytes, endPoint.Port); + } + + public Socket Connect(string host, int port) + { + if (host == null) + { + throw new ArgumentNullException(nameof(host)); + } + + if (host.Length > byte.MaxValue) + { + throw new ArgumentException($@"Cannot be more than {byte.MaxValue} characters.", nameof(host)); + } + + var addressBytes = new byte[host.Length + 2]; + addressBytes[0] = 0x03; + addressBytes[1] = (byte) host.Length; + Encoding.ASCII.GetBytes(host, 0, host.Length, addressBytes, 2); + return Connect(addressBytes, port); + } + + private Socket Connect(byte[] addressBytes, int port) + { + var socket = SocketAbstraction.Connect(_proxyEndPoint, TimeSpan.FromSeconds(5)); + + // Send socks version number + SocketWriteByte(socket, 0x05); + + // Send number of supported authentication methods + SocketWriteByte(socket, 0x02); + + // Send supported authentication methods + SocketWriteByte(socket, 0x00); // No authentication + SocketWriteByte(socket, 0x02); // Username/Password + + var socksVersion = SocketReadByte(socket); + if (socksVersion != 0x05) + { + throw new ProxyException(string.Format("SOCKS Version '{0}' is not supported.", socksVersion)); + } + + var authenticationMethod = SocketReadByte(socket); + switch (authenticationMethod) + { + case 0x00: + break; + case 0x02: + + // Send version + SocketWriteByte(socket, 0x01); + + var username = Encoding.ASCII.GetBytes(_userName); + if (username.Length > byte.MaxValue) + { + throw new ProxyException("Proxy username is too long."); + } + + // Send username length + SocketWriteByte(socket, (byte) username.Length); + + // Send username + SocketAbstraction.Send(socket, username); + + var password = Encoding.ASCII.GetBytes(_password); + + if (password.Length > byte.MaxValue) + { + throw new ProxyException("Proxy password is too long."); + } + + // Send username length + SocketWriteByte(socket, (byte) password.Length); + + // Send username + SocketAbstraction.Send(socket, password); + + var serverVersion = SocketReadByte(socket); + + if (serverVersion != 1) + { + throw new ProxyException("SOCKS5: Server authentication version is not valid."); + } + + var statusCode = SocketReadByte(socket); + if (statusCode != 0) + { + throw new ProxyException("SOCKS5: Username/Password authentication failed."); + } + + break; + case 0xFF: + throw new ProxyException("SOCKS5: No acceptable authentication methods were offered."); + default: + throw new ProxyException("SOCKS5: No acceptable authentication methods were offered."); + } + + // Send socks version number + SocketWriteByte(socket, 0x05); + + // Send command code + SocketWriteByte(socket, 0x01); // establish a TCP/IP stream connection + + // Send reserved, must be 0x00 + SocketWriteByte(socket, 0x00); + + // Send address type and address + SocketAbstraction.Send(socket, addressBytes); + + // Send port + SocketWriteByte(socket, (byte)(port / 0xFF)); + SocketWriteByte(socket, (byte)(port % 0xFF)); + + // Read Server SOCKS5 version + if (SocketReadByte(socket) != 5) + { + throw new ProxyException("SOCKS5: Version 5 is expected."); + } + + // Read response code + var status = SocketReadByte(socket); + + switch (status) + { + case 0x00: + break; + case 0x01: + throw new ProxyException("SOCKS5: General failure."); + case 0x02: + throw new ProxyException("SOCKS5: Connection not allowed by ruleset."); + case 0x03: + throw new ProxyException("SOCKS5: Network unreachable."); + case 0x04: + throw new ProxyException("SOCKS5: Host unreachable."); + case 0x05: + throw new ProxyException("SOCKS5: Connection refused by destination host."); + case 0x06: + throw new ProxyException("SOCKS5: TTL expired."); + case 0x07: + throw new ProxyException("SOCKS5: Command not supported or protocol error."); + case 0x08: + throw new ProxyException("SOCKS5: Address type not supported."); + default: + throw new ProxyException("SOCKS4: Not valid response."); + } + + // Read 0 + if (SocketReadByte(socket) != 0) + { + throw new ProxyException("SOCKS5: 0 byte is expected."); + } + + var addressType = SocketReadByte(socket); + var responseIp = new byte[16]; + + switch (addressType) + { + case 0x01: + SocketRead(socket, responseIp, 0, 4); + break; + case 0x04: + SocketRead(socket, responseIp, 0, 16); + break; + default: + throw new ProxyException(string.Format("Address type '{0}' is not supported.", addressType)); + } + + var portBytes = new byte[2]; + + // Read 2 bytes to be ignored + SocketRead(socket, portBytes, 0, 2); + + return socket; + } + + private static byte[] GetAddressBytes(IPEndPoint endPoint) + { + if (endPoint.AddressFamily == AddressFamily.InterNetwork) + { + var addressBytes = new byte[4 + 1]; + addressBytes[0] = 0x01; + var address = endPoint.Address.GetAddressBytes(); + Buffer.BlockCopy(address, 0, addressBytes, 1, address.Length); + return addressBytes; + } + + if (endPoint.AddressFamily == AddressFamily.InterNetworkV6) + { + var addressBytes = new byte[16 + 1]; + addressBytes[0] = 0x04; + var address = endPoint.Address.GetAddressBytes(); + Buffer.BlockCopy(address, 0, addressBytes, 1, address.Length); + return addressBytes; + } + + throw new ProxyException(string.Format("SOCKS5: IP address '{0}' is not supported.", endPoint.Address)); + } + + private static void SocketWriteByte(Socket socket, byte data) + { + SocketAbstraction.Send(socket, new[] { data }); + } + + private static byte SocketReadByte(Socket socket) + { + var buffer = new byte[1]; + SocketRead(socket, buffer, 0, 1); + return buffer[0]; + } + + private static int SocketRead(Socket socket, byte[] buffer, int offset, int length) + { + var bytesRead = SocketAbstraction.Read(socket, buffer, offset, length, TimeSpan.FromMilliseconds(-1)); + if (bytesRead == 0) + { + // when we're in the disconnecting state (either triggered by client or server), then the + // SshConnectionException will interrupt the message listener loop (if not already interrupted) + // and the exception itself will be ignored (in RaiseError) + throw new SshConnectionException("An established connection was aborted by the server.", + DisconnectReason.ConnectionLost); + } + return bytesRead; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs new file mode 100644 index 000000000..90e2739be --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs @@ -0,0 +1,586 @@ +using System.Diagnostics; +using System.Management; +using System.Text.RegularExpressions; + +using Renci.SshNet.Common; +using Renci.SshNet.IntegrationTests.Common; +using Renci.SshNet.Messages.Transport; + +namespace Renci.SshNet.IntegrationTests +{ + [TestClass] + public class ConnectivityTests : TestBase + { + private const string NetworkConnectionId = "Ethernet 2"; + + private AuthenticationMethodFactory _authenticationMethodFactory; + private IConnectionInfoFactory _connectionInfoFactory; + private IConnectionInfoFactory _adminConnectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _authenticationMethodFactory = new AuthenticationMethodFactory(); + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort, _authenticationMethodFactory); + _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); + _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + } + + [TestMethod] + public void Common_CreateMoreChannelsThanMaxSessions() + { + var connectionInfo = _connectionInfoFactory.Create(); + connectionInfo.MaxSessions = 2; + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + + // create one more channel than the maximum number of sessions + // as that would block indefinitely when creating the last channel + // if the channel would not be properly closed + for (var i = 0; i < connectionInfo.MaxSessions + 1; i++) + { + using (var stream = client.CreateShellStream("vt220", 20, 20, 20, 20, 0)) + { + stream.WriteLine("echo test"); + stream.ReadLine(); + } + } + } + } + + [TestMethod] + public void Common_DisposeAfterLossOfNetworkConnectivity() + { + var hostNetworkConnectionDisabled = false; + + try + { + Exception errorOccurred = null; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.ErrorOccurred += (sender, args) => errorOccurred = args.Exception; + client.Connect(); + + DisableHostNetworkConnection(NetworkConnectionId); + hostNetworkConnectionDisabled = true; + } + + Assert.IsNotNull(errorOccurred); + Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); + + var connectionException = (SshConnectionException) errorOccurred; + Assert.AreEqual(DisconnectReason.ConnectionLost, connectionException.DisconnectReason); + Assert.IsNull(connectionException.InnerException); + Assert.AreEqual("An established connection was aborted by the server.", connectionException.Message); + } + finally + { + if (hostNetworkConnectionDisabled) + { + EnableHostNetworkConnection(NetworkConnectionId); + ResetVirtualMachineNetworkConnection(); + } + } + } + + [TestMethod] + public void Common_DetectLossOfNetworkConnectivityThroughKeepAlive() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + Exception errorOccurred = null; + client.ErrorOccurred += (sender, args) => errorOccurred = args.Exception; + client.KeepAliveInterval = new TimeSpan(0, 0, 0, 0, 50); + client.Connect(); + + DisableHostNetworkConnection(NetworkConnectionId); + + try + { + for (var i = 0; i < 500; i++) + { + if (!client.IsConnected) + { + break; + } + + Thread.Sleep(100); + } + + Assert.IsFalse(client.IsConnected); + + Assert.IsNotNull(errorOccurred); + Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); + + var connectionException = (SshConnectionException)errorOccurred; + Assert.AreEqual(DisconnectReason.ConnectionLost, connectionException.DisconnectReason); + Assert.IsNull(connectionException.InnerException); + Assert.AreEqual("An established connection was aborted by the server.", connectionException.Message); + } + finally + { + EnableHostNetworkConnection(NetworkConnectionId); + ResetVirtualMachineNetworkConnection(); + } + } + } + + [TestMethod] + public void Common_DetectConnectionResetThroughSftpInvocation() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + ManualResetEvent errorOccurredSignaled = new ManualResetEvent(false); + Exception errorOccurred = null; + client.ErrorOccurred += (sender, args) => + { + errorOccurred = args.Exception; + errorOccurredSignaled.Set(); + }; + client.Connect(); + + DisableHostNetworkConnection(NetworkConnectionId); + + try + { + client.ListDirectory("/"); + Assert.Fail(); + } + catch (SshConnectionException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("Client not connected.", ex.Message); + + Assert.IsNotNull(errorOccurred); + Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); + + var connectionException = (SshConnectionException) errorOccurred; + Assert.AreEqual(DisconnectReason.ConnectionLost, connectionException.DisconnectReason); + Assert.IsNull(connectionException.InnerException); + Assert.AreEqual("An established connection was aborted by the server.", connectionException.Message); + } + finally + { + EnableHostNetworkConnection(NetworkConnectionId); + ResetVirtualMachineNetworkConnection(); + } + } + } + + [TestMethod] + public void Common_LossOfNetworkConnectivityDisconnectAndConnect() + { + bool vmNetworkConnectionDisabled = false; + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + Exception errorOccurred = null; + client.ErrorOccurred += (sender, args) => errorOccurred = args.Exception; + + client.Connect(); + + DisableVirtualMachineNetworkConnection(); + vmNetworkConnectionDisabled = true; + ResetVirtualMachineNetworkConnection(); + + // disconnect while network connectivity is lost + client.Disconnect(); + + Assert.IsFalse(client.IsConnected); + + EnableVirtualMachineNetworkConnection(); + vmNetworkConnectionDisabled = false; + ResetVirtualMachineNetworkConnection(); + + // connect when network connectivity is restored + client.Connect(); + client.ChangeDirectory(client.WorkingDirectory); + client.Dispose(); + + Assert.IsNull(errorOccurred); + } + } + finally + { + if (vmNetworkConnectionDisabled) + { + EnableVirtualMachineNetworkConnection(); + ResetVirtualMachineNetworkConnection(); + } + } + } + + [TestMethod] + public void Common_DetectLossOfNetworkConnectivityThroughSftpInvocation() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + ManualResetEvent errorOccurredSignaled = new ManualResetEvent(false); + Exception errorOccurred = null; + client.ErrorOccurred += (sender, args) => + { + errorOccurred = args.Exception; + errorOccurredSignaled.Set(); + }; + client.Connect(); + + DisableVirtualMachineNetworkConnection(); + ResetVirtualMachineNetworkConnection(); + + try + { + client.ListDirectory("/"); + Assert.Fail(); + } + catch (SshConnectionException ex) + { + Assert.AreEqual(DisconnectReason.ConnectionLost, ex.DisconnectReason); + Assert.IsNull(ex.InnerException); + Assert.AreEqual("An established connection was aborted by the server.", ex.Message); + } + finally + { + EnableVirtualMachineNetworkConnection(); + ResetVirtualMachineNetworkConnection(); + } + } + } + + [TestMethod] + public void Common_DetectSessionKilledOnServer() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + ManualResetEvent errorOccurredSignaled = new ManualResetEvent(false); + Exception errorOccurred = null; + client.ErrorOccurred += (sender, args) => + { + errorOccurred = args.Exception; + errorOccurredSignaled.Set(); + }; + client.Connect(); + + // Kill the server session + using (var adminClient = new SshClient(_adminConnectionInfoFactory.Create())) + { + adminClient.Connect(); + + var command = $"sudo ps --no-headers -u {client.ConnectionInfo.Username} -f | grep \"{client.ConnectionInfo.Username}@notty\" | awk '{{print $2}}' | xargs sudo kill -9"; + var sshCommand = adminClient.CreateCommand(command); + var result = sshCommand.Execute(); + Assert.AreEqual(0, sshCommand.ExitStatus, sshCommand.Error); + } + + Assert.IsTrue(errorOccurredSignaled.WaitOne(200)); + Assert.IsNotNull(errorOccurred); + Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); + Assert.IsNull(errorOccurred.InnerException); + Assert.AreEqual("An established connection was aborted by the server.", errorOccurred.Message); + Assert.IsFalse(client.IsConnected); + } + } + + [TestMethod] + public void Common_HostKeyValidation_Failure() + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.HostKeyReceived += (sender, e) => { e.CanTrust = false; }; + + try + { + client.Connect(); + Assert.Fail(); + } + catch (SshConnectionException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("Key exchange negotiation failed.", ex.Message); + } + } + } + + [TestMethod] + public void Common_HostKeyValidation_Success() + { + byte[] host_rsa_key_openssh_fingerprint = + { + 0x3d, 0x90, 0xd8, 0x0d, 0xd5, 0xe0, 0xb6, 0x13, + 0x42, 0x7c, 0x78, 0x1e, 0x19, 0xa3, 0x99, 0x2b + }; + + var hostValidationSuccessful = false; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.HostKeyReceived += (sender, e) => + { + if (host_rsa_key_openssh_fingerprint.Length == e.FingerPrint.Length) + { + for (var i = 0; i < host_rsa_key_openssh_fingerprint.Length; i++) + { + if (host_rsa_key_openssh_fingerprint[i] != e.FingerPrint[i]) + { + e.CanTrust = false; + break; + } + } + + hostValidationSuccessful = e.CanTrust; + } + else + { + e.CanTrust = false; + } + }; + client.Connect(); + } + + Assert.IsTrue(hostValidationSuccessful); + } + + /// + /// Verifies whether we handle a disconnect initiated by the SSH server (through a SSH_MSG_DISCONNECT message). + /// + /// + /// We force this by only configuring keyboard-interactive as authentication method, while ChallengeResponseAuthentication + /// is not enabled. This causes OpenSSH to terminate the connection because there are no authentication methods left. + /// + [TestMethod] + public void Common_ServerRejectsConnection() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserKeyboardInteractiveAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + try + { + client.Connect(); + Assert.Fail(); + } + catch (SshConnectionException ex) + { + Assert.AreEqual(DisconnectReason.ProtocolError, ex.DisconnectReason); + Assert.IsNull(ex.InnerException); + Assert.AreEqual("The connection was closed by the server: no authentication methods enabled (ProtocolError).", ex.Message); + } + } + } + + private static void DisableHostNetworkConnection(string networkConnection) + { + SelectQuery wmiQuery = new SelectQuery("SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionId != NULL"); + ManagementObjectSearcher searchProcedure = new ManagementObjectSearcher(wmiQuery); + foreach (ManagementObject item in searchProcedure.Get()) + { + var netConnectionId = (string)item["NetConnectionId"]; + + if (netConnectionId == networkConnection) + { + var returnValue = item.InvokeMethod("Disable", null); + if (returnValue is uint retValue) + { + if (retValue == 0) + { + return; + } + + throw new ApplicationException($"Failed to disable '{networkConnection}' network connection. Return value is {retValue}.{Environment.NewLine}Make sure you're running the tests with elevated priviliges."); + } + else if (returnValue == null) + { + throw new ApplicationException($"Failed to disable '{networkConnection}' network connection. Return value is null."); + } + else + { + throw new ApplicationException($"Failed to disable '{networkConnection}' network connection. Unexpected return value {returnValue} ({returnValue.GetType()})."); + } + } + } + + throw new ApplicationException($"Failed to disable '{networkConnection}' network connection. Network connection not found."); + } + + private static void EnableHostNetworkConnection(string networkConnection) + { + SelectQuery wmiQuery = new SelectQuery("SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionId != NULL"); + ManagementObjectSearcher searchProcedure = new ManagementObjectSearcher(wmiQuery); + foreach (ManagementObject item in searchProcedure.Get()) + { + var netConnectionId = (string)item["NetConnectionId"]; + + if (netConnectionId == networkConnection) + { + var returnValue = item.InvokeMethod("Enable", null); + if (returnValue is uint retValue) + { + if (retValue == 0u) + { + Console.WriteLine($"Enable host network connection for '{networkConnection}'."); + Thread.Sleep(5000); + return; + } + + throw new ApplicationException($"Failed to enable '{networkConnection}' network connection. Return value is {retValue}..{Environment.NewLine}Make sure you're running the tests with elevated priviliges."); + } + else if (returnValue == null) + { + throw new ApplicationException($"Failed to enable '{networkConnection}' network connection. Return value is null."); + } + else + { + throw new ApplicationException($"Failed to enable '{networkConnection}' network connection. Unexpected return value {returnValue} ({returnValue.GetType()})."); + } + } + } + + throw new ApplicationException($"Failed to enable '{networkConnection}' network connection. Network connection not found."); + } + + private static string VirtualBoxFolder + { + get + { + if (Environment.Is64BitOperatingSystem) + { + if (!Environment.Is64BitProcess) + { + // dotnet test runs tests in a 32-bit process (no watter what I f***in' try), so let's hard-code the + // path to VirtualBox + return Path.Combine("c:\\Program Files", "Oracle", "VirtualBox"); + } + } + + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Oracle", "VirtualBox"); + } + } + + private static List GetRunningVMs() + { + var runningVmRegex = new Regex("\"(?.+?)\"\\s?(?{.+?})"); + + var startInfo = new ProcessStartInfo + { + FileName = Path.Combine(VirtualBoxFolder, "VBoxManage.exe"), + Arguments = "list runningvms", + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new ApplicationException($"Failed to get list of running VMs. Exit code is {process.ExitCode}."); + } + + var runningVms = new List(); + + string line; + + while ((line = process.StandardOutput.ReadLine()) != null) + { + var match = runningVmRegex.Match(line); + if (match != null) + { + runningVms.Add(match.Groups["name"].Value); + } + } + + return runningVms; + } + + private static void SetLinkState(string vmName, bool on) + { + var linkStateValue = (on ? "on" : "off"); + + var startInfo = new ProcessStartInfo + { + FileName = Path.Combine(VirtualBoxFolder, "VBoxManage.exe"), + Arguments = $"controlvm \"{vmName}\" setlinkstate1 {linkStateValue}", + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new ApplicationException($"Failed to set linkstate for VM '{vmName}' to '{linkStateValue}'. Exit code is {process.ExitCode}."); + } + else + { + Console.WriteLine($"Changed linkstate for VM '{vmName}' to '{linkStateValue}."); + } + } + + private static void SetPromiscuousMode(string vmName, string value) + { + var startInfo = new ProcessStartInfo + { + FileName = Path.Combine(VirtualBoxFolder, "VBoxManage.exe"), + Arguments = $"controlvm \"{vmName}\" nicpromisc1 {value}", + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new ApplicationException($"Failed to set promiscuous for VM '{vmName}' to '{value}'. Exit code is {process.ExitCode}."); + } + else + { + Console.WriteLine($"Changed promiscuous for VM '{vmName}' to '{value}'."); + } + } + + private static void DisableVirtualMachineNetworkConnection() + { + var runningVMs = GetRunningVMs(); + Assert.AreEqual(1, runningVMs.Count); + + SetLinkState(runningVMs[0], false); + Thread.Sleep(1000); + } + + private static void EnableVirtualMachineNetworkConnection() + { + var runningVMs = GetRunningVMs(); + Assert.AreEqual(1, runningVMs.Count); + + SetLinkState(runningVMs[0], true); + Thread.Sleep(1000); + } + + private static void ResetVirtualMachineNetworkConnection() + { + var runningVMs = GetRunningVMs(); + Assert.AreEqual(1, runningVMs.Count); + + SetPromiscuousMode(runningVMs[0], "allow-all"); + Thread.Sleep(1000); + SetPromiscuousMode(runningVMs[0], "deny"); + Thread.Sleep(1000); + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Credential.cs b/src/Renci.SshNet.IntegrationTests/Credential.cs new file mode 100644 index 000000000..ee62d0f7b --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Credential.cs @@ -0,0 +1,14 @@ +namespace Renci.SshNet.IntegrationTests +{ + internal class Credential + { + public Credential(string userName, string password) + { + UserName = userName; + Password = password; + } + + public string UserName { get; } + public string Password { get; } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/HostConfig.cs b/src/Renci.SshNet.IntegrationTests/HostConfig.cs new file mode 100644 index 000000000..817bd7026 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/HostConfig.cs @@ -0,0 +1,115 @@ +using System.Net; +using System.Text.RegularExpressions; + +namespace Renci.SshNet.IntegrationTests +{ + class HostConfig + { + private static readonly Regex HostsEntryRegEx = new Regex(@"^(?[\S]+)\s+(?[a-zA-Z]+[a-zA-Z\-\.]*[a-zA-Z]+)\s*(?.+)*$", RegexOptions.Singleline); + + public List Entries { get; } + + private HostConfig() + { + Entries = new List(); + } + + public static HostConfig Read(ScpClient scpClient, string path) + { + HostConfig hostConfig = new HostConfig(); + + using (var ms = new MemoryStream()) + { + scpClient.Download(path, ms); + ms.Position = 0; + + using (var sr = new StreamReader(ms, Encoding.ASCII)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + // skip comments + if (line.StartsWith("#")) + { + continue; + } + + var hostEntryMatch = HostsEntryRegEx.Match(line); + if (!hostEntryMatch.Success) + { + continue; + } + + var entryIPAddress = hostEntryMatch.Groups["IPAddress"].Value; + var entryAliasesGroup = hostEntryMatch.Groups["Aliases"]; + + var entry = new HostEntry(IPAddress.Parse(entryIPAddress), hostEntryMatch.Groups["HostName"].Value); + + if (entryAliasesGroup.Success) + { + var aliases = entryAliasesGroup.Value.Split(' '); + foreach (var alias in aliases) + { + entry.Aliases.Add(alias); + } + } + + hostConfig.Entries.Add(entry); + } + } + } + + return hostConfig; + } + + public void Write(ScpClient scpClient, string path) + { + using (var ms = new MemoryStream()) + using (var sw = new StreamWriter(ms, Encoding.ASCII)) + { + // Use linux line ending + sw.NewLine = "\n"; + + foreach (var hostEntry in Entries) + { + sw.Write(hostEntry.IPAddress); + sw.Write(" "); + sw.Write(hostEntry.HostName); + + if (hostEntry.Aliases.Count > 0) + { + sw.Write(" "); + for (var i = 0; i < hostEntry.Aliases.Count; i++) + { + if (i > 0) + { + sw.Write(' '); + } + sw.Write(hostEntry.Aliases[i]); + } + } + sw.WriteLine(); + } + + sw.Flush(); + ms.Position = 0; + + scpClient.Upload(ms, path); + } + } + } + + public class HostEntry + { + public HostEntry(IPAddress ipAddress, string hostName) + { + IPAddress = ipAddress; + HostName = hostName; + Aliases = new List(); + } + + public IPAddress IPAddress { get; private set; } + public string HostName { get; set; } + public List Aliases { get; } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs b/src/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs new file mode 100644 index 000000000..3732f1e22 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/HostKeyAlgorithmTests.cs @@ -0,0 +1,105 @@ +using Renci.SshNet.Common; +using Renci.SshNet.IntegrationTests.Common; +using Renci.SshNet.TestTools.OpenSSH; + +namespace Renci.SshNet.IntegrationTests +{ + [TestClass] + public class HostKeyAlgorithmTests : IntegrationTestBase + { + private IConnectionInfoFactory _connectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort); + _remoteSshdConfig = new RemoteSshd(new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort)).OpenConfig(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + } + + [TestMethod] + [Ignore] // No longer supported in recent versions of OpenSSH + public void SshDsa() + { + _remoteSshdConfig.ClearHostKeyAlgorithms() + .AddHostKeyAlgorithm(HostKeyAlgorithm.SshDsa) + .ClearHostKeyFiles() + .AddHostKeyFile(HostKeyFile.Dsa.FilePath) + .Update() + .Restart(); + + HostKeyEventArgs hostKeyEventsArgs = null; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.HostKeyReceived += (sender, e) => hostKeyEventsArgs = e; + client.Connect(); + client.Disconnect(); + } + + Assert.IsNotNull(hostKeyEventsArgs); + Assert.AreEqual(HostKeyFile.Dsa.KeyName, hostKeyEventsArgs.HostKeyName); + Assert.AreEqual(1024, hostKeyEventsArgs.KeyLength); + Assert.IsTrue(hostKeyEventsArgs.FingerPrint.SequenceEqual(HostKeyFile.Dsa.FingerPrint)); + } + + [TestMethod] + public void SshRsa() + { + _remoteSshdConfig.ClearHostKeyAlgorithms() + .AddHostKeyAlgorithm(HostKeyAlgorithm.SshRsa) + .Update() + .Restart(); + + HostKeyEventArgs hostKeyEventsArgs = null; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.HostKeyReceived += (sender, e) => hostKeyEventsArgs = e; + client.Connect(); + client.Disconnect(); + } + + Assert.IsNotNull(hostKeyEventsArgs); + Assert.AreEqual(HostKeyFile.Rsa.KeyName, hostKeyEventsArgs.HostKeyName); + Assert.AreEqual(3072, hostKeyEventsArgs.KeyLength); + Assert.IsTrue(hostKeyEventsArgs.FingerPrint.SequenceEqual(HostKeyFile.Rsa.FingerPrint)); + } + + [TestMethod] + public void SshEd25519() + { + _remoteSshdConfig.ClearHostKeyAlgorithms() + .AddHostKeyAlgorithm(HostKeyAlgorithm.SshEd25519) + .ClearHostKeyFiles() + .AddHostKeyFile(HostKeyFile.Ed25519.FilePath) + .Update() + .Restart(); + + HostKeyEventArgs hostKeyEventsArgs = null; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.HostKeyReceived += (sender, e) => hostKeyEventsArgs = e; + client.Connect(); + client.Disconnect(); + } + + Assert.IsNotNull(hostKeyEventsArgs); + Assert.AreEqual(HostKeyFile.Ed25519.KeyName, hostKeyEventsArgs.HostKeyName); + Assert.AreEqual(256, hostKeyEventsArgs.KeyLength); + Assert.IsTrue(hostKeyEventsArgs.FingerPrint.SequenceEqual(HostKeyFile.Ed25519.FingerPrint)); + } + + private void Client_HostKeyReceived(object sender, HostKeyEventArgs e) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/HostKeyFile.cs b/src/Renci.SshNet.IntegrationTests/HostKeyFile.cs new file mode 100644 index 000000000..01cf957f5 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/HostKeyFile.cs @@ -0,0 +1,23 @@ +namespace Renci.SshNet.IntegrationTests +{ + public sealed class HostKeyFile + { + public static readonly HostKeyFile Rsa = new HostKeyFile("ssh-rsa", "/etc/ssh/ssh_host_rsa_key", new byte[] { 0x3d, 0x90, 0xd8, 0x0d, 0xd5, 0xe0, 0xb6, 0x13, 0x42, 0x7c, 0x78, 0x1e, 0x19, 0xa3, 0x99, 0x2b }); + public static readonly HostKeyFile Dsa = new HostKeyFile("ssh-dsa", "/etc/ssh/ssh_host_dsa_key", new byte[] { 0x3d, 0x90, 0xd8, 0x0d, 0xd5, 0xe0, 0xb6, 0x13, 0x42, 0x7c, 0x78, 0x1e, 0x19, 0xa3, 0x99, 0x2b }); + public static readonly HostKeyFile Ed25519 = new HostKeyFile("ssh-ed25519", "/etc/ssh/ssh_host_ed25519_key", new byte[] { 0xb3, 0xb9, 0xd0, 0x1b, 0x73, 0xc4, 0x60, 0xb4, 0xce, 0xed, 0x06, 0xf8, 0x58, 0x49, 0xa3, 0xda }); + public const string Ecdsa = "/etc/ssh/ssh_host_ecdsa_key"; + + private HostKeyFile(string keyName, string filePath, byte[] fingerPrint) + { + KeyName = keyName; + FilePath = filePath; + FingerPrint = fingerPrint; + } + + public string KeyName {get; } + public string FilePath { get; } + public byte[] FingerPrint { get; } + } + + +} diff --git a/src/Renci.SshNet.IntegrationTests/IConnectionInfoFactory.cs b/src/Renci.SshNet.IntegrationTests/IConnectionInfoFactory.cs new file mode 100644 index 000000000..858dd0872 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/IConnectionInfoFactory.cs @@ -0,0 +1,10 @@ +namespace Renci.SshNet.IntegrationTests +{ + public interface IConnectionInfoFactory + { + ConnectionInfo Create(); + ConnectionInfo Create(params AuthenticationMethod[] authenticationMethods); + ConnectionInfo CreateWithProxy(); + ConnectionInfo CreateWithProxy(params AuthenticationMethod[] authenticationMethods); + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/ISshStream.cs b/src/Renci.SshNet.IntegrationTests/Issue67/ISshStream.cs new file mode 100644 index 000000000..5e78bd433 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/ISshStream.cs @@ -0,0 +1,11 @@ +namespace Renci.SshNet.IntegrationTests.Issue67 +{ + public interface ISshStream + { + void Connect(string host, string userName, string password); + void Close(); + void Write(string data); + StreamReader GetStreamReader(); + StreamWriter GetStreamWriter(); + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/Issue67Program.cs b/src/Renci.SshNet.IntegrationTests/Issue67/Issue67Program.cs new file mode 100644 index 000000000..0b9c6fd9d --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/Issue67Program.cs @@ -0,0 +1,64 @@ +using System.Diagnostics; + +namespace Renci.SshNet.IntegrationTests.Issue67 +{ + class Issue67Program + { + private const string Host = "192.168.1.122"; + + public static void Start() + { + Stopwatch stopwatch = new Stopwatch(); + + SshClient sshNet = new SshClient(Host, Users.Regular.UserName, Users.Regular.Password); + stopwatch.Restart(); + sshNet.Connect(); + stopwatch.Stop(); + Console.Write("sshNet.Connect() "); + Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); + stopwatch.Restart(); + SshCommand sshCommand = sshNet.RunCommand("free -m"); + stopwatch.Stop(); + Console.Write("sshNet.RunCommand() "); + Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); + +#if NETFRAMEWORK + Tamir.SharpSsh.SshExec sharpSsh = new Tamir.SharpSsh.SshExec(Host, Users.Regular.UserName, Users.Regular.Password); + stopwatch.Restart(); + sharpSsh.Connect(); + stopwatch.Stop(); + Console.Write("sharpSsh.Connect() "); + Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); + stopwatch.Restart(); + string result = sharpSsh.RunCommand("free -m"); + stopwatch.Stop(); + Console.Write("sharpSsh.RunCommand() "); + Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); +#endif // NETFRAMEWORK + + MySshClient mySshClient_SshNet = new MySshClient(Host, Users.Regular.UserName, Users.Regular.Password, "sshnet"); + stopwatch.Restart(); + mySshClient_SshNet.Connect(); + stopwatch.Stop(); + Console.Write("mySshClient_SshNet.Connect() "); + Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); + stopwatch.Restart(); + string[] results1 = mySshClient_SshNet.RunCommand("free -m"); + stopwatch.Stop(); + Console.Write("mySshClient_SshNet.RunCommand() "); + Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); + + MySshClient mySshClient_SharpSsh = new MySshClient(Host, Users.Regular.UserName, Users.Regular.Password, "sharpssh"); + stopwatch.Restart(); + mySshClient_SharpSsh.Connect(); + stopwatch.Stop(); + Console.Write("mySshClient_SharpSsh.Connect() "); + Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); + stopwatch.Restart(); + string[] results2 = mySshClient_SharpSsh.RunCommand("free -m"); + stopwatch.Stop(); + Console.Write("mySshClient_SharpSsh.RunCommand() "); + Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/MySshClient.cs b/src/Renci.SshNet.IntegrationTests/Issue67/MySshClient.cs new file mode 100644 index 000000000..748c9e219 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/MySshClient.cs @@ -0,0 +1,163 @@ +using System.ComponentModel; + +namespace Renci.SshNet.IntegrationTests.Issue67 +{ + public class MySshClient : IDisposable + { + public MySshClient(string host, string userName, string password, string sshStreamType) + { + _host = host; + _userName = userName; + _password = password; + _sshStreamType = sshStreamType; + } + + private readonly string _host; + private readonly string _userName; + private readonly string _password; + private readonly string _sshStreamType; + private readonly int _noResponseTimeoutSeconds = 60; + + private readonly Component _component = new Component(); + private bool _disposed = false; + + ~MySshClient() + { + Dispose(false); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _component.Dispose(); + } + + Close(); + + _disposed = true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private ISshStream SshStream + { + get; + set; + } + + public void Connect() + { + SshStream = SshStreamFactory.CreateSshStream(_sshStreamType); + SshStream.Connect(_host, _userName, _password); + + try + { + UnblockStreamReader = new UnblockStreamReader(SshStream.GetStreamReader()); + InitEnv(); + } + catch (Exception ex) + { + Close(); + throw ex; + } + } + + protected virtual void InitEnv() + { + SshStream.Write("set +o vi"); + SshStream.Write("set +o viraw"); + SshStream.Write("export PROMPT_COMMAND="); + SshStream.Write("export PS1=" + Prompt); + var response = ReadResponse("export PS1=" + Prompt + "\r\n", _noResponseTimeoutSeconds); + response = ReadResponse(Prompt, _noResponseTimeoutSeconds); + + string helloMessage = "Hello this is test message!"; + SshStream.Write("echo '" + helloMessage + "'"); + response = ReadResponse(helloMessage + "\r\n", _noResponseTimeoutSeconds); + response = ReadResponse(Prompt, _noResponseTimeoutSeconds); + + SshStream.Write("stty columns 512"); + response = ReadResponse(Prompt, _noResponseTimeoutSeconds); + + SshStream.Write("stty rows 24"); + response = ReadResponse(Prompt, _noResponseTimeoutSeconds); + + SshStream.Write("export LANG=en_US.UTF-8"); + response = ReadResponse(Prompt, _noResponseTimeoutSeconds); + + SshStream.Write("export NLS_LANG=American_America.ZHS16GBK"); + response = ReadResponse(Prompt, _noResponseTimeoutSeconds); + + SshStream.Write("unalias grep"); + response = ReadResponse(Prompt, _noResponseTimeoutSeconds); + } + + protected virtual void Close() + { + if (UnblockStreamReader != null) + { + UnblockStreamReader.Close(); + UnblockStreamReader = null; + } + if (SshStream != null) + { + SshStream.Close(); + SshStream = null; + } + } + + protected UnblockStreamReader UnblockStreamReader + { + get; + private set; + } + + public string Prompt + { + get + { + return "[SHINE_COMMAND_PROMPT]"; + } + } + + public void Write(string data) + { + if (SshStream == null) + { + Connect(); + } + if (UnblockStreamReader.GetUnreadBufferLength() > 0) + { + UnblockStreamReader.ReadToEnd(); + } + SshStream.Write(data); + } + + public string[] ReadResponse(string prompt, int noResponseTimeoutSeconds) + { + List untilInfoList = new List() { new UntilInfo(prompt) }; + string[] response = UnblockStreamUtility.ReadUntil(UnblockStreamReader, untilInfoList, noResponseTimeoutSeconds); + return response; + } + + public string[] ReadResponse(List untilInfoList, int noResponseTimeoutSeconds) + { + string[] response = UnblockStreamUtility.ReadUntil(UnblockStreamReader, untilInfoList, noResponseTimeoutSeconds); + return response; + } + + public string[] RunCommand(string command) + { + SshStream.Write(command); + return ReadResponse(Prompt, _noResponseTimeoutSeconds); + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/SharpSshStream.cs b/src/Renci.SshNet.IntegrationTests/Issue67/SharpSshStream.cs new file mode 100644 index 000000000..118746d2e --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/SharpSshStream.cs @@ -0,0 +1,68 @@ +#if NETFRAMEWORK + +using System.IO; + +namespace SshNetTests.Issue67 +{ + internal class SharpSshStream : ISshStream + { + Tamir.SharpSsh.SshStream _sshStream; + + public void Connect(string host, string userName, string password) + { + _sshStream = new Tamir.SharpSsh.SshStream(host, userName, password); + } + + public void Close() + { + if (_sshStream != null) + _sshStream.Close(); + } + + public void Write(string data) + { + StreamWriter streamWriter = GetStreamWriter(); + streamWriter.Write(data + "\r"); + streamWriter.Flush(); + } + + StreamReader _streamReader = null; + public StreamReader GetStreamReader() + { + if (_streamReader != null) + { + return _streamReader; + } + else + { + if (_sshStream == null) + { + return null; + } + _streamReader = new StreamReader(_sshStream); + return _streamReader; + } + } + + StreamWriter _streamWriter = null; + public StreamWriter GetStreamWriter() + { + if (_streamWriter != null) + { + return _streamWriter; + } + else + { + if (_sshStream == null) + { + return null; + } + + _streamWriter = new StreamWriter(_sshStream); + return _streamWriter; + } + } + } +} + +#endif // NETFRAMEWORK \ No newline at end of file diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/SshNetStream.cs b/src/Renci.SshNet.IntegrationTests/Issue67/SshNetStream.cs new file mode 100644 index 000000000..ceec86c5c --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/SshNetStream.cs @@ -0,0 +1,63 @@ +namespace Renci.SshNet.IntegrationTests.Issue67 +{ + internal class SshNetStream : ISshStream + { + Renci.SshNet.ShellStream _shellStream; + Renci.SshNet.SshClient _sshClinet; + + public void Connect(string host, string userName, string password) + { + _sshClinet = new Renci.SshNet.SshClient(host, userName, password); + _sshClinet.Connect(); + _shellStream = _sshClinet.CreateShellStream("ShellStream", 512, 24, 512, 512, 1024); + } + + public void Close() + { + _sshClinet?.Disconnect(); + } + + public void Write(string data) + { + StreamWriter streamWriter = GetStreamWriter(); + streamWriter.Write(data + "\r"); + streamWriter.Flush(); + } + + StreamReader _streamReader = null; + public StreamReader GetStreamReader() + { + if (_streamReader != null) + { + return _streamReader; + } + else + { + if (_shellStream == null) + { + return null; + } + _streamReader = new StreamReader(_shellStream); + return _streamReader; + } + } + + StreamWriter _streamWriter = null; + public StreamWriter GetStreamWriter() + { + if (_streamWriter != null) + { + return _streamWriter; + } + else + { + if (_shellStream == null) + { + return null; + } + _streamWriter = new StreamWriter(_shellStream); + return _streamWriter; + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/SshStreamFactory.cs b/src/Renci.SshNet.IntegrationTests/Issue67/SshStreamFactory.cs new file mode 100644 index 000000000..e8efa56d8 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/SshStreamFactory.cs @@ -0,0 +1,20 @@ +namespace Renci.SshNet.IntegrationTests.Issue67 +{ + internal static class SshStreamFactory + { + public static ISshStream CreateSshStream(string sshStreamType) + { + switch (sshStreamType) + { +#if NETFRAMEWORK + case "sharpssh": + return new SharpSshStream(); +#endif // NETFRAMEWORK + case "sshnet": + return new SshNetStream(); + default: + throw new Exception("Invalid SshStream type:" + sshStreamType); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamReader.cs b/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamReader.cs new file mode 100644 index 000000000..1a9414b86 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamReader.cs @@ -0,0 +1,168 @@ +namespace Renci.SshNet.IntegrationTests.Issue67 +{ + class UnblockStreamReaderLockObject + { + public char[] buffer; + public int len; + public int pos; + }; + + public class UnblockStreamReader + { + const int BUFFER_SIZE = 65536; + readonly Thread _readThread; + private readonly UnblockStreamReaderLockObject _lockObject; + public int GetUnreadBufferLength() + { + return _lockObject.len; + } + public int GetUnreadBufferPosition() + { + return _lockObject.pos; + } + readonly StreamReader _streamReader; + + public UnblockStreamReader(StreamReader streamReader) + { + _lockObject = new UnblockStreamReaderLockObject + { + buffer = new char[BUFFER_SIZE + 1], + len = 0, + pos = 0 + }; + + _streamReader = streamReader; + _readThread = new Thread(ReadThreadProc) + { + Name = "UnblockStreamReader thread" + }; + _readThread.Start(); + } + + public void Close() + { + _readThread.Interrupt(); + lock (_lockObject) + { + _lockObject.len = 0; + _lockObject.pos = 0; + } + } + + private void ReadThreadProc(object param) + { + char[] buf = new char[1]; + int readLen = 0; + bool isSleep = false; + try + { + while (true) + { + lock (_lockObject) + { + if (_lockObject.len >= BUFFER_SIZE) + { + isSleep = true; + } + } + if (isSleep) + { + isSleep = false; + Thread.Sleep(10); + continue; + } + readLen = _streamReader.Read(buf, 0, 1); + if (readLen > 0) + { + lock (_lockObject) + { + if ((_lockObject.pos + _lockObject.len) >= BUFFER_SIZE) + { + for (int i = 0; i < _lockObject.len; i++) + { + _lockObject.buffer[i] = _lockObject.buffer[_lockObject.pos + i]; + } + _lockObject.pos = 0; + } + + _lockObject.buffer[_lockObject.pos + _lockObject.len] = buf[0]; + + _lockObject.len++; + } + } + else + { + Thread.Sleep(10); + } + } + } + catch (Exception e) + { + e.ToString(); + return; + } + } + + public int ReadChar(ref char buf) + { + lock (_lockObject) + { + if (_lockObject.len == 0) + { + return 0; + } + buf = _lockObject.buffer[_lockObject.pos]; + _lockObject.pos++; + _lockObject.len--; + } + return 1; + } + + public string ReadToEnd(bool isRemove = true) + { + string resultString; + lock (_lockObject) + { + if (_lockObject.len == 0) + { + return null; + } + resultString = new string(_lockObject.buffer, _lockObject.pos, _lockObject.len); + if (isRemove) + { + _lockObject.pos = 0; + _lockObject.len = 0; + } + return resultString; + } + } + + public string ReadLine(char lineEndFlag = '\n') + { + string resultString; + while (true) + { + lock (_lockObject) + { + if (_lockObject.len == 0) + { + Thread.Sleep(10); + continue; + } + + for (int i = 0; i < _lockObject.len; i++) + { + if (_lockObject.buffer[_lockObject.pos + i] == lineEndFlag) + { + resultString = new string(_lockObject.buffer, _lockObject.pos, i + 1); + _lockObject.pos = _lockObject.pos + i + 1; + _lockObject.len = _lockObject.len - i - 1; + return resultString; + } + } + } + Thread.Sleep(10); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamUtility.cs b/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamUtility.cs new file mode 100644 index 000000000..6dd99c1fe --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamUtility.cs @@ -0,0 +1,103 @@ +using System.Diagnostics; + +namespace Renci.SshNet.IntegrationTests.Issue67 +{ + public class UnblockStreamUtility + { + internal static string[] ReadUntil(UnblockStreamReader reader, List untilInfoList, int noResponseTimeoutSeconds) + { + List resultList = new List(); + char[] buffer = new char[65536]; + int curBufferLen = 0; + int noResponseTimeoutMilliseconds = noResponseTimeoutSeconds * 1000; + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + while (true) + { + char readChar = new char(); + int readCharLen = reader.ReadChar(ref readChar); + if (readCharLen == 0) + { + Thread.Sleep(10); + if (stopwatch.ElapsedMilliseconds >= noResponseTimeoutMilliseconds) + { + stopwatch.Stop(); + throw new Exception("No Response Timeout!"); + } + continue; + } + else { stopwatch.Restart(); } + buffer[curBufferLen] = readChar; + + foreach (UntilInfo untilInfo in untilInfoList) + { + if (readChar == untilInfo.UntilCharArray[untilInfo.CompareLen]) + { + untilInfo.CompareLen++; + if (untilInfo.CompareLen == untilInfo.UntilCharArray.Length) + { + untilInfo.CompareLen = 0; + string lineStr = new string(buffer, 0, curBufferLen + 1); + if (lineStr.EndsWith("\r\r\n")) + { + lineStr = lineStr.Substring(0, lineStr.Length - 3); + } + else if (lineStr.EndsWith("\r\n")) + { + lineStr = lineStr.Substring(0, lineStr.Length - 2); + } + else if (lineStr.EndsWith("\n")) + { + lineStr = lineStr.Substring(0, lineStr.Length - 1); + } + + resultList.Add(lineStr); + curBufferLen = 0; + + if (untilInfo.ExceptionMessage != null) + { + throw new Exception(untilInfo.ExceptionMessage); + } + else + { + return resultList.ToArray(); + } + } + } + else + { + untilInfo.CompareLen = 0; + } + } + + if (readChar == '\n') + { + string lineStr = new string(buffer, 0, curBufferLen + 1); + + if (lineStr.EndsWith("\r\r\n")) + { + lineStr = lineStr.Substring(0, lineStr.Length - 3); + } + else if (lineStr.EndsWith("\r\n")) + { + lineStr = lineStr.Substring(0, lineStr.Length - 2); + } + else if (lineStr.EndsWith("\n")) + { + lineStr = lineStr.Substring(0, lineStr.Length - 1); + } + + resultList.Add(lineStr); + + curBufferLen = 0; + foreach (UntilInfo untilInfo in untilInfoList) + { + untilInfo.CompareLen = 0; + } + continue; + } + curBufferLen++; + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/UntilInfo.cs b/src/Renci.SshNet.IntegrationTests/Issue67/UntilInfo.cs new file mode 100644 index 000000000..696679d11 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Issue67/UntilInfo.cs @@ -0,0 +1,37 @@ +namespace Renci.SshNet.IntegrationTests.Issue67 +{ + public class UntilInfo + { + public UntilInfo(string untilString, string exceptionMessage = null) + { + UntilString = untilString; + ExceptionMessage = exceptionMessage; + UntilCharArray = untilString.ToCharArray(); + CompareLen = 0; + } + + public string UntilString + { + get; + private set; + } + + public string ExceptionMessage + { + get; + private set; + } + + public char[] UntilCharArray + { + get; + private set; + } + + public int CompareLen + { + get; + set; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/KeyExchangeAlgorithmTests.cs b/src/Renci.SshNet.IntegrationTests/KeyExchangeAlgorithmTests.cs new file mode 100644 index 000000000..dfd6b6394 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/KeyExchangeAlgorithmTests.cs @@ -0,0 +1,206 @@ +using Renci.SshNet.IntegrationTests.Common; +using Renci.SshNet.TestTools.OpenSSH; + +namespace Renci.SshNet.IntegrationTests +{ + [TestClass] + public class KeyExchangeAlgorithmTests : IntegrationTestBase + { + private IConnectionInfoFactory _connectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort); + _remoteSshdConfig = new RemoteSshd(new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort)).OpenConfig(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + } + + [TestMethod] + public void Curve25519Sha256() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.Curve25519Sha256) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void Curve25519Sha256Libssh() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.Curve25519Sha256Libssh) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void DiffieHellmanGroup1Sha1() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.DiffieHellmanGroup1Sha1) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void DiffieHellmanGroup14Sha1() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.DiffieHellmanGroup14Sha1) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void DiffieHellmanGroup14Sha256() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.DiffieHellmanGroup14Sha256) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void DiffieHellmanGroup16Sha512() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.DiffieHellmanGroup16Sha512) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + [Ignore] + public void DiffieHellmanGroup18Sha512() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.DiffieHellmanGroup18Sha512) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void DiffieHellmanGroupExchangeSha1() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.DiffieHellmanGroupExchangeSha1) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void DiffieHellmanGroupExchangeSha256() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.DiffieHellmanGroupExchangeSha256) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void EcdhSha2Nistp256() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.EcdhSha2Nistp256) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void EcdhSha2Nistp384() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.EcdhSha2Nistp384) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void EcdhSha2Nistp521() + { + _remoteSshdConfig.ClearKeyExchangeAlgorithms() + .AddKeyExchangeAlgorithm(KeyExchangeAlgorithm.EcdhSha2Nistp521) + .Update() + .Restart(); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/LinuxAdminConnectionFactory.cs b/src/Renci.SshNet.IntegrationTests/LinuxAdminConnectionFactory.cs new file mode 100644 index 000000000..dfb5c1369 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/LinuxAdminConnectionFactory.cs @@ -0,0 +1,36 @@ +namespace Renci.SshNet.IntegrationTests +{ + public class LinuxAdminConnectionFactory : IConnectionInfoFactory + { + private readonly string _host; + private readonly int _port; + + public LinuxAdminConnectionFactory(string sshServerHostName, ushort sshServerPort) + { + _host = sshServerHostName; + _port = sshServerPort; + } + + public ConnectionInfo Create() + { + var user = Users.Admin; + return new ConnectionInfo(_host, _port, user.UserName, new PasswordAuthenticationMethod(user.UserName, user.Password)); + } + + public ConnectionInfo Create(params AuthenticationMethod[] authenticationMethods) + { + throw new NotImplementedException(); + } + + public ConnectionInfo CreateWithProxy() + { + throw new NotImplementedException(); + } + + public ConnectionInfo CreateWithProxy(params AuthenticationMethod[] authenticationMethods) + { + throw new NotImplementedException(); + } + } +} + diff --git a/src/Renci.SshNet.IntegrationTests/LinuxVMConnectionFactory.cs b/src/Renci.SshNet.IntegrationTests/LinuxVMConnectionFactory.cs new file mode 100644 index 000000000..f2f04dbfc --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/LinuxVMConnectionFactory.cs @@ -0,0 +1,62 @@ +namespace Renci.SshNet.IntegrationTests +{ + public class LinuxVMConnectionFactory : IConnectionInfoFactory + { + + + private const string ProxyHost = "127.0.0.1"; + private const int ProxyPort = 1234; + private const string ProxyUserName = "test"; + private const string ProxyPassword = "123"; + private readonly string _host; + private readonly int _port; + private readonly AuthenticationMethodFactory _authenticationMethodFactory; + + + public LinuxVMConnectionFactory(string sshServerHostName, ushort sshServerPort) + { + _host = sshServerHostName; + _port = sshServerPort; + + _authenticationMethodFactory = new AuthenticationMethodFactory(); + } + + public LinuxVMConnectionFactory(string sshServerHostName, ushort sshServerPort, AuthenticationMethodFactory authenticationMethodFactory) + { + _host = sshServerHostName; + _port = sshServerPort; + + _authenticationMethodFactory = authenticationMethodFactory; + } + + public ConnectionInfo Create() + { + return Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod()); + } + + public ConnectionInfo Create(params AuthenticationMethod[] authenticationMethods) + { + return new ConnectionInfo(_host, _port, Users.Regular.UserName, authenticationMethods); + } + + public ConnectionInfo CreateWithProxy() + { + return CreateWithProxy(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod()); + } + + public ConnectionInfo CreateWithProxy(params AuthenticationMethod[] authenticationMethods) + { + return new ConnectionInfo( + _host, + _port, + Users.Regular.UserName, + ProxyTypes.Socks4, + ProxyHost, + ProxyPort, + ProxyUserName, + ProxyPassword, + authenticationMethods); + } + } +} + diff --git a/src/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs b/src/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs new file mode 100644 index 000000000..83313f4a8 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/PrivateKeyAuthenticationTests.cs @@ -0,0 +1,84 @@ +using Renci.SshNet.IntegrationTests.Common; +using Renci.SshNet.TestTools.OpenSSH; + +namespace Renci.SshNet.IntegrationTests +{ + [TestClass] + public class PrivateKeyAuthenticationTests : TestBase + { + private IConnectionInfoFactory _connectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort); + _remoteSshdConfig = new RemoteSshd(new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort)).OpenConfig(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + } + + [TestMethod] + public void Ecdsa256() + { + _remoteSshdConfig.AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm.EcdsaSha2Nistp256) + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod("key_ecdsa_256_openssh")); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void Ecdsa384() + { + _remoteSshdConfig.AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm.EcdsaSha2Nistp384) + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod("key_ecdsa_384_openssh")); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void EcdsaA521() + { + _remoteSshdConfig.AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm.EcdsaSha2Nistp521) + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(CreatePrivateKeyAuthenticationMethod("key_ecdsa_521_openssh")); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + } + } + + private PrivateKeyAuthenticationMethod CreatePrivateKeyAuthenticationMethod(string keyResource) + { + var privateKey = CreatePrivateKeyFromManifestResource("Renci.SshNet.IntegrationTests.resources.client." + keyResource); + return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKey); + } + + private PrivateKeyFile CreatePrivateKeyFromManifestResource(string resourceName) + { + using (var stream = GetManifestResourceStream(resourceName)) + { + return new PrivateKeyFile(stream); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Program.cs b/src/Renci.SshNet.IntegrationTests/Program.cs new file mode 100644 index 000000000..af2b60d51 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Program.cs @@ -0,0 +1,11 @@ +namespace Renci.SshNet.IntegrationTests +{ + class Program + { +#if NETFRAMEWORK + private static void Main() + { + } +#endif + } +} diff --git a/src/Renci.SshNet.IntegrationTests/RemoteSshd.cs b/src/Renci.SshNet.IntegrationTests/RemoteSshd.cs new file mode 100644 index 000000000..18dc9aefd --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/RemoteSshd.cs @@ -0,0 +1,246 @@ +using Renci.SshNet.TestTools.OpenSSH; + +namespace Renci.SshNet.IntegrationTests +{ + internal class RemoteSshd + { + private readonly IConnectionInfoFactory _connectionInfoFactory; + + public RemoteSshd(IConnectionInfoFactory connectionInfoFactory) + { + _connectionInfoFactory = connectionInfoFactory; + } + + public RemoteSshdConfig OpenConfig() + { + return new RemoteSshdConfig(this, _connectionInfoFactory); + } + + public RemoteSshd Restart() + { + // Restart SSH daemon + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + // Kill all processes that start with 'sshd' and that run as root + var stopCommand = client.CreateCommand("sudo pkill -9 -U 0 sshd.pam"); + var stopOutput = stopCommand.Execute(); + if (stopCommand.ExitStatus != 0) + { + throw new ApplicationException($"Stopping ssh service failed with exit code {stopCommand.ExitStatus}.\r\n{stopOutput}"); + } + + var resetFailedCommand = client.CreateCommand("sudo /usr/sbin/sshd.pam"); + var resetFailedOutput = resetFailedCommand.Execute(); + if (resetFailedCommand.ExitStatus != 0) + { + throw new ApplicationException($"Reset failures for ssh service failed with exit code {resetFailedCommand.ExitStatus}.\r\n{resetFailedOutput}"); + } + } + + return this; + } + } + + internal class RemoteSshdConfig + { + private const string SshdConfigFilePath = "/etc/ssh/sshd_config"; + private static readonly Encoding Utf8NoBom = new UTF8Encoding(false, true); + + private readonly RemoteSshd _remoteSshd; + private readonly IConnectionInfoFactory _connectionInfoFactory; + private readonly SshdConfig _config; + + public RemoteSshdConfig(RemoteSshd remoteSshd, IConnectionInfoFactory connectionInfoFactory) + { + _remoteSshd = remoteSshd; + _connectionInfoFactory = connectionInfoFactory; + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var memoryStream = new MemoryStream()) + { + client.Download(SshdConfigFilePath, memoryStream); + + memoryStream.Position = 0; + _config = SshdConfig.LoadFrom(memoryStream, Encoding.UTF8); + } + } + } + + /// + /// Specifies whether challenge-response authentication is allowed. + /// + /// to allow challenge-response authentication. + /// + /// The current instance. + /// + public RemoteSshdConfig WithChallengeResponseAuthentication(bool? value) + { + _config.ChallengeResponseAuthentication = value; + return this; + } + + /// + /// Specifies whether to allow keyboard-interactive authentication. + /// + /// to allow keyboard-interactive authentication. + /// + /// The current instance. + /// + public RemoteSshdConfig WithKeyboardInteractiveAuthentication(bool value) + { + _config.KeyboardInteractiveAuthentication = value; + return this; + } + + /// + /// Specifies whether sshd should print /etc/motd when a user logs in interactively. + /// + /// if sshd should print /etc/motd when a user logs in interactively. + /// + /// The current instance. + /// + public RemoteSshdConfig PrintMotd(bool? value = true) + { + _config.PrintMotd = value; + return this; + } + + /// + /// Specifies whether TCP forwarding is permitted. + /// + /// to allow TCP forwarding. + /// + /// The current instance. + /// + public RemoteSshdConfig AllowTcpForwarding(bool? value = true) + { + _config.AllowTcpForwarding = value; + return this; + } + + public RemoteSshdConfig WithAuthenticationMethods(string user, string authenticationMethods) + { + var sshNetMatch = _config.Matches.FirstOrDefault(m => m.Users.Contains(user)); + if (sshNetMatch == null) + { + sshNetMatch = new Match(new[] { user }, new string[0]); + _config.Matches.Add(sshNetMatch); + } + + sshNetMatch.AuthenticationMethods = authenticationMethods; + + return this; + } + + public RemoteSshdConfig ClearCiphers() + { + _config.Ciphers.Clear(); + return this; + } + + public RemoteSshdConfig AddCipher(Cipher cipher) + { + _config.Ciphers.Add(cipher); + return this; + } + + public RemoteSshdConfig ClearKeyExchangeAlgorithms() + { + _config.KeyExchangeAlgorithms.Clear(); + return this; + } + + public RemoteSshdConfig AddKeyExchangeAlgorithm(KeyExchangeAlgorithm keyExchangeAlgorithm) + { + _config.KeyExchangeAlgorithms.Add(keyExchangeAlgorithm); + return this; + } + + public RemoteSshdConfig ClearPublicKeyAcceptedAlgorithms() + { + _config.PublicKeyAcceptedAlgorithms.Clear(); + return this; + } + + public RemoteSshdConfig AddPublicKeyAcceptedAlgorithms(PublicKeyAlgorithm publicKeyAlgorithm) + { + _config.PublicKeyAcceptedAlgorithms.Add(publicKeyAlgorithm); + return this; + } + + public RemoteSshdConfig ClearHostKeyAlgorithms() + { + _config.HostKeyAlgorithms.Clear(); + return this; + } + + public RemoteSshdConfig AddHostKeyAlgorithm(HostKeyAlgorithm hostKeyAlgorithm) + { + _config.HostKeyAlgorithms.Add(hostKeyAlgorithm); + return this; + } + + public RemoteSshdConfig ClearSubsystems() + { + _config.Subsystems.Clear(); + return this; + } + + public RemoteSshdConfig AddSubsystem(Subsystem subsystem) + { + _config.Subsystems.Add(subsystem); + return this; + } + + public RemoteSshdConfig WithLogLevel(LogLevel logLevel) + { + _config.LogLevel = logLevel; + return this; + } + + public RemoteSshdConfig WithUsePAM(bool usePAM) + { + _config.UsePAM = usePAM; + return this; + } + + public RemoteSshdConfig ClearHostKeyFiles() + { + _config.HostKeyFiles.Clear(); + return this; + } + + public RemoteSshdConfig AddHostKeyFile(string hostKeyFile) + { + _config.HostKeyFiles.Add(hostKeyFile); + return this; + } + + public RemoteSshd Update() + { + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var memoryStream = new MemoryStream()) + using (var sw = new StreamWriter(memoryStream, Utf8NoBom)) + { + sw.NewLine = "\n"; + _config.SaveTo(sw); + sw.Flush(); + + memoryStream.Position = 0; + + client.Upload(memoryStream, SshdConfigFilePath); + } + } + + return _remoteSshd; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj index 2fdbcfd74..7fce16c58 100644 --- a/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj +++ b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj @@ -3,7 +3,6 @@ net7.0 enable - enable false true @@ -16,16 +15,21 @@ We can stop producing XML docs for test projects (and remove the NoWarn for CS1591) once the following issue is fixed: https://github.com/dotnet/roslyn/issues/41640. --> - $(NoWarn);CS1591 + $(NoWarn);CS1591;SYSLIB0021;SYSLIB1045;SYSLIB0014;IDE0220;IDE0010 + + TRACE;FEATURE_MSTEST_DATATEST;FEATURE_SOCKET_EAP;FEATURE_ENCODING_ASCII;FEATURE_THREAD_SLEEP;FEATURE_THREAD_THREADPOOL + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -44,7 +48,17 @@ + - + + + + + + + + + + diff --git a/src/Renci.SshNet.IntegrationTests/ScpTests.cs b/src/Renci.SshNet.IntegrationTests/ScpTests.cs new file mode 100644 index 000000000..0cb4ab7bd --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/ScpTests.cs @@ -0,0 +1,2380 @@ +using Renci.SshNet.Common; +using Renci.SshNet.IntegrationTests.Common; + +namespace Renci.SshNet.IntegrationTests +{ + // TODO SCP: UPLOAD / DOWNLOAD ZERO LENGTH FILES + // TODO SCP: UPLOAD / DOWNLOAD EMPTY DIRECTORY + // TODO SCP: UPLOAD DIRECTORY THAT ALREADY EXISTS ON REMOTE HOST + + [TestClass] + public class ScpTests : TestBase + { + private IConnectionInfoFactory _connectionInfoFactory; + private IRemotePathTransformation _remotePathTransformation; + + [TestInitialize] + public void SetUp() + { + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort); + _remotePathTransformation = RemotePathTransformation.ShellQuote; + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadStreamDirectoryDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_Stream_DirectoryDoesNotExist() + { + foreach (var data in GetScpDownloadStreamDirectoryDoesNotExistData()) + { + Scp_Download_Stream_DirectoryDoesNotExist((IRemotePathTransformation) data[0], + (string) data[1], + (string) data[2]); + } + } +#endif + public void Scp_Download_Stream_DirectoryDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + // remove complete directory if it's not the home directory of the user + // or else remove the remote file + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + + try + { + using (var downloaded = new MemoryStream()) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Download(completeRemotePath, downloaded); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {completeRemotePath}: No such file or directory", ex.Message); + } + } + } + finally + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadStreamFileDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_Stream_FileDoesNotExist() + { + foreach (var data in GetScpDownloadStreamFileDoesNotExistData()) + { + Scp_Download_Stream_FileDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2]); + } + } +#endif + public void Scp_Download_Stream_FileDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + // remove complete directory if it's not the home directory of the user + // or else remove the remote file + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + + client.CreateDirectory(remotePath); + } + } + + try + { + using (var downloaded = new MemoryStream()) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Download(completeRemotePath, downloaded); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {completeRemotePath}: No such file or directory", ex.Message); + } + } + } + finally + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadDirectoryInfoDirectoryDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_DirectoryInfo_DirectoryDoesNotExist() + { + foreach (var data in GetScpDownloadDirectoryInfoDirectoryDoesNotExistData()) + { + Scp_Download_DirectoryInfo_DirectoryDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Download_DirectoryInfo_DirectoryDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath) + { + var localDirectory = Path.GetTempFileName(); + File.Delete(localDirectory); + Directory.CreateDirectory(localDirectory); + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Download(remotePath, new DirectoryInfo(localDirectory)); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remotePath}: No such file or directory", ex.Message); + } + } + } + finally + { + Directory.Delete(localDirectory, true); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadDirectoryInfoExistingFileData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_DirectoryInfo_ExistingFile() + { + foreach (var data in GetScpDownloadDirectoryInfoExistingFileData()) + { + Scp_Download_DirectoryInfo_ExistingFile((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Download_DirectoryInfo_ExistingFile(IRemotePathTransformation remotePathTransformation, + string remotePath) + { + var content = CreateMemoryStream(100); + content.Position = 0; + + var localDirectory = Path.GetTempFileName(); + File.Delete(localDirectory); + Directory.CreateDirectory(localDirectory); + + var localFile = Path.Combine(localDirectory, PosixPath.GetFileName(remotePath)); + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.UploadFile(content, remotePath); + } + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Download(remotePath, new DirectoryInfo(localDirectory)); + } + + Assert.IsTrue(File.Exists(localFile)); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remotePath, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateFileHash(localFile), CreateHash(downloaded)); + } + } + } + finally + { + Directory.Delete(localDirectory, true); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remotePath)) + { + client.DeleteFile(remotePath); + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadDirectoryInfoExistingDirectoryData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_DirectoryInfo_ExistingDirectory() + { + foreach (var data in GetScpDownloadDirectoryInfoExistingDirectoryData()) + { + Scp_Download_DirectoryInfo_ExistingDirectory((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Download_DirectoryInfo_ExistingDirectory(IRemotePathTransformation remotePathTransformation, + string remotePath) + { + var localDirectory = Path.GetTempFileName(); + File.Delete(localDirectory); + Directory.CreateDirectory(localDirectory); + + var localPathFile1 = Path.Combine(localDirectory, "file1 23"); + var remotePathFile1 = CombinePaths(remotePath, "file1 23"); + var contentFile1 = CreateMemoryStream(1024); + contentFile1.Position = 0; + + var localPathFile2 = Path.Combine(localDirectory, "file2 #$%"); + var remotePathFile2 = CombinePaths(remotePath, "file2 #$%"); + var contentFile2 = CreateMemoryStream(2048); + contentFile2.Position = 0; + + var localPathSubDirectory = Path.Combine(localDirectory, "subdir $1%#"); + var remotePathSubDirectory = CombinePaths(remotePath, "subdir $1%#"); + + var localPathFile3 = Path.Combine(localPathSubDirectory, "file3 %$#"); + var remotePathFile3 = CombinePaths(remotePathSubDirectory, "file3 %$#"); + var contentFile3 = CreateMemoryStream(256); + contentFile3.Position = 0; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remotePathFile1)) + { + client.DeleteFile(remotePathFile1); + } + + if (client.Exists(remotePathFile2)) + { + client.DeleteFile(remotePathFile2); + } + + if (client.Exists(remotePathFile3)) + { + client.DeleteFile(remotePathFile3); + } + + if (client.Exists(remotePathSubDirectory)) + { + client.DeleteDirectory(remotePathSubDirectory); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (!client.Exists(remotePath)) + { + client.CreateDirectory(remotePath); + } + + client.UploadFile(contentFile1, remotePathFile1); + client.UploadFile(contentFile1, remotePathFile2); + + client.CreateDirectory(remotePathSubDirectory); + client.UploadFile(contentFile3, remotePathFile3); + } + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Download(remotePath, new DirectoryInfo(localDirectory)); + } + + var localFiles = Directory.GetFiles(localDirectory); + Assert.AreEqual(2, localFiles.Length); + Assert.IsTrue(localFiles.Contains(localPathFile1)); + Assert.IsTrue(localFiles.Contains(localPathFile2)); + + var localSubDirecties = Directory.GetDirectories(localDirectory); + Assert.AreEqual(1, localSubDirecties.Length); + Assert.AreEqual(localPathSubDirectory, localSubDirecties[0]); + + var localFilesSubDirectory = Directory.GetFiles(localPathSubDirectory); + Assert.AreEqual(1, localFilesSubDirectory.Length); + Assert.AreEqual(localPathFile3, localFilesSubDirectory[0]); + + Assert.AreEqual(0, Directory.GetDirectories(localPathSubDirectory).Length); + } + finally + { + Directory.Delete(localDirectory, true); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remotePathFile1)) + { + client.DeleteFile(remotePathFile1); + } + + if (client.Exists(remotePathFile2)) + { + client.DeleteFile(remotePathFile2); + } + + if (client.Exists(remotePathFile3)) + { + client.DeleteFile(remotePathFile3); + } + + if (client.Exists(remotePathSubDirectory)) + { + client.DeleteDirectory(remotePathSubDirectory); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadFileInfoDirectoryDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_FileInfo_DirectoryDoesNotExist() + { + foreach (var data in GetScpDownloadFileInfoDirectoryDoesNotExistData()) + { + Scp_Download_FileInfo_DirectoryDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2]); + } + } +#endif + public void Scp_Download_FileInfo_DirectoryDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + // remove complete directory if it's not the home directory of the user + // or else remove the remote file + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + + var fileInfo = new FileInfo(Path.GetTempFileName()); + + try + { + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Download(completeRemotePath, fileInfo); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {completeRemotePath}: No such file or directory", ex.Message); + } + } + } + finally + { + fileInfo.Delete(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadFileInfoFileDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_FileInfo_FileDoesNotExist() + { + foreach (var data in GetScpDownloadFileInfoFileDoesNotExistData()) + { + Scp_Download_FileInfo_FileDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2]); + } + } +#endif + public void Scp_Download_FileInfo_FileDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + // remove complete directory if it's not the home directory of the user + // or else remove the remote file + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + + client.CreateDirectory(remotePath); + } + } + + var fileInfo = new FileInfo(Path.GetTempFileName()); + + try + { + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Download(completeRemotePath, fileInfo); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {completeRemotePath}: No such file or directory", ex.Message); + } + } + } + finally + { + fileInfo.Delete(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadFileInfoExistingDirectoryData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_FileInfo_ExistingDirectory() + { + foreach (var data in GetScpDownloadFileInfoExistingDirectoryData()) + { + Scp_Download_FileInfo_ExistingDirectory((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Download_FileInfo_ExistingDirectory(IRemotePathTransformation remotePathTransformation, + string remotePath) + { + // remove complete directory if it's not the home directory of the user + // or else remove the remote file + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + + client.CreateDirectory(remotePath); + } + } + + var fileInfo = new FileInfo(Path.GetTempFileName()); + fileInfo.Delete(); + + try + { + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Download(remotePath, fileInfo); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remotePath}: not a regular file", ex.Message); + } + + Assert.IsFalse(fileInfo.Exists); + } + } + finally + { + fileInfo.Delete(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadFileInfoExistingFileData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_FileInfo_ExistingFile() + { + foreach (var data in GetScpDownloadFileInfoExistingFileData()) + { + Scp_Download_FileInfo_ExistingFile((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2], + (int)data[3]); + } + } +#endif + public void Scp_Download_FileInfo_ExistingFile(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile, + int size) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + // remove complete directory if it's not the home directory of the user + // or else remove the remote file + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + + client.CreateDirectory(remotePath); + } + } + + var fileInfo = new FileInfo(Path.GetTempFileName()); + + try + { + var content = CreateMemoryStream(size); + content.Position = 0; + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Upload(content, completeRemotePath); + } + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Download(completeRemotePath, fileInfo); + } + + using (var fs = fileInfo.OpenRead()) + { + var downloadedBytes = new byte[fs.Length]; + Assert.AreEqual(downloadedBytes.Length, fs.Read(downloadedBytes, 0, downloadedBytes.Length)); + content.Position = 0; + Assert.AreEqual(CreateHash(content), CreateHash(downloadedBytes)); + } + } + finally + { + fileInfo.Delete(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadStreamExistingDirectoryData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_Stream_ExistingDirectory() + { + foreach (var data in GetScpDownloadStreamExistingDirectoryData()) + { + Scp_Download_Stream_ExistingDirectory((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Download_Stream_ExistingDirectory(IRemotePathTransformation remotePathTransformation, + string remotePath) + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + + client.CreateDirectory(remotePath); + } + } + + var file = Path.GetTempFileName(); + File.Delete(file); + + try + { + using (var fs = File.OpenWrite(file)) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Download(remotePath, fs); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remotePath}: not a regular file", ex.Message); + } + + Assert.AreEqual(0, fs.Length); + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpDownloadStreamExistingFileData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Download_Stream_ExistingFile() + { + foreach (var data in GetScpDownloadStreamExistingFileData()) + { + Scp_Download_Stream_ExistingFile((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2], + (int)data[3]); + } + } +#endif + public void Scp_Download_Stream_ExistingFile(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile, + int size) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + // remove complete directory if it's not the home directory of the user + // or else remove the remote file + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + + client.CreateDirectory(remotePath); + } + } + + var file = CreateTempFile(size); + + try + { + using (var fs = File.OpenRead(file)) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Upload(fs, completeRemotePath); + } + + using (var fs = File.OpenRead(file)) + using (var downloaded = new MemoryStream(size)) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + client.Download(completeRemotePath, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateHash(fs), CreateHash(downloaded)); + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadFileStreamDirectoryDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_FileStream_DirectoryDoesNotExist() + { + foreach (var data in GetScpUploadFileStreamDirectoryDoesNotExistData()) + { + Scp_Upload_FileStream_DirectoryDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2]); + } + } +#endif + public void Scp_Upload_FileStream_DirectoryDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + + var file = CreateTempFile(1000); + + try + { + using (var fs = File.OpenRead(file)) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Upload(fs, completeRemotePath); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remotePath}: No such file or directory", ex.Message); + } + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadFileStreamExistingDirectoryData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_FileStream_ExistingDirectory() + { + foreach (var data in GetScpUploadFileStreamExistingDirectoryData()) + { + Scp_Upload_FileStream_ExistingDirectory((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Upload_FileStream_ExistingDirectory(IRemotePathTransformation remotePathTransformation, + string remoteFile) + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteFile))) + { + command.Execute(); + } + } + + var file = CreateTempFile(1000); + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.CreateDirectory(remoteFile); + } + + using (var fs = File.OpenRead(file)) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Upload(fs, remoteFile); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remoteFile}: Is a directory", ex.Message); + } + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteDirectory(remoteFile); + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(ScpUploadFileStreamExistingFileData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_FileStream_ExistingFile() + { + foreach (var data in ScpUploadFileStreamExistingFileData()) + { + Scp_Upload_FileStream_ExistingFile((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Upload_FileStream_ExistingFile(IRemotePathTransformation remotePathTransformation, + string remoteFile) + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + + // original content is bigger than new content to ensure file is fully overwritten + var originalContent = CreateMemoryStream(2000); + var file = CreateTempFile(1000); + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + originalContent.Position = 0; + client.UploadFile(originalContent, remoteFile); + } + + using (var fs = File.OpenRead(file)) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Upload(fs, remoteFile); + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var downloaded = new MemoryStream(1000)) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateFileHash(file), CreateHash(downloaded)); + } + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadFileStreamFileDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_FileStream_FileDoesNotExist() + { + foreach (var data in GetScpUploadFileStreamFileDoesNotExistData()) + { + Scp_Upload_FileStream_FileDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2], + (int)data[3]); + } + } +#endif + public void Scp_Upload_FileStream_FileDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile, + int size) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + // remove complete directory if it's not the home directory of the user + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + + var file = CreateTempFile(size); + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + // create directory if it's not the home directory of the user + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (!client.Exists((remotePath))) + { + client.CreateDirectory(remotePath); + } + } + } + + using (var fs = File.OpenRead(file)) + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Upload(fs, completeRemotePath); + } + + using (var fs = File.OpenRead(file)) + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var sftpFile = client.Get(completeRemotePath); + Assert.AreEqual(GetAbsoluteRemotePath(client, remotePath, remoteFile), sftpFile.FullName); + Assert.AreEqual(size, sftpFile.Length); + + var downloaded = client.ReadAllBytes(completeRemotePath); + Assert.AreEqual(CreateHash(fs), CreateHash(downloaded)); + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + // remove complete directory if it's not the home directory of the user + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + + /// + /// https://github.com/sshnet/SSH.NET/issues/289 + /// +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadFileInfoDirectoryDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_FileInfo_DirectoryDoesNotExist() + { + foreach (var data in GetScpUploadFileInfoDirectoryDoesNotExistData()) + { + Scp_Upload_FileInfo_DirectoryDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2]); + } + } +#endif + public void Scp_Upload_FileInfo_DirectoryDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + + var file = CreateTempFile(1000); + + try + { + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Upload(new FileInfo(file), completeRemotePath); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remotePath}: No such file or directory", ex.Message); + } + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + Assert.IsFalse(client.Exists(completeRemotePath)); + Assert.IsFalse(client.Exists(remotePath)); + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + + /// + /// https://github.com/sshnet/SSH.NET/issues/286 + /// +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadFileInfoExistingDirectoryData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_FileInfo_ExistingDirectory() + { + foreach (var data in GetScpUploadFileInfoExistingDirectoryData()) + { + Scp_Upload_FileInfo_ExistingDirectory((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Upload_FileInfo_ExistingDirectory(IRemotePathTransformation remotePathTransformation, + string remoteFile) + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteFile))) + { + command.Execute(); + } + } + + var file = CreateTempFile(1000); + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.CreateDirectory(remoteFile); + } + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Upload(new FileInfo(file), remoteFile); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remoteFile}: Is a directory", ex.Message); + } + } + } + finally + { + File.Delete(file); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteFile))) + { + command.Execute(); + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadFileInfoExistingFileData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_FileInfo_ExistingFile() + { + foreach (var data in GetScpUploadFileInfoExistingFileData()) + { + Scp_Upload_FileInfo_ExistingFile((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Upload_FileInfo_ExistingFile(IRemotePathTransformation remotePathTransformation, + string remoteFile) + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + + // original content is bigger than new content to ensure file is fully overwritten + var originalContent = CreateMemoryStream(2000); + var file = CreateTempFile(1000); + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + originalContent.Position = 0; + client.UploadFile(originalContent, remoteFile); + } + + var fileInfo = new FileInfo(file) + { + LastAccessTimeUtc = new DateTime(1973, 8, 13, 20, 15, 33, DateTimeKind.Utc), + LastWriteTimeUtc = new DateTime(1974, 1, 24, 3, 55, 12, DateTimeKind.Utc) + }; + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Upload(fileInfo, remoteFile); + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var uploadedFile = client.Get(remoteFile); + Assert.AreEqual(fileInfo.LastAccessTimeUtc, uploadedFile.LastAccessTimeUtc); + Assert.AreEqual(fileInfo.LastWriteTimeUtc, uploadedFile.LastWriteTimeUtc); + + using (var downloaded = new MemoryStream(1000)) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateFileHash(file), CreateHash(downloaded)); + } + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadFileInfoFileDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_FileInfo_FileDoesNotExist() + { + foreach (var data in GetScpUploadFileInfoFileDoesNotExistData()) + { + Scp_Upload_FileInfo_FileDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1], + (string)data[2], + (int)data[3]); + } + } +#endif + public void Scp_Upload_FileInfo_FileDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remotePath, + string remoteFile, + int size) + { + var completeRemotePath = CombinePaths(remotePath, remoteFile); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.DeleteFile(completeRemotePath); + } + + // remove complete directory if it's not the home directory of the user + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + + var file = CreateTempFile(size); + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + // create directory if it's not the home directory of the user + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (!client.Exists(remotePath)) + { + client.CreateDirectory(remotePath); + } + } + } + + var fileInfo = new FileInfo(file) + { + LastAccessTimeUtc = new DateTime(1973, 8, 13, 20, 15, 33, DateTimeKind.Utc), + LastWriteTimeUtc = new DateTime(1974, 1, 24, 3, 55, 12, DateTimeKind.Utc) + }; + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Upload(fileInfo, completeRemotePath); + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var uploadedFile = client.Get(completeRemotePath); + Assert.AreEqual(fileInfo.LastAccessTimeUtc, uploadedFile.LastAccessTimeUtc); + Assert.AreEqual(fileInfo.LastWriteTimeUtc, uploadedFile.LastWriteTimeUtc); + Assert.AreEqual(size, uploadedFile.Length); + + using (var downloaded = new MemoryStream(size)) + { + client.DownloadFile(completeRemotePath, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateFileHash(file), CreateHash(downloaded)); + } + } + } + finally + { + File.Delete(file); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(completeRemotePath)) + { + client.Delete(completeRemotePath); + } + + // remove complete directory if it's not the home directory of the user + if (remotePath.Length > 0 && remotePath != client.WorkingDirectory) + { + if (client.Exists(remotePath)) + { + client.DeleteDirectory(remotePath); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadDirectoryInfoDirectoryDoesNotExistData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_DirectoryInfo_DirectoryDoesNotExist() + { + foreach (var data in GetScpUploadDirectoryInfoDirectoryDoesNotExistData()) + { + Scp_Upload_DirectoryInfo_DirectoryDoesNotExist((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Upload_DirectoryInfo_DirectoryDoesNotExist(IRemotePathTransformation remotePathTransformation, + string remoteDirectory) + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists((remoteDirectory))) + { + client.DeleteDirectory(remoteDirectory); + } + } + + var localDirectory = Path.GetTempFileName(); + File.Delete(localDirectory); + Directory.CreateDirectory(localDirectory); + + try + { + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + try + { + client.Upload(new DirectoryInfo(localDirectory), remoteDirectory); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remoteDirectory}: No such file or directory", ex.Message); + } + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + Assert.IsFalse(client.Exists(remoteDirectory)); + } + } + finally + { + Directory.Delete(localDirectory, true); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists((remoteDirectory))) + { + client.DeleteDirectory(remoteDirectory); + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadDirectoryInfoExistingDirectoryData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_DirectoryInfo_ExistingDirectory() + { + foreach (var data in GetScpUploadDirectoryInfoExistingDirectoryData()) + { + Scp_Upload_DirectoryInfo_ExistingDirectory((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Upload_DirectoryInfo_ExistingDirectory(IRemotePathTransformation remotePathTransformation, + string remoteDirectory) + { + string absoluteRemoteDirectory = GetAbsoluteRemotePath(_connectionInfoFactory, remoteDirectory); + + var remotePathFile1 = CombinePaths(remoteDirectory, "file1"); + var remotePathFile2 = CombinePaths(remoteDirectory, "file2"); + + var absoluteremoteSubDirectory1 = CombinePaths(absoluteRemoteDirectory, "sub1"); + var remoteSubDirectory1 = CombinePaths(remoteDirectory, "sub1"); + var remotePathSubFile1 = CombinePaths(remoteSubDirectory1, "file1"); + var remotePathSubFile2 = CombinePaths(remoteSubDirectory1, "file2"); + var absoluteremoteSubDirectory2 = CombinePaths(absoluteRemoteDirectory, "sub2"); + var remoteSubDirectory2 = CombinePaths(remoteDirectory, "sub2"); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remotePathSubFile1)) + { + client.DeleteFile(remotePathSubFile1); + } + if (client.Exists(remotePathSubFile2)) + { + client.DeleteFile(remotePathSubFile2); + } + if (client.Exists(remoteSubDirectory1)) + { + client.DeleteDirectory(remoteSubDirectory1); + } + if (client.Exists(remoteSubDirectory2)) + { + client.DeleteDirectory(remoteSubDirectory2); + } + if (client.Exists(remotePathFile1)) + { + client.DeleteFile(remotePathFile1); + } + if (client.Exists(remotePathFile2)) + { + client.DeleteFile(remotePathFile2); + } + + if (remoteDirectory.Length > 0 && remoteDirectory != "." && remoteDirectory != client.WorkingDirectory) + { + if (client.Exists(remoteDirectory)) + { + client.DeleteDirectory(remoteDirectory); + } + + client.CreateDirectory(remoteDirectory); + } + } + + var localDirectory = Path.GetTempFileName(); + File.Delete(localDirectory); + Directory.CreateDirectory(localDirectory); + + var localPathFile1 = Path.Combine(localDirectory, "file1"); + var localPathFile2 = Path.Combine(localDirectory, "file2"); + + var localSubDirectory1 = Path.Combine(localDirectory, "sub1"); + var localPathSubFile1 = Path.Combine(localSubDirectory1, "file1"); + var localPathSubFile2 = Path.Combine(localSubDirectory1, "file2"); + var localSubDirectory2 = Path.Combine(localDirectory, "sub2"); + + try + { + CreateFile(localPathFile1, 2000); + File.SetLastWriteTimeUtc(localPathFile1, new DateTime(2015, 8, 24, 5, 32, 16, DateTimeKind.Utc)); + + CreateFile(localPathFile2, 1000); + File.SetLastWriteTimeUtc(localPathFile2, new DateTime(2012, 3, 27, 23, 2, 54, DateTimeKind.Utc)); + + // create subdirectory with two files + Directory.CreateDirectory(localSubDirectory1); + CreateFile(localPathSubFile1, 1000); + File.SetLastWriteTimeUtc(localPathSubFile1, new DateTime(2013, 4, 12, 16, 54, 22, DateTimeKind.Utc)); + CreateFile(localPathSubFile2, 2000); + File.SetLastWriteTimeUtc(localPathSubFile2, new DateTime(2015, 8, 4, 12, 43, 12, DateTimeKind.Utc)); + Directory.SetLastWriteTimeUtc(localSubDirectory1, + new DateTime(2014, 6, 12, 13, 2, 44, DateTimeKind.Utc)); + + // create empty subdirectory + Directory.CreateDirectory(localSubDirectory2); + Directory.SetLastWriteTimeUtc(localSubDirectory2, + new DateTime(2011, 5, 14, 1, 5, 12, DateTimeKind.Utc)); + + Directory.SetLastWriteTimeUtc(localDirectory, new DateTime(2015, 10, 14, 22, 45, 11, DateTimeKind.Utc)); + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + client.Upload(new DirectoryInfo(localDirectory), remoteDirectory); + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + Assert.IsTrue(client.Exists(remoteDirectory)); + + var remoteSftpDirectory = client.Get(remoteDirectory); + Assert.IsNotNull(remoteSftpDirectory); + Assert.AreEqual(absoluteRemoteDirectory, remoteSftpDirectory.FullName); + Assert.IsTrue(remoteSftpDirectory.IsDirectory); + Assert.IsFalse(remoteSftpDirectory.IsRegularFile); + + // Due to CVE-2018-20685, we can no longer set the times or modes on a file or directory + // that refers to the current directory ('.'), the parent directory ('..') or a directory + // containing a forward slash ('/'). + Assert.AreNotEqual(Directory.GetLastWriteTimeUtc(localDirectory), remoteSftpDirectory.LastWriteTimeUtc); + + Assert.IsTrue(client.Exists(remotePathFile1)); + Assert.AreEqual(CreateFileHash(localPathFile1), CreateRemoteFileHash(client, remotePathFile1)); + var remoteSftpFile = client.Get(remotePathFile1); + Assert.IsNotNull(remoteSftpFile); + Assert.IsFalse(remoteSftpFile.IsDirectory); + Assert.IsTrue(remoteSftpFile.IsRegularFile); + Assert.AreEqual(File.GetLastWriteTimeUtc(localPathFile1), remoteSftpFile.LastWriteTimeUtc); + + Assert.IsTrue(client.Exists(remotePathFile2)); + Assert.AreEqual(CreateFileHash(localPathFile2), CreateRemoteFileHash(client, remotePathFile2)); + remoteSftpFile = client.Get(remotePathFile2); + Assert.IsNotNull(remoteSftpFile); + Assert.IsFalse(remoteSftpFile.IsDirectory); + Assert.IsTrue(remoteSftpFile.IsRegularFile); + Assert.AreEqual(File.GetLastWriteTimeUtc(localPathFile2), remoteSftpFile.LastWriteTimeUtc); + + Assert.IsTrue(client.Exists(remoteSubDirectory1)); + remoteSftpDirectory = client.Get(remoteSubDirectory1); + Assert.IsNotNull(remoteSftpDirectory); + Assert.AreEqual(absoluteremoteSubDirectory1, remoteSftpDirectory.FullName); + Assert.IsTrue(remoteSftpDirectory.IsDirectory); + Assert.IsFalse(remoteSftpDirectory.IsRegularFile); + Assert.AreEqual(Directory.GetLastWriteTimeUtc(localSubDirectory1), remoteSftpDirectory.LastWriteTimeUtc); + + Assert.IsTrue(client.Exists(remotePathSubFile1)); + Assert.AreEqual(CreateFileHash(localPathSubFile1), CreateRemoteFileHash(client, remotePathSubFile1)); + + Assert.IsTrue(client.Exists(remotePathSubFile2)); + Assert.AreEqual(CreateFileHash(localPathSubFile2), CreateRemoteFileHash(client, remotePathSubFile2)); + + Assert.IsTrue(client.Exists(remoteSubDirectory2)); + remoteSftpDirectory = client.Get(remoteSubDirectory2); + Assert.IsNotNull(remoteSftpDirectory); + Assert.AreEqual(absoluteremoteSubDirectory2, remoteSftpDirectory.FullName); + Assert.IsTrue(remoteSftpDirectory.IsDirectory); + Assert.IsFalse(remoteSftpDirectory.IsRegularFile); + Assert.AreEqual(Directory.GetLastWriteTimeUtc(localSubDirectory2), remoteSftpDirectory.LastWriteTimeUtc); + } + } + finally + { + Directory.Delete(localDirectory, true); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remotePathSubFile1)) + { + client.DeleteFile(remotePathSubFile1); + } + if (client.Exists(remotePathSubFile2)) + { + client.DeleteFile(remotePathSubFile2); + } + if (client.Exists(remoteSubDirectory1)) + { + client.DeleteDirectory(remoteSubDirectory1); + } + if (client.Exists(remoteSubDirectory2)) + { + client.DeleteDirectory(remoteSubDirectory2); + } + if (client.Exists(remotePathFile1)) + { + client.DeleteFile(remotePathFile1); + } + if (client.Exists(remotePathFile2)) + { + client.DeleteFile(remotePathFile2); + } + + if (remoteDirectory.Length > 0 && remoteDirectory != "." && remoteDirectory != client.WorkingDirectory) + { + if (client.Exists(remoteDirectory)) + { + client.DeleteDirectory(remoteDirectory); + } + } + } + } + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetScpUploadDirectoryInfoExistingFileData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Scp_Upload_DirectoryInfo_ExistingFile() + { + foreach (var data in GetScpUploadDirectoryInfoExistingFileData()) + { + Scp_Upload_DirectoryInfo_ExistingFile((IRemotePathTransformation)data[0], + (string)data[1]); + } + } +#endif + public void Scp_Upload_DirectoryInfo_ExistingFile(IRemotePathTransformation remotePathTransformation, + string remoteDirectory) + { + var remotePathFile1 = CombinePaths(remoteDirectory, "file1"); + var remotePathFile2 = CombinePaths(remoteDirectory, "file2"); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + Console.WriteLine(client.ConnectionInfo.CurrentKeyExchangeAlgorithm); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + + var localDirectory = Path.GetTempFileName(); + File.Delete(localDirectory); + Directory.CreateDirectory(localDirectory); + + var localPathFile1 = Path.Combine(localDirectory, "file1"); + var localPathFile2 = Path.Combine(localDirectory, "file2"); + + try + { + CreateFile(localPathFile1, 50); + CreateFile(localPathFile2, 50); + + using (var client = new ScpClient(_connectionInfoFactory.Create())) + { + if (remotePathTransformation != null) + { + client.RemotePathTransformation = remotePathTransformation; + } + + client.Connect(); + + CreateRemoteFile(client, remoteDirectory, 10); + + try + { + client.Upload(new DirectoryInfo(localDirectory), remoteDirectory); + Assert.Fail(); + } + catch (ScpException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual($"scp: {remoteDirectory}: Not a directory", ex.Message); + } + } + } + finally + { + Directory.Delete(localDirectory, true); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remotePathFile1)) + { + client.DeleteFile(remotePathFile1); + } + if (client.Exists(remotePathFile2)) + { + client.DeleteFile(remotePathFile2); + } + if (client.Exists((remoteDirectory))) + { + client.DeleteFile(remoteDirectory); + } + } + } + } + + private static IEnumerable GetScpDownloadStreamDirectoryDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-directorydoesnotexist", "scp-file" }; + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-directorydoesnotexist", "scp-file" }; + } + + private static IEnumerable GetScpUploadFileInfoFileDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet", "test123", 0 }; + yield return new object[] { RemotePathTransformation.None, "/home/sshnet", "test123", 5 * 1024 * 1024 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/dir|&;<>()$`\"'sp\u0100ce \\tab\tlf\n*?[#~=%", "file123", 1024 }; + yield return new object[] { null, "/home/sshnet/scp test", "file 123", 1024 }; + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-test", "file|&;<>()$`\"'sp\u0100ce \\tab\tlf*?[#~=%", 1024 }; + yield return new object[] { null, "", "scp-issue280", 1024 }; + } + + private static IEnumerable GetScpUploadFileStreamFileDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/dir|&;<>()$`\"'sp\u0100ce \\tab\tlf\n*?[#~=%", "file123", 0 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/dir|&;<>()$`\"'sp\u0100ce \\tab\tlf\n*?[#~=%", "file123", 1024 }; + yield return new object[] { null, "/home/sshnet/scp test", "file 123", 1024 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/scp-test", "file|&;<>()$`\"'sp\u0100ce \\tab\tlf*?[#~=%", 1024 }; + yield return new object[] { RemotePathTransformation.None, "", "scp-issue280", 1024 }; + } + + private static IEnumerable GetScpUploadDirectoryInfoExistingDirectoryData() + { + yield return new object[] { RemotePathTransformation.None, "scp-directorydoesnotexist" }; + yield return new object[] { RemotePathTransformation.None, "." }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/dir|&;<>()$`\"'sp\u0100ce \\tab\tlf*?[#~=%" }; + } + + private static IEnumerable GetScpUploadDirectoryInfoExistingFileData() + { + yield return new object[] { RemotePathTransformation.None, "scp-upload-file" }; + } + + private static IEnumerable ScpUploadFileStreamExistingFileData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-upload-file" }; + } + + private static IEnumerable GetScpDownloadStreamFileDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet", "scp-filedoesnotexist" }; + } + + private static IEnumerable GetScpDownloadDirectoryInfoDirectoryDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-download" }; + } + + private static IEnumerable GetScpDownloadDirectoryInfoExistingFileData() + { + yield return new object[] { RemotePathTransformation.None, "scp-download" }; + } + + private static IEnumerable GetScpDownloadDirectoryInfoExistingDirectoryData() + { + yield return new object[] { RemotePathTransformation.None, "scp-download" }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/dir|&;<>()$`\"'space \\tab\tlf*?[#~=%" }; + } + + private static IEnumerable GetScpDownloadFileInfoDirectoryDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-directorydoesnotexist", "scp-file" }; + } + + private static IEnumerable GetScpDownloadFileInfoFileDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet", "scp-filedoesnotexist" }; + } + + private static IEnumerable GetScpDownloadFileInfoExistingDirectoryData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-test" }; + } + + private static IEnumerable GetScpDownloadFileInfoExistingFileData() + { + yield return new object[] { null, "", "file 123", 0 }; + yield return new object[] { null, "", "file 123", 1024 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "", "file|&;<>()$`\"'sp\u0100ce \\tab\tlf*?[#~=%", 1024 }; + yield return new object[] { null, "/home/sshnet/scp test", "file 123", 1024 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/dir|&;<>()$`\"'sp\u0100ce \\tab\tlf\n*?[#~=%", "file123", 1024 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/scp-test", "file|&;<>()$`\"'sp\u0100ce \\tab\tlf*?[#~=%", 1024 }; + } + + private static IEnumerable GetScpDownloadStreamExistingDirectoryData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-test" }; + } + + private static IEnumerable GetScpDownloadStreamExistingFileData() + { + yield return new object[] { null, "", "file 123", 0 }; + yield return new object[] { null, "", "file 123", 1024 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "", "file|&;<>()$`\"'sp\u0100ce \\tab\tlf*?[#~=%", 1024 }; + yield return new object[] { null, "/home/sshnet/scp test", "file 123", 1024 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/dir|&;<>()$`\"'sp\u0100ce \\tab\tlf\n*?[#~=%", "file123", 1024 }; + yield return new object[] { RemotePathTransformation.ShellQuote, "/home/sshnet/scp-test", "file|&;<>()$`\"'sp\u0100ce \\tab\tlf*?[#~=%", 1024 }; + } + + private static IEnumerable GetScpUploadFileStreamDirectoryDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-issue289", "file123" }; + } + + private static IEnumerable GetScpUploadFileStreamExistingDirectoryData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-issue286" }; + } + + private static IEnumerable GetScpUploadFileInfoDirectoryDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-issue289", "file123" }; + } + + private static IEnumerable GetScpUploadFileInfoExistingDirectoryData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-issue286" }; + } + + private static IEnumerable GetScpUploadFileInfoExistingFileData() + { + yield return new object[] { RemotePathTransformation.None, "/home/sshnet/scp-upload-file" }; + } + + private static IEnumerable GetScpUploadDirectoryInfoDirectoryDoesNotExistData() + { + yield return new object[] { RemotePathTransformation.None, "scp-directorydoesnotexist" }; + } + + private static void CreateRemoteFile(ScpClient client, string remoteFile, int size) + { + var file = CreateTempFile(size); + + try + { + using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + client.Upload(fs, remoteFile); + } + } + finally + { + File.Delete(file); + } + } + + private static string GetAbsoluteRemotePath(SftpClient client, string directoryName, string fileName) + { + var absolutePath = string.Empty; + + if (directoryName.Length == 0) + { + absolutePath += client.WorkingDirectory; + } + else + { + if (directoryName[0] != '/') + { + absolutePath += client.WorkingDirectory + "/" + directoryName; + } + else + { + absolutePath = directoryName; + } + } + + return absolutePath + "/" + fileName; + } + + private static string GetAbsoluteRemotePath(IConnectionInfoFactory connectionInfoFactory, string directoryName) + { + var absolutePath = string.Empty; + + if (directoryName.Length == 0 || directoryName == ".") + { + using (var client = new SftpClient(connectionInfoFactory.Create())) + { + client.Connect(); + + absolutePath += client.WorkingDirectory; + } + } + else + { + if (directoryName[0] != '/') + { + using (var client = new SftpClient(connectionInfoFactory.Create())) + { + client.Connect(); + + absolutePath += client.WorkingDirectory + "/" + directoryName; + } + } + else + { + absolutePath = directoryName; + } + } + + return absolutePath; + } + + private static string CreateRemoteFileHash(SftpClient client, string remotePath) + { + using (var fs = client.OpenRead(remotePath)) + { + return CreateHash(fs); + } + } + + private static string CombinePaths(string path1, string path2) + { + if (path1.Length == 0) + { + return path2; + } + + if (path2.Length == 0) + { + return path1; + } + + return path1 + "/" + path2; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/SftpTests.cs b/src/Renci.SshNet.IntegrationTests/SftpTests.cs new file mode 100644 index 000000000..dc316cfbc --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/SftpTests.cs @@ -0,0 +1,6234 @@ +using System.Diagnostics; + +using Renci.SshNet.Common; +using Renci.SshNet.IntegrationTests.Common; +using Renci.SshNet.Sftp; + +namespace Renci.SshNet.IntegrationTests +{ + // TODO: DeleteDirectory (fail + success + // TODO: DeleteFile (fail + success + // TODO: Delete (fail + success + + [TestClass] + public class SftpTests : TestBase + { + private IConnectionInfoFactory _connectionInfoFactory; + private IConnectionInfoFactory _adminConnectionInfoFactory; + private IRemotePathTransformation _remotePathTransformation; + + [TestInitialize] + public void SetUp() + { + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort); + _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); + _remotePathTransformation = RemotePathTransformation.ShellQuote; + } + +#if FEATURE_MSTEST_DATATEST + [DataTestMethod] + [DynamicData(nameof(GetSftpUploadFileFileStreamData), DynamicDataSourceType.Method)] +#else + [TestMethod] + public void Sftp_Upload_DirectoryInfo_ExistingFile() + { + foreach (var data in GetSftpUploadFileFileStreamData()) + { + Sftp_UploadFile_FileStream((int) data[0]); + } + } +#endif + public void Sftp_UploadFile_FileStream(int size) + { + var file = CreateTempFile(size); + + using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.Delete(remoteFile); + } + + try + { + client.UploadFile(fs, remoteFile); + + using (var memoryStream = new MemoryStream(size)) + { + client.DownloadFile(remoteFile, memoryStream); + memoryStream.Position = 0; + Assert.AreEqual(CreateFileHash(file), CreateHash(memoryStream)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.Delete(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ConnectDisconnect_Serial() + { + const int iterations = 100; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + for (var i = 1; i <= iterations; i++) + { + client.Connect(); + client.Disconnect(); + } + } + } + + [TestMethod] + public void Sftp_ConnectDisconnect_Parallel() + { + const int iterations = 10; + const int threads = 20; + + var startEvent = new ManualResetEvent(false); + + var tasks = Enumerable.Range(1, threads).Select(i => + { + return Task.Factory.StartNew(() => + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + startEvent.WaitOne(); + + for (var j = 0; j < iterations; j++) + { + client.Connect(); + client.Disconnect(); + } + } + }); + }).ToArray(); + + startEvent.Set(); + + Task.WaitAll(tasks); + } + + [TestMethod] + public void Sftp_BeginUploadFile() + { + const string content = "SftpBeginUploadFile"; + + var expectedByteCount = (ulong) Encoding.ASCII.GetByteCount(content); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.Delete(remoteFile); + } + + try + { + using (var memoryStream = new MemoryStream(Encoding.ASCII.GetBytes(content))) + { + IAsyncResult asyncResultCallback = null; + + var asyncResult = client.BeginUploadFile(memoryStream, remoteFile, ar => asyncResultCallback = ar); + + Assert.IsTrue(asyncResult.AsyncWaitHandle.WaitOne(10000)); + + // check async result + var sftpUploadAsyncResult = asyncResult as SftpUploadAsyncResult; + Assert.IsNotNull(sftpUploadAsyncResult); + Assert.IsFalse(sftpUploadAsyncResult.IsUploadCanceled); + Assert.IsTrue(sftpUploadAsyncResult.IsCompleted); + Assert.IsFalse(sftpUploadAsyncResult.CompletedSynchronously); + Assert.AreEqual(expectedByteCount, sftpUploadAsyncResult.UploadedBytes); + + // check async result callback + var sftpUploadAsyncResultCallback = asyncResultCallback as SftpUploadAsyncResult; + Assert.IsNotNull(sftpUploadAsyncResultCallback); + Assert.IsFalse(sftpUploadAsyncResultCallback.IsUploadCanceled); + Assert.IsTrue(sftpUploadAsyncResultCallback.IsCompleted); + Assert.IsFalse(sftpUploadAsyncResultCallback.CompletedSynchronously); + Assert.AreEqual(expectedByteCount, sftpUploadAsyncResultCallback.UploadedBytes); + } + + // check uploaded file + using (var memoryStream = new MemoryStream()) + { + client.DownloadFile(remoteFile, memoryStream); + memoryStream.Position = 0; + var remoteContent = Encoding.ASCII.GetString(memoryStream.ToArray()); + Assert.AreEqual(content, remoteContent); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.Delete(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Create_ExistingFile() + { + var encoding = Encoding.UTF8; + var initialContent = "Gert & Ann & Lisa"; + var newContent1 = "Sofie"; + var newContent1Bytes = GetBytesWithPreamble(newContent1, encoding); + var newContent2 = "Lisa & Sofie"; + var newContent2Bytes = GetBytesWithPreamble(newContent2, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent); + + #region Write less bytes than the current content, overwriting part of that content + + using (var fs = client.Create(remoteFile)) + using (var sw = new StreamWriter(fs, encoding)) + { + sw.Write(newContent1); + } + + var actualContent1 = client.ReadAllBytes(remoteFile); + Assert.IsTrue(newContent1Bytes.IsEqualTo(actualContent1)); + + #endregion Write less bytes than the current content, overwriting part of that content + + #region Write more bytes than the current content, overwriting and appending to that content + + using (var fs = client.Create(remoteFile)) + using (var sw = new StreamWriter(fs, encoding)) + { + sw.Write(newContent2); + } + + var actualContent2 = client.ReadAllBytes(remoteFile); + Assert.IsTrue(newContent2Bytes.IsEqualTo(actualContent2)); + + #endregion Write more bytes than the current content, overwriting and appending to that content + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Create_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + SftpFileStream fs = null; + + try + { + fs = client.Create(remoteFile); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + fs?.Dispose(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Create_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.BufferSize = 512 * 1024; + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var imageStream = GetResourceStream("Renci.SshNet.IntegrationTests.resources.issue #70.png")) + { + using (var fs = client.Create(remoteFile)) + { + byte[] buffer = new byte[Math.Min(client.BufferSize, imageStream.Length)]; + int bytesRead; + + while ((bytesRead = imageStream.Read(buffer, offset: 0, buffer.Length)) > 0) + { + fs.Write(buffer, offset: 0, bytesRead); + } + } + + using (var memoryStream = new MemoryStream()) + { + client.DownloadFile(remoteFile, memoryStream); + + memoryStream.Position = 0; + imageStream.Position = 0; + + Assert.AreEqual(CreateHash(imageStream), CreateHash(memoryStream)); + } + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllLines_NoEncoding_ExistingFile() + { + var initialContent = "\u0100ert & Ann"; + IEnumerable linesToAppend = new[] { "Forever", "&", "\u0116ver" }; + var expectedContent = initialContent + string.Join(Environment.NewLine, linesToAppend) + + Environment.NewLine; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent); + client.AppendAllLines(remoteFile, linesToAppend); + + var text = client.ReadAllText(remoteFile); + Assert.AreEqual(expectedContent, text); + + // ensure we didn't write an UTF-8 BOM + using (var fs = client.OpenRead(remoteFile)) + { + var firstByte = fs.ReadByte(); + Assert.AreEqual(Encoding.UTF8.GetBytes(expectedContent)[0], firstByte); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllLines_NoEncoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + IEnumerable linesToAppend = new[] { "\u0139isa", "&", "Sofie" }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.AppendAllLines(remoteFile, linesToAppend); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllLines_NoEncoding_FileDoesNotExist() + { + IEnumerable linesToAppend = new[] { "\u0139isa", "&", "Sofie" }; + var expectedContent = string.Join(Environment.NewLine, linesToAppend) + Environment.NewLine; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.AppendAllLines(remoteFile, linesToAppend); + + var text = client.ReadAllText(remoteFile); + Assert.AreEqual(expectedContent, text); + + // ensure we didn't write an UTF-8 BOM + using (var fs = client.OpenRead(remoteFile)) + { + var firstByte = fs.ReadByte(); + Assert.AreEqual(Encoding.UTF8.GetBytes(expectedContent)[0], firstByte); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllText_NoEncoding_ExistingFile() + { + var initialContent = "\u0100ert & Ann"; + var contentToAppend = "Forever&\u0116ver"; + var expectedContent = initialContent + contentToAppend; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent); + client.AppendAllText(remoteFile, contentToAppend); + + var text = client.ReadAllText(remoteFile); + Assert.AreEqual(expectedContent, text); + + // ensure we didn't write an UTF-8 BOM + using (var fs = client.OpenRead(remoteFile)) + { + var firstByte = fs.ReadByte(); + Assert.AreEqual(Encoding.UTF8.GetBytes(expectedContent)[0], firstByte); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllText_NoEncoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + var contentToAppend = "Forever&\u0116ver"; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.AppendAllText(remoteFile, contentToAppend); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllText_NoEncoding_FileDoesNotExist() + { + var contentToAppend = "Forever&\u0116ver"; + var expectedContent = contentToAppend; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.AppendAllText(remoteFile, contentToAppend); + + var text = client.ReadAllText(remoteFile); + Assert.AreEqual(expectedContent, text); + + // ensure we didn't write an UTF-8 BOM + using (var fs = client.OpenRead(remoteFile)) + { + var firstByte = fs.ReadByte(); + Assert.AreEqual(Encoding.UTF8.GetBytes(expectedContent)[0], firstByte); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendText_NoEncoding_ExistingFile() + { + var initialContent = "\u0100ert & Ann"; + var contentToAppend = "Forever&\u0116ver"; + var expectedContent = initialContent + contentToAppend; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent); + + using (var sw = client.AppendText(remoteFile)) + { + sw.Write(contentToAppend); + } + + var text = client.ReadAllText(remoteFile); + Assert.AreEqual(expectedContent, text); + + // ensure we didn't write an UTF-8 BOM + using (var fs = client.OpenRead(remoteFile)) + { + var firstByte = fs.ReadByte(); + Assert.AreEqual(Encoding.UTF8.GetBytes(expectedContent)[0], firstByte); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendText_NoEncoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + StreamWriter sw = null; + + try + { + sw = client.AppendText(remoteFile); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + sw?.Dispose(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendText_NoEncoding_FileDoesNotExist() + { + var contentToAppend = "\u0100ert & Ann"; + var expectedContent = contentToAppend; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var sw = client.AppendText(remoteFile)) + { + sw.Write(contentToAppend); + } + + // ensure we didn't write an UTF-8 BOM + using (var fs = client.OpenRead(remoteFile)) + { + var firstByte = fs.ReadByte(); + Assert.AreEqual(Encoding.UTF8.GetBytes(expectedContent)[0], firstByte); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllLines_Encoding_ExistingFile() + { + var initialContent = "\u0100ert & Ann"; + IEnumerable linesToAppend = new[] { "Forever", "&", "\u0116ver" }; + var expectedContent = initialContent + string.Join(Environment.NewLine, linesToAppend) + + Environment.NewLine; + var encoding = GetRandomEncoding(); + var expectedBytes = GetBytesWithPreamble(expectedContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent, encoding); + client.AppendAllLines(remoteFile, linesToAppend, encoding); + + var text = client.ReadAllText(remoteFile, encoding); + Assert.AreEqual(expectedContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllLines_Encoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + IEnumerable linesToAppend = new[] { "Forever", "&", "\u0116ver" }; + var encoding = GetRandomEncoding(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.AppendAllLines(remoteFile, linesToAppend, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllLines_Encoding_FileDoesNotExist() + { + IEnumerable linesToAppend = new[] { "\u0139isa", "&", "Sofie" }; + var expectedContent = string.Join(Environment.NewLine, linesToAppend) + Environment.NewLine; + var encoding = GetRandomEncoding(); + var expectedBytes = GetBytesWithPreamble(expectedContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.AppendAllLines(remoteFile, linesToAppend, encoding); + + var text = client.ReadAllText(remoteFile, encoding); + Assert.AreEqual(expectedContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllText_Encoding_ExistingFile() + { + var initialContent = "\u0100ert & Ann"; + var contentToAppend = "Forever&\u0116ver"; + var expectedContent = initialContent + contentToAppend; + var encoding = GetRandomEncoding(); + var expectedBytes = GetBytesWithPreamble(expectedContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent, encoding); + client.AppendAllText(remoteFile, contentToAppend, encoding); + + var text = client.ReadAllText(remoteFile, encoding); + Assert.AreEqual(expectedContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllText_Encoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + const string contentToAppend = "Forever&\u0116ver"; + var encoding = GetRandomEncoding(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.AppendAllText(remoteFile, contentToAppend, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendAllText_Encoding_FileDoesNotExist() + { + const string contentToAppend = "Forever&\u0116ver"; + var expectedContent = contentToAppend; + var encoding = GetRandomEncoding(); + var expectedBytes = GetBytesWithPreamble(expectedContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.AppendAllText(remoteFile, contentToAppend, encoding); + + var text = client.ReadAllText(remoteFile, encoding); + Assert.AreEqual(expectedContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendText_Encoding_ExistingFile() + { + const string initialContent = "\u0100ert & Ann"; + const string contentToAppend = "Forever&\u0116ver"; + var expectedContent = initialContent + contentToAppend; + var encoding = GetRandomEncoding(); + var expectedBytes = GetBytesWithPreamble(expectedContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent, encoding); + + using (var sw = client.AppendText(remoteFile, encoding)) + { + sw.Write(contentToAppend); + } + + var text = client.ReadAllText(remoteFile, encoding); + Assert.AreEqual(expectedContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + } + } + } + + [TestMethod] + public void Sftp_AppendText_Encoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + var encoding = GetRandomEncoding(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + StreamWriter sw = null; + + try + { + sw = client.AppendText(remoteFile, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + sw?.Dispose(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_AppendText_Encoding_FileDoesNotExist() + { + var encoding = GetRandomEncoding(); + const string contentToAppend = "\u0100ert & Ann"; + var expectedBytes = GetBytesWithPreamble(contentToAppend, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var sw = client.AppendText(remoteFile, encoding)) + { + sw.Write(contentToAppend); + } + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_CreateText_NoEncoding_ExistingFile() + { + var encoding = new UTF8Encoding(false, true); + const string initialContent = "\u0100ert & Ann"; + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + const string newContent = "\u0116ver"; + const string expectedContent = "\u0116ver" + " & Ann"; + var expectedContentBytes = GetBytesWithPreamble(expectedContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent); + + using (client.CreateText(remoteFile)) + { + } + + // verify that original content is left untouched + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(initialContentBytes.IsEqualTo(actualBytes)); + } + + // write content that is less bytes than original content + using (var sw = client.CreateText(remoteFile)) + { + sw.Write(newContent); + } + + // verify that original content is only partially overwritten + var text = client.ReadAllText(remoteFile); + Assert.AreEqual(expectedContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedContentBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_CreateText_NoEncoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + StreamWriter sw = null; + + try + { + sw = client.CreateText(remoteFile); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + sw?.Dispose(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_CreateText_NoEncoding_FileDoesNotExist() + { + var encoding = new UTF8Encoding(false, true); + var initialContent = "\u0100ert & Ann"; + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (client.CreateText(remoteFile)) + { + } + + // verify that empty file was created + Assert.IsTrue(client.Exists(remoteFile)); + + var file = client.GetAttributes(remoteFile); + Assert.AreEqual(0, file.Size); + + client.DeleteFile(remoteFile); + + using (var sw = client.CreateText(remoteFile)) + { + sw.Write(initialContent); + } + + // verify that content is written to file + var text = client.ReadAllText(remoteFile); + Assert.AreEqual(initialContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(initialContentBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_CreateText_Encoding_ExistingFile() + { + var encoding = GetRandomEncoding(); + var initialContent = "\u0100ert & Ann"; + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + var newContent = "\u0116ver"; + var expectedContent = "\u0116ver" + " & Ann"; + var expectedContentBytes = GetBytesWithPreamble(expectedContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent, encoding); + + using (client.CreateText(remoteFile)) + { + } + + // verify that original content is left untouched + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(initialContentBytes.IsEqualTo(actualBytes)); + } + + // write content that is less bytes than original content + using (var sw = client.CreateText(remoteFile, encoding)) + { + sw.Write(newContent); + } + + // verify that original content is only partially overwritten + var text = client.ReadAllText(remoteFile, encoding); + Assert.AreEqual(expectedContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedContentBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_CreateText_Encoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + var encoding = GetRandomEncoding(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + StreamWriter sw = null; + + try + { + sw = client.CreateText(remoteFile, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + sw?.Dispose(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_CreateText_Encoding_FileDoesNotExist() + { + var encoding = GetRandomEncoding(); + var initialContent = "\u0100ert & Ann"; + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (client.CreateText(remoteFile, encoding)) + { + } + + // verify that file containing only preamble was created + Assert.IsTrue(client.Exists(remoteFile)); + + var file = client.GetAttributes(remoteFile); + Assert.AreEqual(encoding.GetPreamble().Length, file.Size); + + client.DeleteFile(remoteFile); + + using (var sw = client.CreateText(remoteFile, encoding)) + { + sw.Write(initialContent); + } + + // verify that content is written to file + var text = client.ReadAllText(remoteFile, encoding); + Assert.AreEqual(initialContent, text); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(initialContentBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_DownloadFile_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + using (var ms = new MemoryStream()) + { + try + { + client.DownloadFile(remoteFile, ms); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllBytes_ExistingFile() + { + var encoding = GetRandomEncoding(); + var content = "\u0100ert & Ann"; + var contentBytes = GetBytesWithPreamble(content, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, content, encoding); + + var actualBytes = client.ReadAllBytes(remoteFile); + Assert.IsTrue(contentBytes.IsEqualTo(actualBytes)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllBytes_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.ReadAllBytes(remoteFile); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllLines_NoEncoding_ExistingFile() + { + var encoding = new UTF8Encoding(false, true); + var lines = new[] { "\u0100ert & Ann", "Forever", "&", "\u0116ver" }; + var linesBytes = GetBytesWithPreamble(string.Join(Environment.NewLine, lines) + Environment.NewLine, + encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var sw = client.AppendText(remoteFile)) + { + for (var i = 0; i < lines.Length; i++) + { + sw.WriteLine(lines[i]); + } + } + + var actualLines = client.ReadAllLines(remoteFile); + Assert.IsNotNull(actualLines); + Assert.AreEqual(lines.Length, actualLines.Length); + + for (var i = 0; i < lines.Length; i++) + { + Assert.AreEqual(lines[i], actualLines[i]); + } + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(linesBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllLines_NoEncoding_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.ReadAllLines(remoteFile); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllLines_Encoding_ExistingFile() + { + var encoding = GetRandomEncoding(); + var lines = new[] { "\u0100ert & Ann", "Forever", "&", "\u0116ver" }; + var linesBytes = GetBytesWithPreamble(string.Join(Environment.NewLine, lines) + Environment.NewLine, + encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var sw = client.AppendText(remoteFile, encoding)) + { + for (var i = 0; i < lines.Length; i++) + { + sw.WriteLine(lines[i]); + } + } + + var actualLines = client.ReadAllLines(remoteFile, encoding); + Assert.IsNotNull(actualLines); + Assert.AreEqual(lines.Length, actualLines.Length); + + for (var i = 0; i < lines.Length; i++) + { + Assert.AreEqual(lines[i], actualLines[i]); + } + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(linesBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllLines_Encoding_FileDoesNotExist() + { + var encoding = GetRandomEncoding(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.ReadAllLines(remoteFile, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllText_NoEncoding_ExistingFile() + { + var encoding = new UTF8Encoding(false, true); + var lines = new[] { "\u0100ert & Ann", "Forever", "&", "\u0116ver" }; + var expectedText = string.Join(Environment.NewLine, lines) + Environment.NewLine; + var expectedBytes = GetBytesWithPreamble(expectedText, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var sw = client.AppendText(remoteFile)) + { + for (var i = 0; i < lines.Length; i++) + { + sw.WriteLine(lines[i]); + } + } + + var actualText = client.ReadAllText(remoteFile); + Assert.AreEqual(actualText, expectedText); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllText_NoEncoding_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.ReadAllText(remoteFile); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllText_Encoding_ExistingFile() + { + var encoding = GetRandomEncoding(); + var lines = new[] { "\u0100ert & Ann", "Forever", "&", "\u0116ver" }; + var expectedText = string.Join(Environment.NewLine, lines) + Environment.NewLine; + var expectedBytes = GetBytesWithPreamble(expectedText, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var sw = client.AppendText(remoteFile, encoding)) + { + for (var i = 0; i < lines.Length; i++) + { + sw.WriteLine(lines[i]); + } + } + + var actualText = client.ReadAllText(remoteFile, encoding); + Assert.AreEqual(expectedText, actualText); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadAllText_Encoding_FileDoesNotExist() + { + var encoding = GetRandomEncoding(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.ReadAllText(remoteFile, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadLines_NoEncoding_ExistingFile() + { + var lines = new[] { "\u0100ert & Ann", "Forever", "&", "\u0116ver" }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var sw = client.AppendText(remoteFile)) + { + for (var i = 0; i < lines.Length; i++) + { + sw.WriteLine(lines[i]); + } + } + + var actualLines = client.ReadLines(remoteFile); + Assert.IsNotNull(actualLines); + + var actualLinesEnum = actualLines.GetEnumerator(); + for (var i = 0; i < lines.Length; i++) + { + Assert.IsTrue(actualLinesEnum.MoveNext()); + var actualLine = actualLinesEnum.Current; + Assert.AreEqual(lines[i], actualLine); + } + + Assert.IsFalse(actualLinesEnum.MoveNext()); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadLines_NoEncoding_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.ReadLines(remoteFile); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadLines_Encoding_ExistingFile() + { + var encoding = GetRandomEncoding(); + var lines = new[] { "\u0100ert & Ann", "Forever", "&", "\u0116ver" }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var sw = client.AppendText(remoteFile, encoding)) + { + for (var i = 0; i < lines.Length; i++) + { + sw.WriteLine(lines[i]); + } + } + + var actualLines = client.ReadLines(remoteFile, encoding); + Assert.IsNotNull(actualLines); + + using (var actualLinesEnum = actualLines.GetEnumerator()) + { + for (var i = 0; i < lines.Length; i++) + { + Assert.IsTrue(actualLinesEnum.MoveNext()); + + var actualLine = actualLinesEnum.Current; + Assert.AreEqual(lines[i], actualLine); + } + + Assert.IsFalse(actualLinesEnum.MoveNext()); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_ReadLines_Encoding_FileDoesNotExist() + { + var encoding = GetRandomEncoding(); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.ReadLines(remoteFile, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllBytes_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + var content = GenerateRandom(size: 5); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllBytes(remoteFile, content); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllBytes_ExistingFile() + { + var initialContent = GenerateRandom(size: 13); + var newContent1 = GenerateRandom(size: 5); + var expectedContent1 = new ArrayBuilder().Add(newContent1) + .Add(initialContent, newContent1.Length, initialContent.Length - newContent1.Length) + .Build(); + var newContent2 = GenerateRandom(size: 50000); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var fs = client.Create(remoteFile)) + { + fs.Write(initialContent, offset: 0, initialContent.Length); + } + + #region Write less bytes than the current content, overwriting part of that content + + client.WriteAllBytes(remoteFile, newContent1); + + var actualContent1 = client.ReadAllBytes(remoteFile); + Assert.IsTrue(expectedContent1.IsEqualTo(actualContent1)); + + #endregion Write less bytes than the initial content, overwriting part of that content + + #region Write more bytes than the current content, overwriting and appending to that content + + client.WriteAllBytes(remoteFile, newContent2); + + var actualContent2 = client.ReadAllBytes(remoteFile); + Assert.IsTrue(newContent2.IsEqualTo(actualContent2)); + + #endregion Write less bytes than the initial content, overwriting part of that content + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllBytes_FileDoesNotExist() + { + var content = GenerateRandom(size: 13); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllBytes(remoteFile, content); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(content.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + + [TestMethod] + public void Sftp_WriteAllLines_IEnumerable_NoEncoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + IEnumerable linesToWrite = new[] { "Forever", "&", "\u0116ver" }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllLines(remoteFile, linesToWrite); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_IEnumerable_NoEncoding_ExistingFile() + { + var encoding = new UTF8Encoding(false, true); + var initialContent = "\u0100ert & Ann Forever & Ever Lisa & Sofie"; + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + IEnumerable linesToWrite1 = new[] { "Forever", "&", "\u0116ver" }; + var linesToWrite1Bytes = + GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite1) + Environment.NewLine, encoding); + var expectedBytes1 = new ArrayBuilder().Add(linesToWrite1Bytes) + .Add(initialContentBytes, + linesToWrite1Bytes.Length, + initialContentBytes.Length - linesToWrite1Bytes.Length) + .Build(); + IEnumerable linesToWrite2 = new[] { "Forever", "&", "\u0116ver", "Gert & Ann", "Lisa + Sofie" }; + var linesToWrite2Bytes = + GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite2) + Environment.NewLine, encoding); + var expectedBytes2 = linesToWrite2Bytes; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + // create initial content + client.WriteAllText(remoteFile, initialContent); + + #region Write less bytes than the current content, overwriting part of that content + + client.WriteAllLines(remoteFile, linesToWrite1); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes1.IsEqualTo(actualBytes)); + } + + #endregion Write less bytes than the current content, overwriting part of that content + + #region Write more bytes than the current content, overwriting and appending to that content + + client.WriteAllLines(remoteFile, linesToWrite2); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes2.IsEqualTo(actualBytes)); + } + + #endregion Write more bytes than the current content, overwriting and appending to that content + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_IEnumerable_NoEncoding_FileDoesNotExist() + { + var encoding = new UTF8Encoding(false, true); + IEnumerable linesToWrite = new[] { "\u0139isa", "&", "Sofie" }; + var linesToWriteBytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite) + Environment.NewLine, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllLines(remoteFile, linesToWrite); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(linesToWriteBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_IEnumerable_Encoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + var encoding = GetRandomEncoding(); + IEnumerable linesToWrite = new[] { "Forever", "&", "\u0116ver" }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllLines(remoteFile, linesToWrite, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_IEnumerable_Encoding_ExistingFile() + { + var encoding = GetRandomEncoding(); + const string initialContent = "\u0100ert & Ann Forever & Ever Lisa & Sofie"; + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + IEnumerable linesToWrite1 = new[] { "Forever", "&", "\u0116ver" }; + var linesToWrite1Bytes = + GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite1) + Environment.NewLine, encoding); + var expectedBytes1 = new ArrayBuilder().Add(linesToWrite1Bytes) + .Add(initialContentBytes, + linesToWrite1Bytes.Length, + initialContentBytes.Length - linesToWrite1Bytes.Length) + .Build(); + IEnumerable linesToWrite2 = new[] { "Forever", "&", "\u0116ver", "Gert & Ann", "Lisa + Sofie" }; + var linesToWrite2Bytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite2) + Environment.NewLine, encoding); + var expectedBytes2 = linesToWrite2Bytes; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + // create initial content + client.WriteAllText(remoteFile, initialContent, encoding); + + #region Write less bytes than the current content, overwriting part of that content + + client.WriteAllLines(remoteFile, linesToWrite1, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes1.IsEqualTo(actualBytes)); + } + + #endregion Write less bytes than the current content, overwriting part of that content + + #region Write more bytes than the current content, overwriting and appending to that content + + client.WriteAllLines(remoteFile, linesToWrite2, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes2.IsEqualTo(actualBytes)); + } + + #endregion Write more bytes than the current content, overwriting and appending to that content + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_IEnumerable_Encoding_FileDoesNotExist() + { + var encoding = GetRandomEncoding(); + IEnumerable linesToWrite = new[] { "\u0139isa", "&", "Sofie" }; + var linesToWriteBytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite) + Environment.NewLine, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllLines(remoteFile, linesToWrite, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(linesToWriteBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_Array_NoEncoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + var linesToWrite = new[] { "Forever", "&", "\u0116ver" }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllLines(remoteFile, linesToWrite); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_Array_NoEncoding_ExistingFile() + { + var encoding = new UTF8Encoding(false, true); + const string initialContent = "\u0100ert & Ann Forever & Ever Lisa & Sofie"; + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + var linesToWrite1 = new[] { "Forever", "&", "\u0116ver" }; + var linesToWrite1Bytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite1) + Environment.NewLine, encoding); + var expectedBytes1 = new ArrayBuilder().Add(linesToWrite1Bytes) + .Add(initialContentBytes, linesToWrite1Bytes.Length, initialContentBytes.Length - linesToWrite1Bytes.Length) + .Build(); + var linesToWrite2 = new[] { "Forever", "&", "\u0116ver", "Gert & Ann", "Lisa + Sofie" }; + var linesToWrite2Bytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite2) + Environment.NewLine, encoding); + var expectedBytes2 = linesToWrite2Bytes; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + // create initial content + client.WriteAllText(remoteFile, initialContent); + + #region Write less bytes than the current content, overwriting part of that content + + client.WriteAllLines(remoteFile, linesToWrite1); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes1.IsEqualTo(actualBytes)); + } + + #endregion Write less bytes than the current content, overwriting part of that content + + #region Write more bytes than the current content, overwriting and appending to that content + + client.WriteAllLines(remoteFile, linesToWrite2); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes2.IsEqualTo(actualBytes)); + } + + #endregion Write more bytes than the current content, overwriting and appending to that content + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_Array_NoEncoding_FileDoesNotExist() + { + var encoding = new UTF8Encoding(false, true); + var linesToWrite = new[] { "\u0139isa", "&", "Sofie" }; + var linesToWriteBytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite) + Environment.NewLine, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllLines(remoteFile, linesToWrite); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(linesToWriteBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_Array_Encoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + var encoding = GetRandomEncoding(); + var linesToWrite = new[] { "Forever", "&", "\u0116ver" }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllLines(remoteFile, linesToWrite, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_Array_Encoding_ExistingFile() + { + const string initialContent = "\u0100ert & Ann Forever & Ever Lisa & Sofie"; + + var encoding = GetRandomEncoding(); + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + var linesToWrite1 = new[] { "Forever", "&", "\u0116ver" }; + var linesToWrite1Bytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite1) + Environment.NewLine, encoding); + var expectedBytes1 = new ArrayBuilder().Add(linesToWrite1Bytes) + .Add(initialContentBytes, linesToWrite1Bytes.Length, initialContentBytes.Length - linesToWrite1Bytes.Length) + .Build(); + var linesToWrite2 = new[] { "Forever", "&", "\u0116ver", "Gert & Ann", "Lisa + Sofie" }; + var linesToWrite2Bytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite2) + Environment.NewLine, encoding); + var expectedBytes2 = linesToWrite2Bytes; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + // create initial content + client.WriteAllText(remoteFile, initialContent, encoding); + + #region Write less bytes than the current content, overwriting part of that content + + client.WriteAllLines(remoteFile, linesToWrite1, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes1.IsEqualTo(actualBytes)); + } + + #endregion Write less bytes than the current content, overwriting part of that content + + #region Write more bytes than the current content, overwriting and appending to that content + + client.WriteAllLines(remoteFile, linesToWrite2, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes2.IsEqualTo(actualBytes)); + } + + #endregion Write more bytes than the current content, overwriting and appending to that content + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllLines_Array_Encoding_FileDoesNotExist() + { + var encoding = GetRandomEncoding(); + var linesToWrite = new[] { "\u0139isa", "&", "Sofie" }; + var linesToWriteBytes = GetBytesWithPreamble(string.Join(Environment.NewLine, linesToWrite) + Environment.NewLine, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllLines(remoteFile, linesToWrite, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(linesToWriteBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + + } + } + + [TestMethod] + public void Sftp_WriteAllText_NoEncoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + const string initialContent = "\u0100ert & Ann Forever & \u0116ver Lisa & Sofie"; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllText_NoEncoding_ExistingFile() + { + const string initialContent = "\u0100ert & Ann Forever & \u0116ver Lisa & Sofie"; + const string newContent1 = "For\u0116ver & Ever"; + + var encoding = new UTF8Encoding(false, true); + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + var newContent1Bytes = GetBytesWithPreamble(newContent1, encoding); + var expectedBytes1 = new ArrayBuilder().Add(newContent1Bytes) + .Add(initialContentBytes, newContent1Bytes.Length, initialContentBytes.Length - newContent1Bytes.Length) + .Build(); + var newContent2 = "Sofie & Lisa For\u0116ver & Ever with \u0100ert & Ann"; + var newContent2Bytes = GetBytesWithPreamble(newContent2, encoding); + var expectedBytes2 = newContent2Bytes; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent); + + #region Write less bytes than the current content, overwriting part of that content + + client.WriteAllText(remoteFile, newContent1); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes1.IsEqualTo(actualBytes)); + } + + #endregion Write less bytes than the current content, overwriting part of that content + + #region Write more bytes than the current content, overwriting and appending to that content + + client.WriteAllText(remoteFile, newContent2); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes2.IsEqualTo(actualBytes)); + } + + #endregion Write more bytes than the current content, overwriting and appending to that content + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllText_NoEncoding_FileDoesNotExist() + { + const string initialContent = "\u0100ert & Ann Forever & \u0116ver Lisa & Sofie"; + + var encoding = new UTF8Encoding(false, true); + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(initialContentBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllText_Encoding_DirectoryDoesNotExist() + { + const string remoteFile = "/home/sshnet/directorydoesnotexist/test"; + + var encoding = GetRandomEncoding(); + const string content = "For\u0116ver & Ever"; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, content, encoding); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllText_Encoding_ExistingFile() + { + const string initialContent = "\u0100ert & Ann Forever & \u0116ver Lisa & Sofie"; + const string newContent1 = "For\u0116ver & Ever"; + const string newContent2 = "Sofie & Lisa For\u0116ver & Ever with \u0100ert & Ann"; + + var encoding = GetRandomEncoding(); + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + var newContent1Bytes = GetBytesWithPreamble(newContent1, encoding); + var expectedBytes1 = new ArrayBuilder().Add(newContent1Bytes) + .Add(initialContentBytes, newContent1Bytes.Length, initialContentBytes.Length - newContent1Bytes.Length) + .Build(); + var newContent2Bytes = GetBytesWithPreamble(newContent2, encoding); + var expectedBytes2 = newContent2Bytes; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent, encoding); + + #region Write less bytes than the current content, overwriting part of that content + + client.WriteAllText(remoteFile, newContent1, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes1.IsEqualTo(actualBytes)); + } + + #endregion Write less bytes than the current content, overwriting part of that content + + #region Write more bytes than the current content, overwriting and appending to that content + + client.WriteAllText(remoteFile, newContent2, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(expectedBytes2.IsEqualTo(actualBytes)); + } + + #endregion Write more bytes than the current content, overwriting and appending to that content + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_WriteAllText_Encoding_FileDoesNotExist() + { + const string initialContent = "\u0100ert & Ann Forever & \u0116ver Lisa & Sofie"; + + var encoding = GetRandomEncoding(); + var initialContentBytes = GetBytesWithPreamble(initialContent, encoding); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, initialContent, encoding); + + using (var fs = client.OpenRead(remoteFile)) + { + var actualBytes = new byte[fs.Length]; + fs.Read(actualBytes, offset: 0, actualBytes.Length); + Assert.IsTrue(initialContentBytes.IsEqualTo(actualBytes)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_BeginDownloadFile_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var ms = new MemoryStream()) + { + var asyncResult = client.BeginDownloadFile(remoteFile, ms); + try + { + client.EndDownloadFile(asyncResult); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure file was not created by us + Assert.IsFalse(client.Exists(remoteFile)); + } + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_BeginListDirectory_DirectoryDoesNotExist() + { + const string remoteDirectory = "/home/sshnet/test123"; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var asyncResult = client.BeginListDirectory(remoteDirectory, null, null); + try + { + client.EndListDirectory(asyncResult); + Assert.Fail(); + } + catch (SftpPathNotFoundException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("No such file", ex.Message); + + // ensure directory was not created by us + Assert.IsFalse(client.Exists(remoteDirectory)); + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPath_DirectoryDoesNotExist() + { + const int size = 50 * 1024 * 1024; + const string remoteDirectory = "/home/sshnet/test123"; + const string remoteFile = remoteDirectory + "/test"; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var memoryStream = CreateMemoryStream(size); + memoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(memoryStream, remoteFile); + try + { + client.EndUploadFile(asyncResult); + Assert.Fail(); + } + catch (SftpPathNotFoundException) + { + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPath_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + var uploadMemoryStream = new MemoryStream(); + var sw = new StreamWriter(uploadMemoryStream, Encoding.UTF8); + sw.Write("Gert & Ann"); + sw.Flush(); + uploadMemoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(uploadMemoryStream, remoteFile); + client.EndUploadFile(asyncResult); + + using (var downloadMemoryStream = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloadMemoryStream); + + downloadMemoryStream.Position = 0; + + using (var sr = new StreamReader(downloadMemoryStream, Encoding.UTF8)) + { + var content = sr.ReadToEnd(); + Assert.AreEqual("Gert & Ann", content); + } + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPath_ExistingFile() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, "Gert & Ann & Lisa"); + + var uploadMemoryStream = new MemoryStream(); + var sw = new StreamWriter(uploadMemoryStream, Encoding.UTF8); + sw.Write("Ann & Gert"); + sw.Flush(); + uploadMemoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(uploadMemoryStream, remoteFile); + client.EndUploadFile(asyncResult); + + using (var downloadMemoryStream = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloadMemoryStream); + + downloadMemoryStream.Position = 0; + + using (var sr = new StreamReader(downloadMemoryStream, Encoding.UTF8)) + { + var content = sr.ReadToEnd(); + Assert.AreEqual("Ann & Gert", content); + } + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPathAndCanOverride_CanOverrideIsFalse_DirectoryDoesNotExist() + { + const int size = 50 * 1024 * 1024; + const string remoteDirectory = "/home/sshnet/test123"; + const string remoteFile = remoteDirectory + "/test"; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var memoryStream = CreateMemoryStream(size); + memoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(memoryStream, remoteFile, false, null, null); + try + { + client.EndUploadFile(asyncResult); + Assert.Fail(); + } + catch (SftpPathNotFoundException) + { + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPathAndCanOverride_CanOverrideIsFalse_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var uploadMemoryStream = new MemoryStream()) + using (var sw = new StreamWriter(uploadMemoryStream, Encoding.UTF8)) + { + sw.Write("Gert & Ann"); + sw.Flush(); + uploadMemoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(uploadMemoryStream, remoteFile, false, null, null); + client.EndUploadFile(asyncResult); + } + + using (var downloadMemoryStream = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloadMemoryStream); + + downloadMemoryStream.Position = 0; + + using (var sr = new StreamReader(downloadMemoryStream, Encoding.UTF8)) + { + var content = sr.ReadToEnd(); + Assert.AreEqual("Gert & Ann", content); + } + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPathAndCanOverride_CanOverrideIsFalse_ExistingFile() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, "Gert & Ann & Lisa"); + + var uploadMemoryStream = new MemoryStream(); + var sw = new StreamWriter(uploadMemoryStream, Encoding.UTF8); + sw.Write("Ann & Gert"); + sw.Flush(); + uploadMemoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(uploadMemoryStream, remoteFile, false, null, null); + + try + { + client.EndUploadFile(asyncResult); + Assert.Fail(); + } + catch (SshException ex) + { + Assert.AreEqual(typeof(SshException), ex.GetType()); + Assert.IsNull(ex.InnerException); + Assert.AreEqual("Failure", ex.Message); + } + } + finally + { + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPathAndCanOverride_CanOverrideIsTrue_DirectoryDoesNotExist() + { + const int size = 50 * 1024 * 1024; + const string remoteDirectory = "/home/sshnet/test123"; + const string remoteFile = remoteDirectory + "/test"; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var memoryStream = CreateMemoryStream(size); + memoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(memoryStream, remoteFile, true, null, null); + try + { + client.EndUploadFile(asyncResult); + Assert.Fail(); + } + catch (SftpPathNotFoundException) + { + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPathAndCanOverride_CanOverrideIsTrue_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var uploadMemoryStream = new MemoryStream()) + using (var sw = new StreamWriter(uploadMemoryStream, Encoding.UTF8)) + { + sw.Write("Gert & Ann"); + sw.Flush(); + uploadMemoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(uploadMemoryStream, remoteFile, true, null, null); + client.EndUploadFile(asyncResult); + } + + using (var downloadMemoryStream = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloadMemoryStream); + + downloadMemoryStream.Position = 0; + + using (var sr = new StreamReader(downloadMemoryStream, Encoding.UTF8)) + { + var content = sr.ReadToEnd(); + Assert.AreEqual("Gert & Ann", content); + } + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_BeginUploadFile_InputAndPathAndCanOverride_CanOverrideIsTrue_ExistingFile() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllText(remoteFile, "Gert & Ann & Lisa"); + + using (var uploadMemoryStream = new MemoryStream()) + using (var sw = new StreamWriter(uploadMemoryStream, Encoding.UTF8)) + { + sw.Write("Ann & Gert"); + sw.Flush(); + uploadMemoryStream.Position = 0; + + var asyncResult = client.BeginUploadFile(uploadMemoryStream, remoteFile, true, null, null); + client.EndUploadFile(asyncResult); + } + + using (var downloadMemoryStream = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloadMemoryStream); + + downloadMemoryStream.Position = 0; + + using (var sr = new StreamReader(downloadMemoryStream, Encoding.UTF8)) + { + var content = sr.ReadToEnd(); + Assert.AreEqual("Ann & Gert", content); + } + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_UploadAndDownloadBigFile() + { + const int size = 50 * 1024 * 1024; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.Delete(remoteFile); + } + + try + { + var memoryStream = CreateMemoryStream(size); + memoryStream.Position = 0; + + client.UploadFile(memoryStream, remoteFile); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + // check uploaded file + memoryStream = new MemoryStream(); + client.DownloadFile(remoteFile, memoryStream); + + Assert.AreEqual(size, memoryStream.Length); + + stopwatch.Stop(); + + Console.WriteLine(@"Elapsed: {0} ms", stopwatch.ElapsedMilliseconds); + Console.WriteLine(@"Transfer speed: {0:N2} KB/s", + CalculateTransferSpeed(memoryStream.Length, stopwatch.ElapsedMilliseconds)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.Delete(remoteFile); + } + } + } + } + + /// + /// Issue 1672 + /// + [TestMethod] + public void Sftp_CurrentWorkingDirectory() + { + const string homeDirectory = "/home/sshnet"; + const string otherDirectory = homeDirectory + "/dir"; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(otherDirectory)) + { + client.DeleteDirectory(otherDirectory); + } + + try + { + client.CreateDirectory(otherDirectory); + client.ChangeDirectory(otherDirectory); + + using (var s = CreateStreamWithContent("A")) + { + client.UploadFile(s, "a.txt"); + } + + using (var s = new MemoryStream()) + { + client.DownloadFile("a.txt", s); + s.Position = 0; + + var content = Encoding.ASCII.GetString(s.ToArray()); + Assert.AreEqual("A", content); + } + + Assert.IsTrue(client.Exists(otherDirectory + "/a.txt")); + client.DeleteFile("a.txt"); + Assert.IsFalse(client.Exists(otherDirectory + "/a.txt")); + client.DeleteDirectory("."); + Assert.IsFalse(client.Exists(otherDirectory)); + } + finally + { + if (client.Exists(otherDirectory)) + { + client.DeleteDirectory(otherDirectory); + } + } + } + } + + [TestMethod] + public void Sftp_Exists() + { + const string remoteHome = "/home/sshnet"; + + #region Setup + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + #region Clean-up + + using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}") + ) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + #endregion Clean-up + + #region Setup + + using (var command = client.CreateCommand($"touch {remoteHome + "/file.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"mkdir {remoteHome + "/directory.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"ln -s {remoteHome + "/file.exists"} {remoteHome + "/symlink.to.file.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"ln -s {remoteHome + "/directory.exists"} {remoteHome + "/symlink.to.directory.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + #endregion Setup + } + + #endregion Setup + + #region Assert + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + Assert.IsFalse(client.Exists(remoteHome + "/DoesNotExist")); + Assert.IsTrue(client.Exists(remoteHome + "/file.exists")); + Assert.IsTrue(client.Exists(remoteHome + "/symlink.to.file.exists")); + Assert.IsTrue(client.Exists(remoteHome + "/directory.exists")); + Assert.IsTrue(client.Exists(remoteHome + "/symlink.to.directory.exists")); + } + + #endregion Assert + + #region Teardown + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/DoesNotExist"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.directory.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/directory.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"rm -Rf {remoteHome + "/symlink.to.file.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + + using (var command = client.CreateCommand($"rm -f {remoteHome + "/file.exists"}")) + { + command.Execute(); + Assert.AreEqual(0, command.ExitStatus, command.Error); + } + } + + #endregion Teardown + } + + [TestMethod] + public void Sftp_ListDirectory() + { + const string remoteDirectory = "/home/sshnet/test123"; + + try + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + client.RunCommand($@"rm -Rf ""{remoteDirectory}"""); + client.RunCommand($@"mkdir -p ""{remoteDirectory}"""); + client.RunCommand($@"mkdir -p ""{remoteDirectory}/sub"""); + client.RunCommand($@"touch ""{remoteDirectory}/file1"""); + client.RunCommand($@"touch ""{remoteDirectory}/file2"""); + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + client.ChangeDirectory(remoteDirectory); + + var directoryContent = client.ListDirectory(".").OrderBy(p => p.Name).ToList(); + Assert.AreEqual(5, directoryContent.Count); + + Assert.AreEqual(".", directoryContent[0].Name); + Assert.AreEqual($"{remoteDirectory}/.", directoryContent[0].FullName); + Assert.IsTrue(directoryContent[0].IsDirectory); + + Assert.AreEqual("..", directoryContent[1].Name); + Assert.AreEqual($"{remoteDirectory}/..", directoryContent[1].FullName); + Assert.IsTrue(directoryContent[1].IsDirectory); + + Assert.AreEqual("file1", directoryContent[2].Name); + Assert.AreEqual($"{remoteDirectory}/file1", directoryContent[2].FullName); + Assert.IsFalse(directoryContent[2].IsDirectory); + + Assert.AreEqual("file2", directoryContent[3].Name); + Assert.AreEqual($"{remoteDirectory}/file2", directoryContent[3].FullName); + Assert.IsFalse(directoryContent[3].IsDirectory); + + Assert.AreEqual("sub", directoryContent[4].Name); + Assert.AreEqual($"{remoteDirectory}/sub", directoryContent[4].FullName); + Assert.IsTrue(directoryContent[4].IsDirectory); + } + } + finally + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + } + } + + [TestMethod] + public void Sftp_ChangeDirectory_DirectoryDoesNotExist() + { + const string remoteDirectory = "/home/sshnet/test123"; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + try + { + client.ChangeDirectory(remoteDirectory); + Assert.Fail(); + } + catch (SftpPathNotFoundException) + { + } + } + } + + [TestMethod] + public void Sftp_ChangeDirectory_DirectoryExists() + { + const string remoteDirectory = "/home/sshnet/test123"; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + + using (var command = client.CreateCommand("mkdir -p " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + client.ChangeDirectory(remoteDirectory); + + Assert.AreEqual(remoteDirectory, client.WorkingDirectory); + + using (var uploadStream = CreateMemoryStream(100)) + { + uploadStream.Position = 0; + + client.UploadFile(uploadStream, "gert.txt"); + + uploadStream.Position = 0; + + using (var downloadStream = client.OpenRead(remoteDirectory + "/gert.txt")) + { + Assert.AreEqual(CreateHash(uploadStream), CreateHash(downloadStream)); + } + } + } + } + finally + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var command = client.CreateCommand("rm -Rf " + _remotePathTransformation.Transform(remoteDirectory))) + { + command.Execute(); + } + } + } + } + + [TestMethod] + public void Sftp_DownloadFile_MemoryStream() + { + const int fileSize = 500 * 1024; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + SftpCreateRemoteFile(client, remoteFile, fileSize); + + try + { + using (var memoryStream = new MemoryStream()) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + client.DownloadFile(remoteFile, memoryStream); + stopwatch.Stop(); + + var transferSpeed = CalculateTransferSpeed(memoryStream.Length, stopwatch.ElapsedMilliseconds); + Console.WriteLine(@"Elapsed: {0} ms", stopwatch.ElapsedMilliseconds); + Console.WriteLine(@"Transfer speed: {0:N2} KB/s", transferSpeed); + + Assert.AreEqual(fileSize, memoryStream.Length); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_SubsystemExecution_Failed() + { + var remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); + + // Disable SFTP subsystem + remoteSshdConfig.ClearSubsystems() + .Update() + .Restart(); + + var remoteSshdReconfiguredToDefaultState = false; + + try + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + try + { + client.Connect(); + Assert.Fail("Establishing SFTP connection should have failed."); + } + catch (SshException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("Subsystem 'sftp' could not be executed.", ex.Message); + } + + // Re-enable SFTP subsystem + remoteSshdConfig.Reset(); + + remoteSshdReconfiguredToDefaultState = true; + + // ensure we can reconnect the same SftpClient instance + client.Connect(); + // ensure SFTP session is correctly established + Assert.IsTrue(client.Exists(".")); + } + } + finally + { + if (!remoteSshdReconfiguredToDefaultState) + { + remoteSshdConfig.Reset(); + } + } + } + + [TestMethod] + public void Sftp_SftpFileStream_ReadAndWrite() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var s = client.Open(remoteFile, FileMode.CreateNew, FileAccess.Write)) + { + s.Write(new byte[] { 5, 4, 3, 2, 1 }, 1, 3); + } + + // switch from read to write mode + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.ReadWrite)) + { + Assert.AreEqual(4, s.ReadByte()); + Assert.AreEqual(3, s.ReadByte()); + + Assert.AreEqual(2, s.Position); + + s.WriteByte(7); + s.Write(new byte[] { 8, 9, 10, 11, 12 }, 1, 3); + + Assert.AreEqual(6, s.Position); + } + + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.Read)) + { + Assert.AreEqual(6, s.Length); + + var buffer = new byte[s.Length]; + Assert.AreEqual(6, s.Read(buffer, offset: 0, buffer.Length)); + + CollectionAssert.AreEqual(new byte[] { 4, 3, 7, 9, 10, 11 }, buffer); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, s.ReadByte()); + } + + // switch from read to write mode, and back to read mode and finally + // append a byte + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.ReadWrite)) + { + Assert.AreEqual(4, s.ReadByte()); + Assert.AreEqual(3, s.ReadByte()); + Assert.AreEqual(7, s.ReadByte()); + + s.Write(new byte[] { 0, 1, 6, 4 }, 1, 2); + + Assert.AreEqual(11, s.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, s.ReadByte()); + + s.WriteByte(12); + } + + // switch from write to read mode, and back to write mode + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.ReadWrite)) + { + s.WriteByte(5); + Assert.AreEqual(3, s.ReadByte()); + s.WriteByte(13); + + Assert.AreEqual(3, s.Position); + } + + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.Read)) + { + Assert.AreEqual(7, s.Length); + + var buffer = new byte[s.Length]; + Assert.AreEqual(7, s.Read(buffer, offset: 0, buffer.Length)); + + CollectionAssert.AreEqual(new byte[] { 5, 3, 13, 1, 6, 11, 12 }, buffer); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, s.ReadByte()); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_SftpFileStream_SetLength_ReduceLength() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var s = client.Open(remoteFile, FileMode.CreateNew, FileAccess.Write)) + { + s.Write(new byte[] { 5, 4, 3, 2, 1 }, 1, 3); + } + + // reduce length while in write mode, with data in write buffer, and before + // current position + using (var s = client.Open(remoteFile, FileMode.Append, FileAccess.Write)) + { + s.Position = 3; + s.Write(new byte[] { 6, 7, 8, 9 }, offset: 0, count: 4); + + Assert.AreEqual(7, s.Position); + + // verify buffer has not yet been flushed + using (var fs = client.Open(remoteFile, FileMode.Open, FileAccess.Read)) + { + Assert.AreEqual(4, fs.ReadByte()); + Assert.AreEqual(3, fs.ReadByte()); + Assert.AreEqual(2, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + s.SetLength(5); + + Assert.AreEqual(5, s.Position); + + // verify that buffer was flushed and size has been modified + using (var fs = client.Open(remoteFile, FileMode.Open, FileAccess.Read)) + { + Assert.AreEqual(4, fs.ReadByte()); + Assert.AreEqual(3, fs.ReadByte()); + Assert.AreEqual(2, fs.ReadByte()); + Assert.AreEqual(6, fs.ReadByte()); + Assert.AreEqual(7, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + s.WriteByte(1); + } + + // verify that last byte was correctly written to the file + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.Read)) + { + Assert.AreEqual(6, s.Length); + + var buffer = new byte[s.Length + 2]; + Assert.AreEqual(6, s.Read(buffer, offset: 0, buffer.Length)); + + CollectionAssert.AreEqual(new byte[] { 4, 3, 2, 6, 7, 1, 0, 0 }, buffer); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, s.ReadByte()); + } + + // reduce length while in read mode, but beyond current position + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.ReadWrite)) + { + var buffer = new byte[1]; + Assert.AreEqual(1, s.Read(buffer, offset: 0, buffer.Length)); + + CollectionAssert.AreEqual(new byte[] { 4 }, buffer); + + s.SetLength(3); + + using (var w = client.Open(remoteFile, FileMode.Open, FileAccess.Write)) + { + w.Write(new byte[] { 8, 1, 6, 2 }, offset: 0, count: 4); + } + + // verify that position was not changed + Assert.AreEqual(1, s.Position); + + // verify that read buffer was cleared + Assert.AreEqual(1, s.ReadByte()); + Assert.AreEqual(6, s.ReadByte()); + Assert.AreEqual(2, s.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, s.ReadByte()); + + Assert.AreEqual(4, s.Length); + } + + // reduce length while in read mode, but before current position + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.ReadWrite)) + { + var buffer = new byte[4]; + Assert.AreEqual(4, s.Read(buffer, offset: 0, buffer.Length)); + + CollectionAssert.AreEqual(new byte[] { 8, 1, 6, 2 }, buffer); + + Assert.AreEqual(4, s.Position); + + s.SetLength(3); + + // verify that position was moved to last byte + Assert.AreEqual(3, s.Position); + + using (var w = client.Open(remoteFile, FileMode.Open, FileAccess.Read)) + { + Assert.AreEqual(3, w.Length); + + Assert.AreEqual(8, w.ReadByte()); + Assert.AreEqual(1, w.ReadByte()); + Assert.AreEqual(6, w.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, w.ReadByte()); + } + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, s.ReadByte()); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_SftpFileStream_Seek_BeyondEndOfFile_SeekOriginBegin() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.BufferSize = 500; + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF but not beyond buffer size + // do not write anything + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: 3L, SeekOrigin.Begin); + + Assert.AreEqual(3, newPosition); + Assert.AreEqual(3, fs.Position); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF and beyond buffer size + // do not write anything + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: 700L, SeekOrigin.Begin); + + Assert.AreEqual(700, newPosition); + Assert.AreEqual(700, fs.Position); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF but not beyond buffer size + // write less bytes than buffer size + var seekOffset = 3L; + + // buffer holding the data that we'll write to the file + var writeBuffer = GenerateRandom(size: 7); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.Begin); + + Assert.AreEqual(seekOffset, newPosition); + Assert.AreEqual(seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(seekOffset + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBufferffer = new byte[seekOffset - 1]; + Assert.AreEqual(soughtOverReadBufferffer.Length, fs.Read(soughtOverReadBufferffer, offset: 0, soughtOverReadBufferffer.Length)); + Assert.IsTrue(new byte[soughtOverReadBufferffer.Length].IsEqualTo(soughtOverReadBufferffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF and beyond buffer size + // write less bytes than buffer size + seekOffset = 700L; + + // buffer holding the data that we'll write to the file + writeBuffer = GenerateRandom(size: 4); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.Begin); + + Assert.AreEqual(seekOffset, newPosition); + Assert.AreEqual(seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(seekOffset + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBufferffer = new byte[seekOffset - 1]; + Assert.AreEqual(soughtOverReadBufferffer.Length, fs.Read(soughtOverReadBufferffer, offset: 0, soughtOverReadBufferffer.Length)); + Assert.IsTrue(new byte[soughtOverReadBufferffer.Length].IsEqualTo(soughtOverReadBufferffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF but not beyond buffer size + // write more bytes than buffer size + writeBuffer = GenerateRandom(size: 600); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: 3L, SeekOrigin.Begin); + + Assert.AreEqual(3, newPosition); + Assert.AreEqual(3, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(3 + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + Assert.AreEqual(0x00, fs.ReadByte()); + Assert.AreEqual(0x00, fs.ReadByte()); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF and beyond buffer size + // write more bytes than buffer size + writeBuffer = GenerateRandom(size: 600); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: 550, SeekOrigin.Begin); + + Assert.AreEqual(550, newPosition); + Assert.AreEqual(550, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(550 + writeBuffer.Length, fs.Length); + + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBuffer = new byte[550 - 1]; + Assert.AreEqual(550 - 1, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length)); + Assert.IsTrue(new byte[550 - 1].IsEqualTo(soughtOverReadBuffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_SftpFileStream_Seek_BeyondEndOfFile_SeekOriginEnd() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.BufferSize = 500; + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF but not beyond buffer size + // do not write anything + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: 3L, SeekOrigin.End); + + Assert.AreEqual(4, newPosition); + Assert.AreEqual(4, fs.Position); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF and beyond buffer size + // do not write anything + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: 700L, SeekOrigin.End); + + Assert.AreEqual(701, newPosition); + Assert.AreEqual(701, fs.Position); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF but not beyond buffer size + // write less bytes than buffer size + var seekOffset = 3L; + + // buffer holding the data that we'll write to the file + var writeBuffer = GenerateRandom(size: 7); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.End); + + Assert.AreEqual(4, newPosition); + Assert.AreEqual(4, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1 + seekOffset + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBuffer = new byte[seekOffset]; + Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length)); + Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF and beyond buffer size + // write less bytes than buffer size + seekOffset = 700L; + + // buffer holding the data that we'll write to the file + writeBuffer = GenerateRandom(size: 4); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.End); + + Assert.AreEqual(1 + seekOffset, newPosition); + Assert.AreEqual(1 + seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1 + seekOffset + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBuffer = new byte[seekOffset]; + Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length)); + Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF but not beyond buffer size + // write more bytes than buffer size + seekOffset = 3L; + writeBuffer = GenerateRandom(size: 600); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.End); + + Assert.AreEqual(1 + seekOffset, newPosition); + Assert.AreEqual(1 + seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1 + seekOffset + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBuffer = new byte[seekOffset]; + Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length)); + Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + // seek beyond EOF and beyond buffer size + // write more bytes than buffer size + seekOffset = 550L; + writeBuffer = GenerateRandom(size: 600); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.End); + + Assert.AreEqual(1 + seekOffset, newPosition); + Assert.AreEqual(1 + seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1 + seekOffset + writeBuffer.Length, fs.Length); + + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBuffer = new byte[seekOffset]; + Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length)); + Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_SftpFileStream_Seek_NegativeOffSet_SeekOriginEnd() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.BufferSize = 500; + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + fs.WriteByte(0x07); + fs.WriteByte(0x05); + } + + // seek within file and not beyond buffer size + // do not write anything + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: -2L, SeekOrigin.End); + + Assert.AreEqual(1, newPosition); + Assert.AreEqual(1, fs.Position); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(3, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + Assert.AreEqual(0x07, fs.ReadByte()); + Assert.AreEqual(0x05, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // buffer holding the data that we'll write to the file + var writeBuffer = GenerateRandom(size: (int) client.BufferSize + 200); + + using (var fs = client.OpenWrite(remoteFile)) + { + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + // seek within EOF and beyond buffer size + // do not write anything + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: -100L, SeekOrigin.End); + + Assert.AreEqual(600, newPosition); + Assert.AreEqual(600, fs.Position); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(writeBuffer.Length, fs.Length); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // seek within EOF and within buffer size + // write less bytes than buffer size + using (var fs = client.OpenWrite(remoteFile)) + { + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + + var newPosition = fs.Seek(offset: -3, SeekOrigin.End); + + Assert.AreEqual(697, newPosition); + Assert.AreEqual(697, fs.Position); + + fs.WriteByte(0x01); + fs.WriteByte(0x05); + fs.WriteByte(0x04); + fs.WriteByte(0x07); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(writeBuffer.Length + 1, fs.Length); + + var readBuffer = new byte[writeBuffer.Length - 3]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(readBuffer.SequenceEqual(writeBuffer.Take(readBuffer.Length))); + + Assert.AreEqual(0x01, fs.ReadByte()); + Assert.AreEqual(0x05, fs.ReadByte()); + Assert.AreEqual(0x04, fs.ReadByte()); + Assert.AreEqual(0x07, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + // buffer holding the data that we'll write to the file + writeBuffer = GenerateRandom(size: (int) client.BufferSize * 4); + + // seek within EOF and beyond buffer size + // write less bytes than buffer size + using (var fs = client.OpenWrite(remoteFile)) + { + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + + var newPosition = fs.Seek(offset: -(client.BufferSize * 2), SeekOrigin.End); + + Assert.AreEqual(1000, newPosition); + Assert.AreEqual(1000, fs.Position); + + fs.WriteByte(0x01); + fs.WriteByte(0x05); + fs.WriteByte(0x04); + fs.WriteByte(0x07); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(writeBuffer.Length, fs.Length); + + // First part of file should not have been touched + var readBuffer = new byte[(int) client.BufferSize * 2]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(readBuffer.SequenceEqual(writeBuffer.Take(readBuffer.Length))); + + // Check part that should have been updated + Assert.AreEqual(0x01, fs.ReadByte()); + Assert.AreEqual(0x05, fs.ReadByte()); + Assert.AreEqual(0x04, fs.ReadByte()); + Assert.AreEqual(0x07, fs.ReadByte()); + + // Remaining bytes should not have been touched + readBuffer = new byte[((int) client.BufferSize * 2) - 4]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(readBuffer.SequenceEqual(writeBuffer.Skip(((int)client.BufferSize * 2) + 4).Take(readBuffer.Length))); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + /// https://github.com/sshnet/SSH.NET/issues/253 + [TestMethod] + public void Sftp_SftpFileStream_Seek_Issue253() + { + var buf = Encoding.UTF8.GetBytes("123456"); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var ws = client.OpenWrite(remoteFile)) + { + ws.Write(buf, offset: 0, count: 3); + } + + using (var ws = client.OpenWrite(remoteFile)) + { + var newPosition = ws.Seek(offset: 3, SeekOrigin.Begin); + + Assert.AreEqual(3, newPosition); + Assert.AreEqual(3, ws.Position); + + ws.Write(buf, 3, 3); + } + + var actual = client.ReadAllText(remoteFile, Encoding.UTF8); + Assert.AreEqual("123456", actual); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_SftpFileStream_Seek_WithinReadBuffer() + { + var originalContent = GenerateRandom(size: 800); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.BufferSize = 500; + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var fs = client.OpenWrite(remoteFile)) + { + fs.Write(originalContent, offset: 0, originalContent.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + var readBuffer = new byte[200]; + + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + + var newPosition = fs.Seek(offset: 3L, SeekOrigin.Begin); + + Assert.AreEqual(3L, newPosition); + Assert.AreEqual(3L, fs.Position); + } + + client.DeleteFile(remoteFile); + + #region Seek beyond EOF and beyond buffer size do not write anything + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(offset: 700L, SeekOrigin.Begin); + + Assert.AreEqual(700L, newPosition); + Assert.AreEqual(700L, fs.Position); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(1, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + #endregion Seek beyond EOF and beyond buffer size do not write anything + + #region Seek beyond EOF but not beyond buffer size and write less bytes than buffer size + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + var seekOffset = 3L; + var writeBuffer = GenerateRandom(size: 7); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.Begin); + + Assert.AreEqual(seekOffset, newPosition); + Assert.AreEqual(seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(seekOffset + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBuffer = new byte[seekOffset - 1]; + Assert.AreEqual(soughtOverReadBuffer.Length, fs.Read(soughtOverReadBuffer, offset: 0, soughtOverReadBuffer.Length)); + Assert.IsTrue(new byte[soughtOverReadBuffer.Length].IsEqualTo(soughtOverReadBuffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + #endregion Seek beyond EOF but not beyond buffer size and write less bytes than buffer size + + #region Seek beyond EOF and beyond buffer size and write less bytes than buffer size + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + seekOffset = 700L; + writeBuffer = GenerateRandom(size: 4); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.Begin); + + Assert.AreEqual(seekOffset, newPosition); + Assert.AreEqual(seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(seekOffset + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBufferffer = new byte[seekOffset - 1]; + Assert.AreEqual(soughtOverReadBufferffer.Length, fs.Read(soughtOverReadBufferffer, offset: 0, soughtOverReadBufferffer.Length)); + Assert.IsTrue(new byte[soughtOverReadBufferffer.Length].IsEqualTo(soughtOverReadBufferffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(readBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + #endregion Seek beyond EOF and beyond buffer size and write less bytes than buffer size + + #region Seek beyond EOF but not beyond buffer size and write more bytes than buffer size + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + seekOffset = 3L; + writeBuffer = GenerateRandom(size: 600); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.Begin); + + Assert.AreEqual(seekOffset, newPosition); + Assert.AreEqual(seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(seekOffset + writeBuffer.Length, fs.Length); + Assert.AreEqual(0x04, fs.ReadByte()); + Assert.AreEqual(0x00, fs.ReadByte()); + Assert.AreEqual(0x00, fs.ReadByte()); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + #endregion Seek beyond EOF but not beyond buffer size and write more bytes than buffer size + + #region Seek beyond EOF and beyond buffer size and write more bytes than buffer size + + // create single-byte file + using (var fs = client.OpenWrite(remoteFile)) + { + fs.WriteByte(0x04); + } + + seekOffset = 550L; + writeBuffer = GenerateRandom(size: 600); + + using (var fs = client.OpenWrite(remoteFile)) + { + var newPosition = fs.Seek(seekOffset, SeekOrigin.Begin); + + Assert.AreEqual(seekOffset, newPosition); + Assert.AreEqual(seekOffset, fs.Position); + + fs.Write(writeBuffer, offset: 0, writeBuffer.Length); + } + + using (var fs = client.OpenRead(remoteFile)) + { + Assert.AreEqual(seekOffset + writeBuffer.Length, fs.Length); + + Assert.AreEqual(0x04, fs.ReadByte()); + + var soughtOverReadBufferffer = new byte[seekOffset - 1]; + Assert.AreEqual(seekOffset - 1, fs.Read(soughtOverReadBufferffer, offset: 0, soughtOverReadBufferffer.Length)); + Assert.IsTrue(new byte[seekOffset - 1].IsEqualTo(soughtOverReadBufferffer)); + + var readBuffer = new byte[writeBuffer.Length]; + Assert.AreEqual(writeBuffer.Length, fs.Read(readBuffer, offset: 0, readBuffer.Length)); + Assert.IsTrue(writeBuffer.IsEqualTo(readBuffer)); + + // Ensure we've reached end of the stream + Assert.AreEqual(-1, fs.ReadByte()); + } + + client.DeleteFile(remoteFile); + + #endregion Seek beyond EOF and beyond buffer size and write more bytes than buffer size + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_SftpFileStream_SetLength_FileDoesNotExist() + { + var size = new Random().Next(500, 5000); + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + using (var s = client.Open(remoteFile, FileMode.Append, FileAccess.Write)) + { + s.SetLength(size); + } + + Assert.IsTrue(client.Exists(remoteFile)); + + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(size, attributes.Size); + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateHash(new byte[size]), CreateHash(downloaded)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_Append_Write_ExistingFile() + { + const int fileSize = 5 * 1024; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + using (var input = CreateMemoryStream(fileSize)) + { + input.Position = 0; + + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.UploadFile(input, remoteFile); + + using (var s = client.Open(remoteFile, FileMode.Append, FileAccess.Write)) + { + var buffer = new byte[] { 0x05, 0x0f, 0x0d, 0x0a, 0x04 }; + s.Write(buffer, offset: 0, buffer.Length); + input.Write(buffer, offset: 0, buffer.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + + input.Position = 0; + downloaded.Position = 0; + Assert.AreEqual(CreateHash(input), CreateHash(downloaded)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_Append_Write_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + #region Verify if merely opening the file for append creates a zero-byte file + + using (client.Open(remoteFile, FileMode.Append, FileAccess.Write)) + { + } + + Assert.IsTrue(client.Exists(remoteFile)); + + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(0L, attributes.Size); + + #endregion Verify if merely opening the file for append creates it + + client.DeleteFile(remoteFile); + + #region Verify if content is actually written to the file + + var content = GenerateRandom(size: 100); + + using (var s = client.Open(remoteFile, FileMode.Append, FileAccess.Write)) + { + s.Write(content, offset: 0, content.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateHash(content), CreateHash(downloaded)); + } + + #endregion Verify if content is actually written to the file + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + + + [TestMethod] + public void Sftp_Open_PathAndMode_ModeIsCreate_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + #region Verify if merely opening the file for create creates a zero-byte file + + using (client.Open(remoteFile, FileMode.Create)) + { + } + + Assert.IsTrue(client.Exists(remoteFile)); + + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(0L, attributes.Size); + + #endregion Verify if merely opening the file for create creates a zero-byte file + + client.DeleteFile(remoteFile); + + #region Verify if content is actually written to the file + + var content = GenerateRandom(size: 100); + + using (var s = client.Open(remoteFile, FileMode.Create)) + { + s.Write(content, offset: 0, content.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateHash(content), CreateHash(downloaded)); + } + + #endregion Verify if content is actually written to the file + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_PathAndMode_ModeIsCreate_ExistingFile() + { + const int fileSize = 5 * 1024; + var newContent = new byte[] { 0x07, 0x03, 0x02, 0x0b }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + using (var input = CreateMemoryStream(fileSize)) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + input.Position = 0; + client.UploadFile(input, remoteFile); + + using (var stream = client.Open(remoteFile, FileMode.Create)) + { + // Verify if merely opening the file for create overwrites the file + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(0L, attributes.Size); + + stream.Write(newContent, offset: 0, newContent.Length); + stream.Position = 0; + + Assert.AreEqual(CreateHash(newContent), CreateHash(stream)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_PathAndModeAndAccess_ModeIsCreate_AccessIsReadWrite_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + #region Verify if merely opening the file for create creates a zero-byte file + + using (client.Open(remoteFile, FileMode.Create, FileAccess.ReadWrite)) + { + } + + Assert.IsTrue(client.Exists(remoteFile)); + + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(0L, attributes.Size); + + #endregion Verify if merely opening the file for create creates a zero-byte file + + client.DeleteFile(remoteFile); + + #region Verify if content is actually written to the file + + var content = GenerateRandom(size: 100); + + using (var s = client.Open(remoteFile, FileMode.Create, FileAccess.ReadWrite)) + { + s.Write(content, offset: 0, content.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateHash(content), CreateHash(downloaded)); + } + + #endregion Verify if content is actually written to the file + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_PathAndModeAndAccess_ModeIsCreate_AccessIsReadWrite_ExistingFile() + { + const int fileSize = 5 * 1024; + var newContent = new byte[] { 0x07, 0x03, 0x02, 0x0b }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + using (var input = CreateMemoryStream(fileSize)) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + input.Position = 0; + client.UploadFile(input, remoteFile); + + using (var stream = client.Open(remoteFile, FileMode.Create, FileAccess.ReadWrite)) + { + // Verify if merely opening the file for create overwrites the file + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(0L, attributes.Size); + + stream.Write(newContent, offset: 0, newContent.Length); + stream.Position = 0; + + Assert.AreEqual(CreateHash(newContent), CreateHash(stream)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_PathAndModeAndAccess_ModeIsCreate_AccessIsWrite_ExistingFile() + { + // use new content that contains less bytes than original content to + // verify whether file is first truncated + var originalContent = new byte[] { 0x05, 0x0f, 0x0d, 0x0a, 0x04 }; + var newContent = new byte[] { 0x07, 0x03, 0x02, 0x0b }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllBytes(remoteFile, originalContent); + + using (var s = client.Open(remoteFile, FileMode.Create, FileAccess.Write)) + { + s.Write(newContent, offset: 0, newContent.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + + downloaded.Position = 0; + Assert.AreEqual(CreateHash(newContent), CreateHash(downloaded)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_PathAndModeAndAccess_ModeIsCreate_AccessIsWrite_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + #region Verify if merely opening the file for create creates a zero-byte file + + using (client.Open(remoteFile, FileMode.Create, FileAccess.Write)) + { + } + + Assert.IsTrue(client.Exists(remoteFile)); + + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(0L, attributes.Size); + + #endregion Verify if merely opening the file for create creates a zero-byte file + + client.DeleteFile(remoteFile); + + #region Verify if content is actually written to the file + + var content = GenerateRandom(size: 100); + + using (var s = client.Open(remoteFile, FileMode.Create, FileAccess.Write)) + { + s.Write(content, offset: 0, content.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateHash(content), CreateHash(downloaded)); + } + + #endregion Verify if content is actually written to the file + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_CreateNew_Write_ExistingFile() + { + const int fileSize = 5 * 1024; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + using (var input = CreateMemoryStream(fileSize)) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + input.Position = 0; + + try + { + client.UploadFile(input, remoteFile); + + Stream stream = null; + + try + { + stream = client.Open(remoteFile, FileMode.CreateNew, FileAccess.Write); + Assert.Fail(); + } + catch (SshException) + { + } + finally + { + stream?.Dispose(); + } + + // Verify that the file was not modified + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + + input.Position = 0; + downloaded.Position = 0; + Assert.AreEqual(CreateHash(input), CreateHash(downloaded)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_CreateNew_Write_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + #region Verify if merely opening the file creates a zero-byte file + + using (client.Open(remoteFile, FileMode.CreateNew, FileAccess.Write)) + { + } + + Assert.IsTrue(client.Exists(remoteFile)); + + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(0L, attributes.Size); + + #endregion Verify if merely opening the file creates it + + client.DeleteFile(remoteFile); + + #region Verify if content is actually written to the file + + var content = GenerateRandom(size: 100); + + using (var s = client.Open(remoteFile, FileMode.CreateNew, FileAccess.Write)) + { + s.Write(content, offset: 0, content.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateHash(content), CreateHash(downloaded)); + } + + #endregion Verify if content is actually written to the file + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_Open_Write_ExistingFile() + { + // use new content that contains less bytes than original content to + // verify whether file is first truncated + var originalContent = new byte[] { 0x05, 0x0f, 0x0d, 0x0a, 0x04 }; + var newContent = new byte[] { 0x07, 0x03, 0x02, 0x0b }; + var expectedContent = new byte[] { 0x07, 0x03, 0x02, 0x0b, 0x04 }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllBytes(remoteFile, originalContent); + + using (var s = client.Open(remoteFile, FileMode.Open, FileAccess.Write)) + { + s.Write(newContent, offset: 0, newContent.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + + downloaded.Position = 0; + Assert.AreEqual(CreateHash(expectedContent), CreateHash(downloaded)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_Open_Write_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + Stream stream = null; + + try + { + stream = client.Open(remoteFile, FileMode.Open, FileAccess.Write); + Assert.Fail(); + } + catch (SshException) + { + } + finally + { + stream?.Dispose(); + } + + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_OpenOrCreate_Write_ExistingFile() + { + // use new content that contains less bytes than original content to + // verify whether file is first truncated + var originalContent = new byte[] { 0x05, 0x0f, 0x0d, 0x0a, 0x04 }; + var newContent = new byte[] { 0x07, 0x03, 0x02, 0x0b }; + var expectedContent = new byte[] { 0x07, 0x03, 0x02, 0x0b, 0x04 }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + client.WriteAllBytes(remoteFile, originalContent); + + using (var s = client.Open(remoteFile, FileMode.OpenOrCreate, FileAccess.Write)) + { + s.Write(newContent, offset: 0, newContent.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + + downloaded.Position = 0; + Assert.AreEqual(CreateHash(expectedContent), CreateHash(downloaded)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_OpenOrCreate_Write_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + #region Verify if merely opening the file creates a zero-byte file + + using (client.Open(remoteFile, FileMode.OpenOrCreate, FileAccess.Write)) + { + } + + Assert.IsTrue(client.Exists(remoteFile)); + + var attributes = client.GetAttributes(remoteFile); + Assert.IsTrue(attributes.IsRegularFile); + Assert.AreEqual(0L, attributes.Size); + + #endregion Verify if merely opening the file creates it + + client.DeleteFile(remoteFile); + + #region Verify if content is actually written to the file + + var content = GenerateRandom(size: 100); + + using (var s = client.Open(remoteFile, FileMode.OpenOrCreate, FileAccess.Write)) + { + s.Write(content, offset: 0, content.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + downloaded.Position = 0; + Assert.AreEqual(CreateHash(content), CreateHash(downloaded)); + } + + #endregion Verify if content is actually written to the file + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + + + + + + + [TestMethod] + public void Sftp_Open_Truncate_Write_ExistingFile() + { + const int fileSize = 5 * 1024; + + // use new content that contains less bytes than original content to + // verify whether file is first truncated + var originalContent = new byte[] { 0x05, 0x0f, 0x0d, 0x0a, 0x04 }; + var newContent = new byte[] { 0x07, 0x03, 0x02, 0x0b }; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + using (var input = CreateMemoryStream(fileSize)) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + input.Position = 0; + + try + { + client.WriteAllBytes(remoteFile, originalContent); + + using (var s = client.Open(remoteFile, FileMode.Truncate, FileAccess.Write)) + { + s.Write(newContent, offset: 0, newContent.Length); + } + + using (var downloaded = new MemoryStream()) + { + client.DownloadFile(remoteFile, downloaded); + + input.Position = 0; + downloaded.Position = 0; + Assert.AreEqual(CreateHash(newContent), CreateHash(downloaded)); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_Open_Truncate_Write_FileDoesNotExist() + { + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + try + { + Stream stream = null; + + try + { + stream = client.Open(remoteFile, FileMode.Truncate, FileAccess.Write); + Assert.Fail(); + } + catch (SshException) + { + } + + Assert.IsFalse(client.Exists(remoteFile)); + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + [TestMethod] + public void Sftp_OpenRead() + { + const int fileSize = 5 * 1024 * 1024; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var remoteFile = GenerateUniqueRemoteFileName(); + + SftpCreateRemoteFile(client, remoteFile, fileSize); + + try + { + using (var s = client.OpenRead(remoteFile)) + { + var buffer = new byte[s.Length]; + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + var bytesRead = s.Read(buffer, offset: 0, buffer.Length); + + stopwatch.Stop(); + + var transferSpeed = CalculateTransferSpeed(bytesRead, stopwatch.ElapsedMilliseconds); + Console.WriteLine(@"Elapsed: {0} ms", stopwatch.ElapsedMilliseconds); + Console.WriteLine(@"Transfer speed: {0:N2} KB/s", transferSpeed); + + Assert.AreEqual(fileSize, bytesRead); + } + } + finally + { + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + } + } + } + + private static IEnumerable GetSftpUploadFileFileStreamData() + { + yield return new object[] { 0 }; + yield return new object[] { 5 * 1024 * 1024 }; + } + + private static Encoding GetRandomEncoding() + { + var random = new Random().Next(1, 3); + switch (random) + { + case 1: + return Encoding.Unicode; + case 2: + return Encoding.UTF8; + case 3: + return Encoding.UTF32; + default: + throw new NotImplementedException(); + } + } + + private static byte[] GetBytesWithPreamble(string text, Encoding encoding) + { + var preamble = encoding.GetPreamble(); + var textBytes = encoding.GetBytes(text); + + if (preamble.Length != 0) + { + var textAndPreambleBytes = new byte[preamble.Length + textBytes.Length]; + Buffer.BlockCopy(preamble, srcOffset: 0, textAndPreambleBytes, dstOffset: 0, preamble.Length); + Buffer.BlockCopy(textBytes, srcOffset: 0, textAndPreambleBytes, preamble.Length, textBytes.Length); + return textAndPreambleBytes; + } + + return textBytes; + } + + private static Stream GetResourceStream(string resourceName) + { + var type = typeof(SftpTests); + var resourceStream = type.Assembly.GetManifestResourceStream(resourceName); + if (resourceStream == null) + { + throw new ArgumentException($"Resource '{resourceName}' not found in assembly '{type.Assembly.FullName}'.", nameof(resourceName)); + } + return resourceStream; + } + + private static decimal CalculateTransferSpeed(long length, long elapsedMilliseconds) + { + return (length / 1024m) / (elapsedMilliseconds / 1000m); + } + + private static void SftpCreateRemoteFile(SftpClient client, string remoteFile, int size) + { + var file = CreateTempFile(size); + + try + { + using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + client.UploadFile(fs, remoteFile); + } + } + finally + { + File.Delete(file); + } + } + + private static byte[] GenerateRandom(int size) + { + var random = new Random(); + var randomContent = new byte[size]; + random.NextBytes(randomContent); + return randomContent; + } + + private static Stream CreateStreamWithContent(string content) + { + var memoryStream = new MemoryStream(); + var sw = new StreamWriter(memoryStream, Encoding.ASCII, 1024); + sw.Write(content); + sw.Flush(); + memoryStream.Position = 0; + return memoryStream; + } + + private static string GenerateUniqueRemoteFileName() + { + return $"/home/sshnet/{Guid.NewGuid():D}"; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/SshTests.cs b/src/Renci.SshNet.IntegrationTests/SshTests.cs new file mode 100644 index 000000000..217094d75 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/SshTests.cs @@ -0,0 +1,971 @@ +using System.Net; +using System.Net.Sockets; + +using Renci.SshNet.Common; +using Renci.SshNet.IntegrationTests.Common; + +namespace Renci.SshNet.IntegrationTests +{ + [TestClass] + public class SshTests : TestBase + { + private IConnectionInfoFactory _connectionInfoFactory; + private IConnectionInfoFactory _adminConnectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort); + _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); + + _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); + _remoteSshdConfig.AllowTcpForwarding() + .PrintMotd(false) + .Update() + .Restart(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + } + + /// + /// Test for a channel that is being closed by the server. + /// + [TestMethod] + public void Ssh_ShellStream_Exit() + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var terminalModes = new Dictionary + { + { TerminalModes.ECHO, 0 } + }; + + using (var shellStream = client.CreateShellStream("xterm", 80, 24, 800, 600, 1024, terminalModes)) + { + shellStream.WriteLine("echo Hello!"); + shellStream.WriteLine("exit"); + + Thread.Sleep(1000); + + try + { + shellStream.Write("ABC"); + Assert.Fail(); + } + catch (ObjectDisposedException ex) + { + Assert.IsNull(ex.InnerException); + Assert.AreEqual("ShellStream", ex.ObjectName); + Assert.AreEqual($"Cannot access a disposed object.{Environment.NewLine}Object name: '{ex.ObjectName}'.", ex.Message); + } + + var line = shellStream.ReadLine(); + Assert.IsNotNull(line); + Assert.IsTrue(line.EndsWith("Hello!"), line); + + // TODO: ReadLine should return null when the buffer is empty and the channel has been closed (issue #672) + try + { + line = shellStream.ReadLine(); + Assert.Fail(line); + } + catch (NullReferenceException) + { + + } + } + } + } + + /// + /// https://github.com/sshnet/SSH.NET/issues/63 + /// + [TestMethod] + public void Ssh_ShellStream_IntermittendOutput() + { + const string remoteFile = "/home/sshnet/test.sh"; + + var expectedResult = string.Join("\n", + "Line 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 4 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 5 ", + "Line 6"); + + var scriptBuilder = new StringBuilder(); + scriptBuilder.Append("#!/bin/sh\n"); + scriptBuilder.Append("echo Line 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("sleep .5\n"); + scriptBuilder.Append("echo Line 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("echo Line 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("echo Line 4 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("sleep 2\n"); + scriptBuilder.Append("echo \"Line 5 \"\n"); + scriptBuilder.Append("echo Line 6 \n"); + scriptBuilder.Append("exit 13\n"); + + using (var sshClient = new SshClient(_connectionInfoFactory.Create())) + { + sshClient.Connect(); + + CreateShellScript(_connectionInfoFactory, remoteFile, scriptBuilder.ToString()); + + try + { + var terminalModes = new Dictionary + { + { TerminalModes.ECHO, 0 } + }; + + using (var shellStream = sshClient.CreateShellStream("xterm", 80, 24, 800, 600, 1024, terminalModes)) + { + shellStream.WriteLine(remoteFile); + Thread.Sleep(1200); + using (var reader = new StreamReader(shellStream, new UTF8Encoding(false), false, 10)) + { + var lines = new List(); + string line = null; + while ((line = reader.ReadLine()) != null) + { + lines.Add(line); + } + Assert.AreEqual(6, lines.Count, string.Join("\n", lines)); + Assert.AreEqual(expectedResult, string.Join("\n", lines)); + } + } + } + finally + { + RemoveFileOrDirectory(sshClient, remoteFile); + } + } + } + + /// + /// Issue 1555 + /// + [TestMethod] + public void Ssh_CreateShell() + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + using (var input = new MemoryStream()) + using (var output = new MemoryStream()) + using (var extOutput = new MemoryStream()) + { + var shell = client.CreateShell(input, output, extOutput); + shell.Start(); + + var inputWriter = new StreamWriter(input, Encoding.ASCII, 1024); + inputWriter.WriteLine("echo $PATH"); + + var outputReader = new StreamReader(output, Encoding.ASCII, false, 1024); + Console.WriteLine(outputReader.ReadToEnd()); + + shell.Stop(); + } + } + } + + [TestMethod] + public void Ssh_Command_IntermittendOutput_EndExecute() + { + const string remoteFile = "/home/sshnet/test.sh"; + + var expectedResult = string.Join("\n", + "Line 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 4 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 5 ", + "Line 6", + ""); + + var scriptBuilder = new StringBuilder(); + scriptBuilder.Append("#!/bin/sh\n"); + scriptBuilder.Append("echo Line 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("sleep .5\n"); + scriptBuilder.Append("echo Line 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("echo Line 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("echo Line 4 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("sleep 2\n"); + scriptBuilder.Append("echo \"Line 5 \"\n"); + scriptBuilder.Append("echo Line 6 \n"); + scriptBuilder.Append("exit 13\n"); + + using (var sshClient = new SshClient(_connectionInfoFactory.Create())) + { + sshClient.Connect(); + + CreateShellScript(_connectionInfoFactory, remoteFile, scriptBuilder.ToString()); + + try + { + using (var cmd = sshClient.CreateCommand("chmod 777 " + remoteFile)) + { + cmd.Execute(); + + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } + + using (var command = sshClient.CreateCommand(remoteFile)) + { + var asyncResult = command.BeginExecute(); + var actualResult = command.EndExecute(asyncResult); + + Assert.AreEqual(expectedResult, actualResult); + Assert.AreEqual(expectedResult, command.Result); + Assert.AreEqual(13, command.ExitStatus); + } + } + finally + { + RemoveFileOrDirectory(sshClient, remoteFile); + } + } + } + + /// + /// Ignored for now, because: + /// * OutputStream.Read(...) does not block when no data is available + /// * SshCommand.(Begin)Execute consumes *OutputStream*, advancing its position. + /// + /// https://github.com/sshnet/SSH.NET/issues/650 + /// + [TestMethod] + [Ignore] + public void Ssh_Command_IntermittendOutput_OutputStream() + { + const string remoteFile = "/home/sshnet/test.sh"; + + var expectedResult = string.Join("\n", + "Line 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 4 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Line 5 ", + "Line 6"); + + var scriptBuilder = new StringBuilder(); + scriptBuilder.Append("#!/bin/sh\n"); + scriptBuilder.Append("echo Line 1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("sleep .5\n"); + scriptBuilder.Append("echo Line 2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("echo Line 3 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("echo Line 4 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n"); + scriptBuilder.Append("sleep 2\n"); + scriptBuilder.Append("echo \"Line 5 \"\n"); + scriptBuilder.Append("echo Line 6 \n"); + scriptBuilder.Append("exit 13\n"); + + using (var sshClient = new SshClient(_connectionInfoFactory.Create())) + { + sshClient.Connect(); + + CreateShellScript(_connectionInfoFactory, remoteFile, scriptBuilder.ToString()); + + try + { + using (var cmd = sshClient.CreateCommand("chmod 777 " + remoteFile)) + { + cmd.Execute(); + + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } + + using (var command = sshClient.CreateCommand(remoteFile)) + { + var asyncResult = command.BeginExecute(); + + using (var reader = new StreamReader(command.OutputStream, new UTF8Encoding(false), false, 10)) + { + var lines = new List(); + string line = null; + while ((line = reader.ReadLine()) != null) + { + lines.Add(line); + } + + Assert.AreEqual(6, lines.Count, string.Join("\n", lines)); + Assert.AreEqual(expectedResult, string.Join("\n", lines)); + Assert.AreEqual(13, command.ExitStatus); + } + + var actualResult = command.EndExecute(asyncResult); + + Assert.AreEqual(expectedResult, actualResult); + Assert.AreEqual(expectedResult, command.Result); + } + } + finally + { + RemoveFileOrDirectory(sshClient, remoteFile); + } + } + } + + [TestMethod] + public void Ssh_DynamicPortForwarding_DisposeSshClientWithoutStoppingPort() + { + const string searchText = "HTTP/1.1 301 Moved Permanently"; + const string hostName = "github.com"; + + var httpGetRequest = Encoding.ASCII.GetBytes($"GET / HTTP/1.1\r\nHost: {hostName}\r\n\r\n"); + Socket socksSocket; + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.ConnectionInfo.Timeout = TimeSpan.FromSeconds(200); + client.Connect(); + + var forwardedPort = new ForwardedPortDynamic(1080); + forwardedPort.Exception += (sender, args) => Console.WriteLine(args.Exception.ToString()); + client.AddForwardedPort(forwardedPort); + forwardedPort.Start(); + + var socksClient = new Socks5Handler(new IPEndPoint(IPAddress.Loopback, 1080), + string.Empty, + string.Empty); + + socksSocket = socksClient.Connect(hostName, 80); + socksSocket.Send(httpGetRequest); + + var httpResponse = GetHttpResponse(socksSocket, Encoding.ASCII); + Assert.IsTrue(httpResponse.Contains(searchText), httpResponse); + } + + Assert.IsTrue(socksSocket.Connected); + + // check if client socket was properly closed + Assert.AreEqual(0, socksSocket.Receive(new byte[1], 0, 1, SocketFlags.None)); + } + + [TestMethod] + public void Ssh_DynamicPortForwarding_DomainName() + { + const string searchText = "HTTP/1.1 301 Moved Permanently"; + const string hostName = "github.com"; + + // Set-up a host alias for google.be on the remote server that is not known locally; this allows us to + // verify whether the host name is resolved remotely. + const string hostNameAlias = "dynamicportforwarding-test.for.sshnet"; + + // Construct a HTTP request for which we expected the response to contain the search text. + var httpGetRequest = Encoding.ASCII.GetBytes($"GET / HTTP/1.1\r\nHost: {hostName}\r\n\r\n"); + + var ipAddresses = Dns.GetHostAddresses(hostName); + var hostsFileUpdated = AddOrUpdateHostsEntry(_adminConnectionInfoFactory, ipAddresses[0], hostNameAlias); + + try + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.ConnectionInfo.Timeout = TimeSpan.FromSeconds(200); + client.Connect(); + + var forwardedPort = new ForwardedPortDynamic(1080); + forwardedPort.Exception += (sender, args) => Console.WriteLine(args.Exception.ToString()); + client.AddForwardedPort(forwardedPort); + forwardedPort.Start(); + + var socksClient = new Socks5Handler(new IPEndPoint(IPAddress.Loopback, 1080), + string.Empty, + string.Empty); + var socksSocket = socksClient.Connect(hostNameAlias, 80); + + socksSocket.Send(httpGetRequest); + var httpResponse = GetHttpResponse(socksSocket, Encoding.ASCII); + Assert.IsTrue(httpResponse.Contains(searchText), httpResponse); + + // Verify if port is still open + socksSocket.Send(httpGetRequest); + httpResponse = GetHttpResponse(socksSocket, Encoding.ASCII); + Assert.IsTrue(httpResponse.Contains(searchText), httpResponse); + + forwardedPort.Stop(); + + Assert.IsTrue(socksSocket.Connected); + + // check if client socket was properly closed + Assert.AreEqual(0, socksSocket.Receive(new byte[1], 0, 1, SocketFlags.None)); + + forwardedPort.Start(); + + // create new SOCKS connection and very whether the forwarded port is functional again + socksSocket = socksClient.Connect(hostNameAlias, 80); + + socksSocket.Send(httpGetRequest); + httpResponse = GetHttpResponse(socksSocket, Encoding.ASCII); + Assert.IsTrue(httpResponse.Contains(searchText), httpResponse); + + forwardedPort.Dispose(); + + Assert.IsTrue(socksSocket.Connected); + + // check if client socket was properly closed + Assert.AreEqual(0, socksSocket.Receive(new byte[1], 0, 1, SocketFlags.None)); + + forwardedPort.Dispose(); + } + } + finally + { + if (hostsFileUpdated) + { + RemoveHostsEntry(_adminConnectionInfoFactory, ipAddresses[0], hostNameAlias); + } + } + } + + [TestMethod] + public void Ssh_DynamicPortForwarding_IPv4() + { + const string searchText = "HTTP/1.1 301 Moved Permanently"; + const string hostName = "github.com"; + + var httpGetRequest = Encoding.ASCII.GetBytes($"GET /null HTTP/1.1\r\nHost: {hostName}\r\n\r\n"); + var httpResponseBuffer = new byte[2048]; + + var ipv4 = Dns.GetHostAddresses(hostName).FirstOrDefault(p => p.AddressFamily == AddressFamily.InterNetwork); + Assert.IsNotNull(ipv4, $@"No IPv4 address found for '{hostName}'."); + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.ConnectionInfo.Timeout = TimeSpan.FromSeconds(200); + client.Connect(); + + var forwardedPort = new ForwardedPortDynamic(1080); + forwardedPort.Exception += (sender, args) => Console.WriteLine(args.Exception.ToString()); + client.AddForwardedPort(forwardedPort); + forwardedPort.Start(); + + var socksClient = new Socks5Handler(new IPEndPoint(IPAddress.Loopback, 1080), + string.Empty, + string.Empty); + var socksSocket = socksClient.Connect(new IPEndPoint(ipv4, 80)); + + socksSocket.Send(httpGetRequest); + var httpResponse = GetHttpResponse(socksSocket, Encoding.ASCII); + Assert.IsTrue(httpResponse.Contains(searchText), httpResponse); + + forwardedPort.Dispose(); + + // check if client socket was properly closed + Assert.AreEqual(0, socksSocket.Receive(new byte[1], 0, 1, SocketFlags.None)); + } + } + + /// + /// Verifies whether channels are effectively closed. + /// + [TestMethod] + public void Ssh_LocalPortForwardingCloseChannels() + { + const string hostNameAlias = "localportforwarding-test.for.sshnet"; + const string hostName = "github.com"; + + var ipAddress = Dns.GetHostAddresses(hostName)[0]; + + var hostsFileUpdated = AddOrUpdateHostsEntry(_adminConnectionInfoFactory, ipAddress, hostNameAlias); + + try + { + var connectionInfo = _connectionInfoFactory.Create(); + connectionInfo.MaxSessions = 1; + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + + var localEndPoint = new IPEndPoint(IPAddress.Loopback, 1225); + + for (var i = 0; i < (connectionInfo.MaxSessions + 1); i++) + { + var forwardedPort = new ForwardedPortLocal(localEndPoint.Address.ToString(), + (uint)localEndPoint.Port, + hostNameAlias, + 80); + client.AddForwardedPort(forwardedPort); + forwardedPort.Start(); + + try + { + var httpRequest = (HttpWebRequest) WebRequest.Create("http://" + localEndPoint); + httpRequest.Host = hostName; + httpRequest.Method = "GET"; + httpRequest.AllowAutoRedirect = false; + + try + { + using (var httpResponse = (HttpWebResponse)httpRequest.GetResponse()) + { + Assert.AreEqual(HttpStatusCode.MovedPermanently, httpResponse.StatusCode); + } + } + catch (WebException ex) + { + Assert.AreEqual(WebExceptionStatus.ProtocolError, ex.Status); + Assert.IsNotNull(ex.Response); + + using (var httpResponse = ex.Response as HttpWebResponse) + { + Assert.IsNotNull(httpResponse); + Assert.AreEqual(HttpStatusCode.MovedPermanently, httpResponse.StatusCode); + } + } + } + finally + { + client.RemoveForwardedPort(forwardedPort); + } + } + } + } + finally + { + if (hostsFileUpdated) + { + RemoveHostsEntry(_adminConnectionInfoFactory, ipAddress, hostNameAlias); + } + } + } + + [TestMethod] + public void Ssh_LocalPortForwarding() + { + const string hostNameAlias = "localportforwarding-test.for.sshnet"; + const string hostName = "github.com"; + + var ipAddress = Dns.GetHostAddresses(hostName)[0]; + + var hostsFileUpdated = AddOrUpdateHostsEntry(_adminConnectionInfoFactory, ipAddress, hostNameAlias); + + try + { + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + var localEndPoint = new IPEndPoint(IPAddress.Loopback, 1225); + + var forwardedPort = new ForwardedPortLocal(localEndPoint.Address.ToString(), + (uint)localEndPoint.Port, + hostNameAlias, + 80); + forwardedPort.Exception += + (sender, args) => Console.WriteLine(@"ForwardedPort exception: " + args.Exception); + client.AddForwardedPort(forwardedPort); + forwardedPort.Start(); + + try + { + var httpRequest = (HttpWebRequest) WebRequest.Create("http://" + localEndPoint); + httpRequest.Host = hostName; + httpRequest.Method = "GET"; + httpRequest.Accept = "text/html"; + httpRequest.AllowAutoRedirect = false; + + try + { + using (var httpResponse = (HttpWebResponse)httpRequest.GetResponse()) + { + Assert.AreEqual(HttpStatusCode.MovedPermanently, httpResponse.StatusCode); + } + } + catch (WebException ex) + { + Assert.AreEqual(WebExceptionStatus.ProtocolError, ex.Status); + Assert.IsNotNull(ex.Response); + + using (var httpResponse = ex.Response as HttpWebResponse) + { + Assert.IsNotNull(httpResponse); + Assert.AreEqual(HttpStatusCode.MovedPermanently, httpResponse.StatusCode); + } + } + } + finally + { + client.RemoveForwardedPort(forwardedPort); + } + } + } + finally + { + if (hostsFileUpdated) + { + RemoveHostsEntry(_adminConnectionInfoFactory, ipAddress, hostNameAlias); + } + } + } + + [TestMethod] + public void Ssh_RemotePortForwarding() + { + var hostAddresses = Dns.GetHostAddresses(Dns.GetHostName()); + var ipv4HostAddress = hostAddresses.First(p => p.AddressFamily == AddressFamily.InterNetwork); + + var endpoint1 = new IPEndPoint(ipv4HostAddress, 666); + var endpoint2 = new IPEndPoint(ipv4HostAddress, 667); + + var bytesReceivedOnListener1 = new List(); + var bytesReceivedOnListener2 = new List(); + + using (var socketListener1 = new AsyncSocketListener(endpoint1)) + using (var socketListener2 = new AsyncSocketListener(endpoint2)) + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + socketListener1.BytesReceived += (received, socket) => bytesReceivedOnListener1.AddRange(received); + socketListener1.Start(); + + socketListener2.BytesReceived += (received, socket) => bytesReceivedOnListener2.AddRange(received); + socketListener2.Start(); + + client.Connect(); + + var forwardedPort1 = new ForwardedPortRemote(IPAddress.Loopback, + 10000, + endpoint1.Address, + (uint)endpoint1.Port); + forwardedPort1.Exception += (sender, args) => Console.WriteLine(@"forwardedPort1 exception: " + args.Exception); + client.AddForwardedPort(forwardedPort1); + forwardedPort1.Start(); + + var forwardedPort2 = new ForwardedPortRemote(IPAddress.Loopback, + 10001, + endpoint2.Address, + (uint)endpoint2.Port); + forwardedPort2.Exception += (sender, args) => Console.WriteLine(@"forwardedPort2 exception: " + args.Exception); + client.AddForwardedPort(forwardedPort2); + forwardedPort2.Start(); + + using (var s = client.CreateShellStream("a", 80, 25, 800, 600, 200)) + { + s.WriteLine($"telnet {forwardedPort1.BoundHost} {forwardedPort1.BoundPort}"); + s.Expect($"Connected to {forwardedPort1.BoundHost}\r\n"); + s.WriteLine("ABC"); + s.Flush(); + s.Expect("ABC"); + s.Close(); + } + + using (var s = client.CreateShellStream("b", 80, 25, 800, 600, 200)) + { + s.WriteLine($"telnet {forwardedPort2.BoundHost} {forwardedPort2.BoundPort}"); + s.Expect($"Connected to {forwardedPort2.BoundHost}\r\n"); + s.WriteLine("DEF"); + s.Flush(); + s.Expect("DEF"); + s.Close(); + } + + forwardedPort1.Stop(); + forwardedPort2.Stop(); + } + + var textReceivedOnListener1 = Encoding.ASCII.GetString(bytesReceivedOnListener1.ToArray()); + Assert.AreEqual("ABC\r\n", textReceivedOnListener1); + + var textReceivedOnListener2 = Encoding.ASCII.GetString(bytesReceivedOnListener2.ToArray()); + Assert.AreEqual("DEF\r\n", textReceivedOnListener2); + } + + /// + /// Issue 1591 + /// + [TestMethod] + public void Ssh_ExecuteShellScript() + { + const string remoteFile = "/home/sshnet/run.sh"; + const string content = "#\bin\bash\necho Hello World!"; + + using (var client = new SftpClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + if (client.Exists(remoteFile)) + { + client.DeleteFile(remoteFile); + } + + using (var memoryStream = new MemoryStream()) + using (var sw = new StreamWriter(memoryStream, Encoding.ASCII)) + { + sw.Write(content); + sw.Flush(); + memoryStream.Position = 0; + client.UploadFile(memoryStream, remoteFile); + } + } + + using (var client = new SshClient(_connectionInfoFactory.Create())) + { + client.Connect(); + + try + { + var runChmod = client.RunCommand("chmod u+x " + remoteFile); + runChmod.Execute(); + Assert.AreEqual(0, runChmod.ExitStatus, runChmod.Error); + + var runLs = client.RunCommand("ls " + remoteFile); + var asyncResultLs = runLs.BeginExecute(); + + var runScript = client.RunCommand(remoteFile); + var asyncResultScript = runScript.BeginExecute(); + + Assert.IsTrue(asyncResultScript.AsyncWaitHandle.WaitOne(10000)); + var resultScript = runScript.EndExecute(asyncResultScript); + Assert.AreEqual("Hello World!\n", resultScript); + + Assert.IsTrue(asyncResultLs.AsyncWaitHandle.WaitOne(10000)); + var resultLs = runLs.EndExecute(asyncResultLs); + Assert.AreEqual(remoteFile + "\n", resultLs); + } + finally + { + RemoveFileOrDirectory(client, remoteFile); + } + } + } + + /// + /// Verifies if a hosts file contains an entry for a given combination of IP address and hostname, + /// and if necessary add either the host entry or an alias to an exist entry for the specified IP + /// address. + /// + /// + /// + /// + /// + /// if an entry was added or updated in the specified hosts file; otherwise, + /// . + /// + private static bool AddOrUpdateHostsEntry(IConnectionInfoFactory linuxAdminConnectionFactory, + IPAddress ipAddress, + string hostName) + { + const string hostsFile = "/etc/hosts"; + + using (var client = new ScpClient(linuxAdminConnectionFactory.Create())) + { + client.Connect(); + + var hostConfig = HostConfig.Read(client, hostsFile); + + var hostEntry = hostConfig.Entries.SingleOrDefault(h => h.IPAddress.Equals(ipAddress)); + if (hostEntry != null) + { + if (hostEntry.HostName == hostName) + { + return false; + } + + foreach (var alias in hostEntry.Aliases) + { + if (alias == hostName) + { + return false; + } + } + + hostEntry.Aliases.Add(hostName); + } + else + { + bool mappingFound = false; + + for (var i = (hostConfig.Entries.Count - 1); i >= 0; i--) + { + hostEntry = hostConfig.Entries[i]; + + if (hostEntry.HostName == hostName) + { + if (hostEntry.IPAddress.Equals(ipAddress)) + { + mappingFound = true; + continue; + } + + // If hostname is currently mapped to another IP address, then remove the + // current mapping + hostConfig.Entries.RemoveAt(i); + } + else + { + for (var j = (hostEntry.Aliases.Count - 1); j >= 0; j--) + { + var alias = hostEntry.Aliases[j]; + + if (alias == hostName) + { + hostEntry.Aliases.RemoveAt(j); + } + } + } + } + + if (!mappingFound) + { + hostEntry = new HostEntry(ipAddress, hostName); + hostConfig.Entries.Add(hostEntry); + } + } + + hostConfig.Write(client, hostsFile); + return true; + } + } + + /// + /// Remove the mapping between a given IP address and host name from the remote hosts file either by + /// removing a host entry entirely (if no other aliases are defined for the IP address) or removing + /// the aliases that match the host name for the IP address. + /// + /// + /// + /// + /// + /// if the hosts file was updated; otherwise, . + /// + private static bool RemoveHostsEntry(IConnectionInfoFactory linuxAdminConnectionFactory, + IPAddress ipAddress, + string hostName) + { + const string hostsFile = "/etc/hosts"; + + using (var client = new ScpClient(linuxAdminConnectionFactory.Create())) + { + client.Connect(); + + var hostConfig = HostConfig.Read(client, hostsFile); + + var hostEntry = hostConfig.Entries.SingleOrDefault(h => h.IPAddress.Equals(ipAddress)); + if (hostEntry == null) + { + return false; + } + + if (hostEntry.HostName == hostName) + { + if (hostEntry.Aliases.Count == 0) + { + hostConfig.Entries.Remove(hostEntry); + } + else + { + // Use one of the aliases (that are different from the specified host name) as host name + // of the host entry. + + for (var i = hostEntry.Aliases.Count - 1; i >= 0; i--) + { + var alias = hostEntry.Aliases[i]; + if (alias == hostName) + { + hostEntry.Aliases.RemoveAt(i); + } + else if (hostEntry.HostName == hostName) + { + // If we haven't already used one of the aliases as host name of the host entry + // then do this now and remove the alias. + + hostEntry.HostName = alias; + hostEntry.Aliases.RemoveAt(i); + } + } + + // If for some reason the host name of the host entry matched the specified host name + // and it only had aliases that match the host name, then remove the host entry altogether. + if (hostEntry.Aliases.Count == 0 && hostEntry.HostName == hostName) + { + hostConfig.Entries.Remove(hostEntry); + } + } + } + else + { + var aliasRemoved = false; + + for (var i = hostEntry.Aliases.Count - 1; i >= 0; i--) + { + if (hostEntry.Aliases[i] == hostName) + { + hostEntry.Aliases.RemoveAt(i); + aliasRemoved = true; + } + } + + if (!aliasRemoved) + { + return false; + } + } + + hostConfig.Write(client, hostsFile); + return true; + } + } + + private static string GetHttpResponse(Socket socket, Encoding encoding) + { + var httpResponseBuffer = new byte[2048]; + + // We expect: + // * The response to contain the searchText in the first receive. + // * The full response to be returned in the first receive. + + var bytesReceived = socket.Receive(httpResponseBuffer, + 0, + httpResponseBuffer.Length, + SocketFlags.None); + if (bytesReceived == 0) + { + return null; + } + + if (bytesReceived == httpResponseBuffer.Length) + { + throw new Exception("We expect the HTTP response to be less than the buffer size. If not, we won't consume the full response."); + } + + using (var sr = new StringReader(encoding.GetString(httpResponseBuffer, 0, bytesReceived))) + { + return sr.ReadToEnd(); + } + } + + private static void CreateShellScript(IConnectionInfoFactory connectionInfoFactory, string remoteFile, string script) + { + using (var sftpClient = new SftpClient(connectionInfoFactory.Create())) + { + sftpClient.Connect(); + + using (var sw = sftpClient.CreateText(remoteFile, new UTF8Encoding(false))) + { + sw.Write(script); + } + + sftpClient.ChangePermissions(remoteFile, 0x1FF); + } + } + + private static void RemoveFileOrDirectory(SshClient client, string remoteFile) + { + using (var cmd = client.CreateCommand("rm -Rf " + remoteFile)) + { + cmd.Execute(); + Assert.AreEqual(0, cmd.ExitStatus, cmd.Error); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/TestBase.cs b/src/Renci.SshNet.IntegrationTests/TestBase.cs new file mode 100644 index 000000000..511bb144d --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/TestBase.cs @@ -0,0 +1,80 @@ +using System.Security.Cryptography; + +namespace Renci.SshNet.IntegrationTests +{ + public abstract class TestBase : IntegrationTestBase + { + protected static MemoryStream CreateMemoryStream(int size) + { + var memoryStream = new MemoryStream(); + FillStream(memoryStream, size); + return memoryStream; + } + + protected static void FillStream(Stream stream, int size) + { + var randomContent = new byte[50]; + var random = new Random(); + + var numberOfBytesToWrite = size; + + while (numberOfBytesToWrite > 0) + { + random.NextBytes(randomContent); + + var numberOfCharsToWrite = Math.Min(numberOfBytesToWrite, randomContent.Length); + stream.Write(randomContent, 0, numberOfCharsToWrite); + numberOfBytesToWrite -= numberOfCharsToWrite; + } + } + + protected static string CreateHash(Stream stream) + { + MD5 md5 = new MD5CryptoServiceProvider(); + var hash = md5.ComputeHash(stream); + return Encoding.ASCII.GetString(hash); + } + + protected static string CreateHash(byte[] buffer) + { + using (var ms = new MemoryStream(buffer)) + { + return CreateHash(ms); + } + } + + protected static string CreateFileHash(string path) + { + using (var fs = File.OpenRead(path)) + { + return CreateHash(fs); + } + } + + protected static string CreateTempFile(int size) + { + var file = Path.GetTempFileName(); + CreateFile(file, size); + return file; + } + + protected static void CreateFile(string fileName, int size) + { + using (var fs = File.OpenWrite(fileName)) + { + FillStream(fs, size); + } + } + + protected Stream GetManifestResourceStream(string resourceName) + { + var type = GetType(); + var resourceStream = type.Assembly.GetManifestResourceStream(resourceName); + if (resourceStream == null) + { + throw new ArgumentException($"Resource '{resourceName}' not found in assembly '{type.Assembly.FullName}'.", nameof(resourceName)); + } + return resourceStream; + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs b/src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs index 6c3ff2093..b98de1267 100644 --- a/src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs +++ b/src/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs @@ -20,11 +20,11 @@ public static InfrastructureFixture Instance } } - private IContainer? _sshServer; + private IContainer _sshServer; - private IFutureDockerImage? _sshServerImage; + private IFutureDockerImage _sshServerImage; - public string? SshServerHostName { get; set; } + public string SshServerHostName { get; set; } public ushort SshServerPort { get; set; } diff --git a/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs b/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs index ba9865d74..34194e095 100644 --- a/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs +++ b/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs @@ -10,7 +10,7 @@ public abstract class IntegrationTestBase /// /// The SSH Server host name. /// - public string? SshServerHostName + public string SshServerHostName { get { diff --git a/src/Renci.SshNet.IntegrationTests/Users.cs b/src/Renci.SshNet.IntegrationTests/Users.cs new file mode 100644 index 000000000..043ab63ec --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/Users.cs @@ -0,0 +1,8 @@ +namespace Renci.SshNet.IntegrationTests +{ + internal static class Users + { + public static readonly Credential Regular = new Credential("sshnet", "ssh4ever"); + public static readonly Credential Admin = new Credential("sshnetadm", "ssh4ever"); + } +} diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/id_dsa b/src/Renci.SshNet.IntegrationTests/resources/client/id_dsa new file mode 100644 index 000000000..6c84e0c65 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/id_dsa @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBuwIBAAKBgQC1Zd32ntjuKsLACveUlEoV9CjFDT/spf7k95Rh/U/2abZx0pa0 +8Z01lwpdIPdShHgmhJww4S8ZuGgr9QIOAf8r3DOLt+KpJmjS8ti4gMYqnaG2XDRu +tT6sKneSA5Kd/CwbJ/LZm9dsbvTWpaVHzokhAdXMgq+MxWTK5tMLXciUFQIVAK76 +I2Sp/9g4BiNisdIIcWZYB8RhAoGADlSeN+FAEdx5+pQOZ1jXxTrlFR91u5yWj9BU +CYiD8exlG3cTvarQzU21pFi93PasefgezpXuMTO3L8lz6zUFGAxwhZUvlHtsdyHi +a5HX2ZB/Xjz9ucuQNCeP3PvF170Go+MwOZ38Nd6MuT7cne3dyqubRAzPColXSIcJ +F41ANz0CgYEAm8IGZQatS7M6AfNITNWG4TI7Z2aRQjLb9/MWJIID7c/VQ4zdTZdG +3kpk0Gj9n4xreopK5NmYAdj8rtFfPBgmXltsLqt+bBcXkpxW//7WC29WOXW3t90y +STh+cWuWfr9fV7mf4Ql/6u/ZIgpQNvnNYezazt3fK8EXjI1dAXEuQxECFBhGOzk+ +Aimeob964E8+HsQNlyde +-----END DSA PRIVATE KEY----- diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/id_dsa.ppk b/src/Renci.SshNet.IntegrationTests/resources/client/id_dsa.ppk new file mode 100644 index 000000000..b73384f82 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/id_dsa.ppk @@ -0,0 +1,17 @@ +PuTTY-User-Key-File-2: ssh-dss +Encryption: none +Comment: imported-openssh-key +Public-Lines: 10 +AAAAB3NzaC1kc3MAAACBALVl3fae2O4qwsAK95SUShX0KMUNP+yl/uT3lGH9T/Zp +tnHSlrTxnTWXCl0g91KEeCaEnDDhLxm4aCv1Ag4B/yvcM4u34qkmaNLy2LiAxiqd +obZcNG61Pqwqd5IDkp38LBsn8tmb12xu9NalpUfOiSEB1cyCr4zFZMrm0wtdyJQV +AAAAFQCu+iNkqf/YOAYjYrHSCHFmWAfEYQAAAIAOVJ434UAR3Hn6lA5nWNfFOuUV +H3W7nJaP0FQJiIPx7GUbdxO9qtDNTbWkWL3c9qx5+B7Ole4xM7cvyXPrNQUYDHCF +lS+Ue2x3IeJrkdfZkH9ePP25y5A0J4/c+8XXvQaj4zA5nfw13oy5Ptyd7d3Kq5tE +DM8KiVdIhwkXjUA3PQAAAIEAm8IGZQatS7M6AfNITNWG4TI7Z2aRQjLb9/MWJIID +7c/VQ4zdTZdG3kpk0Gj9n4xreopK5NmYAdj8rtFfPBgmXltsLqt+bBcXkpxW//7W +C29WOXW3t90ySTh+cWuWfr9fV7mf4Ql/6u/ZIgpQNvnNYezazt3fK8EXjI1dAXEu +QxE= +Private-Lines: 1 +AAAAFBhGOzk+Aimeob964E8+HsQNlyde +Private-MAC: 1c254f3882a6661c98fb82dea1a55638a23633e5 diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/id_noaccess.rsa b/src/Renci.SshNet.IntegrationTests/resources/client/id_noaccess.rsa new file mode 100644 index 000000000..cf2cc9795 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/id_noaccess.rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEAuTtXn+BatX1oJuvhqfJZw5jc/pcIxJUPmuoFCH3+bXfKBJ/9 +4ixNETzZBasyvT/ozboAbCG3qcJOYxf2BEeTAIXe1jLAoTd1GKCwMvZOyjnsPN95 +/lChwfdnBbMzpZYTGfoUylXme/mzjjLu/J0qXgR5lyk9HFT+x5YEtRl8VSHiDkLK +TZ37dwhsqgcs+PkfvYMUK+C8evnfE0tgWgKZk0Eatl87nLWyVXB4LzhSDtGKLCPA +OgrX7fYfplDwJ2WK1N6nG0FnxW1HhDeSK7e2TbAa2vZQgvFXMWnO4O/NZKp4COpO +ReyliWhdtKAjr/+cD4yDfPjhjjKOYfxbvdRG4QIBIwKCAQAqVrTxV9o4HKoXhl93 +TVZYl/f/rX5Y0Z0quSW4zFdpendRg6e+qwpNFTjrWlS9ivNiOSSrAGR+ktAWpmQe +PD7bjFAw9ahfXSIUQfxja3+5Mc+Y4p+KlhZYOIyTlqy4Ik2CR8o84G8yR7QDPteK +Mo1XUXrguPgGedPV2SWlvK60XyAXqsewDhi7SeImZomKzbh33SXjVxakzHfa8BEU +eIIeR9oFlQMuYdo4GrHhFO2T+g/gqw/kVd1zkeEwt06fZVDErVwp+twewxxvwrk4 +CKUCzavfhDfi5sJ5YdzhDBRgkyBgJI+f15dKyqqOiAparV9+uzrD6vIuNnlVoqQA +iugLAoGBAPBliy32e83nshBknBn5HOK2rO3a1zHxvYr/NzITXtdZOjatNyfXtkwi +Ll/el5tZhJvKe9nItSI/4w7mvlvXZfW8h3MR0qb8at4jWa8ya2hwEerqaJonqjjb ++eBhg27ltZIQRk8Bv6ApXTAWkc+dFGhEIysokDQX7V72Bdrizup1AoGBAMVBLHK0 +5IFb8x7danlAmDX6bqCObId4Pce2OeONFIj1jIowvCXaE0t9zU4X5SdN5ujqu4Dq +XgzUdNeKcJxWpFO74MDRxT3CbMz36fikJnvxWl/+q0HalYuCY8gm14VYcThUBAro +3c941INueybGNLIA9jc7RMnsFtyVTvNYpaU9AoGAFJr9TRUgjf3qsPKuS15+0Zqh +G7OsC5hgtCSBEuu3rA72XHU/Pe3rDdcLSgvD2h2dpvQZPo2L3l0/WQx2t2o78H3f +uWftfAcB2Iav6nIJNNZn75BvXaug4E1ej5NUaJdYtL+Q/3UtrqR1s6opwVabWWTt +ElPvGmhzboodwk30en8CgYAyuPzNCfGdm00lMZ8JPH7pTwaBDq4xdrDM9FgHUCna +E0FlXP0uTgT2J6nSQKijtPI75JadfhgvL1E+vTLmX2wViBU45XvcrlZ92Vlr0nBL +wbgnUB1otIzauyD49AuIsFegxSWcZ8QCJmKIMlouir0X1FyR3Apfzv6Qfio+kyNH +vwKBgQCtwxojkzUSfV3zDt6bYSLBzgXgo/Zr9lS+gSggP72DzINmW2gbA0fkM2Zu +JltcfakKv4gVX/1zooz+7t+4bj6dqt+bl7hYz0VnTSDZGuo5LKDif/4gSGrdblC2 +QLTuX2HjWCZdsue7mRwL7cXR4zlIoE99+Ryhdxvc5wHSfYr/JA== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa b/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa new file mode 100644 index 000000000..da8f397ea --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuDdE+laKZ8D0mEFMNMGkDYQY89A2BUML5L+DsFmEn//3yRdz +Omx+Nk59ifiJUPuP3whZxN5vfbHleU9ZshI+37LqTbXmNrChRlxDSxrQA/++hTNI +gZl8e+sZUk8mUkpjQyEVOMVEdt3H3PmpfOBeZjtpRJPuWa90J+SVKbnAX4rOL7bt +LDA4GeMNXdLXLl5E/zBCZ9ol9nJ8W0suajQDx7u2/ixH1wb9jqzA6j68f1+kERII +t8OUcS+ZM+274rVrCMCL290k9gEQAKwcG/KRMqdgP8Oadtq5ELS/t4m2fH5JqYJ9 +9399QGT4LTPvoiTwZjyhYwK3FhCfXyFQf+gwIwIDAQABAoIBAGLChsFrKfJr2PXT +dAaIleoFGteDlaKGilbNcc1WgKrCsNXnM4hr59I3jEgurXd0FnKs6GuKEN2jRPIf +X2f/LiQBqGmXDl/dm+i7x/v42PJ75mlE0Cdi4QESTlX5RwMxDDxN/TGdWJIdXmwS +kRH4u8M1ML9qS4tba/uDKZDgG8lcF/z6K0RbXDMxL0azfbd5e+jca1e8Fs93X5s7 +mJotXaA+L33R7lCpBBOa1OY517Ug7bdI+uWh59o0bw8v2q8vr8ISOHU3N+JvKZ62 +2z5O6lLyB94sF6ltXRv9pmfcSBjfB8CJx5q1yejUZKM6VfN98MTSKo8WKMEtGmqk +BtZFTlECgYEA8WYG6vntqXPCHhFFfgCymh8OoXL/mPDSkqsswKfeD4O+Ml9hRUtg +xl6FDxFAMR7WJ4Vb8u9IMOc7Xx+nzlZNsdC5m7FAVRIEUPPjEucte+ZYKjSy+WOd +dAtQ07O3z9fi+JNplSjisKtBWaemfqc2TcYXOeIIgwJnkaCf2C7I2bsCgYEAw1vJ +9c5VLTisPj7ijMeGLWISG5E0aidOrb15E8xcnXuT9TEW1Dc1EgRK/D03tlnoxr1I +CISPx4EmdLTiEl2AVi33DOhCeFAt8TOd/y3chKsbewb+BYEMmBD93mhsKg+YmC5E +284SCV3fCcyFJfo9oy5Z2tIELerjT0cnpHgPCLkCgYEAij3OemRUeTUklol3jXgi +z+Y3P7gWreREAuBqSY4YujPNCRXcI43ORuu8MWvEohyxsYJKrO3hHrhdJNWBCMYd +ylXo5UN1vwIJXL6+bIXdY1X/aXQyhmVItzr/t6z0997/STFKRrRaVahNTWWYEHH7 +xEBL7scF7tjCrQAaafgo558CgYBkrrm3ZU+grsSWj/JSe8I7QX/zlTJeQ0PZZv0v +pvNUdowaoeISHSHM10mOFj7QTCYbxxGI0kkHmRgordCVhnrN74KTtGANgcUrul6D +VS+BcG4JSeFBFPFYreko5shYJRGP3MjAP8Qr76Uzd6RnnkCGCS1mCTb+M0BTa2iS +6w1UgQKBgQDQ2qV4s7xH3dixy4MWhDBFmrQlFpQkNNkrJ/ImHrxI2tFyNaq1fE+Q +PrXJi8mjwb/ETVN2C5iBTtIyVg1pZk3YAWAvIt9SPaRVYQWj8IJeOTTaNEZEvp5K +1LJBWO0ksgJK28f/z7FwejeGbBwg8ch9wVhtFwIV++rZ76smP7C+9Q== +-----END RSA PRIVATE KEY----- diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa.pub b/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa.pub new file mode 100644 index 000000000..24ba2f7dd --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4N0T6VopnwPSYQUw0waQNhBjz0DYFQwvkv4OwWYSf//fJF3M6bH42Tn2J+IlQ+4/fCFnE3m99seV5T1myEj7fsupNteY2sKFGXENLGtAD/76FM0iBmXx76xlSTyZSSmNDIRU4xUR23cfc+al84F5mO2lEk+5Zr3Qn5JUpucBfis4vtu0sMDgZ4w1d0tcuXkT/MEJn2iX2cnxbSy5qNAPHu7b+LEfXBv2OrMDqPrx/X6QREgi3w5RxL5kz7bvitWsIwIvb3ST2ARAArBwb8pEyp2A/w5p22rkQtL+3ibZ8fkmpgn33f31AZPgtM++iJPBmPKFjArcWEJ9fIVB/6DAj diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_256_openssh b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_256_openssh new file mode 100644 index 000000000..29ecb9073 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_256_openssh @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQT886z6SLRIzRu7VNA6SSeKZCNNRPXe +iutTik1T3RUEshgnTI/V3T/d5QurCQPvf2ob3+Rd4FhCsVCS9gilIhVsAAAAsH3KX4d9yl ++HAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPzzrPpItEjNG7tU +0DpJJ4pkI01E9d6K61OKTVPdFQSyGCdMj9XdP93lC6sJA+9/ahvf5F3gWEKxUJL2CKUiFW +wAAAAgYxeSyo7MVNup52COOCarcvARKlWhKIP2CKzj4qa5/6EAAAAYc3NobmV0QFVidW50 +dTE5MTBEZXNrdG9w +-----END OPENSSH PRIVATE KEY----- diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_256_openssh.pub b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_256_openssh.pub new file mode 100644 index 000000000..33524cea6 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_256_openssh.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPzzrPpItEjNG7tU0DpJJ4pkI01E9d6K61OKTVPdFQSyGCdMj9XdP93lC6sJA+9/ahvf5F3gWEKxUJL2CKUiFWw= sshnet@Ubuntu1910Desktop diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_384_openssh b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_384_openssh new file mode 100644 index 000000000..e720d2407 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_384_openssh @@ -0,0 +1,10 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS +1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQS0rLvxzSomgC4UNulA7/jdABXTG9un +zFazinvOughrumo0n/R1DoSRXY4bHooQdq02pTD6/DK3FzS7n4ouJi/LuJfX1EFxYMJzP2 +aYlT0rOgvvIuLv2Q4OzcdjV8mzSIEAAADo1T5ZUtU+WVIAAAATZWNkc2Etc2hhMi1uaXN0 +cDM4NAAAAAhuaXN0cDM4NAAAAGEEtKy78c0qJoAuFDbpQO/43QAV0xvbp8xWs4p7zroIa7 +pqNJ/0dQ6EkV2OGx6KEHatNqUw+vwytxc0u5+KLiYvy7iX19RBcWDCcz9mmJU9KzoL7yLi +79kODs3HY1fJs0iBAAAAMQDgRb336Dk9e4VxOpSqnwBqHRsJ3QmSME9qMBvx5SXykHFAsK +wzVKEvIQizmg/+sWcAAAAYc3NobmV0QFVidW50dTE5MTBEZXNrdG9wAQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_384_openssh.pub b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_384_openssh.pub new file mode 100644 index 000000000..99878d2ca --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_384_openssh.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBLSsu/HNKiaALhQ26UDv+N0AFdMb26fMVrOKe866CGu6ajSf9HUOhJFdjhseihB2rTalMPr8MrcXNLufii4mL8u4l9fUQXFgwnM/ZpiVPSs6C+8i4u/ZDg7Nx2NXybNIgQ== sshnet@Ubuntu1910Desktop diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_521_openssh b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_521_openssh new file mode 100644 index 000000000..47ee8ff8b --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_521_openssh @@ -0,0 +1,12 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS +1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQAgeFoEYBgUaOlJPnEYIPLSTwmxLRl +EUVpVAOzww3q10fj/Tuppty/fRLcbMoeVzWKl8mjDbR+XOdaKDGo6xcHGsgByITz2/F9wr +E8BHyFEPemg8h0DKLW0X55J+rnn3lE0a0jXKngJ3VLVcgKgXam7KtpoCWFx689jVCpTxWI +GrkIvlkAAAEYr0LrEa9C6xEAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ +AAAIUEAIHhaBGAYFGjpST5xGCDy0k8JsS0ZRFFaVQDs8MN6tdH4/07qabcv30S3GzKHlc1 +ipfJow20flznWigxqOsXBxrIAciE89vxfcKxPAR8hRD3poPIdAyi1tF+eSfq5595RNGtI1 +yp4Cd1S1XICoF2puyraaAlhcevPY1QqU8ViBq5CL5ZAAAAQQ50pmBidmKTIknaRpdO5WIu +nYEGMUkLqdZ0egk9Ggg63mOHiLykf+XWcGbHbHM95CISXhqlvMtCYeGwOpP6FoMGAAAAGH +NzaG5ldEBVYnVudHUxOTEwRGVza3RvcAECAw== +-----END OPENSSH PRIVATE KEY----- diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_521_openssh.pub b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_521_openssh.pub new file mode 100644 index 000000000..085fa07bf --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/key_ecdsa_521_openssh.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACB4WgRgGBRo6Uk+cRgg8tJPCbEtGURRWlUA7PDDerXR+P9O6mm3L99Etxsyh5XNYqXyaMNtH5c51ooMajrFwcayAHIhPPb8X3CsTwEfIUQ96aDyHQMotbRfnkn6uefeUTRrSNcqeAndUtVyAqBdqbsq2mgJYXHrz2NUKlPFYgauQi+WQ== sshnet@Ubuntu1910Desktop diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/key_ed25519_openssh b/src/Renci.SshNet.IntegrationTests/resources/client/key_ed25519_openssh new file mode 100644 index 000000000..0bcb6e755 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/key_ed25519_openssh @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAJDRj1Tk7s7ik4Bnx3L3tjEY+e8l4ZmYJFMovUaZia5QAAAKBKTcfHSk3H +xwAAAAtzc2gtZWQyNTUxOQAAACAJDRj1Tk7s7ik4Bnx3L3tjEY+e8l4ZmYJFMovUaZia5Q +AAAEBMMOaGa8RU8Vy0vLFlRT5iVSxl3ji9NBKaO/RS0aFL3QkNGPVOTuzuKTgGfHcve2MR +j57yXhmZgkUyi9RpmJrlAAAAGHNzaG5ldEBVYnVudHUxOTEwRGVza3RvcAECAwQF +-----END OPENSSH PRIVATE KEY----- diff --git a/src/Renci.SshNet.IntegrationTests/resources/issue #70.png b/src/Renci.SshNet.IntegrationTests/resources/issue #70.png new file mode 100644 index 0000000000000000000000000000000000000000..8c9723796f2280bc537b6e8097277bffc6d384e9 GIT binary patch literal 312036 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

866!RtPY&1#&$s>W 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

CTT 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 literal 0 HcmV?d00001 diff --git a/src/Renci.SshNet.TestTools.OpenSSH/Renci.SshNet.TestTools.OpenSSH.csproj b/src/Renci.SshNet.TestTools.OpenSSH/Renci.SshNet.TestTools.OpenSSH.csproj index 3eb6ff527..bd59aa4e5 100644 --- a/src/Renci.SshNet.TestTools.OpenSSH/Renci.SshNet.TestTools.OpenSSH.csproj +++ b/src/Renci.SshNet.TestTools.OpenSSH/Renci.SshNet.TestTools.OpenSSH.csproj @@ -13,7 +13,7 @@ We can stop producing XML docs for test projects (and remove the NoWarn for CS1591) once the following issue is fixed: https://github.com/dotnet/roslyn/issues/41640. --> - $(NoWarn);CS1591 + $(NoWarn);CS1591;SYSLIB0021;SYSLIB1045 From e44f631a84ca9d1d44ea23526f9818416f7c0572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Thu, 7 Sep 2023 06:55:13 +0200 Subject: [PATCH 04/15] Move old tests to new integration tests --- .../AuthenticationTests.cs | 45 ++ .../Common/Extensions.cs | 41 -- .../Common/PosixPath.cs | 38 -- .../ConnectivityTests.cs | 25 + .../OldIntegrationTests/AesCipherTests.cs | 148 +++++ .../ForwardedPortLocalTest.cs | 158 +++++ .../OldIntegrationTests/HMacTest.cs | 67 ++ .../SftpClientTest.CreateDirectory.cs | 72 +++ .../SftpClientTest.DeleteDirectory.cs | 71 +++ .../SftpClientTest.Download.cs | 108 ++++ .../SftpClientTest.ListDirectory.cs | 263 ++++++++ .../SftpClientTest.RenameFile.cs | 53 ++ .../SftpClientTest.RenameFileAsync.cs | 56 ++ .../SftpClientTest.SynchronizeDirectories.cs | 80 +++ .../SftpClientTest.Upload.cs | 378 ++++++++++++ .../OldIntegrationTests/SftpClientTest.cs | 68 +++ .../OldIntegrationTests/SshCommandTest.cs | 545 +++++++++++++++++ .../RemoteSshd.cs | 4 +- src/Renci.SshNet.IntegrationTests/ScpTests.cs | 1 - .../TestsFixtures/IntegrationTestBase.cs | 19 + .../Classes/ForwardedPortLocalTest.cs | 222 ------- .../Classes/ForwardedPortRemoteTest.cs | 103 ---- .../KeyboardInteractiveConnectionInfoTest.cs | 40 +- .../PasswordAuthenticationMethodTest.cs | 29 +- .../Cryptography/Ciphers/AesCipherTest.cs | 122 +--- .../Cryptography/Ciphers/Arc4CipherTest.cs | 38 +- .../Classes/Security/Cryptography/HMacTest.cs | 134 ---- .../Classes/Sftp/SftpFileTest.cs | 122 +--- .../Classes/SshCommandTest.cs | 574 ------------------ src/Renci.SshNet/Properties/AssemblyInfo.cs | 1 + 30 files changed, 2167 insertions(+), 1458 deletions(-) delete mode 100644 src/Renci.SshNet.IntegrationTests/Common/Extensions.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Common/PosixPath.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/AesCipherTests.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/ForwardedPortLocalTest.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/HMacTest.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.CreateDirectory.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.DeleteDirectory.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.RenameFile.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.RenameFileAsync.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.SynchronizeDirectories.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/Security/Cryptography/HMacTest.cs diff --git a/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs b/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs index 5d36bc148..ff3f02365 100644 --- a/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs +++ b/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs @@ -255,8 +255,10 @@ public void KeyboardInteractive_PasswordExpired() int authenticationPromptCount = 0; keyboardInteractive.AuthenticationPrompt += (sender, args) => { + Console.WriteLine(args.Instruction); foreach (var authenticationPrompt in args.Prompts) { + Console.WriteLine(authenticationPrompt.Request); switch (authenticationPromptCount) { case 0: @@ -288,5 +290,48 @@ public void KeyboardInteractive_PasswordExpired() Assert.AreEqual(4, authenticationPromptCount); } } + + [TestMethod] + public void KeyboardInteractiveConnectionInfo() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive") + .WithChallengeResponseAuthentication(true) + .WithKeyboardInteractiveAuthentication(true) + .WithUsePAM(true) + .Update() + .Restart(); + + var host = SshServerHostName; + var port = SshServerPort; + var username = User.UserName; + var password = User.Password; + + #region Example KeyboardInteractiveConnectionInfo AuthenticationPrompt + + var connectionInfo = new KeyboardInteractiveConnectionInfo(host, port, username); + connectionInfo.AuthenticationPrompt += delegate (object sender, AuthenticationPromptEventArgs e) + { + Console.WriteLine(e.Instruction); + + foreach (var prompt in e.Prompts) + { + Console.WriteLine(prompt.Request); + prompt.Response = password; + } + }; + + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + + // Do something here + client.Disconnect(); + } + + #endregion + + Assert.AreEqual(connectionInfo.Host, SshServerHostName); + Assert.AreEqual(connectionInfo.Username, User.UserName); + } } } diff --git a/src/Renci.SshNet.IntegrationTests/Common/Extensions.cs b/src/Renci.SshNet.IntegrationTests/Common/Extensions.cs deleted file mode 100644 index a3ac0bdd4..000000000 --- a/src/Renci.SshNet.IntegrationTests/Common/Extensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace Renci.SshNet.IntegrationTests.Common -{ - /// - /// Collection of different extension method - /// - internal static partial class Extensions - { - public static bool IsEqualTo(this byte[] left, byte[] right) - { - if (left == null) - { - throw new ArgumentNullException("left"); - } - - if (right == null) - { - throw new ArgumentNullException("right"); - } - - if (left == right) - { - return true; - } - - if (left.Length != right.Length) - { - return false; - } - - for (var i = 0; i < left.Length; i++) - { - if (left[i] != right[i]) - { - return false; - } - } - - return true; - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Common/PosixPath.cs b/src/Renci.SshNet.IntegrationTests/Common/PosixPath.cs deleted file mode 100644 index 6fd468dcc..000000000 --- a/src/Renci.SshNet.IntegrationTests/Common/PosixPath.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Renci.SshNet.IntegrationTests.Common -{ - internal class PosixPath - { - /// - /// Gets the file name part of a given POSIX path. - /// - /// The POSIX path to get the file name for. - /// - /// The file name part of . - /// - /// is null. - /// - /// - /// If contains no forward slash, then - /// is returned. - /// - /// - /// If path has a trailing slash, but return a zero-length string. - /// - /// - public static string GetFileName(string path) - { - var pathEnd = path.LastIndexOf('/'); - if (pathEnd == -1) - { - return path; - } - - if (pathEnd == path.Length - 1) - { - return string.Empty; - } - - return path.Substring(pathEnd + 1); - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs index 90e2739be..1cf7a9d52 100644 --- a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs +++ b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs @@ -382,6 +382,31 @@ public void Common_ServerRejectsConnection() } } + + [TestMethod] + [WorkItem(1140)] + [Description("Test whether IsConnected is false after disconnect.")] + [Owner("Kenneth_aa")] + public void Test_BaseClient_IsConnected_True_After_Disconnect() + { + // 2012-04-29 - Kenneth_aa + // The problem with this test, is that after SSH Net calls .Disconnect(), the library doesn't wait + // for the server to confirm disconnect before IsConnected is checked. And now I'm not mentioning + // anything about Socket's either. + + var connectionInfo = new PasswordAuthenticationMethod(User.UserName, User.Password); + + using (SftpClient client = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + Assert.AreEqual(true, client.IsConnected, "IsConnected is not true after Connect() was called."); + + client.Disconnect(); + + Assert.AreEqual(false, client.IsConnected, "IsConnected is true after Disconnect() was called."); + } + } + private static void DisableHostNetworkConnection(string networkConnection) { SelectQuery wmiQuery = new SelectQuery("SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionId != NULL"); diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/AesCipherTests.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/AesCipherTests.cs new file mode 100644 index 000000000..d93dd5e66 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/AesCipherTests.cs @@ -0,0 +1,148 @@ +using Renci.SshNet.IntegrationTests.Common; +using Renci.SshNet.Security.Cryptography.Ciphers; +using Renci.SshNet.Security.Cryptography.Ciphers.Modes; +using Renci.SshNet.TestTools.OpenSSH; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + [TestClass] + public class AesCipherTests : IntegrationTestBase + { + private IConnectionInfoFactory _adminConnectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); + _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + } + + + [TestMethod] + [Owner("olegkap")] + [TestCategory("Cipher")] + public void Test_Cipher_AEes128CBC_Connection() + { + _remoteSshdConfig.AddCipher(Cipher.Aes128Cbc) + .Update() + .Restart(); + + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.Encryptions.Clear(); + connectionInfo.Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), null); })); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + [Owner("olegkap")] + [TestCategory("Cipher")] + public void Test_Cipher_Aes192CBC_Connection() + { + _remoteSshdConfig.AddCipher(Cipher.Aes192Cbc) + .Update() + .Restart(); + + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.Encryptions.Clear(); + connectionInfo.Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), null); })); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + [Owner("olegkap")] + [TestCategory("Cipher")] + public void Test_Cipher_Aes256CBC_Connection() + { + _remoteSshdConfig.AddCipher(Cipher.Aes256Cbc) + .Update() + .Restart(); + + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.Encryptions.Clear(); + connectionInfo.Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), null); })); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + [Owner("olegkap")] + [TestCategory("Cipher")] + public void Test_Cipher_Aes128CTR_Connection() + { + _remoteSshdConfig.AddCipher(Cipher.Aes128Ctr) + .Update() + .Restart(); + + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.Encryptions.Clear(); + connectionInfo.Encryptions.Add("aes128-ctr", new CipherInfo(128, (key, iv) => { return new AesCipher(key, new CtrCipherMode(iv), null); })); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + [Owner("olegkap")] + [TestCategory("Cipher")] + public void Test_Cipher_Aes192CTR_Connection() + { + _remoteSshdConfig.AddCipher(Cipher.Aes192Ctr) + .Update() + .Restart(); + + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.Encryptions.Clear(); + connectionInfo.Encryptions.Add("aes192-ctr", new CipherInfo(192, (key, iv) => { return new AesCipher(key, new CtrCipherMode(iv), null); })); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + [Owner("olegkap")] + [TestCategory("Cipher")] + public void Test_Cipher_Aes256CTR_Connection() + { + _remoteSshdConfig.AddCipher(Cipher.Aes256Ctr) + .Update() + .Restart(); + + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.Encryptions.Clear(); + connectionInfo.Encryptions.Add("aes256-ctr", new CipherInfo(256, (key, iv) => { return new AesCipher(key, new CtrCipherMode(iv), null); })); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/ForwardedPortLocalTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/ForwardedPortLocalTest.cs new file mode 100644 index 000000000..bf6292faa --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/ForwardedPortLocalTest.cs @@ -0,0 +1,158 @@ +using System.Diagnostics; + +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Provides functionality for local port forwarding + /// + [TestClass] + public class ForwardedPortLocalTest : IntegrationTestBase + { + [TestMethod] + [WorkItem(713)] + [Owner("Kenneth_aa")] + [TestCategory("PortForwarding")] + [Description("Test if calling Stop on ForwardedPortLocal instance causes wait.")] + public void Test_PortForwarding_Local_Stop_Hangs_On_Wait() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + + var port1 = new ForwardedPortLocal("localhost", 8084, "www.google.com", 80); + client.AddForwardedPort(port1); + port1.Exception += delegate (object sender, ExceptionEventArgs e) + { + Assert.Fail(e.Exception.ToString()); + }; + + port1.Start(); + + var hasTestedTunnel = false; + + _ = ThreadPool.QueueUserWorkItem(delegate (object state) + { + try + { + var url = "http://www.google.com/"; + Debug.WriteLine("Starting web request to \"" + url + "\""); + +#if NET6_0_OR_GREATER + var httpClient = new HttpClient(); + var response = httpClient.GetAsync(url) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); +#else + var request = (HttpWebRequest) WebRequest.Create(url); + var response = (HttpWebResponse) request.GetResponse(); +#endif // NET6_0_OR_GREATER + + Assert.IsNotNull(response); + + Debug.WriteLine("Http Response status code: " + response.StatusCode.ToString()); + + response.Dispose(); + + hasTestedTunnel = true; + } + catch (Exception ex) + { + Assert.Fail(ex.ToString()); + } + }); + + // Wait for the web request to complete. + while (!hasTestedTunnel) + { + Thread.Sleep(1000); + } + + try + { + // Try stop the port forwarding, wait 3 seconds and fail if it is still started. + _ = ThreadPool.QueueUserWorkItem(delegate (object state) + { + Debug.WriteLine("Trying to stop port forward."); + port1.Stop(); + Debug.WriteLine("Port forwarding stopped."); + }); + + Thread.Sleep(3000); + if (port1.IsStarted) + { + Assert.Fail("Port forwarding not stopped."); + } + } + catch (Exception ex) + { + Assert.Fail(ex.ToString()); + } + client.RemoveForwardedPort(port1); + client.Disconnect(); + Debug.WriteLine("Success."); + } + } + + [TestMethod] + [ExpectedException(typeof(SshConnectionException))] + public void Test_PortForwarding_Local_Without_Connecting() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + var port1 = new ForwardedPortLocal("localhost", 8084, "www.renci.org", 80); + client.AddForwardedPort(port1); + port1.Exception += delegate (object sender, ExceptionEventArgs e) + { + Assert.Fail(e.Exception.ToString()); + }; + port1.Start(); + + var test = Parallel.For(0, + 100, + counter => + { + var start = DateTime.Now; + +#if NET6_0_OR_GREATER + var httpClient = new HttpClient(); + using (var response = httpClient.GetAsync("http://localhost:8084").GetAwaiter().GetResult()) + { + var data = ReadStream(response.Content.ReadAsStream()); +#else + var request = (HttpWebRequest) WebRequest.Create("http://localhost:8084"); + using (var response = (HttpWebResponse) request.GetResponse()) + { + var data = ReadStream(response.GetResponseStream()); +#endif // NET6_0_OR_GREATER + var end = DateTime.Now; + + Debug.WriteLine(string.Format("Request# {2}: Lenght: {0} Time: {1}", data.Length, end - start, counter)); + } + }); + } + } + + private static byte[] ReadStream(Stream stream) + { + var buffer = new byte[1024]; + using (var ms = new MemoryStream()) + { + while (true) + { + var read = stream.Read(buffer, 0, buffer.Length); + if (read > 0) + { + ms.Write(buffer, 0, read); + } + else + { + return ms.ToArray(); + } + } + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/HMacTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/HMacTest.cs new file mode 100644 index 000000000..c8df34e9d --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/HMacTest.cs @@ -0,0 +1,67 @@ +using Renci.SshNet.Abstractions; +using Renci.SshNet.IntegrationTests.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + [TestClass] + public class HMacTest : IntegrationTestBase + { + private IConnectionInfoFactory _adminConnectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); + _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + } + + [TestMethod] + public void Test_HMac_Sha1_Connection() + { + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.HmacAlgorithms.Clear(); + connectionInfo.HmacAlgorithms.Add("hmac-sha1", new HashInfo(20 * 8, CryptoAbstraction.CreateHMACSHA1)); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void Test_HMac_Sha256_Connection() + { + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.HmacAlgorithms.Clear(); + connectionInfo.HmacAlgorithms.Add("hmac-sha2-256", new HashInfo(32 * 8, CryptoAbstraction.CreateHMACSHA256)); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + + [TestMethod] + public void Test_HMac_Sha2_512_Connection() + { + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.HmacAlgorithms.Clear(); + connectionInfo.HmacAlgorithms.Add("hmac-sha2-512", new HashInfo(64 * 8, CryptoAbstraction.CreateHMACSHA512)); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.CreateDirectory.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.CreateDirectory.cs new file mode 100644 index 000000000..43b176697 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.CreateDirectory.cs @@ -0,0 +1,72 @@ + +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_CreateDirectory_In_Current_Location() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.CreateDirectory("test-in-current"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPermissionDeniedException))] + public void Test_Sftp_CreateDirectory_In_Forbidden_Directory() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, AdminUser.UserName, AdminUser.Password)) + { + sftp.Connect(); + + sftp.CreateDirectory("/sbin/test"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPathNotFoundException))] + public void Test_Sftp_CreateDirectory_Invalid_Path() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.CreateDirectory("/abcdefg/abcefg"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SshException))] + public void Test_Sftp_CreateDirectory_Already_Exists() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.CreateDirectory("test"); + + sftp.CreateDirectory("test"); + + sftp.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.DeleteDirectory.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.DeleteDirectory.cs new file mode 100644 index 000000000..30697ddce --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.DeleteDirectory.cs @@ -0,0 +1,71 @@ +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPathNotFoundException))] + public void Test_Sftp_DeleteDirectory_Which_Doesnt_Exists() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.DeleteDirectory("abcdef"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPermissionDeniedException))] + public void Test_Sftp_DeleteDirectory_Which_No_Permissions() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, AdminUser.UserName, AdminUser.Password)) + { + sftp.Connect(); + + sftp.DeleteDirectory("/usr"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_DeleteDirectory() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.CreateDirectory("abcdef"); + sftp.DeleteDirectory("abcdef"); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to DeleteDirectory.")] + [ExpectedException(typeof(ArgumentException))] + public void Test_Sftp_DeleteDirectory_Null() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.DeleteDirectory(null); + + sftp.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs new file mode 100644 index 000000000..318834e4c --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Download.cs @@ -0,0 +1,108 @@ +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPermissionDeniedException))] + public void Test_Sftp_Download_Forbidden() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, AdminUser.UserName, AdminUser.Password)) + { + sftp.Connect(); + + string remoteFileName = "/root/.profile"; + + using (var ms = new MemoryStream()) + { + sftp.DownloadFile(remoteFileName, ms); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPathNotFoundException))] + public void Test_Sftp_Download_File_Not_Exists() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + string remoteFileName = "/xxx/eee/yyy"; + using (var ms = new MemoryStream()) + { + sftp.DownloadFile(remoteFileName, ms); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to BeginDownloadFile")] + [ExpectedException(typeof(ArgumentNullException))] + public void Test_Sftp_BeginDownloadFile_StreamIsNull() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + sftp.BeginDownloadFile("aaaa", null, null, null); + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to BeginDownloadFile")] + [ExpectedException(typeof(ArgumentException))] + public void Test_Sftp_BeginDownloadFile_FileNameIsWhiteSpace() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + sftp.BeginDownloadFile(" ", new MemoryStream(), null, null); + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to BeginDownloadFile")] + [ExpectedException(typeof(ArgumentException))] + public void Test_Sftp_BeginDownloadFile_FileNameIsNull() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + sftp.BeginDownloadFile(null, new MemoryStream(), null, null); + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(ArgumentException))] + public void Test_Sftp_EndDownloadFile_Invalid_Async_Handle() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + var filename = Path.GetTempFileName(); + CreateTestFile(filename, 1); + sftp.UploadFile(File.OpenRead(filename), "test123"); + var async1 = sftp.BeginListDirectory("/", null, null); + var async2 = sftp.BeginDownloadFile("test123", new MemoryStream(), null, null); + sftp.EndDownloadFile(async1); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs new file mode 100644 index 000000000..e37e937f9 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ListDirectory.cs @@ -0,0 +1,263 @@ +using System.Diagnostics; + +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPermissionDeniedException))] + public void Test_Sftp_ListDirectory_Permission_Denied() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var files = sftp.ListDirectory("/root"); + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPathNotFoundException))] + public void Test_Sftp_ListDirectory_Not_Exists() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var files = sftp.ListDirectory("/asdfgh"); + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_ListDirectory_Current() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var files = sftp.ListDirectory("."); + + Assert.IsTrue(files.Count() > 0); + + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + +#if NET6_0_OR_GREATER + [TestMethod] + [TestCategory("Sftp")] + public async Task Test_Sftp_ListDirectoryAsync_Current() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(1)); + var count = 0; + await foreach(var file in sftp.ListDirectoryAsync(".", cts.Token)) + { + count++; + Debug.WriteLine(file.FullName); + } + + Assert.IsTrue(count > 0); + + sftp.Disconnect(); + } + } +#endif + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_ListDirectory_Empty() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var files = sftp.ListDirectory(string.Empty); + + Assert.IsTrue(files.Count() > 0); + + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to ListDirectory.")] + [ExpectedException(typeof(ArgumentNullException))] + public void Test_Sftp_ListDirectory_Null() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var files = sftp.ListDirectory(null); + + Assert.IsTrue(files.Count() > 0); + + foreach (var file in files) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_ListDirectory_HugeDirectory() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + // Create 10000 directory items + for (int i = 0; i < 10000; i++) + { + sftp.CreateDirectory(string.Format("test_{0}", i)); + } + + var files = sftp.ListDirectory("."); + + // Ensure that directory has at least 10000 items + Assert.IsTrue(files.Count() > 10000); + + sftp.Disconnect(); + } + + RemoveAllFiles(); + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Change_Directory() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet"); + + sftp.CreateDirectory("test1"); + + sftp.ChangeDirectory("test1"); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet/test1"); + + sftp.CreateDirectory("test1_1"); + sftp.CreateDirectory("test1_2"); + sftp.CreateDirectory("test1_3"); + + var files = sftp.ListDirectory("."); + + Assert.IsTrue(files.First().FullName.StartsWith(string.Format("{0}", sftp.WorkingDirectory))); + + sftp.ChangeDirectory("test1_1"); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet/test1/test1_1"); + + sftp.ChangeDirectory("../test1_2"); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet/test1/test1_2"); + + sftp.ChangeDirectory(".."); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet/test1"); + + sftp.ChangeDirectory(".."); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet"); + + files = sftp.ListDirectory("test1/test1_1"); + + Assert.IsTrue(files.First().FullName.StartsWith(string.Format("{0}/test1/test1_1", sftp.WorkingDirectory))); + + sftp.ChangeDirectory("test1/test1_1"); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet/test1/test1_1"); + + sftp.ChangeDirectory("/home/sshnet/test1/test1_1"); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet/test1/test1_1"); + + sftp.ChangeDirectory("/home/sshnet/test1/test1_1/../test1_2"); + + Assert.AreEqual(sftp.WorkingDirectory, "/home/sshnet/test1/test1_2"); + + sftp.ChangeDirectory("../../"); + + sftp.DeleteDirectory("test1/test1_1"); + sftp.DeleteDirectory("test1/test1_2"); + sftp.DeleteDirectory("test1/test1_3"); + sftp.DeleteDirectory("test1"); + + sftp.Disconnect(); + } + + RemoveAllFiles(); + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to ChangeDirectory.")] + [ExpectedException(typeof(ArgumentNullException))] + public void Test_Sftp_ChangeDirectory_Null() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.ChangeDirectory(null); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test calling EndListDirectory method more then once.")] + [ExpectedException(typeof(ArgumentException))] + public void Test_Sftp_Call_EndListDirectory_Twice() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + var ar = sftp.BeginListDirectory("/", null, null); + var result = sftp.EndListDirectory(ar); + var result1 = sftp.EndListDirectory(ar); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.RenameFile.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.RenameFile.cs new file mode 100644 index 000000000..e74a8e7ed --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.RenameFile.cs @@ -0,0 +1,53 @@ +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Rename_File() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + string uploadedFileName = Path.GetTempFileName(); + string remoteFileName1 = Path.GetRandomFileName(); + string remoteFileName2 = Path.GetRandomFileName(); + + CreateTestFile(uploadedFileName, 1); + + using (var file = File.OpenRead(uploadedFileName)) + { + sftp.UploadFile(file, remoteFileName1); + } + + sftp.RenameFile(remoteFileName1, remoteFileName2); + + File.Delete(uploadedFileName); + + sftp.Disconnect(); + } + + RemoveAllFiles(); + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to RenameFile.")] + [ExpectedException(typeof(ArgumentNullException))] + public void Test_Sftp_RenameFile_Null() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.RenameFile(null, null); + + sftp.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.RenameFileAsync.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.RenameFileAsync.cs new file mode 100644 index 000000000..ade91099c --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.RenameFileAsync.cs @@ -0,0 +1,56 @@ +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + public async Task Test_Sftp_RenameFileAsync() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + await sftp.ConnectAsync(default); + + string uploadedFileName = Path.GetTempFileName(); + string remoteFileName1 = Path.GetRandomFileName(); + string remoteFileName2 = Path.GetRandomFileName(); + + CreateTestFile(uploadedFileName, 1); + + using (var file = File.OpenRead(uploadedFileName)) + { + using (Stream remoteStream = await sftp.OpenAsync(remoteFileName1, FileMode.CreateNew, FileAccess.Write, default)) + { + await file.CopyToAsync(remoteStream, 81920, default); + } + } + + await sftp.RenameFileAsync(remoteFileName1, remoteFileName2, default); + + File.Delete(uploadedFileName); + + sftp.Disconnect(); + } + + RemoveAllFiles(); + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to RenameFile.")] + [ExpectedException(typeof(ArgumentNullException))] + public async Task Test_Sftp_RenameFileAsync_Null() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + await sftp.ConnectAsync(default); + + await sftp.RenameFileAsync(null, null, default); + + sftp.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.SynchronizeDirectories.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.SynchronizeDirectories.cs new file mode 100644 index 000000000..4964715ef --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.SynchronizeDirectories.cs @@ -0,0 +1,80 @@ +using System.Diagnostics; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_SynchronizeDirectories() + { + RemoveAllFiles(); + + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + string uploadedFileName = Path.GetTempFileName(); + + string sourceDir = Path.GetDirectoryName(uploadedFileName); + string destDir = "/home/sshnet/"; + string searchPattern = Path.GetFileName(uploadedFileName); + var upLoadedFiles = sftp.SynchronizeDirectories(sourceDir, destDir, searchPattern); + + Assert.IsTrue(upLoadedFiles.Count() > 0); + + foreach (var file in upLoadedFiles) + { + Debug.WriteLine(file.FullName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_BeginSynchronizeDirectories() + { + RemoveAllFiles(); + + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + string uploadedFileName = Path.GetTempFileName(); + + string sourceDir = Path.GetDirectoryName(uploadedFileName); + string destDir = "/home/sshnet/"; + string searchPattern = Path.GetFileName(uploadedFileName); + + var asyncResult = sftp.BeginSynchronizeDirectories(sourceDir, + destDir, + searchPattern, + null, + null + ); + + // Wait for the WaitHandle to become signaled. + asyncResult.AsyncWaitHandle.WaitOne(1000); + + var upLoadedFiles = sftp.EndSynchronizeDirectories(asyncResult); + + Assert.IsTrue(upLoadedFiles.Count() > 0); + + foreach (var file in upLoadedFiles) + { + Debug.WriteLine(file.FullName); + } + + // Close the wait handle. + asyncResult.AsyncWaitHandle.Close(); + + sftp.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs new file mode 100644 index 000000000..10197bf73 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs @@ -0,0 +1,378 @@ +using Renci.SshNet.Common; +using Renci.SshNet.Sftp; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + public partial class SftpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Upload_And_Download_1MB_File() + { + RemoveAllFiles(); + + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var uploadedFileName = Path.GetTempFileName(); + var remoteFileName = Path.GetRandomFileName(); + + CreateTestFile(uploadedFileName, 1); + + // Calculate has value + var uploadedHash = CalculateMD5(uploadedFileName); + + using (var file = File.OpenRead(uploadedFileName)) + { + sftp.UploadFile(file, remoteFileName); + } + + var downloadedFileName = Path.GetTempFileName(); + + using (var file = File.OpenWrite(downloadedFileName)) + { + sftp.DownloadFile(remoteFileName, file); + } + + var downloadedHash = CalculateMD5(downloadedFileName); + + sftp.DeleteFile(remoteFileName); + + File.Delete(uploadedFileName); + File.Delete(downloadedFileName); + + sftp.Disconnect(); + + Assert.AreEqual(uploadedHash, downloadedHash); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPermissionDeniedException))] + public void Test_Sftp_Upload_Forbidden() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var uploadedFileName = Path.GetTempFileName(); + var remoteFileName = "/root/1"; + + CreateTestFile(uploadedFileName, 1); + + using (var file = File.OpenRead(uploadedFileName)) + { + sftp.UploadFile(file, remoteFileName); + } + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_Multiple_Async_Upload_And_Download_10Files_5MB_Each() + { + var maxFiles = 10; + var maxSize = 5; + + RemoveAllFiles(); + + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.OperationTimeout = TimeSpan.FromMinutes(1); + sftp.Connect(); + + var testInfoList = new Dictionary(); + + for (var i = 0; i < maxFiles; i++) + { + var testInfo = new TestInfo + { + UploadedFileName = Path.GetTempFileName(), + DownloadedFileName = Path.GetTempFileName(), + RemoteFileName = Path.GetRandomFileName() + }; + + CreateTestFile(testInfo.UploadedFileName, maxSize); + + // Calculate hash value + testInfo.UploadedHash = CalculateMD5(testInfo.UploadedFileName); + + testInfoList.Add(testInfo.RemoteFileName, testInfo); + } + + var uploadWaitHandles = new List(); + + // Start file uploads + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + testInfo.UploadedFile = File.OpenRead(testInfo.UploadedFileName); + + testInfo.UploadResult = sftp.BeginUploadFile(testInfo.UploadedFile, + remoteFile, + null, + null) as SftpUploadAsyncResult; + + uploadWaitHandles.Add(testInfo.UploadResult.AsyncWaitHandle); + } + + // Wait for upload to finish + var uploadCompleted = false; + while (!uploadCompleted) + { + // Assume upload completed + uploadCompleted = true; + + foreach (var testInfo in testInfoList.Values) + { + var sftpResult = testInfo.UploadResult; + + if (!testInfo.UploadResult.IsCompleted) + { + uploadCompleted = false; + } + } + Thread.Sleep(500); + } + + // End file uploads + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + + sftp.EndUploadFile(testInfo.UploadResult); + testInfo.UploadedFile.Dispose(); + } + + // Start file downloads + + var downloadWaitHandles = new List(); + + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + testInfo.DownloadedFile = File.OpenWrite(testInfo.DownloadedFileName); + testInfo.DownloadResult = sftp.BeginDownloadFile(remoteFile, + testInfo.DownloadedFile, + null, + null) as SftpDownloadAsyncResult; + + downloadWaitHandles.Add(testInfo.DownloadResult.AsyncWaitHandle); + } + + // Wait for download to finish + var downloadCompleted = false; + while (!downloadCompleted) + { + // Assume download completed + downloadCompleted = true; + + foreach (var testInfo in testInfoList.Values) + { + var sftpResult = testInfo.DownloadResult; + + if (!testInfo.DownloadResult.IsCompleted) + { + downloadCompleted = false; + } + } + Thread.Sleep(500); + } + + var hashMatches = true; + var uploadDownloadSizeOk = true; + + // End file downloads + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + + sftp.EndDownloadFile(testInfo.DownloadResult); + + testInfo.DownloadedFile.Dispose(); + + testInfo.DownloadedHash = CalculateMD5(testInfo.DownloadedFileName); + + if (!(testInfo.UploadResult.UploadedBytes > 0 && testInfo.DownloadResult.DownloadedBytes > 0 && testInfo.DownloadResult.DownloadedBytes == testInfo.UploadResult.UploadedBytes)) + { + uploadDownloadSizeOk = false; + } + + if (!testInfo.DownloadedHash.Equals(testInfo.UploadedHash)) + { + hashMatches = false; + } + } + + // Clean up after test + foreach (var remoteFile in testInfoList.Keys) + { + var testInfo = testInfoList[remoteFile]; + + sftp.DeleteFile(remoteFile); + + File.Delete(testInfo.UploadedFileName); + File.Delete(testInfo.DownloadedFileName); + } + + sftp.Disconnect(); + + Assert.IsTrue(hashMatches, "Hash does not match"); + Assert.IsTrue(uploadDownloadSizeOk, "Uploaded and downloaded bytes does not match"); + } + } + + // TODO: Split this test into multiple tests + [TestMethod] + [TestCategory("Sftp")] + [Description("Test that delegates passed to BeginUploadFile, BeginDownloadFile and BeginListDirectory are actually called.")] + public void Test_Sftp_Ensure_Async_Delegates_Called_For_BeginFileUpload_BeginFileDownload_BeginListDirectory() + { + RemoveAllFiles(); + + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var remoteFileName = Path.GetRandomFileName(); + var localFileName = Path.GetRandomFileName(); + var uploadDelegateCalled = false; + var downloadDelegateCalled = false; + var listDirectoryDelegateCalled = false; + IAsyncResult asyncResult; + + // Test for BeginUploadFile. + + CreateTestFile(localFileName, 1); + + using (var fileStream = File.OpenRead(localFileName)) + { + asyncResult = sftp.BeginUploadFile(fileStream, + remoteFileName, + delegate(IAsyncResult ar) + { + sftp.EndUploadFile(ar); + uploadDelegateCalled = true; + }, + null); + + while (!asyncResult.IsCompleted) + { + Thread.Sleep(500); + } + } + + File.Delete(localFileName); + + Assert.IsTrue(uploadDelegateCalled, "BeginUploadFile"); + + // Test for BeginDownloadFile. + + asyncResult = null; + using (var fileStream = File.OpenWrite(localFileName)) + { + asyncResult = sftp.BeginDownloadFile(remoteFileName, + fileStream, + delegate(IAsyncResult ar) + { + sftp.EndDownloadFile(ar); + downloadDelegateCalled = true; + }, + null); + + while (!asyncResult.IsCompleted) + { + Thread.Sleep(500); + } + } + + File.Delete(localFileName); + + Assert.IsTrue(downloadDelegateCalled, "BeginDownloadFile"); + + // Test for BeginListDirectory. + + asyncResult = null; + asyncResult = sftp.BeginListDirectory(sftp.WorkingDirectory, + delegate(IAsyncResult ar) + { + _ = sftp.EndListDirectory(ar); + listDirectoryDelegateCalled = true; + }, + null); + + while (!asyncResult.IsCompleted) + { + Thread.Sleep(500); + } + + Assert.IsTrue(listDirectoryDelegateCalled, "BeginListDirectory"); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to BeginUploadFile")] + [ExpectedException(typeof(ArgumentNullException))] + public void Test_Sftp_BeginUploadFile_StreamIsNull() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + _ = sftp.BeginUploadFile(null, "aaaaa", null, null); + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to BeginUploadFile")] + [ExpectedException(typeof(ArgumentException))] + public void Test_Sftp_BeginUploadFile_FileNameIsWhiteSpace() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + _ = sftp.BeginUploadFile(new MemoryStream(), " ", null, null); + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to BeginUploadFile")] + [ExpectedException(typeof(ArgumentException))] + public void Test_Sftp_BeginUploadFile_FileNameIsNull() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + _ = sftp.BeginUploadFile(new MemoryStream(), null, null, null); + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(ArgumentException))] + public void Test_Sftp_EndUploadFile_Invalid_Async_Handle() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + var async1 = sftp.BeginListDirectory("/", null, null); + var filename = Path.GetTempFileName(); + CreateTestFile(filename, 100); + var async2 = sftp.BeginUploadFile(File.OpenRead(filename), "test", null, null); + sftp.EndUploadFile(async1); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs new file mode 100644 index 000000000..1c7def55b --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.cs @@ -0,0 +1,68 @@ +using System.Security.Cryptography; + +using Renci.SshNet.Sftp; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. + /// + [TestClass] + public partial class SftpClientTest + { + protected static string CalculateMD5(string fileName) + { + using (FileStream file = new FileStream(fileName, FileMode.Open)) + { + var hash = MD5.HashData(file); + + file.Close(); + + StringBuilder sb = new StringBuilder(); + for (var i = 0; i < hash.Length; i++) + { + sb.Append(hash[i].ToString("x2")); + } + return sb.ToString(); + } + } + + private void RemoveAllFiles() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + client.RunCommand("rm -rf *"); + client.Disconnect(); + } + } + + /// + /// Helper class to help with upload and download testing + /// + private class TestInfo + { + public string RemoteFileName { get; set; } + + public string UploadedFileName { get; set; } + + public string DownloadedFileName { get; set; } + + //public ulong UploadedBytes { get; set; } + + //public ulong DownloadedBytes { get; set; } + + public FileStream UploadedFile { get; set; } + + public FileStream DownloadedFile { get; set; } + + public string UploadedHash { get; set; } + + public string DownloadedHash { get; set; } + + public SftpUploadAsyncResult UploadResult { get; set; } + + public SftpDownloadAsyncResult DownloadResult { get; set; } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs new file mode 100644 index 000000000..d852a093a --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshCommandTest.cs @@ -0,0 +1,545 @@ +using System.Diagnostics; + +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Represents SSH command that can be executed. + /// + [TestClass] + public class SshCommandTest : IntegrationTestBase + { + [TestMethod] + public void Test_Run_SingleCommand() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand RunCommand Result + client.Connect(); + + var testValue = Guid.NewGuid().ToString(); + var command = client.RunCommand(string.Format("echo {0}", testValue)); + var result = command.Result; + result = result.Substring(0, result.Length - 1); // Remove \n character returned by command + + client.Disconnect(); + #endregion + + Assert.IsTrue(result.Equals(testValue)); + } + } + + [TestMethod] + public void Test_Execute_SingleCommand() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand CreateCommand Execute + client.Connect(); + + var testValue = Guid.NewGuid().ToString(); + var command = string.Format("echo {0}", testValue); + var cmd = client.CreateCommand(command); + var result = cmd.Execute(); + result = result.Substring(0, result.Length - 1); // Remove \n character returned by command + + client.Disconnect(); + #endregion + + Assert.IsTrue(result.Equals(testValue)); + } + } + + [TestMethod] + public void Test_Execute_OutputStream() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand CreateCommand Execute OutputStream + client.Connect(); + + var cmd = client.CreateCommand("ls -l"); // very long list + var asynch = cmd.BeginExecute(); + + var reader = new StreamReader(cmd.OutputStream); + + while (!asynch.IsCompleted) + { + var result = reader.ReadToEnd(); + if (string.IsNullOrEmpty(result)) + { + continue; + } + + Console.Write(result); + } + + _ = cmd.EndExecute(asynch); + + client.Disconnect(); + #endregion + + Assert.Inconclusive(); + } + } + + [TestMethod] + public void Test_Execute_ExtendedOutputStream() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand CreateCommand Execute ExtendedOutputStream + + client.Connect(); + var cmd = client.CreateCommand("echo 12345; echo 654321 >&2"); + var result = cmd.Execute(); + + Console.Write(result); + + var reader = new StreamReader(cmd.ExtendedOutputStream); + Console.WriteLine("DEBUG:"); + Console.Write(reader.ReadToEnd()); + + client.Disconnect(); + + #endregion + + Assert.Inconclusive(); + } + } + + [TestMethod] + [ExpectedException(typeof(SshOperationTimeoutException))] + public void Test_Execute_Timeout() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand CreateCommand Execute CommandTimeout + client.Connect(); + var cmd = client.CreateCommand("sleep 10s"); + cmd.CommandTimeout = TimeSpan.FromSeconds(5); + cmd.Execute(); + client.Disconnect(); + #endregion + } + } + + [TestMethod] + public void Test_Execute_Infinite_Timeout() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + var cmd = client.CreateCommand("sleep 10s"); + cmd.Execute(); + client.Disconnect(); + } + } + + [TestMethod] + public void Test_Execute_InvalidCommand() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + + var cmd = client.CreateCommand(";"); + cmd.Execute(); + if (string.IsNullOrEmpty(cmd.Error)) + { + Assert.Fail("Operation should fail"); + } + Assert.IsTrue(cmd.ExitStatus > 0); + + client.Disconnect(); + } + } + + [TestMethod] + public void Test_Execute_InvalidCommand_Then_Execute_ValidCommand() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + var cmd = client.CreateCommand(";"); + cmd.Execute(); + if (string.IsNullOrEmpty(cmd.Error)) + { + Assert.Fail("Operation should fail"); + } + Assert.IsTrue(cmd.ExitStatus > 0); + + var result = ExecuteTestCommand(client); + + client.Disconnect(); + + Assert.IsTrue(result); + } + } + + [TestMethod] + public void Test_Execute_Command_with_ExtendedOutput() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + var cmd = client.CreateCommand("echo 12345; echo 654321 >&2"); + cmd.Execute(); + + //var extendedData = Encoding.ASCII.GetString(cmd.ExtendedOutputStream.ToArray()); + var extendedData = new StreamReader(cmd.ExtendedOutputStream, Encoding.ASCII).ReadToEnd(); + client.Disconnect(); + + Assert.AreEqual("12345\n", cmd.Result); + Assert.AreEqual("654321\n", extendedData); + } + } + + [TestMethod] + public void Test_Execute_Command_Reconnect_Execute_Command() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + var result = ExecuteTestCommand(client); + Assert.IsTrue(result); + + client.Disconnect(); + client.Connect(); + result = ExecuteTestCommand(client); + Assert.IsTrue(result); + client.Disconnect(); + } + } + + [TestMethod] + public void Test_Execute_Command_ExitStatus() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand RunCommand ExitStatus + client.Connect(); + + var cmd = client.RunCommand("exit 128"); + + Console.WriteLine(cmd.ExitStatus); + + client.Disconnect(); + #endregion + + Assert.IsTrue(cmd.ExitStatus == 128); + } + } + + [TestMethod] + public void Test_Execute_Command_Asynchronously() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + + var cmd = client.CreateCommand("sleep 5s; echo 'test'"); + var asyncResult = cmd.BeginExecute(null, null); + while (!asyncResult.IsCompleted) + { + Thread.Sleep(100); + } + + cmd.EndExecute(asyncResult); + + Assert.IsTrue(cmd.Result == "test\n"); + + client.Disconnect(); + } + } + + [TestMethod] + public void Test_Execute_Command_Asynchronously_With_Error() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + + var cmd = client.CreateCommand("sleep 5s; ;"); + var asyncResult = cmd.BeginExecute(null, null); + while (!asyncResult.IsCompleted) + { + Thread.Sleep(100); + } + + cmd.EndExecute(asyncResult); + + Assert.IsFalse(string.IsNullOrEmpty(cmd.Error)); + + client.Disconnect(); + } + } + + [TestMethod] + public void Test_Execute_Command_Asynchronously_With_Callback() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + + var callbackCalled = false; + + var cmd = client.CreateCommand("sleep 5s; echo 'test'"); + var asyncResult = cmd.BeginExecute(new AsyncCallback((s) => + { + callbackCalled = true; + }), null); + while (!asyncResult.IsCompleted) + { + Thread.Sleep(100); + } + + cmd.EndExecute(asyncResult); + + Assert.IsTrue(callbackCalled); + + client.Disconnect(); + } + } + + [TestMethod] + public void Test_Execute_Command_Asynchronously_With_Callback_On_Different_Thread() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + int callbackThreadId = 0; + + var cmd = client.CreateCommand("sleep 5s; echo 'test'"); + var asyncResult = cmd.BeginExecute(new AsyncCallback((s) => + { + callbackThreadId = Thread.CurrentThread.ManagedThreadId; + }), null); + while (!asyncResult.IsCompleted) + { + Thread.Sleep(100); + } + + cmd.EndExecute(asyncResult); + + Assert.AreNotEqual(currentThreadId, callbackThreadId); + + client.Disconnect(); + } + } + + /// + /// Tests for Issue 563. + /// + [WorkItem(563), TestMethod] + public void Test_Execute_Command_Same_Object_Different_Commands() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + var cmd = client.CreateCommand("echo 12345"); + cmd.Execute(); + Assert.AreEqual("12345\n", cmd.Result); + cmd.Execute("echo 23456"); + Assert.AreEqual("23456\n", cmd.Result); + client.Disconnect(); + } + } + + [TestMethod] + public void Test_Get_Result_Without_Execution() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + var cmd = client.CreateCommand("ls -l"); + + Assert.IsTrue(string.IsNullOrEmpty(cmd.Result)); + client.Disconnect(); + } + } + + [TestMethod] + public void Test_Get_Error_Without_Execution() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + var cmd = client.CreateCommand("ls -l"); + + Assert.IsTrue(string.IsNullOrEmpty(cmd.Error)); + client.Disconnect(); + } + } + + [WorkItem(703), TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void Test_EndExecute_Before_BeginExecute() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + var cmd = client.CreateCommand("ls -l"); + cmd.EndExecute(null); + client.Disconnect(); + } + } + + /// + ///A test for BeginExecute + /// + [TestMethod()] + public void BeginExecuteTest() + { + string expected = "123\n"; + string result; + + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand CreateCommand BeginExecute IsCompleted EndExecute + + client.Connect(); + + var cmd = client.CreateCommand("sleep 15s;echo 123"); // Perform long running task + + var asynch = cmd.BeginExecute(); + + while (!asynch.IsCompleted) + { + // Waiting for command to complete... + Thread.Sleep(2000); + } + result = cmd.EndExecute(asynch); + client.Disconnect(); + + #endregion + + Assert.IsNotNull(asynch); + Assert.AreEqual(expected, result); + } + } + + [TestMethod] + public void Test_Execute_Invalid_Command() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + #region Example SshCommand CreateCommand Error + + client.Connect(); + + var cmd = client.CreateCommand(";"); + cmd.Execute(); + if (!string.IsNullOrEmpty(cmd.Error)) + { + Console.WriteLine(cmd.Error); + } + + client.Disconnect(); + + #endregion + + Assert.Inconclusive(); + } + } + + [TestMethod] + public void Test_MultipleThread_Example_MultipleConnections() + { + try + { +#region Example SshCommand RunCommand Parallel + Parallel.For(0, 100, + () => + { + var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password); + client.Connect(); + return client; + }, + (int counter, ParallelLoopState pls, SshClient client) => + { + var result = client.RunCommand("echo 123"); + Debug.WriteLine(string.Format("TestMultipleThreadMultipleConnections #{0}", counter)); + return client; + }, + (SshClient client) => + { + client.Disconnect(); + client.Dispose(); + } + ); +#endregion + + } + catch (Exception exp) + { + Assert.Fail(exp.ToString()); + } + } + + [TestMethod] + + public void Test_MultipleThread_100_MultipleConnections() + { + try + { + Parallel.For(0, 100, + () => + { + var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password); + client.Connect(); + return client; + }, + (int counter, ParallelLoopState pls, SshClient client) => + { + var result = ExecuteTestCommand(client); + Debug.WriteLine(string.Format("TestMultipleThreadMultipleConnections #{0}", counter)); + Assert.IsTrue(result); + return client; + }, + (SshClient client) => + { + client.Disconnect(); + client.Dispose(); + } + ); + } + catch (Exception exp) + { + Assert.Fail(exp.ToString()); + } + } + + [TestMethod] + public void Test_MultipleThread_100_MultipleSessions() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + Parallel.For(0, 100, + (counter) => + { + var result = ExecuteTestCommand(client); + Debug.WriteLine(string.Format("TestMultipleThreadMultipleConnections #{0}", counter)); + Assert.IsTrue(result); + } + ); + + client.Disconnect(); + } + } + + private static bool ExecuteTestCommand(SshClient s) + { + var testValue = Guid.NewGuid().ToString(); + var command = string.Format("echo {0}", testValue); + var cmd = s.CreateCommand(command); + var result = cmd.Execute(); + result = result.Substring(0, result.Length - 1); // Remove \n character returned by command + return result.Equals(testValue); + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/RemoteSshd.cs b/src/Renci.SshNet.IntegrationTests/RemoteSshd.cs index 18dc9aefd..c81bf96f5 100644 --- a/src/Renci.SshNet.IntegrationTests/RemoteSshd.cs +++ b/src/Renci.SshNet.IntegrationTests/RemoteSshd.cs @@ -28,14 +28,14 @@ public RemoteSshd Restart() var stopOutput = stopCommand.Execute(); if (stopCommand.ExitStatus != 0) { - throw new ApplicationException($"Stopping ssh service failed with exit code {stopCommand.ExitStatus}.\r\n{stopOutput}"); + throw new ApplicationException($"Stopping ssh service failed with exit code {stopCommand.ExitStatus}.\r\n{stopOutput}\r\n{stopCommand.Error}"); } var resetFailedCommand = client.CreateCommand("sudo /usr/sbin/sshd.pam"); var resetFailedOutput = resetFailedCommand.Execute(); if (resetFailedCommand.ExitStatus != 0) { - throw new ApplicationException($"Reset failures for ssh service failed with exit code {resetFailedCommand.ExitStatus}.\r\n{resetFailedOutput}"); + throw new ApplicationException($"Reset failures for ssh service failed with exit code {resetFailedCommand.ExitStatus}.\r\n{resetFailedOutput}\r\n{resetFailedCommand.Error}"); } } diff --git a/src/Renci.SshNet.IntegrationTests/ScpTests.cs b/src/Renci.SshNet.IntegrationTests/ScpTests.cs index 0cb4ab7bd..2fd4ea0ce 100644 --- a/src/Renci.SshNet.IntegrationTests/ScpTests.cs +++ b/src/Renci.SshNet.IntegrationTests/ScpTests.cs @@ -1,5 +1,4 @@ using Renci.SshNet.Common; -using Renci.SshNet.IntegrationTests.Common; namespace Renci.SshNet.IntegrationTests { diff --git a/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs b/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs index 34194e095..1d6658fc2 100644 --- a/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs +++ b/src/Renci.SshNet.IntegrationTests/TestsFixtures/IntegrationTestBase.cs @@ -62,5 +62,24 @@ private void ShowInfrastructureInformation() Console.WriteLine($"SSH Server host name: {_infrastructureFixture.SshServerHostName}"); Console.WriteLine($"SSH Server port: {_infrastructureFixture.SshServerPort}"); } + + /// + /// Creates the test file. + /// + /// Name of the file. + /// Size in megabytes. + protected void CreateTestFile(string fileName, int size) + { + using (var testFile = File.Create(fileName)) + { + var random = new Random(); + for (int i = 0; i < 1024 * size; i++) + { + var buffer = new byte[1024]; + random.NextBytes(buffer); + testFile.Write(buffer, 0, buffer.Length); + } + } + } } } diff --git a/src/Renci.SshNet.Tests/Classes/ForwardedPortLocalTest.cs b/src/Renci.SshNet.Tests/Classes/ForwardedPortLocalTest.cs index 82459d00a..766dd23a6 100644 --- a/src/Renci.SshNet.Tests/Classes/ForwardedPortLocalTest.cs +++ b/src/Renci.SshNet.Tests/Classes/ForwardedPortLocalTest.cs @@ -1,16 +1,7 @@ using System; -using System.Diagnostics; -#if NET6_0_OR_GREATER -using System.Net.Http; -#else -using System.Net; -#endif // NET6_0_OR_GREATER -using System.Threading; -using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; using Renci.SshNet.Tests.Common; using Renci.SshNet.Tests.Properties; @@ -22,92 +13,6 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public partial class ForwardedPortLocalTest : TestBase { - [TestMethod] - [WorkItem(713)] - [Owner("Kenneth_aa")] - [TestCategory("PortForwarding")] - [TestCategory("integration")] - [Description("Test if calling Stop on ForwardedPortLocal instance causes wait.")] - public void Test_PortForwarding_Local_Stop_Hangs_On_Wait() - { - using (var client = new SshClient(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - - var port1 = new ForwardedPortLocal("localhost", 8084, "www.google.com", 80); - client.AddForwardedPort(port1); - port1.Exception += delegate(object sender, ExceptionEventArgs e) - { - Assert.Fail(e.Exception.ToString()); - }; - - port1.Start(); - - var hasTestedTunnel = false; - - _ = ThreadPool.QueueUserWorkItem(delegate(object state) - { - try - { - var url = "http://www.google.com/"; - Debug.WriteLine("Starting web request to \"" + url + "\""); - -#if NET6_0_OR_GREATER - var httpClient = new HttpClient(); - var response = httpClient.GetAsync(url) - .ConfigureAwait(false) - .GetAwaiter() - .GetResult(); -#else - var request = (HttpWebRequest) WebRequest.Create(url); - var response = (HttpWebResponse) request.GetResponse(); -#endif // NET6_0_OR_GREATER - - Assert.IsNotNull(response); - - Debug.WriteLine("Http Response status code: " + response.StatusCode.ToString()); - - response.Dispose(); - - hasTestedTunnel = true; - } - catch (Exception ex) - { - Assert.Fail(ex.ToString()); - } - }); - - // Wait for the web request to complete. - while (!hasTestedTunnel) - { - Thread.Sleep(1000); - } - - try - { - // Try stop the port forwarding, wait 3 seconds and fail if it is still started. - _ = ThreadPool.QueueUserWorkItem(delegate(object state) - { - Debug.WriteLine("Trying to stop port forward."); - port1.Stop(); - Debug.WriteLine("Port forwarding stopped."); - }); - - Thread.Sleep(3000); - if (port1.IsStarted) - { - Assert.Fail("Port forwarding not stopped."); - } - } - catch (Exception ex) - { - Assert.Fail(ex.ToString()); - } - client.Disconnect(); - Debug.WriteLine("Success."); - } - } - [TestMethod] public void ConstructorShouldThrowArgumentNullExceptionWhenBoundHostIsNull() { @@ -177,132 +82,5 @@ public void ConstructorShouldNotThrowExceptionWhenHostIsInvalidDnsName() Assert.AreSame(host, forwardedPort.Host); } - - /// - ///A test for ForwardedPortRemote Constructor - /// - [TestMethod] - [TestCategory("integration")] - public void Test_ForwardedPortRemote() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - #region Example SshClient AddForwardedPort Start Stop ForwardedPortLocal - client.Connect(); - var port = new ForwardedPortLocal(8082, "www.cnn.com", 80); - client.AddForwardedPort(port); - port.Exception += delegate(object sender, ExceptionEventArgs e) - { - Console.WriteLine(e.Exception.ToString()); - }; - port.Start(); - - Thread.Sleep(1000 * 60 * 20); // Wait 20 minutes for port to be forwarded - - port.Stop(); - #endregion - } - Assert.Inconclusive("TODO: Implement code to verify target"); - } - - [TestMethod] - [TestCategory("integration")] - [ExpectedException(typeof(SshConnectionException))] - public void Test_PortForwarding_Local_Without_Connecting() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - var port1 = new ForwardedPortLocal("localhost", 8084, "www.renci.org", 80); - client.AddForwardedPort(port1); - port1.Exception += delegate (object sender, ExceptionEventArgs e) - { - Assert.Fail(e.Exception.ToString()); - }; - port1.Start(); - - _ = Parallel.For(0, - 100, - counter => - { - var start = DateTime.Now; - -#if NET6_0_OR_GREATER - var httpClient = new HttpClient(); - using (var response = httpClient.GetAsync("http://localhost:8084").GetAwaiter().GetResult()) - { - var data = ReadStream(response.Content.ReadAsStream()); -#else - var request = (HttpWebRequest) WebRequest.Create("http://localhost:8084"); - using (var response = (HttpWebResponse) request.GetResponse()) - { - var data = ReadStream(response.GetResponseStream()); -#endif // NET6_0_OR_GREATER - var end = DateTime.Now; - - Debug.WriteLine(string.Format("Request# {2}: Lenght: {0} Time: {1}", data.Length, end - start, counter)); - } - }); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_PortForwarding_Local() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var port1 = new ForwardedPortLocal("localhost", 8084, "www.renci.org", 80); - client.AddForwardedPort(port1); - port1.Exception += delegate (object sender, ExceptionEventArgs e) - { - Assert.Fail(e.Exception.ToString()); - }; - port1.Start(); - - _ = Parallel.For(0, - 100, - counter => - { - var start = DateTime.Now; - -#if NET6_0_OR_GREATER - var httpClient = new HttpClient(); - using (var response = httpClient.GetAsync("http://localhost:8084").GetAwaiter().GetResult()) - { - var data = ReadStream(response.Content.ReadAsStream()); -#else - var request = (HttpWebRequest) WebRequest.Create("http://localhost:8084"); - using (var response = (HttpWebResponse) request.GetResponse()) - { - var data = ReadStream(response.GetResponseStream()); -#endif // NET6_0_OR_GREATER - var end = DateTime.Now; - - Debug.WriteLine(string.Format("Request# {2}: Length: {0} Time: {1}", data.Length, end - start, counter)); - } - }); - } - } - - private static byte[] ReadStream(System.IO.Stream stream) - { - var buffer = new byte[1024]; - using (var ms = new System.IO.MemoryStream()) - { - while (true) - { - var read = stream.Read(buffer, 0, buffer.Length); - if (read > 0) - { - ms.Write(buffer, 0, read); - } - else - { - return ms.ToArray(); - } - } - } - } } } diff --git a/src/Renci.SshNet.Tests/Classes/ForwardedPortRemoteTest.cs b/src/Renci.SshNet.Tests/Classes/ForwardedPortRemoteTest.cs index 5f0777190..be6adef7b 100644 --- a/src/Renci.SshNet.Tests/Classes/ForwardedPortRemoteTest.cs +++ b/src/Renci.SshNet.Tests/Classes/ForwardedPortRemoteTest.cs @@ -1,12 +1,8 @@ using System; -using System.Net; -using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; namespace Renci.SshNet.Tests.Classes { @@ -16,64 +12,6 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public partial class ForwardedPortRemoteTest : TestBase { - [TestMethod] - [Description("Test passing null to AddForwardedPort hosts (remote).")] - [ExpectedException(typeof(ArgumentNullException))] - [TestCategory("integration")] - public void Test_AddForwardedPort_Remote_Hosts_Are_Null() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var port1 = new ForwardedPortRemote(boundHost: null, 8080, host: null, 80); - client.AddForwardedPort(port1); - client.Disconnect(); - } - } - - [TestMethod] - [Description("Test passing invalid port numbers to AddForwardedPort.")] - [ExpectedException(typeof(ArgumentOutOfRangeException))] - [TestCategory("integration")] - public void Test_AddForwardedPort_Invalid_PortNumber() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var port1 = new ForwardedPortRemote("localhost", IPEndPoint.MaxPort + 1, "www.renci.org", IPEndPoint.MaxPort + 1); - client.AddForwardedPort(port1); - client.Disconnect(); - } - } - - /// - ///A test for ForwardedPortRemote Constructor - /// - [TestMethod] - [TestCategory("integration")] - public void Test_ForwardedPortRemote() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - #region Example SshClient AddForwardedPort Start Stop ForwardedPortRemote - client.Connect(); - var port = new ForwardedPortRemote(8082, "www.cnn.com", 80); - client.AddForwardedPort(port); - port.Exception += delegate(object sender, ExceptionEventArgs e) - { - Console.WriteLine(e.Exception.ToString()); - }; - port.Start(); - - Thread.Sleep(1000 * 60 * 20); // Wait 20 minutes for port to be forwarded - - port.Stop(); - #endregion - } - Assert.Inconclusive("TODO: Implement code to verify target"); - } - - /// ///A test for Stop /// @@ -152,46 +90,5 @@ public void ForwardedPortRemoteConstructorTest1() var target = new ForwardedPortRemote(boundPort, host, port); Assert.Inconclusive("TODO: Implement code to verify target"); } - -#if FEATURE_TPL - [TestMethod] - [TestCategory("integration")] - public void Test_PortForwarding_Remote() - { - // ****************************************************************** - // ************* Tests are still in not finished ******************** - // ****************************************************************** - - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var port1 = new ForwardedPortRemote(8082, "www.renci.org", 80); - client.AddForwardedPort(port1); - port1.Exception += delegate (object sender, ExceptionEventArgs e) - { - Assert.Fail(e.Exception.ToString()); - }; - port1.Start(); - var boundport = port1.BoundPort; - - System.Threading.Tasks.Parallel.For(0, 5, - - //new ParallelOptions - //{ - // MaxDegreeOfParallelism = 1, - //}, - (counter) => - { - var cmd = client.CreateCommand(string.Format("wget -O- http://localhost:{0}", boundport)); - var result = cmd.Execute(); - var end = DateTime.Now; - System.Diagnostics.Debug.WriteLine(string.Format("Length: {0}", result.Length)); - } - ); - Thread.Sleep(1000 * 100); - port1.Stop(); - } - } -#endif // FEATURE_TPL } } diff --git a/src/Renci.SshNet.Tests/Classes/KeyboardInteractiveConnectionInfoTest.cs b/src/Renci.SshNet.Tests/Classes/KeyboardInteractiveConnectionInfoTest.cs index 0fc12c462..f50aaf9b7 100644 --- a/src/Renci.SshNet.Tests/Classes/KeyboardInteractiveConnectionInfoTest.cs +++ b/src/Renci.SshNet.Tests/Classes/KeyboardInteractiveConnectionInfoTest.cs @@ -1,8 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; + using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; -using System; namespace Renci.SshNet.Tests.Classes { @@ -12,40 +10,6 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class KeyboardInteractiveConnectionInfoTest : TestBase { - [TestMethod] - [TestCategory("KeyboardInteractiveConnectionInfo")] - [TestCategory("integration")] - public void Test_KeyboardInteractiveConnectionInfo() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - #region Example KeyboardInteractiveConnectionInfo AuthenticationPrompt - var connectionInfo = new KeyboardInteractiveConnectionInfo(host, username); - connectionInfo.AuthenticationPrompt += delegate(object sender, AuthenticationPromptEventArgs e) - { - System.Console.WriteLine(e.Instruction); - - foreach (var prompt in e.Prompts) - { - Console.WriteLine(prompt.Request); - prompt.Response = Console.ReadLine(); - } - }; - - using (var client = new SftpClient(connectionInfo)) - { - client.Connect(); - // Do something here - client.Disconnect(); - } - #endregion - - Assert.AreEqual(connectionInfo.Host, Resources.HOST); - Assert.AreEqual(connectionInfo.Username, Resources.USERNAME); - } - /// ///A test for Dispose /// @@ -192,4 +156,4 @@ public void KeyboardInteractiveConnectionInfoConstructorTest7() Assert.Inconclusive("TODO: Implement code to verify target"); } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.Tests/Classes/PasswordAuthenticationMethodTest.cs b/src/Renci.SshNet.Tests/Classes/PasswordAuthenticationMethodTest.cs index ec0d3fef8..951314d1c 100644 --- a/src/Renci.SshNet.Tests/Classes/PasswordAuthenticationMethodTest.cs +++ b/src/Renci.SshNet.Tests/Classes/PasswordAuthenticationMethodTest.cs @@ -1,6 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; using System; namespace Renci.SshNet.Tests.Classes @@ -59,32 +58,6 @@ public void Password_Test_Pass_Valid() new PasswordAuthenticationMethod("valid", string.Empty); } - [TestMethod] - [WorkItem(1140)] - [TestCategory("BaseClient")] - [TestCategory("integration")] - [Description("Test whether IsConnected is false after disconnect.")] - [Owner("Kenneth_aa")] - public void Test_BaseClient_IsConnected_True_After_Disconnect() - { - // 2012-04-29 - Kenneth_aa - // The problem with this test, is that after SSH Net calls .Disconnect(), the library doesn't wait - // for the server to confirm disconnect before IsConnected is checked. And now I'm not mentioning - // anything about Socket's either. - - var connectionInfo = new PasswordAuthenticationMethod(Resources.USERNAME, Resources.PASSWORD); - - using (SftpClient client = new SftpClient(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - Assert.AreEqual(true, client.IsConnected, "IsConnected is not true after Connect() was called."); - - client.Disconnect(); - - Assert.AreEqual(false, client.IsConnected, "IsConnected is true after Disconnect() was called."); - } - } - /// ///A test for Name /// @@ -158,4 +131,4 @@ public void PasswordAuthenticationMethodConstructorTest1() Assert.Inconclusive("TODO: Implement code to verify target"); } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/AesCipherTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/AesCipherTest.cs index ef6b69197..03a2e34e9 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/AesCipherTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/AesCipherTest.cs @@ -3,7 +3,6 @@ using Renci.SshNet.Security.Cryptography.Ciphers; using Renci.SshNet.Security.Cryptography.Ciphers.Modes; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; namespace Renci.SshNet.Tests.Classes.Security.Cryptography.Ciphers { @@ -84,125 +83,6 @@ public void Decrypt_InputAndOffsetAndLength_128_CTR() Assert.IsTrue(expected.IsEqualTo(actual)); } - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_AEes128CBC_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Aes192CBC_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Aes256CBC_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Aes128CTR_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("aes128-ctr", new CipherInfo(128, (key, iv) => { return new AesCipher(key, new CtrCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Aes192CTR_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("aes192-ctr", new CipherInfo(192, (key, iv) => { return new AesCipher(key, new CtrCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Aes256CTR_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("aes256-ctr", new CipherInfo(256, (key, iv) => { return new AesCipher(key, new CtrCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Arcfour_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("arcfour", new CipherInfo(128, (key, iv) => { return new Arc4Cipher(key, false); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - /// ///A test for DecryptBlock /// @@ -263,4 +143,4 @@ public void EncryptBlockTest() Assert.Inconclusive("Verify the correctness of this test method."); } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/Arc4CipherTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/Arc4CipherTest.cs index f83aa7aa5..5b972021b 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/Arc4CipherTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/Arc4CipherTest.cs @@ -3,7 +3,6 @@ using Renci.SshNet.Common; using Renci.SshNet.Security.Cryptography.Ciphers; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; namespace Renci.SshNet.Tests.Classes.Security.Cryptography.Ciphers { @@ -156,40 +155,5 @@ public void EncryptBlockTest() Assert.AreEqual(expected, actual); Assert.Inconclusive("Verify the correctness of this test method."); } - - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Arcfour128_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("arcfour128", new CipherInfo(128, (key, iv) => { return new Arc4Cipher(key, true); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Arcfour256_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("arcfour256", new CipherInfo(256, (key, iv) => { return new Arc4Cipher(key, true); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/HMacTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/HMacTest.cs deleted file mode 100644 index 94e558b2a..000000000 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/HMacTest.cs +++ /dev/null @@ -1,134 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; -using Renci.SshNet.Abstractions; - -namespace Renci.SshNet.Tests.Classes.Security.Cryptography -{ - /// - /// Provides HMAC algorithm implementation. - /// - [TestClass] - public class HMacTest : TestBase - { - [TestMethod] - [TestCategory("integration")] - public void Test_HMac_MD5_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HmacAlgorithms.Clear(); - connectionInfo.HmacAlgorithms.Add("hmac-md5", new HashInfo(16 * 8, CryptoAbstraction.CreateHMACMD5)); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_HMac_Sha1_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HmacAlgorithms.Clear(); - connectionInfo.HmacAlgorithms.Add("hmac-sha1", new HashInfo(20 * 8, CryptoAbstraction.CreateHMACSHA1)); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_HMac_MD5_96_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HmacAlgorithms.Clear(); - connectionInfo.HmacAlgorithms.Add("hmac-md5", new HashInfo(16 * 8, key => CryptoAbstraction.CreateHMACMD5(key, 96))); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_HMac_Sha1_96_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HmacAlgorithms.Clear(); - connectionInfo.HmacAlgorithms.Add("hmac-sha1", new HashInfo(20 * 8, key => CryptoAbstraction.CreateHMACSHA1(key, 96))); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_HMac_Sha256_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HmacAlgorithms.Clear(); - connectionInfo.HmacAlgorithms.Add("hmac-sha2-256", new HashInfo(32 * 8, CryptoAbstraction.CreateHMACSHA256)); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_HMac_Sha256_96_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HmacAlgorithms.Clear(); - connectionInfo.HmacAlgorithms.Add("hmac-sha2-256-96", new HashInfo(32 * 8, (key) => CryptoAbstraction.CreateHMACSHA256(key, 96))); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_HMac_RIPEMD160_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HmacAlgorithms.Clear(); - connectionInfo.HmacAlgorithms.Add("hmac-ripemd160", new HashInfo(160, CryptoAbstraction.CreateHMACRIPEMD160)); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_HMac_RIPEMD160_OPENSSH_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HmacAlgorithms.Clear(); - connectionInfo.HmacAlgorithms.Add("hmac-ripemd160@openssh.com", new HashInfo(160, CryptoAbstraction.CreateHMACRIPEMD160)); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - } -} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileTest.cs b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileTest.cs index d1716a957..285d4aa51 100644 --- a/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Sftp/SftpFileTest.cs @@ -1,10 +1,9 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; + using Renci.SshNet.Sftp; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; + using System; -using System.IO; namespace Renci.SshNet.Tests.Classes.Sftp { @@ -14,121 +13,6 @@ namespace Renci.SshNet.Tests.Classes.Sftp [TestClass] public class SftpFileTest : TestBase { - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Get_Root_Directory() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - var directory = sftp.Get("/"); - - Assert.AreEqual("/", directory.FullName); - Assert.IsTrue(directory.IsDirectory); - Assert.IsFalse(directory.IsRegularFile); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPathNotFoundException))] - public void Test_Get_Invalid_Directory() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.Get("/xyz"); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Get_File() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.UploadFile(new MemoryStream(), "abc.txt"); - - var file = sftp.Get("abc.txt"); - - Assert.AreEqual("/home/tester/abc.txt", file.FullName); - Assert.IsTrue(file.IsRegularFile); - Assert.IsFalse(file.IsDirectory); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to Get.")] - [ExpectedException(typeof(ArgumentNullException))] - public void Test_Get_File_Null() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var file = sftp.Get(null); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Get_International_File() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.UploadFile(new MemoryStream(), "test-üöä-"); - - var file = sftp.Get("test-üöä-"); - - Assert.AreEqual("/home/tester/test-üöä-", file.FullName); - Assert.IsTrue(file.IsRegularFile); - Assert.IsFalse(file.IsDirectory); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_SftpFile_MoveTo() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - string uploadedFileName = Path.GetTempFileName(); - string remoteFileName = Path.GetRandomFileName(); - string newFileName = Path.GetRandomFileName(); - - this.CreateTestFile(uploadedFileName, 1); - - using (var file = File.OpenRead(uploadedFileName)) - { - sftp.UploadFile(file, remoteFileName); - } - - var sftpFile = sftp.Get(remoteFileName); - - sftpFile.MoveTo(newFileName); - - Assert.AreEqual(newFileName, sftpFile.Name); - - sftp.Disconnect(); - } - } - /// ///A test for Delete /// @@ -623,4 +507,4 @@ public void UserIdTest() } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.Tests/Classes/SshCommandTest.cs b/src/Renci.SshNet.Tests/Classes/SshCommandTest.cs index d43a11e5e..3f5450952 100644 --- a/src/Renci.SshNet.Tests/Classes/SshCommandTest.cs +++ b/src/Renci.SshNet.Tests/Classes/SshCommandTest.cs @@ -3,13 +3,7 @@ using Renci.SshNet.Tests.Common; using Renci.SshNet.Tests.Properties; using System; -using System.IO; using System.Text; -using System.Threading; -#if FEATURE_TPL -using System.Diagnostics; -using System.Threading.Tasks; -#endif // FEATURE_TPL namespace Renci.SshNet.Tests.Classes { @@ -31,480 +25,6 @@ public void Test_Execute_SingleCommand_Without_Connecting() } } - [TestMethod] - [TestCategory("integration")] - public void Test_Run_SingleCommand() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - using (var client = new SshClient(host, username, password)) - { - #region Example SshCommand RunCommand Result - client.Connect(); - - var testValue = Guid.NewGuid().ToString(); - var command = client.RunCommand(string.Format("echo {0}", testValue)); - var result = command.Result; - result = result.Substring(0, result.Length - 1); // Remove \n character returned by command - - client.Disconnect(); - #endregion - - Assert.IsTrue(result.Equals(testValue)); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_SingleCommand() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - using (var client = new SshClient(host, username, password)) - { - #region Example SshCommand CreateCommand Execute - client.Connect(); - - var testValue = Guid.NewGuid().ToString(); - var command = string.Format("echo {0}", testValue); - var cmd = client.CreateCommand(command); - var result = cmd.Execute(); - result = result.Substring(0, result.Length - 1); // Remove \n character returned by command - - client.Disconnect(); - #endregion - - Assert.IsTrue(result.Equals(testValue)); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_OutputStream() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - using (var client = new SshClient(host, username, password)) - { - #region Example SshCommand CreateCommand Execute OutputStream - client.Connect(); - - var cmd = client.CreateCommand("ls -l"); // very long list - var asynch = cmd.BeginExecute(); - - var reader = new StreamReader(cmd.OutputStream); - - while (!asynch.IsCompleted) - { - var result = reader.ReadToEnd(); - if (string.IsNullOrEmpty(result)) - { - continue; - } - - Console.Write(result); - } - - _ = cmd.EndExecute(asynch); - - client.Disconnect(); - #endregion - - Assert.Inconclusive(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_ExtendedOutputStream() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - using (var client = new SshClient(host, username, password)) - { - #region Example SshCommand CreateCommand Execute ExtendedOutputStream - - client.Connect(); - var cmd = client.CreateCommand("echo 12345; echo 654321 >&2"); - var result = cmd.Execute(); - - Console.Write(result); - - var reader = new StreamReader(cmd.ExtendedOutputStream); - Console.WriteLine("DEBUG:"); - Console.Write(reader.ReadToEnd()); - - client.Disconnect(); - - #endregion - - Assert.Inconclusive(); - } - } - - [TestMethod] - [TestCategory("integration")] - [ExpectedException(typeof(SshOperationTimeoutException))] - public void Test_Execute_Timeout() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - #region Example SshCommand CreateCommand Execute CommandTimeout - client.Connect(); - var cmd = client.CreateCommand("sleep 10s"); - cmd.CommandTimeout = TimeSpan.FromSeconds(5); - cmd.Execute(); - client.Disconnect(); - #endregion - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Infinite_Timeout() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var cmd = client.CreateCommand("sleep 10s"); - cmd.Execute(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_InvalidCommand() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - - var cmd = client.CreateCommand(";"); - cmd.Execute(); - if (string.IsNullOrEmpty(cmd.Error)) - { - Assert.Fail("Operation should fail"); - } - Assert.IsTrue(cmd.ExitStatus > 0); - - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_InvalidCommand_Then_Execute_ValidCommand() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var cmd = client.CreateCommand(";"); - cmd.Execute(); - if (string.IsNullOrEmpty(cmd.Error)) - { - Assert.Fail("Operation should fail"); - } - Assert.IsTrue(cmd.ExitStatus > 0); - - var result = ExecuteTestCommand(client); - - client.Disconnect(); - - Assert.IsTrue(result); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Command_with_ExtendedOutput() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var cmd = client.CreateCommand("echo 12345; echo 654321 >&2"); - cmd.Execute(); - - //var extendedData = Encoding.ASCII.GetString(cmd.ExtendedOutputStream.ToArray()); - var extendedData = new StreamReader(cmd.ExtendedOutputStream, Encoding.ASCII).ReadToEnd(); - client.Disconnect(); - - Assert.AreEqual("12345\n", cmd.Result); - Assert.AreEqual("654321\n", extendedData); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Command_Reconnect_Execute_Command() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var result = ExecuteTestCommand(client); - Assert.IsTrue(result); - - client.Disconnect(); - client.Connect(); - result = ExecuteTestCommand(client); - Assert.IsTrue(result); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Command_ExitStatus() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - #region Example SshCommand RunCommand ExitStatus - client.Connect(); - - var cmd = client.RunCommand("exit 128"); - - Console.WriteLine(cmd.ExitStatus); - - client.Disconnect(); - #endregion - - Assert.IsTrue(cmd.ExitStatus == 128); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Command_Asynchronously() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - - var cmd = client.CreateCommand("sleep 5s; echo 'test'"); - var asyncResult = cmd.BeginExecute(null, null); - while (!asyncResult.IsCompleted) - { - Thread.Sleep(100); - } - - cmd.EndExecute(asyncResult); - - Assert.IsTrue(cmd.Result == "test\n"); - - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Command_Asynchronously_With_Error() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - - var cmd = client.CreateCommand("sleep 5s; ;"); - var asyncResult = cmd.BeginExecute(null, null); - while (!asyncResult.IsCompleted) - { - Thread.Sleep(100); - } - - cmd.EndExecute(asyncResult); - - Assert.IsFalse(string.IsNullOrEmpty(cmd.Error)); - - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Command_Asynchronously_With_Callback() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - - var callbackCalled = false; - - var cmd = client.CreateCommand("sleep 5s; echo 'test'"); - var asyncResult = cmd.BeginExecute(new AsyncCallback((s) => - { - callbackCalled = true; - }), null); - while (!asyncResult.IsCompleted) - { - Thread.Sleep(100); - } - - cmd.EndExecute(asyncResult); - - Assert.IsTrue(callbackCalled); - - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Command_Asynchronously_With_Callback_On_Different_Thread() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - - var currentThreadId = Thread.CurrentThread.ManagedThreadId; - int callbackThreadId = 0; - - var cmd = client.CreateCommand("sleep 5s; echo 'test'"); - var asyncResult = cmd.BeginExecute(new AsyncCallback((s) => - { - callbackThreadId = Thread.CurrentThread.ManagedThreadId; - }), null); - while (!asyncResult.IsCompleted) - { - Thread.Sleep(100); - } - - cmd.EndExecute(asyncResult); - - Assert.AreNotEqual(currentThreadId, callbackThreadId); - - client.Disconnect(); - } - } - - /// - /// Tests for Issue 563. - /// - [WorkItem(563), TestMethod] - [TestCategory("integration")] - public void Test_Execute_Command_Same_Object_Different_Commands() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var cmd = client.CreateCommand("echo 12345"); - cmd.Execute(); - Assert.AreEqual("12345\n", cmd.Result); - cmd.Execute("echo 23456"); - Assert.AreEqual("23456\n", cmd.Result); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Get_Result_Without_Execution() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var cmd = client.CreateCommand("ls -l"); - - Assert.IsTrue(string.IsNullOrEmpty(cmd.Result)); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Get_Error_Without_Execution() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var cmd = client.CreateCommand("ls -l"); - - Assert.IsTrue(string.IsNullOrEmpty(cmd.Error)); - client.Disconnect(); - } - } - - [WorkItem(703), TestMethod] - [ExpectedException(typeof(ArgumentException))] - [TestCategory("integration")] - public void Test_EndExecute_Before_BeginExecute() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - var cmd = client.CreateCommand("ls -l"); - cmd.EndExecute(null); - client.Disconnect(); - } - } - - /// - ///A test for BeginExecute - /// - [TestMethod()] - [TestCategory("integration")] - public void BeginExecuteTest() - { - string expected = "123\n"; - string result; - - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - #region Example SshCommand CreateCommand BeginExecute IsCompleted EndExecute - - client.Connect(); - - var cmd = client.CreateCommand("sleep 15s;echo 123"); // Perform long running task - - var asynch = cmd.BeginExecute(); - - while (!asynch.IsCompleted) - { - // Waiting for command to complete... - Thread.Sleep(2000); - } - result = cmd.EndExecute(asynch); - client.Disconnect(); - - #endregion - - Assert.IsNotNull(asynch); - Assert.AreEqual(expected, result); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_Execute_Invalid_Command() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - #region Example SshCommand CreateCommand Error - - client.Connect(); - - var cmd = client.CreateCommand(";"); - cmd.Execute(); - if (!string.IsNullOrEmpty(cmd.Error)) - { - Console.WriteLine(cmd.Error); - } - - client.Disconnect(); - - #endregion - - Assert.Inconclusive(); - } - } - - /// ///A test for BeginExecute /// @@ -630,100 +150,6 @@ public void ResultTest() Assert.Inconclusive("Verify the correctness of this test method."); } -#if FEATURE_TPL - [TestMethod] - [TestCategory("integration")] - public void Test_MultipleThread_Example_MultipleConnections() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - try - { -#region Example SshCommand RunCommand Parallel - System.Threading.Tasks.Parallel.For(0, 10000, - () => - { - var client = new SshClient(host, username, password); - client.Connect(); - return client; - }, - (int counter, ParallelLoopState pls, SshClient client) => - { - var result = client.RunCommand("echo 123"); - Debug.WriteLine(string.Format("TestMultipleThreadMultipleConnections #{0}", counter)); - return client; - }, - (SshClient client) => - { - client.Disconnect(); - client.Dispose(); - } - ); -#endregion - - } - catch (Exception exp) - { - Assert.Fail(exp.ToString()); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_MultipleThread_10000_MultipleConnections() - { - try - { - System.Threading.Tasks.Parallel.For(0, 10000, - () => - { - var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD); - client.Connect(); - return client; - }, - (int counter, ParallelLoopState pls, SshClient client) => - { - var result = ExecuteTestCommand(client); - Debug.WriteLine(string.Format("TestMultipleThreadMultipleConnections #{0}", counter)); - Assert.IsTrue(result); - return client; - }, - (SshClient client) => - { - client.Disconnect(); - client.Dispose(); - } - ); - } - catch (Exception exp) - { - Assert.Fail(exp.ToString()); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_MultipleThread_10000_MultipleSessions() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - System.Threading.Tasks.Parallel.For(0, 10000, - (counter) => - { - var result = ExecuteTestCommand(client); - Debug.WriteLine(string.Format("TestMultipleThreadMultipleConnections #{0}", counter)); - Assert.IsTrue(result); - } - ); - - client.Disconnect(); - } - } -#endif // FEATURE_TPL - private static bool ExecuteTestCommand(SshClient s) { var testValue = Guid.NewGuid().ToString(); diff --git a/src/Renci.SshNet/Properties/AssemblyInfo.cs b/src/Renci.SshNet/Properties/AssemblyInfo.cs index cde7dcdeb..07f66e5fe 100644 --- a/src/Renci.SshNet/Properties/AssemblyInfo.cs +++ b/src/Renci.SshNet/Properties/AssemblyInfo.cs @@ -5,4 +5,5 @@ [assembly: AssemblyTitle("SSH.NET")] [assembly: Guid("ad816c5e-6f13-4589-9f3e-59523f8b77a4")] [assembly: InternalsVisibleTo("Renci.SshNet.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9194e1eb66b7e2575aaee115ee1d27bc100920e7150e43992d6f668f9737de8b9c7ae892b62b8a36dd1d57929ff1541665d101dc476d6e02390846efae7e5186eec409710fdb596e3f83740afef0d4443055937649bc5a773175b61c57615dac0f0fd10f52b52fedf76c17474cc567b3f7a79de95dde842509fb39aaf69c6c2")] +[assembly: InternalsVisibleTo("Renci.SshNet.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f9194e1eb66b7e2575aaee115ee1d27bc100920e7150e43992d6f668f9737de8b9c7ae892b62b8a36dd1d57929ff1541665d101dc476d6e02390846efae7e5186eec409710fdb596e3f83740afef0d4443055937649bc5a773175b61c57615dac0f0fd10f52b52fedf76c17474cc567b3f7a79de95dde842509fb39aaf69c6c2")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] From 051a1c52bdb5231acdecff45bdd8c0baf526ee7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Thu, 7 Sep 2023 06:55:41 +0200 Subject: [PATCH 05/15] Move old integration tests to new integration tests --- .../SftpClientTest.ChangeDirectory.cs | 28 +- .../OldIntegrationTests/SftpFileTest.cs | 120 ++++++ ...rTest_Connect_TimeoutReadingHttpContent.cs | 1 - ...orTest_Connect_TimeoutReadingStatusLine.cs | 1 - ...changeTest_ServerResponseValid_Comments.cs | 2 +- ...angeTest_ServerResponseValid_NoComments.cs | 2 +- ...rminatedByLineFeedWithoutCarriageReturn.cs | 1 - ...nnectorTest_Connect_ConnectionSucceeded.cs | 1 - ...onnect_TimeoutReadingDestinationAddress.cs | 1 - ...torTest_Connect_TimeoutReadingReplyCode.cs | 1 - ...Test_Connect_TimeoutReadingReplyVersion.cs | 1 - .../Classes/Security/KeyHostAlgorithmTest.cs | 123 ------ .../Classes/SftpClientTest.CreateDirectory.cs | 106 ----- .../Classes/SftpClientTest.DeleteDirectory.cs | 71 ---- .../Classes/SftpClientTest.Download.cs | 123 ------ .../Classes/SftpClientTest.ListDirectory.cs | 269 +----------- .../Classes/SftpClientTest.RenameFile.cs | 60 --- .../Classes/SftpClientTest.RenameFileAsync.cs | 64 --- .../SftpClientTest.SynchronizeDirectories.cs | 86 ---- .../Classes/SftpClientTest.Upload.cs | 393 ------------------ .../Classes/SftpClientTest.cs | 66 --- 21 files changed, 133 insertions(+), 1387 deletions(-) rename src/{Renci.SshNet.Tests/Classes => Renci.SshNet.IntegrationTests/OldIntegrationTests}/SftpClientTest.ChangeDirectory.cs (66%) create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/Security/KeyHostAlgorithmTest.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/SftpClientTest.CreateDirectory.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/SftpClientTest.Download.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/SftpClientTest.RenameFile.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/SftpClientTest.RenameFileAsync.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/SftpClientTest.SynchronizeDirectories.cs delete mode 100644 src/Renci.SshNet.Tests/Classes/SftpClientTest.Upload.cs diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.ChangeDirectory.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ChangeDirectory.cs similarity index 66% rename from src/Renci.SshNet.Tests/Classes/SftpClientTest.ChangeDirectory.cs rename to src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ChangeDirectory.cs index 5c58e1fa1..6fb518506 100644 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.ChangeDirectory.cs +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.ChangeDirectory.cs @@ -1,21 +1,18 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; -using Renci.SshNet.Tests.Properties; +using Renci.SshNet.Common; -namespace Renci.SshNet.Tests.Classes +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests { /// /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. /// - public partial class SftpClientTest + public partial class SftpClientTest : IntegrationTestBase { [TestMethod] [TestCategory("Sftp")] - [TestCategory("integration")] [ExpectedException(typeof(SftpPathNotFoundException))] public void Test_Sftp_ChangeDirectory_Root_Dont_Exists() { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { sftp.Connect(); sftp.ChangeDirectory("/asdasd"); @@ -24,11 +21,10 @@ public void Test_Sftp_ChangeDirectory_Root_Dont_Exists() [TestMethod] [TestCategory("Sftp")] - [TestCategory("integration")] [ExpectedException(typeof(SftpPathNotFoundException))] public void Test_Sftp_ChangeDirectory_Root_With_Slash_Dont_Exists() { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { sftp.Connect(); sftp.ChangeDirectory("/asdasd/"); @@ -37,11 +33,10 @@ public void Test_Sftp_ChangeDirectory_Root_With_Slash_Dont_Exists() [TestMethod] [TestCategory("Sftp")] - [TestCategory("integration")] [ExpectedException(typeof(SftpPathNotFoundException))] public void Test_Sftp_ChangeDirectory_Subfolder_Dont_Exists() { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { sftp.Connect(); sftp.ChangeDirectory("/asdasd/sssddds"); @@ -50,11 +45,10 @@ public void Test_Sftp_ChangeDirectory_Subfolder_Dont_Exists() [TestMethod] [TestCategory("Sftp")] - [TestCategory("integration")] [ExpectedException(typeof(SftpPathNotFoundException))] public void Test_Sftp_ChangeDirectory_Subfolder_With_Slash_Dont_Exists() { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { sftp.Connect(); sftp.ChangeDirectory("/asdasd/sssddds/"); @@ -63,10 +57,9 @@ public void Test_Sftp_ChangeDirectory_Subfolder_With_Slash_Dont_Exists() [TestMethod] [TestCategory("Sftp")] - [TestCategory("integration")] public void Test_Sftp_ChangeDirectory_Which_Exists() { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { sftp.Connect(); sftp.ChangeDirectory("/usr"); @@ -76,10 +69,9 @@ public void Test_Sftp_ChangeDirectory_Which_Exists() [TestMethod] [TestCategory("Sftp")] - [TestCategory("integration")] public void Test_Sftp_ChangeDirectory_Which_Exists_With_Slash() { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) { sftp.Connect(); sftp.ChangeDirectory("/usr/"); @@ -87,4 +79,4 @@ public void Test_Sftp_ChangeDirectory_Which_Exists_With_Slash() } } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs new file mode 100644 index 000000000..7b325841f --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs @@ -0,0 +1,120 @@ +using Renci.SshNet.Common; + +namespace Renci.SshNet.Tests.Classes.Sftp +{ + /// + /// Represents SFTP file information + /// + [TestClass] + public class SftpFileTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Sftp")] + public void Test_Get_Root_Directory() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + var directory = sftp.Get("/"); + + Assert.AreEqual("/", directory.FullName); + Assert.IsTrue(directory.IsDirectory); + Assert.IsFalse(directory.IsRegularFile); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [ExpectedException(typeof(SftpPathNotFoundException))] + public void Test_Get_Invalid_Directory() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.Get("/xyz"); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Get_File() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.UploadFile(new MemoryStream(), "abc.txt"); + + var file = sftp.Get("abc.txt"); + + Assert.AreEqual("/home/tester/abc.txt", file.FullName); + Assert.IsTrue(file.IsRegularFile); + Assert.IsFalse(file.IsDirectory); + } + } + + [TestMethod] + [TestCategory("Sftp")] + [Description("Test passing null to Get.")] + [ExpectedException(typeof(ArgumentNullException))] + public void Test_Get_File_Null() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + var file = sftp.Get(null); + + sftp.Disconnect(); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Get_International_File() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + sftp.UploadFile(new MemoryStream(), "test-üöä-"); + + var file = sftp.Get("test-üöä-"); + + Assert.AreEqual("/home/tester/test-üöä-", file.FullName); + Assert.IsTrue(file.IsRegularFile); + Assert.IsFalse(file.IsDirectory); + } + } + + [TestMethod] + [TestCategory("Sftp")] + public void Test_Sftp_SftpFile_MoveTo() + { + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + + string uploadedFileName = Path.GetTempFileName(); + string remoteFileName = Path.GetRandomFileName(); + string newFileName = Path.GetRandomFileName(); + + CreateTestFile(uploadedFileName, 1); + + using (var file = File.OpenRead(uploadedFileName)) + { + sftp.UploadFile(file, remoteFileName); + } + + var sftpFile = sftp.Get(remoteFileName); + + sftpFile.MoveTo(newFileName); + + Assert.AreEqual(newFileName, sftpFile.Name); + + sftp.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingHttpContent.cs b/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingHttpContent.cs index 5db6b2353..98ffde6aa 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingHttpContent.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingHttpContent.cs @@ -12,7 +12,6 @@ using Moq; using Renci.SshNet.Common; -using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Connection diff --git a/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingStatusLine.cs b/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingStatusLine.cs index 10aa8bd91..bec205f1a 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingStatusLine.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_TimeoutReadingStatusLine.cs @@ -10,7 +10,6 @@ using Moq; using Renci.SshNet.Common; -using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Connection diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_Comments.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_Comments.cs index 56ad91011..4108c7c09 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_Comments.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_Comments.cs @@ -1,5 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; + using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; using System; diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_NoComments.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_NoComments.cs index 03c1832df..f3ce9ccf8 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_NoComments.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_NoComments.cs @@ -1,5 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; + using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; using System; diff --git a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_TerminatedByLineFeedWithoutCarriageReturn.cs b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_TerminatedByLineFeedWithoutCarriageReturn.cs index 7aa26b309..2c35bce53 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_TerminatedByLineFeedWithoutCarriageReturn.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/ProtocolVersionExchangeTest_ServerResponseValid_TerminatedByLineFeedWithoutCarriageReturn.cs @@ -8,7 +8,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; diff --git a/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_ConnectionSucceeded.cs b/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_ConnectionSucceeded.cs index b482b753c..5aabb8164 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_ConnectionSucceeded.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_ConnectionSucceeded.cs @@ -10,7 +10,6 @@ using Moq; using Renci.SshNet.Common; -using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Connection diff --git a/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingDestinationAddress.cs b/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingDestinationAddress.cs index d8121e894..d87969ced 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingDestinationAddress.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingDestinationAddress.cs @@ -10,7 +10,6 @@ using Moq; using Renci.SshNet.Common; -using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Connection diff --git a/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyCode.cs b/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyCode.cs index 1aaa36e4a..8f6ee9019 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyCode.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyCode.cs @@ -10,7 +10,6 @@ using Moq; using Renci.SshNet.Common; -using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Connection diff --git a/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyVersion.cs b/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyVersion.cs index 37b7f8389..4ca6e0c58 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyVersion.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/Socks4ConnectorTest_Connect_TimeoutReadingReplyVersion.cs @@ -1,7 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Renci.SshNet.Common; -using Renci.SshNet.Connection; using Renci.SshNet.Tests.Common; using System; using System.Diagnostics; diff --git a/src/Renci.SshNet.Tests/Classes/Security/KeyHostAlgorithmTest.cs b/src/Renci.SshNet.Tests/Classes/Security/KeyHostAlgorithmTest.cs deleted file mode 100644 index 9bd5d10c3..000000000 --- a/src/Renci.SshNet.Tests/Classes/Security/KeyHostAlgorithmTest.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Security; -using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; - -namespace Renci.SshNet.Tests.Classes.Security -{ - /// - /// Implements key support for host algorithm. - /// - [TestClass] - public class KeyHostAlgorithmTest : TestBase - { - [TestMethod] - [TestCategory("integration")] - public void Test_HostKey_SshRsa_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HostKeyAlgorithms.Clear(); - connectionInfo.HostKeyAlgorithms.Add("ssh-rsa", (data) => { return new KeyHostAlgorithm("ssh-rsa", new RsaKey(), data); }); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("integration")] - public void Test_HostKey_SshDss_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.HostKeyAlgorithms.Clear(); - connectionInfo.HostKeyAlgorithms.Add("ssh-dss", (data) => { return new KeyHostAlgorithm("ssh-dss", new DsaKey(), data); }); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - - /// - ///A test for KeyHostAlgorithm Constructor - /// - [TestMethod] - [Ignore] // placeholder for actual test - public void KeyHostAlgorithmConstructorTest() - { - string name = string.Empty; // TODO: Initialize to an appropriate value - Key key = null; // TODO: Initialize to an appropriate value - KeyHostAlgorithm target = new KeyHostAlgorithm(name, key); - Assert.Inconclusive("TODO: Implement code to verify target"); - } - - /// - ///A test for KeyHostAlgorithm Constructor - /// - [TestMethod] - [Ignore] // placeholder for actual test - public void KeyHostAlgorithmConstructorTest1() - { - string name = string.Empty; // TODO: Initialize to an appropriate value - Key key = null; // TODO: Initialize to an appropriate value - byte[] data = null; // TODO: Initialize to an appropriate value - KeyHostAlgorithm target = new KeyHostAlgorithm(name, key, data); - Assert.Inconclusive("TODO: Implement code to verify target"); - } - - /// - ///A test for Sign - /// - [TestMethod] - [Ignore] // placeholder for actual test - public void SignTest() - { - string name = string.Empty; // TODO: Initialize to an appropriate value - Key key = null; // TODO: Initialize to an appropriate value - KeyHostAlgorithm target = new KeyHostAlgorithm(name, key); // TODO: Initialize to an appropriate value - byte[] data = null; // TODO: Initialize to an appropriate value - byte[] expected = null; // TODO: Initialize to an appropriate value - byte[] actual; - actual = target.Sign(data); - Assert.AreEqual(expected, actual); - Assert.Inconclusive("Verify the correctness of this test method."); - } - - /// - ///A test for VerifySignature - /// - [TestMethod] - [Ignore] // placeholder for actual test - public void VerifySignatureTest() - { - string name = string.Empty; // TODO: Initialize to an appropriate value - Key key = null; // TODO: Initialize to an appropriate value - KeyHostAlgorithm target = new KeyHostAlgorithm(name, key); // TODO: Initialize to an appropriate value - byte[] data = null; // TODO: Initialize to an appropriate value - byte[] signature = null; // TODO: Initialize to an appropriate value - bool expected = false; // TODO: Initialize to an appropriate value - bool actual; - actual = target.VerifySignature(data, signature); - Assert.AreEqual(expected, actual); - Assert.Inconclusive("Verify the correctness of this test method."); - } - - /// - ///A test for Data - /// - [TestMethod] - [Ignore] // placeholder for actual test - public void DataTest() - { - string name = string.Empty; // TODO: Initialize to an appropriate value - Key key = null; // TODO: Initialize to an appropriate value - KeyHostAlgorithm target = new KeyHostAlgorithm(name, key); // TODO: Initialize to an appropriate value - byte[] actual; - actual = target.Data; - Assert.Inconclusive("Verify the correctness of this test method."); - } - } -} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.CreateDirectory.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.CreateDirectory.cs deleted file mode 100644 index 73e5f3c83..000000000 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.CreateDirectory.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; -using Renci.SshNet.Tests.Properties; -using System; - -namespace Renci.SshNet.Tests.Classes -{ - /// - /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. - /// - public partial class SftpClientTest - { - [TestMethod] - [TestCategory("Sftp")] - [ExpectedException(typeof(SshConnectionException))] - public void Test_Sftp_CreateDirectory_Without_Connecting() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.CreateDirectory("test"); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_CreateDirectory_In_Current_Location() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.CreateDirectory("test"); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPermissionDeniedException))] - public void Test_Sftp_CreateDirectory_In_Forbidden_Directory() - { - if (Resources.USERNAME == "root") - { - Assert.Fail("Must not run this test as root!"); - } - - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.CreateDirectory("/sbin/test"); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPathNotFoundException))] - public void Test_Sftp_CreateDirectory_Invalid_Path() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.CreateDirectory("/abcdefg/abcefg"); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SshException))] - public void Test_Sftp_CreateDirectory_Already_Exists() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.CreateDirectory("test"); - - sftp.CreateDirectory("test"); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [Description("Test passing null to CreateDirectory.")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_CreateDirectory_Null() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.CreateDirectory(null); - } - } - } -} diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.DeleteDirectory.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.DeleteDirectory.cs index 0964d4690..d25fb37fd 100644 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.DeleteDirectory.cs +++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest.DeleteDirectory.cs @@ -1,7 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Common; using Renci.SshNet.Tests.Properties; -using System; namespace Renci.SshNet.Tests.Classes { @@ -20,75 +19,5 @@ public void Test_Sftp_DeleteDirectory_Without_Connecting() sftp.DeleteDirectory("test"); } } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPathNotFoundException))] - public void Test_Sftp_DeleteDirectory_Which_Doesnt_Exists() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.DeleteDirectory("abcdef"); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPermissionDeniedException))] - public void Test_Sftp_DeleteDirectory_Which_No_Permissions() - { - if (Resources.USERNAME == "root") - { - Assert.Fail("Must not run this test as root!"); - } - - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.DeleteDirectory("/usr"); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_DeleteDirectory() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.CreateDirectory("abcdef"); - sftp.DeleteDirectory("abcdef"); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to DeleteDirectory.")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_DeleteDirectory_Null() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.DeleteDirectory(null); - - sftp.Disconnect(); - } - } } } diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.Download.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.Download.cs deleted file mode 100644 index 41dbdf33f..000000000 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.Download.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; -using Renci.SshNet.Tests.Properties; -using System; -using System.IO; - -namespace Renci.SshNet.Tests.Classes -{ - /// - /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. - /// - public partial class SftpClientTest - { - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPermissionDeniedException))] - public void Test_Sftp_Download_Forbidden() - { - if (Resources.USERNAME == "root") - { - Assert.Fail("Must not run this test as root!"); - } - - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - string remoteFileName = "/root/.profile"; - - using (var ms = new MemoryStream()) - { - sftp.DownloadFile(remoteFileName, ms); - } - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPathNotFoundException))] - public void Test_Sftp_Download_File_Not_Exists() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - string remoteFileName = "/xxx/eee/yyy"; - using (var ms = new MemoryStream()) - { - sftp.DownloadFile(remoteFileName, ms); - } - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to BeginDownloadFile")] - [ExpectedException(typeof(ArgumentNullException))] - public void Test_Sftp_BeginDownloadFile_StreamIsNull() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - sftp.BeginDownloadFile("aaaa", null, null, null); - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to BeginDownloadFile")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_BeginDownloadFile_FileNameIsWhiteSpace() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - sftp.BeginDownloadFile(" ", new MemoryStream(), null, null); - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to BeginDownloadFile")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_BeginDownloadFile_FileNameIsNull() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - sftp.BeginDownloadFile(null, new MemoryStream(), null, null); - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_EndDownloadFile_Invalid_Async_Handle() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - var filename = Path.GetTempFileName(); - this.CreateTestFile(filename, 1); - sftp.UploadFile(File.OpenRead(filename), "test123"); - var async1 = sftp.BeginListDirectory("/", null, null); - var async2 = sftp.BeginDownloadFile("test123", new MemoryStream(), null, null); - sftp.EndDownloadFile(async1); - } - } - } -} diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs index 6a19ce5a3..ceafd4c50 100644 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs +++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest.ListDirectory.cs @@ -1,13 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Common; using Renci.SshNet.Tests.Properties; -using System; + using System.Diagnostics; -using System.Linq; -#if NET6_0_OR_GREATER -using System.Threading; -using System.Threading.Tasks; -#endif namespace Renci.SshNet.Tests.Classes { @@ -30,267 +25,5 @@ public void Test_Sftp_ListDirectory_Without_Connecting() } } } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPermissionDeniedException))] - public void Test_Sftp_ListDirectory_Permission_Denied() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var files = sftp.ListDirectory("/root"); - foreach (var file in files) - { - Debug.WriteLine(file.FullName); - } - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPathNotFoundException))] - public void Test_Sftp_ListDirectory_Not_Exists() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var files = sftp.ListDirectory("/asdfgh"); - foreach (var file in files) - { - Debug.WriteLine(file.FullName); - } - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_ListDirectory_Current() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var files = sftp.ListDirectory("."); - - Assert.IsTrue(files.Count() > 0); - - foreach (var file in files) - { - Debug.WriteLine(file.FullName); - } - - sftp.Disconnect(); - } - } - -#if NET6_0_OR_GREATER - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public async Task Test_Sftp_ListDirectoryAsync_Current() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromMinutes(1)); - var count = 0; - await foreach(var file in sftp.ListDirectoryAsync(".", cts.Token)) - { - count++; - Debug.WriteLine(file.FullName); - } - - Assert.IsTrue(count > 0); - - sftp.Disconnect(); - } - } -#endif - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_ListDirectory_Empty() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var files = sftp.ListDirectory(string.Empty); - - Assert.IsTrue(files.Count() > 0); - - foreach (var file in files) - { - Debug.WriteLine(file.FullName); - } - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to ListDirectory.")] - [ExpectedException(typeof(ArgumentNullException))] - public void Test_Sftp_ListDirectory_Null() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var files = sftp.ListDirectory(null); - - Assert.IsTrue(files.Count() > 0); - - foreach (var file in files) - { - Debug.WriteLine(file.FullName); - } - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_ListDirectory_HugeDirectory() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - // Create 10000 directory items - for (int i = 0; i < 10000; i++) - { - sftp.CreateDirectory(string.Format("test_{0}", i)); - Debug.WriteLine("Created " + i); - } - - var files = sftp.ListDirectory("."); - - // Ensure that directory has at least 10000 items - Assert.IsTrue(files.Count() > 10000); - - sftp.Disconnect(); - } - - RemoveAllFiles(); - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_Change_Directory() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester"); - - sftp.CreateDirectory("test1"); - - sftp.ChangeDirectory("test1"); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester/test1"); - - sftp.CreateDirectory("test1_1"); - sftp.CreateDirectory("test1_2"); - sftp.CreateDirectory("test1_3"); - - var files = sftp.ListDirectory("."); - - Assert.IsTrue(files.First().FullName.StartsWith(string.Format("{0}", sftp.WorkingDirectory))); - - sftp.ChangeDirectory("test1_1"); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester/test1/test1_1"); - - sftp.ChangeDirectory("../test1_2"); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester/test1/test1_2"); - - sftp.ChangeDirectory(".."); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester/test1"); - - sftp.ChangeDirectory(".."); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester"); - - files = sftp.ListDirectory("test1/test1_1"); - - Assert.IsTrue(files.First().FullName.StartsWith(string.Format("{0}/test1/test1_1", sftp.WorkingDirectory))); - - sftp.ChangeDirectory("test1/test1_1"); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester/test1/test1_1"); - - sftp.ChangeDirectory("/home/tester/test1/test1_1"); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester/test1/test1_1"); - - sftp.ChangeDirectory("/home/tester/test1/test1_1/../test1_2"); - - Assert.AreEqual(sftp.WorkingDirectory, "/home/tester/test1/test1_2"); - - sftp.ChangeDirectory("../../"); - - sftp.DeleteDirectory("test1/test1_1"); - sftp.DeleteDirectory("test1/test1_2"); - sftp.DeleteDirectory("test1/test1_3"); - sftp.DeleteDirectory("test1"); - - sftp.Disconnect(); - } - - RemoveAllFiles(); - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to ChangeDirectory.")] - [ExpectedException(typeof(ArgumentNullException))] - public void Test_Sftp_ChangeDirectory_Null() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.ChangeDirectory(null); - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test calling EndListDirectory method more then once.")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_Call_EndListDirectory_Twice() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - var ar = sftp.BeginListDirectory("/", null, null); - var result = sftp.EndListDirectory(ar); - var result1 = sftp.EndListDirectory(ar); - } - } } } diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.RenameFile.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.RenameFile.cs deleted file mode 100644 index e1685d099..000000000 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.RenameFile.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Tests.Properties; -using System; -using System.IO; - -namespace Renci.SshNet.Tests.Classes -{ - /// - /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. - /// - public partial class SftpClientTest - { - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_Rename_File() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - string uploadedFileName = Path.GetTempFileName(); - string remoteFileName1 = Path.GetRandomFileName(); - string remoteFileName2 = Path.GetRandomFileName(); - - this.CreateTestFile(uploadedFileName, 1); - - using (var file = File.OpenRead(uploadedFileName)) - { - sftp.UploadFile(file, remoteFileName1); - } - - sftp.RenameFile(remoteFileName1, remoteFileName2); - - File.Delete(uploadedFileName); - - sftp.Disconnect(); - } - - RemoveAllFiles(); - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to RenameFile.")] - [ExpectedException(typeof(ArgumentNullException))] - public void Test_Sftp_RenameFile_Null() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - sftp.RenameFile(null, null); - - sftp.Disconnect(); - } - } - } -} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.RenameFileAsync.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.RenameFileAsync.cs deleted file mode 100644 index a24ec0942..000000000 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.RenameFileAsync.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Tests.Properties; -using System; -using System.IO; -using System.Threading.Tasks; - -namespace Renci.SshNet.Tests.Classes -{ - /// - /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. - /// - public partial class SftpClientTest - { - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public async Task Test_Sftp_RenameFileAsync() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - await sftp.ConnectAsync(default); - - string uploadedFileName = Path.GetTempFileName(); - string remoteFileName1 = Path.GetRandomFileName(); - string remoteFileName2 = Path.GetRandomFileName(); - - this.CreateTestFile(uploadedFileName, 1); - - using (var file = File.OpenRead(uploadedFileName)) - { - using (Stream remoteStream = await sftp.OpenAsync(remoteFileName1, FileMode.CreateNew, FileAccess.Write, default)) - { - await file.CopyToAsync(remoteStream, 81920, default); - } - } - - await sftp.RenameFileAsync(remoteFileName1, remoteFileName2, default); - - File.Delete(uploadedFileName); - - sftp.Disconnect(); - } - - RemoveAllFiles(); - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to RenameFile.")] - [ExpectedException(typeof(ArgumentNullException))] - public async Task Test_Sftp_RenameFileAsync_Null() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - await sftp.ConnectAsync(default); - - await sftp.RenameFileAsync(null, null, default); - - sftp.Disconnect(); - } - } - } -} diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.SynchronizeDirectories.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.SynchronizeDirectories.cs deleted file mode 100644 index 2c9ddb3e0..000000000 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.SynchronizeDirectories.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Tests.Properties; -using System.Diagnostics; -using System.IO; -using System.Linq; - -namespace Renci.SshNet.Tests.Classes -{ - /// - /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. - /// - public partial class SftpClientTest - { - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_SynchronizeDirectories() - { - RemoveAllFiles(); - - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - string uploadedFileName = Path.GetTempFileName(); - - string sourceDir = Path.GetDirectoryName(uploadedFileName); - string destDir = "/"; - string searchPattern = Path.GetFileName(uploadedFileName); - var upLoadedFiles = sftp.SynchronizeDirectories(sourceDir, destDir, searchPattern); - - Assert.IsTrue(upLoadedFiles.Count() > 0); - - foreach (var file in upLoadedFiles) - { - Debug.WriteLine(file.FullName); - } - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_BeginSynchronizeDirectories() - { - RemoveAllFiles(); - - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - string uploadedFileName = Path.GetTempFileName(); - - string sourceDir = Path.GetDirectoryName(uploadedFileName); - string destDir = "/"; - string searchPattern = Path.GetFileName(uploadedFileName); - - var asyncResult = sftp.BeginSynchronizeDirectories(sourceDir, - destDir, - searchPattern, - null, - null - ); - - // Wait for the WaitHandle to become signaled. - asyncResult.AsyncWaitHandle.WaitOne(1000); - - var upLoadedFiles = sftp.EndSynchronizeDirectories(asyncResult); - - Assert.IsTrue(upLoadedFiles.Count() > 0); - - foreach (var file in upLoadedFiles) - { - Debug.WriteLine(file.FullName); - } - - // Close the wait handle. - asyncResult.AsyncWaitHandle.Close(); - - sftp.Disconnect(); - } - } - } -} \ No newline at end of file diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.Upload.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.Upload.cs deleted file mode 100644 index 2018c9f0d..000000000 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.Upload.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; - -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using Renci.SshNet.Common; -using Renci.SshNet.Sftp; -using Renci.SshNet.Tests.Properties; - -namespace Renci.SshNet.Tests.Classes -{ - /// - /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH. - /// - public partial class SftpClientTest - { - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_Upload_And_Download_1MB_File() - { - RemoveAllFiles(); - - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var uploadedFileName = Path.GetTempFileName(); - var remoteFileName = Path.GetRandomFileName(); - - CreateTestFile(uploadedFileName, 1); - - // Calculate has value - var uploadedHash = CalculateMD5(uploadedFileName); - - using (var file = File.OpenRead(uploadedFileName)) - { - sftp.UploadFile(file, remoteFileName); - } - - var downloadedFileName = Path.GetTempFileName(); - - using (var file = File.OpenWrite(downloadedFileName)) - { - sftp.DownloadFile(remoteFileName, file); - } - - var downloadedHash = CalculateMD5(downloadedFileName); - - sftp.DeleteFile(remoteFileName); - - File.Delete(uploadedFileName); - File.Delete(downloadedFileName); - - sftp.Disconnect(); - - Assert.AreEqual(uploadedHash, downloadedHash); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(SftpPermissionDeniedException))] - public void Test_Sftp_Upload_Forbidden() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var uploadedFileName = Path.GetTempFileName(); - var remoteFileName = "/root/1"; - - CreateTestFile(uploadedFileName, 1); - - using (var file = File.OpenRead(uploadedFileName)) - { - sftp.UploadFile(file, remoteFileName); - } - - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - public void Test_Sftp_Multiple_Async_Upload_And_Download_10Files_5MB_Each() - { - var maxFiles = 10; - var maxSize = 5; - - RemoveAllFiles(); - - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var testInfoList = new Dictionary(); - - for (var i = 0; i < maxFiles; i++) - { - var testInfo = new TestInfo - { - UploadedFileName = Path.GetTempFileName(), - DownloadedFileName = Path.GetTempFileName(), - RemoteFileName = Path.GetRandomFileName() - }; - - CreateTestFile(testInfo.UploadedFileName, maxSize); - - // Calculate hash value - testInfo.UploadedHash = CalculateMD5(testInfo.UploadedFileName); - - testInfoList.Add(testInfo.RemoteFileName, testInfo); - } - - var uploadWaitHandles = new List(); - - // Start file uploads - foreach (var remoteFile in testInfoList.Keys) - { - var testInfo = testInfoList[remoteFile]; - testInfo.UploadedFile = File.OpenRead(testInfo.UploadedFileName); - - testInfo.UploadResult = sftp.BeginUploadFile(testInfo.UploadedFile, - remoteFile, - null, - null) as SftpUploadAsyncResult; - - uploadWaitHandles.Add(testInfo.UploadResult.AsyncWaitHandle); - } - - // Wait for upload to finish - var uploadCompleted = false; - while (!uploadCompleted) - { - // Assume upload completed - uploadCompleted = true; - - foreach (var testInfo in testInfoList.Values) - { - var sftpResult = testInfo.UploadResult; - - if (!testInfo.UploadResult.IsCompleted) - { - uploadCompleted = false; - } - } - Thread.Sleep(500); - } - - // End file uploads - foreach (var remoteFile in testInfoList.Keys) - { - var testInfo = testInfoList[remoteFile]; - - sftp.EndUploadFile(testInfo.UploadResult); - testInfo.UploadedFile.Dispose(); - } - - // Start file downloads - - var downloadWaitHandles = new List(); - - foreach (var remoteFile in testInfoList.Keys) - { - var testInfo = testInfoList[remoteFile]; - testInfo.DownloadedFile = File.OpenWrite(testInfo.DownloadedFileName); - testInfo.DownloadResult = sftp.BeginDownloadFile(remoteFile, - testInfo.DownloadedFile, - null, - null) as SftpDownloadAsyncResult; - - downloadWaitHandles.Add(testInfo.DownloadResult.AsyncWaitHandle); - } - - // Wait for download to finish - var downloadCompleted = false; - while (!downloadCompleted) - { - // Assume download completed - downloadCompleted = true; - - foreach (var testInfo in testInfoList.Values) - { - var sftpResult = testInfo.DownloadResult; - - if (!testInfo.DownloadResult.IsCompleted) - { - downloadCompleted = false; - } - } - Thread.Sleep(500); - } - - var hashMatches = true; - var uploadDownloadSizeOk = true; - - // End file downloads - foreach (var remoteFile in testInfoList.Keys) - { - var testInfo = testInfoList[remoteFile]; - - sftp.EndDownloadFile(testInfo.DownloadResult); - - testInfo.DownloadedFile.Dispose(); - - testInfo.DownloadedHash = CalculateMD5(testInfo.DownloadedFileName); - - if (!(testInfo.UploadResult.UploadedBytes > 0 && testInfo.DownloadResult.DownloadedBytes > 0 && testInfo.DownloadResult.DownloadedBytes == testInfo.UploadResult.UploadedBytes)) - { - uploadDownloadSizeOk = false; - } - - if (!testInfo.DownloadedHash.Equals(testInfo.UploadedHash)) - { - hashMatches = false; - } - } - - // Clean up after test - foreach (var remoteFile in testInfoList.Keys) - { - var testInfo = testInfoList[remoteFile]; - - sftp.DeleteFile(remoteFile); - - File.Delete(testInfo.UploadedFileName); - File.Delete(testInfo.DownloadedFileName); - } - - sftp.Disconnect(); - - Assert.IsTrue(hashMatches, "Hash does not match"); - Assert.IsTrue(uploadDownloadSizeOk, "Uploaded and downloaded bytes does not match"); - } - } - - // TODO: Split this test into multiple tests - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test that delegates passed to BeginUploadFile, BeginDownloadFile and BeginListDirectory are actually called.")] - public void Test_Sftp_Ensure_Async_Delegates_Called_For_BeginFileUpload_BeginFileDownload_BeginListDirectory() - { - RemoveAllFiles(); - - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - - var remoteFileName = Path.GetRandomFileName(); - var localFileName = Path.GetRandomFileName(); - var uploadDelegateCalled = false; - var downloadDelegateCalled = false; - var listDirectoryDelegateCalled = false; - IAsyncResult asyncResult; - - // Test for BeginUploadFile. - - CreateTestFile(localFileName, 1); - - using (var fileStream = File.OpenRead(localFileName)) - { - asyncResult = sftp.BeginUploadFile(fileStream, - remoteFileName, - delegate(IAsyncResult ar) - { - sftp.EndUploadFile(ar); - uploadDelegateCalled = true; - }, - null); - - while (!asyncResult.IsCompleted) - { - Thread.Sleep(500); - } - } - - File.Delete(localFileName); - - Assert.IsTrue(uploadDelegateCalled, "BeginUploadFile"); - - // Test for BeginDownloadFile. - - asyncResult = null; - using (var fileStream = File.OpenWrite(localFileName)) - { - asyncResult = sftp.BeginDownloadFile(remoteFileName, - fileStream, - delegate(IAsyncResult ar) - { - sftp.EndDownloadFile(ar); - downloadDelegateCalled = true; - }, - null); - - while (!asyncResult.IsCompleted) - { - Thread.Sleep(500); - } - } - - File.Delete(localFileName); - - Assert.IsTrue(downloadDelegateCalled, "BeginDownloadFile"); - - // Test for BeginListDirectory. - - asyncResult = null; - asyncResult = sftp.BeginListDirectory(sftp.WorkingDirectory, - delegate(IAsyncResult ar) - { - _ = sftp.EndListDirectory(ar); - listDirectoryDelegateCalled = true; - }, - null); - - while (!asyncResult.IsCompleted) - { - Thread.Sleep(500); - } - - Assert.IsTrue(listDirectoryDelegateCalled, "BeginListDirectory"); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to BeginUploadFile")] - [ExpectedException(typeof(ArgumentNullException))] - public void Test_Sftp_BeginUploadFile_StreamIsNull() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - _ = sftp.BeginUploadFile(null, "aaaaa", null, null); - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to BeginUploadFile")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_BeginUploadFile_FileNameIsWhiteSpace() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - _ = sftp.BeginUploadFile(new MemoryStream(), " ", null, null); - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [Description("Test passing null to BeginUploadFile")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_BeginUploadFile_FileNameIsNull() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - _ = sftp.BeginUploadFile(new MemoryStream(), null, null, null); - sftp.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Sftp")] - [TestCategory("integration")] - [ExpectedException(typeof(ArgumentException))] - public void Test_Sftp_EndUploadFile_Invalid_Async_Handle() - { - using (var sftp = new SftpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - sftp.Connect(); - var async1 = sftp.BeginListDirectory("/", null, null); - var filename = Path.GetTempFileName(); - CreateTestFile(filename, 100); - var async2 = sftp.BeginUploadFile(File.OpenRead(filename), "test", null, null); - sftp.EndUploadFile(async1); - } - } - } -} diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.cs index 200abae89..9e1c1f3c7 100644 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.cs +++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest.cs @@ -1,11 +1,9 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Sftp; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; using System; using System.Collections.Generic; using System.IO; -using System.Security.Cryptography; using System.Text; namespace Renci.SshNet.Tests.Classes @@ -1355,69 +1353,5 @@ public void WorkingDirectoryTest() actual = target.WorkingDirectory; Assert.Inconclusive("Verify the correctness of this test method."); } - - protected static string CalculateMD5(string fileName) - { - using (FileStream file = new FileStream(fileName, FileMode.Open)) - { -#if NET7_0_OR_GREATER - var hash = MD5.HashData(file); -#else -#if NET6_0 - var md5 = MD5.Create(); -#else - MD5 md5 = new MD5CryptoServiceProvider(); -#endif // NET6_0 - var hash = md5.ComputeHash(file); -#endif // NET7_0_OR_GREATER - - file.Close(); - - StringBuilder sb = new StringBuilder(); - for (var i = 0; i < hash.Length; i++) - { - sb.Append(hash[i].ToString("x2")); - } - return sb.ToString(); - } - } - - private static void RemoveAllFiles() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - client.RunCommand("rm -rf *"); - client.Disconnect(); - } - } - - /// - /// Helper class to help with upload and download testing - /// - private class TestInfo - { - public string RemoteFileName { get; set; } - - public string UploadedFileName { get; set; } - - public string DownloadedFileName { get; set; } - - //public ulong UploadedBytes { get; set; } - - //public ulong DownloadedBytes { get; set; } - - public FileStream UploadedFile { get; set; } - - public FileStream DownloadedFile { get; set; } - - public string UploadedHash { get; set; } - - public string DownloadedHash { get; set; } - - public SftpUploadAsyncResult UploadResult { get; set; } - - public SftpDownloadAsyncResult DownloadResult { get; set; } - } } } From a590aaa6e004483c0ad1ee393a38049475dd672f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Fri, 8 Sep 2023 16:38:28 +0200 Subject: [PATCH 06/15] Move more tests --- .../OldIntegrationTests/AesCipherTests.cs | 1 - .../OldIntegrationTests/ScpClientTest.cs | 336 ++++++++++++++++++ .../OldIntegrationTests/SftpFileTest.cs | 2 +- .../TripleDesCipherTest.cs | 50 +++ .../Classes/ScpClientTest.cs | 324 ----------------- .../Ciphers/BlowfishCipherTest.cs | 17 - .../Cryptography/Ciphers/CastCipherTest.cs | 18 +- .../Ciphers/TripleDesCipherTest.cs | 16 - 8 files changed, 388 insertions(+), 376 deletions(-) create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/ScpClientTest.cs create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/TripleDesCipherTest.cs diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/AesCipherTests.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/AesCipherTests.cs index d93dd5e66..7e9e97b01 100644 --- a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/AesCipherTests.cs +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/AesCipherTests.cs @@ -24,7 +24,6 @@ public void TearDown() _remoteSshdConfig?.Reset(); } - [TestMethod] [Owner("olegkap")] [TestCategory("Cipher")] diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/ScpClientTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/ScpClientTest.cs new file mode 100644 index 000000000..e9015a3c6 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/ScpClientTest.cs @@ -0,0 +1,336 @@ +using System.Security.Cryptography; + +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Provides SCP client functionality. + /// + [TestClass] + public partial class ScpClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Scp")] + public void Test_Scp_File_Upload_Download() + { + RemoveAllFiles(); + + using (var scp = new ScpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + scp.Connect(); + + var uploadedFileName = Path.GetTempFileName(); + var downloadedFileName = Path.GetTempFileName(); + + CreateTestFile(uploadedFileName, 1); + + scp.Upload(new FileInfo(uploadedFileName), Path.GetFileName(uploadedFileName)); + + scp.Download(Path.GetFileName(uploadedFileName), new FileInfo(downloadedFileName)); + + // Calculate MD5 value + var uploadedHash = CalculateMD5(uploadedFileName); + var downloadedHash = CalculateMD5(downloadedFileName); + + File.Delete(uploadedFileName); + File.Delete(downloadedFileName); + + scp.Disconnect(); + + Assert.AreEqual(uploadedHash, downloadedHash); + } + } + + [TestMethod] + [TestCategory("Scp")] + public void Test_Scp_Stream_Upload_Download() + { + RemoveAllFiles(); + + using (var scp = new ScpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + scp.Connect(); + + var uploadedFileName = Path.GetTempFileName(); + var downloadedFileName = Path.GetTempFileName(); + + CreateTestFile(uploadedFileName, 1); + + // Calculate has value + using (var stream = File.OpenRead(uploadedFileName)) + { + scp.Upload(stream, Path.GetFileName(uploadedFileName)); + } + + using (var stream = File.OpenWrite(downloadedFileName)) + { + scp.Download(Path.GetFileName(uploadedFileName), stream); + } + + // Calculate MD5 value + var uploadedHash = CalculateMD5(uploadedFileName); + var downloadedHash = CalculateMD5(downloadedFileName); + + File.Delete(uploadedFileName); + File.Delete(downloadedFileName); + + scp.Disconnect(); + + Assert.AreEqual(uploadedHash, downloadedHash); + } + } + + [TestMethod] + [TestCategory("Scp")] + public void Test_Scp_10MB_File_Upload_Download() + { + RemoveAllFiles(); + + using (var scp = new ScpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + scp.Connect(); + + var uploadedFileName = Path.GetTempFileName(); + var downloadedFileName = Path.GetTempFileName(); + + CreateTestFile(uploadedFileName, 10); + + scp.Upload(new FileInfo(uploadedFileName), Path.GetFileName(uploadedFileName)); + + scp.Download(Path.GetFileName(uploadedFileName), new FileInfo(downloadedFileName)); + + // Calculate MD5 value + var uploadedHash = CalculateMD5(uploadedFileName); + var downloadedHash = CalculateMD5(downloadedFileName); + + File.Delete(uploadedFileName); + File.Delete(downloadedFileName); + + scp.Disconnect(); + + Assert.AreEqual(uploadedHash, downloadedHash); + } + } + + [TestMethod] + [TestCategory("Scp")] + public void Test_Scp_10MB_Stream_Upload_Download() + { + RemoveAllFiles(); + + using (var scp = new ScpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + scp.Connect(); + + var uploadedFileName = Path.GetTempFileName(); + var downloadedFileName = Path.GetTempFileName(); + + CreateTestFile(uploadedFileName, 10); + + // Calculate has value + using (var stream = File.OpenRead(uploadedFileName)) + { + scp.Upload(stream, Path.GetFileName(uploadedFileName)); + } + + using (var stream = File.OpenWrite(downloadedFileName)) + { + scp.Download(Path.GetFileName(uploadedFileName), stream); + } + + // Calculate MD5 value + var uploadedHash = CalculateMD5(uploadedFileName); + var downloadedHash = CalculateMD5(downloadedFileName); + + File.Delete(uploadedFileName); + File.Delete(downloadedFileName); + + scp.Disconnect(); + + Assert.AreEqual(uploadedHash, downloadedHash); + } + } + + [TestMethod] + [TestCategory("Scp")] + public void Test_Scp_Directory_Upload_Download() + { + RemoveAllFiles(); + using (var sftp = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + sftp.Connect(); + sftp.CreateDirectory("uploaded_dir"); + } + + using (var scp = new ScpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + scp.Connect(); + + var uploadDirectory = + Directory.CreateDirectory(string.Format("{0}\\{1}", Path.GetTempPath(), Path.GetRandomFileName())); + for (var i = 0; i < 3; i++) + { + var subfolder = Directory.CreateDirectory(string.Format(@"{0}\folder_{1}", uploadDirectory.FullName, i)); + + for (var j = 0; j < 5; j++) + { + CreateTestFile(string.Format(@"{0}\file_{1}", subfolder.FullName, j), 1); + } + + CreateTestFile(string.Format(@"{0}\file_{1}", uploadDirectory.FullName, i), 1); + } + + scp.Upload(uploadDirectory, "uploaded_dir"); + + var downloadDirectory = + Directory.CreateDirectory(string.Format("{0}\\{1}", Path.GetTempPath(), Path.GetRandomFileName())); + + scp.Download("uploaded_dir", downloadDirectory); + + var uploadedFiles = uploadDirectory.GetFiles("*.*", SearchOption.AllDirectories); + var downloadFiles = downloadDirectory.GetFiles("*.*", SearchOption.AllDirectories); + + var result = from f1 in uploadedFiles + from f2 in downloadFiles + where + f1.FullName.Substring(uploadDirectory.FullName.Length) == + f2.FullName.Substring(downloadDirectory.FullName.Length) + && CalculateMD5(f1.FullName) == CalculateMD5(f2.FullName) + select f1; + + var counter = result.Count(); + + scp.Disconnect(); + + Assert.IsTrue(counter == uploadedFiles.Length && uploadedFiles.Length == downloadFiles.Length); + } + RemoveAllFiles(); + } + + [TestMethod] + [TestCategory("Scp")] + public void Test_Scp_File_20_Parallel_Upload_Download() + { + using (var scp = new ScpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + scp.Connect(); + + var uploadFilenames = new string[20]; + for (var i = 0; i < uploadFilenames.Length; i++) + { + uploadFilenames[i] = Path.GetTempFileName(); + CreateTestFile(uploadFilenames[i], 1); + } + + _ = Parallel.ForEach(uploadFilenames, + filename => + { + scp.Upload(new FileInfo(filename), Path.GetFileName(filename)); + }); + _ = Parallel.ForEach(uploadFilenames, + filename => + { + scp.Download(Path.GetFileName(filename), new FileInfo(string.Format("{0}.down", filename))); + }); + + var result = from file in uploadFilenames + where CalculateMD5(file) == CalculateMD5(string.Format("{0}.down", file)) + select file; + + scp.Disconnect(); + + Assert.IsTrue(result.Count() == uploadFilenames.Length); + } + } + + [TestMethod] + [TestCategory("Scp")] + public void Test_Scp_File_Upload_Download_Events() + { + using (var scp = new ScpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + scp.Connect(); + + var uploadFilenames = new string[10]; + + for (var i = 0; i < uploadFilenames.Length; i++) + { + uploadFilenames[i] = Path.GetTempFileName(); + CreateTestFile(uploadFilenames[i], 1); + } + + var uploadedFiles = uploadFilenames.ToDictionary(Path.GetFileName, (filename) => 0L); + var downloadedFiles = uploadFilenames.ToDictionary((filename) => string.Format("{0}.down", Path.GetFileName(filename)), (filename) => 0L); + + scp.Uploading += delegate (object sender, ScpUploadEventArgs e) + { + uploadedFiles[e.Filename] = e.Uploaded; + }; + + scp.Downloading += delegate (object sender, ScpDownloadEventArgs e) + { + downloadedFiles[string.Format("{0}.down", e.Filename)] = e.Downloaded; + }; + + _ = Parallel.ForEach(uploadFilenames, + filename => + { + scp.Upload(new FileInfo(filename), Path.GetFileName(filename)); + }); + _ = Parallel.ForEach(uploadFilenames, + filename => + { + scp.Download(Path.GetFileName(filename), new FileInfo(string.Format("{0}.down", filename))); + }); + + var result = from uf in uploadedFiles + from df in downloadedFiles + where string.Format("{0}.down", uf.Key) == df.Key && uf.Value == df.Value + select uf; + + scp.Disconnect(); + + Assert.IsTrue(result.Count() == uploadFilenames.Length && uploadFilenames.Length == uploadedFiles.Count && uploadedFiles.Count == downloadedFiles.Count); + } + } + + protected static string CalculateMD5(string fileName) + { + using (var file = new FileStream(fileName, FileMode.Open)) + { +#if NET7_0_OR_GREATER + var hash = MD5.HashData(file); +#else +#if NET6_0 + var md5 = MD5.Create(); +#else + MD5 md5 = new MD5CryptoServiceProvider(); +#endif // NET6_0 + var hash = md5.ComputeHash(file); +#endif // NET7_0_OR_GREATER + + file.Close(); + + var sb = new StringBuilder(); + + for (var i = 0; i < hash.Length; i++) + { + _ = sb.Append(i.ToString("x2")); + } + + return sb.ToString(); + } + } + + private void RemoveAllFiles() + { + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.Connect(); + _ = client.RunCommand("rm -rf *"); + client.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs index 7b325841f..13d3832a6 100644 --- a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs @@ -1,6 +1,6 @@ using Renci.SshNet.Common; -namespace Renci.SshNet.Tests.Classes.Sftp +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests { /// /// Represents SFTP file information diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/TripleDesCipherTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/TripleDesCipherTest.cs new file mode 100644 index 000000000..a9195cf5e --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/TripleDesCipherTest.cs @@ -0,0 +1,50 @@ +using Renci.SshNet.IntegrationTests.Common; +using Renci.SshNet.Security.Cryptography.Ciphers; +using Renci.SshNet.Security.Cryptography.Ciphers.Modes; +using Renci.SshNet.TestTools.OpenSSH; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Implements 3DES cipher algorithm. + /// + [TestClass] + public class TripleDesCipherTest : IntegrationTestBase + { + private IConnectionInfoFactory _adminConnectionInfoFactory; + private RemoteSshdConfig _remoteSshdConfig; + + [TestInitialize] + public void SetUp() + { + _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); + _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); + } + + [TestCleanup] + public void TearDown() + { + _remoteSshdConfig?.Reset(); + } + + [TestMethod] + [Owner("olegkap")] + [TestCategory("Cipher")] + public void Test_Cipher_TripleDESCBC_Connection() + { + _remoteSshdConfig.AddCipher(Cipher.TripledesCbc) + .Update() + .Restart(); + + var connectionInfo = new PasswordConnectionInfo(SshServerHostName, SshServerPort, User.UserName, User.Password); + connectionInfo.Encryptions.Clear(); + connectionInfo.Encryptions.Add("3des-cbc", new CipherInfo(192, (key, iv) => { return new TripleDesCipher(key, new CbcCipherMode(iv), null); })); + + using (var client = new SshClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + } + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/ScpClientTest.cs b/src/Renci.SshNet.Tests/Classes/ScpClientTest.cs index a9af0c3dd..b4115e129 100644 --- a/src/Renci.SshNet.Tests/Classes/ScpClientTest.cs +++ b/src/Renci.SshNet.Tests/Classes/ScpClientTest.cs @@ -219,203 +219,6 @@ public void RemotePathTransformation_Value_Null() Assert.AreSame(RemotePathTransformation.ShellQuote, client.RemotePathTransformation); } - [TestMethod] - [TestCategory("Scp")] - [TestCategory("integration")] - public void Test_Scp_File_Upload_Download() - { - RemoveAllFiles(); - - using (var scp = new ScpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - scp.Connect(); - - var uploadedFileName = Path.GetTempFileName(); - var downloadedFileName = Path.GetTempFileName(); - - CreateTestFile(uploadedFileName, 1); - - scp.Upload(new FileInfo(uploadedFileName), Path.GetFileName(uploadedFileName)); - - scp.Download(Path.GetFileName(uploadedFileName), new FileInfo(downloadedFileName)); - - // Calculate MD5 value - var uploadedHash = CalculateMD5(uploadedFileName); - var downloadedHash = CalculateMD5(downloadedFileName); - - File.Delete(uploadedFileName); - File.Delete(downloadedFileName); - - scp.Disconnect(); - - Assert.AreEqual(uploadedHash, downloadedHash); - } - } - - [TestMethod] - [TestCategory("Scp")] - [TestCategory("integration")] - public void Test_Scp_Stream_Upload_Download() - { - RemoveAllFiles(); - - using (var scp = new ScpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - scp.Connect(); - - var uploadedFileName = Path.GetTempFileName(); - var downloadedFileName = Path.GetTempFileName(); - - CreateTestFile(uploadedFileName, 1); - - // Calculate has value - using (var stream = File.OpenRead(uploadedFileName)) - { - scp.Upload(stream, Path.GetFileName(uploadedFileName)); - } - - using (var stream = File.OpenWrite(downloadedFileName)) - { - scp.Download(Path.GetFileName(uploadedFileName), stream); - } - - // Calculate MD5 value - var uploadedHash = CalculateMD5(uploadedFileName); - var downloadedHash = CalculateMD5(downloadedFileName); - - File.Delete(uploadedFileName); - File.Delete(downloadedFileName); - - scp.Disconnect(); - - Assert.AreEqual(uploadedHash, downloadedHash); - } - } - - [TestMethod] - [TestCategory("Scp")] - [TestCategory("integration")] - public void Test_Scp_10MB_File_Upload_Download() - { - RemoveAllFiles(); - - using (var scp = new ScpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - scp.Connect(); - - var uploadedFileName = Path.GetTempFileName(); - var downloadedFileName = Path.GetTempFileName(); - - CreateTestFile(uploadedFileName, 10); - - scp.Upload(new FileInfo(uploadedFileName), Path.GetFileName(uploadedFileName)); - - scp.Download(Path.GetFileName(uploadedFileName), new FileInfo(downloadedFileName)); - - // Calculate MD5 value - var uploadedHash = CalculateMD5(uploadedFileName); - var downloadedHash = CalculateMD5(downloadedFileName); - - File.Delete(uploadedFileName); - File.Delete(downloadedFileName); - - scp.Disconnect(); - - Assert.AreEqual(uploadedHash, downloadedHash); - } - } - - [TestMethod] - [TestCategory("Scp")] - [TestCategory("integration")] - public void Test_Scp_10MB_Stream_Upload_Download() - { - RemoveAllFiles(); - - using (var scp = new ScpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - scp.Connect(); - - var uploadedFileName = Path.GetTempFileName(); - var downloadedFileName = Path.GetTempFileName(); - - CreateTestFile(uploadedFileName, 10); - - // Calculate has value - using (var stream = File.OpenRead(uploadedFileName)) - { - scp.Upload(stream, Path.GetFileName(uploadedFileName)); - } - - using (var stream = File.OpenWrite(downloadedFileName)) - { - scp.Download(Path.GetFileName(uploadedFileName), stream); - } - - // Calculate MD5 value - var uploadedHash = CalculateMD5(uploadedFileName); - var downloadedHash = CalculateMD5(downloadedFileName); - - File.Delete(uploadedFileName); - File.Delete(downloadedFileName); - - scp.Disconnect(); - - Assert.AreEqual(uploadedHash, downloadedHash); - } - } - - [TestMethod] - [TestCategory("Scp")] - [TestCategory("integration")] - public void Test_Scp_Directory_Upload_Download() - { - RemoveAllFiles(); - - using (var scp = new ScpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - scp.Connect(); - - var uploadDirectory = - Directory.CreateDirectory(string.Format("{0}\\{1}", Path.GetTempPath(), Path.GetRandomFileName())); - for (var i = 0; i < 3; i++) - { - var subfolder = Directory.CreateDirectory(string.Format(@"{0}\folder_{1}", uploadDirectory.FullName, i)); - - for (var j = 0; j < 5; j++) - { - CreateTestFile(string.Format(@"{0}\file_{1}", subfolder.FullName, j), 1); - } - - CreateTestFile(string.Format(@"{0}\file_{1}", uploadDirectory.FullName, i), 1); - } - - scp.Upload(uploadDirectory, "uploaded_dir"); - - var downloadDirectory = - Directory.CreateDirectory(string.Format("{0}\\{1}", Path.GetTempPath(), Path.GetRandomFileName())); - - scp.Download("uploaded_dir", downloadDirectory); - - var uploadedFiles = uploadDirectory.GetFiles("*.*", SearchOption.AllDirectories); - var downloadFiles = downloadDirectory.GetFiles("*.*", SearchOption.AllDirectories); - - var result = from f1 in uploadedFiles - from f2 in downloadFiles - where - f1.FullName.Substring(uploadDirectory.FullName.Length) == - f2.FullName.Substring(downloadDirectory.FullName.Length) - && CalculateMD5(f1.FullName) == CalculateMD5(f2.FullName) - select f1; - - var counter = result.Count(); - - scp.Disconnect(); - - Assert.IsTrue(counter == uploadedFiles.Length && uploadedFiles.Length == downloadFiles.Length); - } - } - /// ///A test for OperationTimeout /// @@ -539,133 +342,6 @@ public void DownloadTest2() Assert.Inconclusive("A method that does not return a value cannot be verified."); } - [TestMethod] - [TestCategory("Scp")] - [TestCategory("integration")] - public void Test_Scp_File_20_Parallel_Upload_Download() - { - using (var scp = new ScpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - scp.Connect(); - - var uploadFilenames = new string[20]; - for (var i = 0; i < uploadFilenames.Length; i++) - { - uploadFilenames[i] = Path.GetTempFileName(); - CreateTestFile(uploadFilenames[i], 1); - } - - _ = Parallel.ForEach(uploadFilenames, - filename => - { - scp.Upload(new FileInfo(filename), Path.GetFileName(filename)); - }); - _ = Parallel.ForEach(uploadFilenames, - filename => - { - scp.Download(Path.GetFileName(filename), new FileInfo(string.Format("{0}.down", filename))); - }); - - var result = from file in uploadFilenames - where CalculateMD5(file) == CalculateMD5(string.Format("{0}.down", file)) - select file; - - scp.Disconnect(); - - Assert.IsTrue(result.Count() == uploadFilenames.Length); - } - } - - [TestMethod] - [TestCategory("Scp")] - [TestCategory("integration")] - public void Test_Scp_File_Upload_Download_Events() - { - using (var scp = new ScpClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - scp.Connect(); - - var uploadFilenames = new string[10]; - - for (var i = 0; i < uploadFilenames.Length; i++) - { - uploadFilenames[i] = Path.GetTempFileName(); - CreateTestFile(uploadFilenames[i], 1); - } - - var uploadedFiles = uploadFilenames.ToDictionary(Path.GetFileName, (filename) => 0L); - var downloadedFiles = uploadFilenames.ToDictionary((filename) => string.Format("{0}.down", Path.GetFileName(filename)), (filename) => 0L); - - scp.Uploading += delegate (object sender, ScpUploadEventArgs e) - { - uploadedFiles[e.Filename] = e.Uploaded; - }; - - scp.Downloading += delegate (object sender, ScpDownloadEventArgs e) - { - downloadedFiles[string.Format("{0}.down", e.Filename)] = e.Downloaded; - }; - - _ = Parallel.ForEach(uploadFilenames, - filename => - { - scp.Upload(new FileInfo(filename), Path.GetFileName(filename)); - }); - _ = Parallel.ForEach(uploadFilenames, - filename => - { - scp.Download(Path.GetFileName(filename), new FileInfo(string.Format("{0}.down", filename))); - }); - - var result = from uf in uploadedFiles - from df in downloadedFiles - where string.Format("{0}.down", uf.Key) == df.Key && uf.Value == df.Value - select uf; - - scp.Disconnect(); - - Assert.IsTrue(result.Count() == uploadFilenames.Length && uploadFilenames.Length == uploadedFiles.Count && uploadedFiles.Count == downloadedFiles.Count); - } - } - - protected static string CalculateMD5(string fileName) - { - using (var file = new FileStream(fileName, FileMode.Open)) - { -#if NET7_0_OR_GREATER - var hash = MD5.HashData(file); -#else -#if NET6_0 - var md5 = MD5.Create(); -#else - MD5 md5 = new MD5CryptoServiceProvider(); -#endif // NET6_0 - var hash = md5.ComputeHash(file); -#endif // NET7_0_OR_GREATER - - file.Close(); - - var sb = new StringBuilder(); - - for (var i = 0; i < hash.Length; i++) - { - _ = sb.Append(i.ToString("x2")); - } - - return sb.ToString(); - } - } - - private static void RemoveAllFiles() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - _ = client.RunCommand("rm -rf *"); - client.Disconnect(); - } - } - private PrivateKeyFile GetRsaKey() { using (var stream = GetData("Key.RSA.txt")) diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/BlowfishCipherTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/BlowfishCipherTest.cs index 1c2d0aec8..41ab8e7b3 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/BlowfishCipherTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/BlowfishCipherTest.cs @@ -29,23 +29,6 @@ public void Test_Cipher_Blowfish_128_CBC() } } - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_BlowfishCBC_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("blowfish-cbc", new CipherInfo(128, (key, iv) => { return new BlowfishCipher(key, new CbcCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } - /// ///A test for BlowfishCipher Constructor /// diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/CastCipherTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/CastCipherTest.cs index c077a3098..c32159c4e 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/CastCipherTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/CastCipherTest.cs @@ -39,22 +39,6 @@ public void Decrypt_128_CBC() Assert.IsTrue(r.SequenceEqual(input)); } - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_Cast128CBC_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("cast128-cbc", new CipherInfo(128, (key, iv) => { return new CastCipher(key, new CbcCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } /// ///A test for CastCipher Constructor /// @@ -115,4 +99,4 @@ public void EncryptBlockTest() Assert.Inconclusive("Verify the correctness of this test method."); } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/TripleDesCipherTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/TripleDesCipherTest.cs index 5b4f97bac..e95ce12d3 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/TripleDesCipherTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/TripleDesCipherTest.cs @@ -32,22 +32,6 @@ public void Test_Cipher_3DES_CBC() } } - [TestMethod] - [Owner("olegkap")] - [TestCategory("Cipher")] - [TestCategory("integration")] - public void Test_Cipher_TripleDESCBC_Connection() - { - var connectionInfo = new PasswordConnectionInfo(Resources.HOST, int.Parse(Resources.PORT), Resources.USERNAME, Resources.PASSWORD); - connectionInfo.Encryptions.Clear(); - connectionInfo.Encryptions.Add("3des-cbc", new CipherInfo(192, (key, iv) => { return new TripleDesCipher(key, new CbcCipherMode(iv), null); })); - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - } /// ///A test for TripleDesCipher Constructor /// From df6fbdb4d614cbf096b1c89847433084d3d98962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Sat, 9 Sep 2023 05:19:18 +0200 Subject: [PATCH 07/15] Move authentication tests --- .../AuthenticationMethodFactory.cs | 18 +++++- .../AuthenticationTests.cs | 57 +++++++++++++++++++ .../Renci.SshNet.IntegrationTests.csproj | 1 + .../resources/client/id_rsa_with_pass | 28 +++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/Renci.SshNet.IntegrationTests/resources/client/id_rsa_with_pass diff --git a/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs b/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs index 7c52e1a56..1db128766 100644 --- a/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs +++ b/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs @@ -14,6 +14,19 @@ public PrivateKeyAuthenticationMethod CreateRegularUserPrivateKeyAuthenticationM return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKeyFile); } + public PrivateKeyAuthenticationMethod CreateRegularUserMultiplePrivateKeyAuthenticationMethod() + { + var privateKeyFile1 = GetPrivateKey("Renci.SshNet.IntegrationTests.resources.client.id_rsa"); + var privateKeyFile2 = GetPrivateKey("Renci.SshNet.IntegrationTests.resources.client.id_rsa"); + return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKeyFile1, privateKeyFile2); + } + + public PrivateKeyAuthenticationMethod CreateRegularUserPrivateKeyWithPassPhraseAuthenticationMethod() + { + var privateKeyFile = GetPrivateKey("Renci.SshNet.IntegrationTests.resources.client.id_rsa_with_pass", "tester"); + return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKeyFile); + } + public PrivateKeyAuthenticationMethod CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey() { var privateKeyFile = GetPrivateKey("Renci.SshNet.IntegrationTests.resources.client.id_noaccess.rsa"); @@ -43,12 +56,11 @@ public KeyboardInteractiveAuthenticationMethod CreateRegularUserKeyboardInteract return keyboardInteractive; } - - private PrivateKeyFile GetPrivateKey(string resourceName) + private PrivateKeyFile GetPrivateKey(string resourceName, string passPhrase = null) { using (var stream = GetResourceStream(resourceName)) { - return new PrivateKeyFile(stream); + return new PrivateKeyFile(stream, passPhrase); } } diff --git a/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs b/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs index ff3f02365..916e02a42 100644 --- a/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs +++ b/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs @@ -43,6 +43,63 @@ public void TearDown() } } + [TestMethod] + public void Multifactor_PublicKey() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void Multifactor_PublicKeyWithPassPhrase() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyWithPassPhraseAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void Multifactor_PublicKey_MultiplePrivateKey() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserMultiplePrivateKeyAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + + [TestMethod] + public void Multifactor_PublicKey_MultipleAuthenticationMethod() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod(), + _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + [TestMethod] public void Multifactor_KeyboardInteractiveAndPublicKey() { diff --git a/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj index 7fce16c58..175192709 100644 --- a/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj +++ b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj @@ -56,6 +56,7 @@ + diff --git a/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa_with_pass b/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa_with_pass new file mode 100644 index 000000000..841532d1f --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/resources/client/id_rsa_with_pass @@ -0,0 +1,28 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABC9v0UCCP +T+1yNEu9m0w939AAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQC4N0T6Vopn +wPSYQUw0waQNhBjz0DYFQwvkv4OwWYSf//fJF3M6bH42Tn2J+IlQ+4/fCFnE3m99seV5T1 +myEj7fsupNteY2sKFGXENLGtAD/76FM0iBmXx76xlSTyZSSmNDIRU4xUR23cfc+al84F5m +O2lEk+5Zr3Qn5JUpucBfis4vtu0sMDgZ4w1d0tcuXkT/MEJn2iX2cnxbSy5qNAPHu7b+LE +fXBv2OrMDqPrx/X6QREgi3w5RxL5kz7bvitWsIwIvb3ST2ARAArBwb8pEyp2A/w5p22rkQ +tL+3ibZ8fkmpgn33f31AZPgtM++iJPBmPKFjArcWEJ9fIVB/6DAjAAADwPBAapXaoQvm3O +2i9sBnmO+d8kCdm2nhGbEXNzswb0toARYyx7/rPON15BHv470NLK4GjtxWb8SbkUBjWIXJ +dpJR1feYAgJQ27yaU6SBEJvnQBFI3EvH+h9ykaikDP/SzgZuGup5NZIoB09PzCPk4SbAwn +skFT3s1v3ufaULYTiAO2xtWzABkjxfw8HOo+PF3UqGIF4145kGLUT1pUN7iy5EQZA8evRb +Yt/9+ChcyBgKONgXdpjLNf02XIM/jQofZkROBg7ZAKCjtL3yGpvtOpwzCKy1hWDmgjtkeK +Xn84/qwXWEobBa2wrDQ4Mjj7AIimRsCciO05bVB5KtNjT+WzCalpTzfj2nazukteNRKTD3 +bQR0gLFfFX4/YodXmtu2n+0R2AKkdPW5ZhoEEpT1FjfYUImAuElSEy5FEeR3bwE5uGQkF4 +uIMJWD+89QxO9PWKTloVI2hrOF9/z+UzUi7p16FQFDlB82qCQAiaIOHgotgDn9+OwMw0ew +Gu/D3T8ZpKXcTAxK1JeoDFh2h+CE37JDvftNIxIhTp7lrhCdioj1DSBorwfke/q4+OvLUH +8SZ6ZgppHjJ4jg6lB9TWCpD5PECDW+NuQQwUb5V4NKoBqnJrPaNUSbI7SL5Pq3mOXXNRmv +q1Va06CfcHL3JupICFifux5xBDlQY0foAt7DFOgA6qANaCnRl2H2oFsEdRhBbL6EPP0bAQ +7PBvWJlt5Aqr1V7QzcCHNZcyGpjiHeXsQxSWXzb1xQprlrDSnlGZpusrBTcZTLWUtOAgqQ +dNbUNeUq1E74rZ/RBiVliaLHNBKwTCE9KyZTl/DcfgInB7DDEd7Vlmujzar7xAXRJot7k2 +a12gT69eVsqiO5si23493JFl0JB5vbQvQWARAvcdNPDPiILoJVpbgyL9oMzVzTjJyqYS1x +cHXKE+rL4ZHeQhcfySXplZlUY9ADVY5QywAj2kl+5gT3Fohu/95axF7w9dAHyMntxbVnA3 +r0zOmqbAKOYEYEXxZ+Vq09YdEyJh33V48PUmvCusWWyeKIfjO4R1nD2+7iyiF7joF7bOBm +o0LXuJdr81BCMueryPUaqSRpGhg/P/nUqzxj7p0mAh9uUOr/CtOpbc6wNY70RgdiMGFWhz +zhO54p7iEg2zZNUZ5zRM1hTBikTeYyInpw8IQiMv9GH7/9gWwqPsh0qRVjj5kjqjSu069d +FeuGjsMGCnLLG6WvOQ9DgH9JR4cfinBL3JwtpHFBIHwhsh2EIuSseVHvQlFlEc1U+7Yoxc +0dn1VVtg== +-----END OPENSSH PRIVATE KEY----- From e2df8ff14bc72a9e3f367288c78768993fba6fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Sat, 9 Sep 2023 05:45:28 +0200 Subject: [PATCH 08/15] Move SshClientTests --- .../AuthenticationMethodFactory.cs | 6 + .../AuthenticationTests.cs | 33 +++ .../OldIntegrationTests/SshClientTest.cs | 42 +++ .../Classes/PrivateKeyConnectionInfoTest.cs | 52 +--- .../Classes/SshClientTest.cs | 243 ------------------ 5 files changed, 82 insertions(+), 294 deletions(-) create mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs diff --git a/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs b/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs index 1db128766..638b285d8 100644 --- a/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs +++ b/src/Renci.SshNet.IntegrationTests/AuthenticationMethodFactory.cs @@ -27,6 +27,12 @@ public PrivateKeyAuthenticationMethod CreateRegularUserPrivateKeyWithPassPhraseA return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKeyFile); } + public PrivateKeyAuthenticationMethod CreateRegularUserPrivateKeyWithEmptyPassPhraseAuthenticationMethod() + { + var privateKeyFile = GetPrivateKey("Renci.SshNet.IntegrationTests.resources.client.id_rsa_with_pass", null); + return new PrivateKeyAuthenticationMethod(Users.Regular.UserName, privateKeyFile); + } + public PrivateKeyAuthenticationMethod CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey() { var privateKeyFile = GetPrivateKey("Renci.SshNet.IntegrationTests.resources.client.id_noaccess.rsa"); diff --git a/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs b/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs index 916e02a42..c3968e949 100644 --- a/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs +++ b/src/Renci.SshNet.IntegrationTests/AuthenticationTests.cs @@ -57,6 +57,24 @@ public void Multifactor_PublicKey() } } + [TestMethod] + [TestCategory("Authentication")] + public void Multifactor_PublicKey_Connect_Then_Reconnect() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + client.Disconnect(); + client.Connect(); + client.Disconnect(); + } + } + [TestMethod] public void Multifactor_PublicKeyWithPassPhrase() { @@ -71,6 +89,21 @@ public void Multifactor_PublicKeyWithPassPhrase() } } + [TestMethod] + [ExpectedException(typeof(SshPassPhraseNullOrEmptyException))] + public void Multifactor_PublicKeyWithEmptyPassPhrase() + { + _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey") + .Update() + .Restart(); + + var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyWithEmptyPassPhraseAuthenticationMethod()); + using (var client = new SftpClient(connectionInfo)) + { + client.Connect(); + } + } + [TestMethod] public void Multifactor_PublicKey_MultiplePrivateKey() { diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs new file mode 100644 index 000000000..cbfc7cfd0 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs @@ -0,0 +1,42 @@ +using Renci.SshNet.Common; + +namespace Renci.SshNet.IntegrationTests.OldIntegrationTests +{ + /// + /// Provides client connection to SSH server. + /// + [TestClass] + public class SshClientTest : IntegrationTestBase + { + [TestMethod] + [TestCategory("Authentication")] + public void Test_Connect_Handle_HostKeyReceived() + { + var hostKeyValidated = false; + + #region Example SshClient Connect HostKeyReceived + using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) + { + client.HostKeyReceived += delegate(object sender, HostKeyEventArgs e) + { + hostKeyValidated = true; + + if (e.FingerPrint.SequenceEqual(new byte[] { 179, 185, 208, 27, 115, 196, 96, 180, 206, 237, 6, 248, 88, 73, 163, 218 })) + { + e.CanTrust = true; + } + else + { + e.CanTrust = false; + } + }; + client.Connect(); + // Do something here + client.Disconnect(); + } + #endregion + + Assert.IsTrue(hostKeyValidated); + } + } +} diff --git a/src/Renci.SshNet.Tests/Classes/PrivateKeyConnectionInfoTest.cs b/src/Renci.SshNet.Tests/Classes/PrivateKeyConnectionInfoTest.cs index f8c863ba0..40a781174 100644 --- a/src/Renci.SshNet.Tests/Classes/PrivateKeyConnectionInfoTest.cs +++ b/src/Renci.SshNet.Tests/Classes/PrivateKeyConnectionInfoTest.cs @@ -1,8 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; -using System.IO; -using System.Text; namespace Renci.SshNet.Tests.Classes { @@ -12,53 +9,6 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class PrivateKeyConnectionInfoTest : TestBase { - [TestMethod] - [TestCategory("PrivateKeyConnectionInfo")] - [TestCategory("integration")] - public void Test_PrivateKeyConnectionInfo() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - MemoryStream keyFileStream = new MemoryStream(Encoding.ASCII.GetBytes(Resources.RSA_KEY_WITHOUT_PASS)); - - #region Example PrivateKeyConnectionInfo PrivateKeyFile - var connectionInfo = new PrivateKeyConnectionInfo(host, username, new PrivateKeyFile(keyFileStream)); - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - #endregion - - Assert.AreEqual(connectionInfo.Host, Resources.HOST); - Assert.AreEqual(connectionInfo.Username, Resources.USERNAME); - } - - [TestMethod] - [TestCategory("PrivateKeyConnectionInfo")] - [TestCategory("integration")] - public void Test_PrivateKeyConnectionInfo_MultiplePrivateKey() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - MemoryStream keyFileStream1 = new MemoryStream(Encoding.ASCII.GetBytes(Resources.RSA_KEY_WITHOUT_PASS)); - MemoryStream keyFileStream2 = new MemoryStream(Encoding.ASCII.GetBytes(Resources.RSA_KEY_WITHOUT_PASS)); - - #region Example PrivateKeyConnectionInfo PrivateKeyFile Multiple - var connectionInfo = new PrivateKeyConnectionInfo(host, username, - new PrivateKeyFile(keyFileStream1), - new PrivateKeyFile(keyFileStream2)); - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - client.Disconnect(); - } - #endregion - - Assert.AreEqual(connectionInfo.Host, Resources.HOST); - Assert.AreEqual(connectionInfo.Username, Resources.USERNAME); - } - /// ///A test for Dispose /// @@ -214,4 +164,4 @@ public void PrivateKeyConnectionInfoConstructorTest7() Assert.Inconclusive("TODO: Implement code to verify target"); } } -} \ No newline at end of file +} diff --git a/src/Renci.SshNet.Tests/Classes/SshClientTest.cs b/src/Renci.SshNet.Tests/Classes/SshClientTest.cs index 1eabcae20..ad55e5622 100644 --- a/src/Renci.SshNet.Tests/Classes/SshClientTest.cs +++ b/src/Renci.SshNet.Tests/Classes/SshClientTest.cs @@ -16,249 +16,6 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class SshClientTest : TestBase { - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Using_Correct_Password() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - #region Example SshClient(host, username) Connect - using (var client = new SshClient(host, username, password)) - { - client.Connect(); - // Do something here - client.Disconnect(); - } - #endregion - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Handle_HostKeyReceived() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - var hostKeyValidated = false; - - #region Example SshClient Connect HostKeyReceived - using (var client = new SshClient(host, username, password)) - { - client.HostKeyReceived += delegate(object sender, HostKeyEventArgs e) - { - hostKeyValidated = true; - - if (e.FingerPrint.SequenceEqual(new byte[] { 0x00, 0x01, 0x02, 0x03 })) - { - e.CanTrust = true; - } - else - { - e.CanTrust = false; - } - }; - client.Connect(); - // Do something here - client.Disconnect(); - } - #endregion - - Assert.IsTrue(hostKeyValidated); - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Timeout() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - #region Example SshClient Connect Timeout - - var connectionInfo = new PasswordConnectionInfo(host, username, password) - { - Timeout = TimeSpan.FromSeconds(30) - }; - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - // Do something here - client.Disconnect(); - } - #endregion - Assert.Inconclusive(); - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Handle_ErrorOccurred() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - var exceptionOccured = false; - - #region Example SshClient Connect ErrorOccurred - using (var client = new SshClient(host, username, password)) - { - client.ErrorOccurred += delegate(object sender, ExceptionEventArgs e) - { - Console.WriteLine("Error occured: " + e.Exception); - exceptionOccured = true; - }; - - client.Connect(); - // Do something here - client.Disconnect(); - } - #endregion - Assert.IsTrue(exceptionOccured); - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - [ExpectedException(typeof(SshAuthenticationException))] - public void Test_Connect_Using_Invalid_Password() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, "invalid password")) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Using_Rsa_Key_Without_PassPhrase() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - MemoryStream keyFileStream = new MemoryStream(Encoding.ASCII.GetBytes(Resources.RSA_KEY_WITHOUT_PASS)); - - #region Example SshClient(host, username) Connect PrivateKeyFile - using (var client = new SshClient(host, username, new PrivateKeyFile(keyFileStream))) - { - client.Connect(); - client.Disconnect(); - } - #endregion - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Using_RsaKey_With_PassPhrase() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var passphrase = Resources.PASSWORD; - MemoryStream keyFileStream = new MemoryStream(Encoding.ASCII.GetBytes(Resources.RSA_KEY_WITH_PASS)); - - #region Example SshClient(host, username) Connect PrivateKeyFile PassPhrase - using (var client = new SshClient(host, username, new PrivateKeyFile(keyFileStream, passphrase))) - { - client.Connect(); - client.Disconnect(); - } - #endregion - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - [ExpectedException(typeof(SshPassPhraseNullOrEmptyException))] - public void Test_Connect_Using_Key_With_Empty_PassPhrase() - { - MemoryStream keyFileStream = new MemoryStream(Encoding.ASCII.GetBytes(Resources.RSA_KEY_WITH_PASS)); - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, new PrivateKeyFile(keyFileStream, null))) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Using_DsaKey_Without_PassPhrase() - { - MemoryStream keyFileStream = new MemoryStream(Encoding.ASCII.GetBytes(Resources.DSA_KEY_WITHOUT_PASS)); - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, new PrivateKeyFile(keyFileStream))) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Using_DsaKey_With_PassPhrase() - { - MemoryStream keyFileStream = new MemoryStream(Encoding.ASCII.GetBytes(Resources.DSA_KEY_WITH_PASS)); - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, new PrivateKeyFile(keyFileStream, Resources.PASSWORD))) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - [ExpectedException(typeof(SshAuthenticationException))] - public void Test_Connect_Using_Invalid_PrivateKey() - { - MemoryStream keyFileStream = new MemoryStream(Encoding.ASCII.GetBytes(Resources.INVALID_KEY)); - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, new PrivateKeyFile(keyFileStream))) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Using_Multiple_PrivateKeys() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, - new PrivateKeyFile(new MemoryStream(Encoding.ASCII.GetBytes(Resources.INVALID_KEY))), - new PrivateKeyFile(new MemoryStream(Encoding.ASCII.GetBytes(Resources.DSA_KEY_WITH_PASS)), Resources.PASSWORD), - new PrivateKeyFile(new MemoryStream(Encoding.ASCII.GetBytes(Resources.RSA_KEY_WITH_PASS)), Resources.PASSWORD), - new PrivateKeyFile(new MemoryStream(Encoding.ASCII.GetBytes(Resources.RSA_KEY_WITHOUT_PASS))), - new PrivateKeyFile(new MemoryStream(Encoding.ASCII.GetBytes(Resources.DSA_KEY_WITHOUT_PASS))) - )) - { - client.Connect(); - client.Disconnect(); - } - } - - [TestMethod] - [TestCategory("Authentication")] - [TestCategory("integration")] - public void Test_Connect_Then_Reconnect() - { - using (var client = new SshClient(Resources.HOST, Resources.USERNAME, Resources.PASSWORD)) - { - client.Connect(); - client.Disconnect(); - client.Connect(); - client.Disconnect(); - } - } - [TestMethod] public void CreateShellStream1_NeverConnected() { From a8d8bcd880b337143656e28a72945097120fdcf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Sat, 9 Sep 2023 07:33:38 +0200 Subject: [PATCH 09/15] Fix some tests --- .../OldIntegrationTests/SftpFileTest.cs | 4 +- .../OldIntegrationTests/SshClientTest.cs | 2 +- src/Renci.SshNet.IntegrationTests/SshTests.cs | 1 + ...ctConnectorTest_Connect_HostNameInvalid.cs | 2 +- ...pConnectorTest_Connect_ProxyHostInvalid.cs | 2 +- .../Classes/PasswordConnectionInfoTest.cs | 134 +----------------- .../Classes/ScpClientTest.cs | 4 - .../Ciphers/BlowfishCipherTest.cs | 2 +- .../Cryptography/Ciphers/CastCipherTest.cs | 2 +- .../Ciphers/TripleDesCipherTest.cs | 2 - .../Classes/SftpClientTest.Connect.cs | 4 +- .../Classes/SshClientTest.cs | 3 +- 12 files changed, 12 insertions(+), 150 deletions(-) diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs index 13d3832a6..058442511 100644 --- a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpFileTest.cs @@ -48,7 +48,7 @@ public void Test_Get_File() var file = sftp.Get("abc.txt"); - Assert.AreEqual("/home/tester/abc.txt", file.FullName); + Assert.AreEqual("/home/sshnet/abc.txt", file.FullName); Assert.IsTrue(file.IsRegularFile); Assert.IsFalse(file.IsDirectory); } @@ -82,7 +82,7 @@ public void Test_Get_International_File() var file = sftp.Get("test-üöä-"); - Assert.AreEqual("/home/tester/test-üöä-", file.FullName); + Assert.AreEqual("/home/sshnet/test-üöä-", file.FullName); Assert.IsTrue(file.IsRegularFile); Assert.IsFalse(file.IsDirectory); } diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs index cbfc7cfd0..de96a5fae 100644 --- a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs @@ -20,7 +20,7 @@ public void Test_Connect_Handle_HostKeyReceived() client.HostKeyReceived += delegate(object sender, HostKeyEventArgs e) { hostKeyValidated = true; - + Console.WriteLine(string.Join(", ", e.FingerPrint)); if (e.FingerPrint.SequenceEqual(new byte[] { 179, 185, 208, 27, 115, 196, 96, 180, 206, 237, 6, 248, 88, 73, 163, 218 })) { e.CanTrust = true; diff --git a/src/Renci.SshNet.IntegrationTests/SshTests.cs b/src/Renci.SshNet.IntegrationTests/SshTests.cs index 217094d75..b757dbd03 100644 --- a/src/Renci.SshNet.IntegrationTests/SshTests.cs +++ b/src/Renci.SshNet.IntegrationTests/SshTests.cs @@ -390,6 +390,7 @@ public void Ssh_DynamicPortForwarding_DomainName() // Verify if port is still open socksSocket.Send(httpGetRequest); httpResponse = GetHttpResponse(socksSocket, Encoding.ASCII); + Console.WriteLine(httpResponse); Assert.IsTrue(httpResponse.Contains(searchText), httpResponse); forwardedPort.Stop(); diff --git a/src/Renci.SshNet.Tests/Classes/Connection/DirectConnectorTest_Connect_HostNameInvalid.cs b/src/Renci.SshNet.Tests/Classes/Connection/DirectConnectorTest_Connect_HostNameInvalid.cs index 068169248..7cd0f6ec6 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/DirectConnectorTest_Connect_HostNameInvalid.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/DirectConnectorTest_Connect_HostNameInvalid.cs @@ -36,7 +36,7 @@ public void ConnectShouldHaveThrownSocketException() { Assert.IsNotNull(_actualException); Assert.IsNull(_actualException.InnerException); - Assert.AreEqual(SocketError.HostNotFound, _actualException.SocketErrorCode); + Assert.IsTrue(_actualException.SocketErrorCode is SocketError.HostNotFound or SocketError.TryAgain); } } } diff --git a/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_ProxyHostInvalid.cs b/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_ProxyHostInvalid.cs index 59524ffd7..7a87e9d6f 100644 --- a/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_ProxyHostInvalid.cs +++ b/src/Renci.SshNet.Tests/Classes/Connection/HttpConnectorTest_Connect_ProxyHostInvalid.cs @@ -43,7 +43,7 @@ public void ConnectShouldHaveThrownSocketException() { Assert.IsNotNull(_actualException); Assert.IsNull(_actualException.InnerException); - Assert.AreEqual(SocketError.HostNotFound, _actualException.SocketErrorCode); + Assert.IsTrue(_actualException.SocketErrorCode is SocketError.HostNotFound or SocketError.TryAgain); } } } diff --git a/src/Renci.SshNet.Tests/Classes/PasswordConnectionInfoTest.cs b/src/Renci.SshNet.Tests/Classes/PasswordConnectionInfoTest.cs index 824f649f1..56a510140 100644 --- a/src/Renci.SshNet.Tests/Classes/PasswordConnectionInfoTest.cs +++ b/src/Renci.SshNet.Tests/Classes/PasswordConnectionInfoTest.cs @@ -1,5 +1,4 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; -using Renci.SshNet.Common; using Renci.SshNet.Tests.Common; using Renci.SshNet.Tests.Properties; using System; @@ -13,84 +12,6 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class PasswordConnectionInfoTest : TestBase { - [TestMethod] - [TestCategory("PasswordConnectionInfo")] - [TestCategory("integration")] - public void Test_PasswordConnectionInfo() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - #region Example PasswordConnectionInfo - var connectionInfo = new PasswordConnectionInfo(host, username, password); - using (var client = new SftpClient(connectionInfo)) - { - client.Connect(); - // Do something here - client.Disconnect(); - } - #endregion - - Assert.AreEqual(connectionInfo.Host, Resources.HOST); - Assert.AreEqual(connectionInfo.Username, Resources.USERNAME); - } - - [TestMethod] - [TestCategory("PasswordConnectionInfo")] - [TestCategory("integration")] - public void Test_PasswordConnectionInfo_PasswordExpired() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - #region Example PasswordConnectionInfo PasswordExpired - var connectionInfo = new PasswordConnectionInfo("host", "username", "password"); - var encoding = SshData.Ascii; - connectionInfo.PasswordExpired += delegate(object sender, AuthenticationPasswordChangeEventArgs e) - { - e.NewPassword = encoding.GetBytes("123456"); - }; - - using (var client = new SshClient(connectionInfo)) - { - client.Connect(); - - client.Disconnect(); - } - #endregion - - Assert.Inconclusive(); - } - [TestMethod] - [TestCategory("PasswordConnectionInfo")] - [TestCategory("integration")] - public void Test_PasswordConnectionInfo_AuthenticationBanner() - { - var host = Resources.HOST; - var username = Resources.USERNAME; - var password = Resources.PASSWORD; - - #region Example PasswordConnectionInfo AuthenticationBanner - var connectionInfo = new PasswordConnectionInfo(host, username, password); - connectionInfo.AuthenticationBanner += delegate(object sender, AuthenticationBannerEventArgs e) - { - Console.WriteLine(e.BannerMessage); - }; - using (var client = new SftpClient(connectionInfo)) - { - client.Connect(); - // Do something here - client.Disconnect(); - } - #endregion - - Assert.AreEqual(connectionInfo.Host, Resources.HOST); - Assert.AreEqual(connectionInfo.Username, Resources.USERNAME); - } - - [WorkItem(703), TestMethod] [TestCategory("PasswordConnectionInfo")] public void Test_ConnectionInfo_Host_Is_Null() @@ -148,60 +69,7 @@ public void Test_ConnectionInfo_BigPortNumber() { _ = new PasswordConnectionInfo(Resources.HOST, IPEndPoint.MaxPort + 1, Resources.USERNAME, Resources.PASSWORD); } - - [TestMethod] - [Owner("Kenneth_aa")] - [Description("Test connect to remote server via a SOCKS4 proxy server.")] - [TestCategory("Proxy")] - [TestCategory("integration")] - public void Test_Ssh_Connect_Via_Socks4() - { - var connInfo = new PasswordConnectionInfo(Resources.HOST, Resources.USERNAME, Resources.PASSWORD, ProxyTypes.Socks4, Resources.PROXY_HOST, int.Parse(Resources.PROXY_PORT)); - using (var client = new SshClient(connInfo)) - { - client.Connect(); - - var ret = client.RunCommand("ls -la"); - - client.Disconnect(); - } - } - - [TestMethod] - [Owner("Kenneth_aa")] - [Description("Test connect to remote server via a TCP SOCKS5 proxy server.")] - [TestCategory("Proxy")] - [TestCategory("integration")] - public void Test_Ssh_Connect_Via_TcpSocks5() - { - var connInfo = new PasswordConnectionInfo(Resources.HOST, Resources.USERNAME, Resources.PASSWORD, ProxyTypes.Socks5, Resources.PROXY_HOST, int.Parse(Resources.PROXY_PORT)); - using (var client = new SshClient(connInfo)) - { - client.Connect(); - - var ret = client.RunCommand("ls -la"); - client.Disconnect(); - } - } - - [TestMethod] - [Owner("Kenneth_aa")] - [Description("Test connect to remote server via a HTTP proxy server.")] - [TestCategory("Proxy")] - [TestCategory("integration")] - public void Test_Ssh_Connect_Via_HttpProxy() - { - var connInfo = new PasswordConnectionInfo(Resources.HOST, Resources.USERNAME, Resources.PASSWORD, ProxyTypes.Http, Resources.PROXY_HOST, int.Parse(Resources.PROXY_PORT)); - using (var client = new SshClient(connInfo)) - { - client.Connect(); - - var ret = client.RunCommand("ls -la"); - - client.Disconnect(); - } - } - + /// ///A test for Dispose /// diff --git a/src/Renci.SshNet.Tests/Classes/ScpClientTest.cs b/src/Renci.SshNet.Tests/Classes/ScpClientTest.cs index b4115e129..bcdf93966 100644 --- a/src/Renci.SshNet.Tests/Classes/ScpClientTest.cs +++ b/src/Renci.SshNet.Tests/Classes/ScpClientTest.cs @@ -1,15 +1,11 @@ using System; using System.IO; -using System.Linq; -using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Common; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; namespace Renci.SshNet.Tests.Classes { diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/BlowfishCipherTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/BlowfishCipherTest.cs index 41ab8e7b3..168b1eaf8 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/BlowfishCipherTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/BlowfishCipherTest.cs @@ -2,7 +2,7 @@ using Renci.SshNet.Security.Cryptography.Ciphers; using Renci.SshNet.Security.Cryptography.Ciphers.Modes; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; + using System.Linq; namespace Renci.SshNet.Tests.Classes.Security.Cryptography.Ciphers diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/CastCipherTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/CastCipherTest.cs index c32159c4e..a04c5f2db 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/CastCipherTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/CastCipherTest.cs @@ -2,7 +2,7 @@ using Renci.SshNet.Security.Cryptography.Ciphers; using Renci.SshNet.Security.Cryptography.Ciphers.Modes; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; + using System.Linq; namespace Renci.SshNet.Tests.Classes.Security.Cryptography.Ciphers diff --git a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/TripleDesCipherTest.cs b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/TripleDesCipherTest.cs index e95ce12d3..9dc6a2b65 100644 --- a/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/TripleDesCipherTest.cs +++ b/src/Renci.SshNet.Tests/Classes/Security/Cryptography/Ciphers/TripleDesCipherTest.cs @@ -5,8 +5,6 @@ using Renci.SshNet.Security.Cryptography.Ciphers; using Renci.SshNet.Security.Cryptography.Ciphers.Modes; using Renci.SshNet.Tests.Common; -using Renci.SshNet.Tests.Properties; - namespace Renci.SshNet.Tests.Classes.Security.Cryptography.Ciphers { diff --git a/src/Renci.SshNet.Tests/Classes/SftpClientTest.Connect.cs b/src/Renci.SshNet.Tests/Classes/SftpClientTest.Connect.cs index 9ae69f6ec..c2798104a 100644 --- a/src/Renci.SshNet.Tests/Classes/SftpClientTest.Connect.cs +++ b/src/Renci.SshNet.Tests/Classes/SftpClientTest.Connect.cs @@ -20,7 +20,7 @@ public void Connect_HostNameInvalid_ShouldThrowSocketExceptionWithErrorCodeHostN } catch (SocketException ex) { - Assert.AreEqual(ex.ErrorCode, (int) SocketError.HostNotFound); + Assert.IsTrue(ex.ErrorCode is (int) SocketError.HostNotFound or (int) SocketError.TryAgain); } } @@ -38,7 +38,7 @@ public void Connect_ProxyHostNameInvalid_ShouldThrowSocketExceptionWithErrorCode } catch (SocketException ex) { - Assert.AreEqual(ex.ErrorCode, (int)SocketError.HostNotFound); + Assert.IsTrue(ex.ErrorCode is (int) SocketError.HostNotFound or (int) SocketError.TryAgain); } } } diff --git a/src/Renci.SshNet.Tests/Classes/SshClientTest.cs b/src/Renci.SshNet.Tests/Classes/SshClientTest.cs index ad55e5622..8ee74c2ca 100644 --- a/src/Renci.SshNet.Tests/Classes/SshClientTest.cs +++ b/src/Renci.SshNet.Tests/Classes/SshClientTest.cs @@ -2,11 +2,10 @@ using Renci.SshNet.Common; using Renci.SshNet.Tests.Common; using Renci.SshNet.Tests.Properties; -using System; + using System.Collections.Generic; using System.IO; using System.Text; -using System.Linq; namespace Renci.SshNet.Tests.Classes { From e59188b95ac044ebf56e90a0c87bbf918803ae21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Sat, 9 Sep 2023 10:34:19 +0200 Subject: [PATCH 10/15] Remove duplicated test --- .../OldIntegrationTests/SshClientTest.cs | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs deleted file mode 100644 index de96a5fae..000000000 --- a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SshClientTest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Renci.SshNet.Common; - -namespace Renci.SshNet.IntegrationTests.OldIntegrationTests -{ - /// - /// Provides client connection to SSH server. - /// - [TestClass] - public class SshClientTest : IntegrationTestBase - { - [TestMethod] - [TestCategory("Authentication")] - public void Test_Connect_Handle_HostKeyReceived() - { - var hostKeyValidated = false; - - #region Example SshClient Connect HostKeyReceived - using (var client = new SshClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) - { - client.HostKeyReceived += delegate(object sender, HostKeyEventArgs e) - { - hostKeyValidated = true; - Console.WriteLine(string.Join(", ", e.FingerPrint)); - if (e.FingerPrint.SequenceEqual(new byte[] { 179, 185, 208, 27, 115, 196, 96, 180, 206, 237, 6, 248, 88, 73, 163, 218 })) - { - e.CanTrust = true; - } - else - { - e.CanTrust = false; - } - }; - client.Connect(); - // Do something here - client.Disconnect(); - } - #endregion - - Assert.IsTrue(hostKeyValidated); - } - } -} From d76a7112a2bc3235932b663e4b846d3a6e1fff43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Mon, 11 Sep 2023 17:29:29 +0200 Subject: [PATCH 11/15] Poc of ProcessDisruptor --- .../ConnectivityTests.cs | 402 +++++------------- .../SftpClientTest.Upload.cs | 5 + .../ProcessDisruptor.cs | 41 ++ .../ProcessDisruptorOperation.cs | 36 ++ 4 files changed, 185 insertions(+), 299 deletions(-) create mode 100644 src/Renci.SshNet.IntegrationTests/ProcessDisruptor.cs create mode 100644 src/Renci.SshNet.IntegrationTests/ProcessDisruptorOperation.cs diff --git a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs index 1cf7a9d52..60ab2a74d 100644 --- a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs +++ b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs @@ -1,22 +1,17 @@ -using System.Diagnostics; -using System.Management; -using System.Text.RegularExpressions; - -using Renci.SshNet.Common; +using Renci.SshNet.Common; using Renci.SshNet.IntegrationTests.Common; using Renci.SshNet.Messages.Transport; namespace Renci.SshNet.IntegrationTests { [TestClass] - public class ConnectivityTests : TestBase + public class ConnectivityTests : IntegrationTestBase { - private const string NetworkConnectionId = "Ethernet 2"; - private AuthenticationMethodFactory _authenticationMethodFactory; private IConnectionInfoFactory _connectionInfoFactory; private IConnectionInfoFactory _adminConnectionInfoFactory; private RemoteSshdConfig _remoteSshdConfig; + private ProcessDisruptor _processDisruptor; [TestInitialize] public void SetUp() @@ -25,6 +20,7 @@ public void SetUp() _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort, _authenticationMethodFactory); _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); + _processDisruptor = new ProcessDisruptor(_adminConnectionInfoFactory); } [TestCleanup] @@ -61,20 +57,26 @@ public void Common_CreateMoreChannelsThanMaxSessions() public void Common_DisposeAfterLossOfNetworkConnectivity() { var hostNetworkConnectionDisabled = false; - + ProcessDisruptorOperation disruptor = null; try { Exception errorOccurred = null; - + int count = 0; using (var client = new SftpClient(_connectionInfoFactory.Create())) { - client.ErrorOccurred += (sender, args) => errorOccurred = args.Exception; + client.ErrorOccurred += (sender, args) => + { + Console.WriteLine("Exception " + count++); + Console.WriteLine(args.Exception); + errorOccurred = args.Exception; + }; client.Connect(); - DisableHostNetworkConnection(NetworkConnectionId); + disruptor = _processDisruptor.BreakConnections(); hostNetworkConnectionDisabled = true; + WaitForConnectionInterruption(client); } - + Assert.IsNotNull(errorOccurred); Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); @@ -87,8 +89,8 @@ public void Common_DisposeAfterLossOfNetworkConnectivity() { if (hostNetworkConnectionDisabled) { - EnableHostNetworkConnection(NetworkConnectionId); - ResetVirtualMachineNetworkConnection(); + disruptor?.ResumeSshd(); + disruptor?.Dispose(); } } } @@ -99,57 +101,73 @@ public void Common_DetectLossOfNetworkConnectivityThroughKeepAlive() using (var client = new SftpClient(_connectionInfoFactory.Create())) { Exception errorOccurred = null; - client.ErrorOccurred += (sender, args) => errorOccurred = args.Exception; + int count = 0; + client.ErrorOccurred += (sender, args) => + { + Console.WriteLine("Exception "+ count++); + Console.WriteLine(args.Exception); + errorOccurred = args.Exception; + }; client.KeepAliveInterval = new TimeSpan(0, 0, 0, 0, 50); client.Connect(); - DisableHostNetworkConnection(NetworkConnectionId); + var disruptor = _processDisruptor.BreakConnections(); try { - for (var i = 0; i < 500; i++) - { - if (!client.IsConnected) - { - break; - } - - Thread.Sleep(100); - } + WaitForConnectionInterruption(client); Assert.IsFalse(client.IsConnected); Assert.IsNotNull(errorOccurred); Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); - var connectionException = (SshConnectionException)errorOccurred; + var connectionException = (SshConnectionException) errorOccurred; Assert.AreEqual(DisconnectReason.ConnectionLost, connectionException.DisconnectReason); Assert.IsNull(connectionException.InnerException); Assert.AreEqual("An established connection was aborted by the server.", connectionException.Message); } finally { - EnableHostNetworkConnection(NetworkConnectionId); - ResetVirtualMachineNetworkConnection(); + disruptor?.ResumeSshd(); + disruptor?.Dispose(); } } } + private static void WaitForConnectionInterruption(SftpClient client) + { + for (var i = 0; i < 500; i++) + { + if (!client.IsConnected) + { + break; + } + + Thread.Sleep(100); + } + + // After interruption, you have to wait for the events to propagate. + Thread.Sleep(100); + } + [TestMethod] public void Common_DetectConnectionResetThroughSftpInvocation() { using (var client = new SftpClient(_connectionInfoFactory.Create())) { + client.KeepAliveInterval = TimeSpan.FromSeconds(1); + client.OperationTimeout = TimeSpan.FromSeconds(60); ManualResetEvent errorOccurredSignaled = new ManualResetEvent(false); Exception errorOccurred = null; client.ErrorOccurred += (sender, args) => - { - errorOccurred = args.Exception; - errorOccurredSignaled.Set(); - }; + { + errorOccurred = args.Exception; + errorOccurredSignaled.Set(); + }; client.Connect(); - DisableHostNetworkConnection(NetworkConnectionId); + var disruptor = _processDisruptor.BreakConnections(); try { @@ -171,8 +189,8 @@ public void Common_DetectConnectionResetThroughSftpInvocation() } finally { - EnableHostNetworkConnection(NetworkConnectionId); - ResetVirtualMachineNetworkConnection(); + disruptor.ResumeSshd(); + disruptor.Dispose(); } } } @@ -181,7 +199,7 @@ public void Common_DetectConnectionResetThroughSftpInvocation() public void Common_LossOfNetworkConnectivityDisconnectAndConnect() { bool vmNetworkConnectionDisabled = false; - + ProcessDisruptorOperation disruptor = null; try { using (var client = new SftpClient(_connectionInfoFactory.Create())) @@ -191,34 +209,39 @@ public void Common_LossOfNetworkConnectivityDisconnectAndConnect() client.Connect(); - DisableVirtualMachineNetworkConnection(); + disruptor = _processDisruptor.BreakConnections(); vmNetworkConnectionDisabled = true; - ResetVirtualMachineNetworkConnection(); + WaitForConnectionInterruption(client); // disconnect while network connectivity is lost client.Disconnect(); Assert.IsFalse(client.IsConnected); - EnableVirtualMachineNetworkConnection(); + disruptor.ResumeSshd(); vmNetworkConnectionDisabled = false; - ResetVirtualMachineNetworkConnection(); // connect when network connectivity is restored client.Connect(); client.ChangeDirectory(client.WorkingDirectory); client.Dispose(); - Assert.IsNull(errorOccurred); + Assert.IsNotNull(errorOccurred); + Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); + + var connectionException = (SshConnectionException) errorOccurred; + Assert.AreEqual(DisconnectReason.ConnectionLost, connectionException.DisconnectReason); + Assert.IsNull(connectionException.InnerException); + Assert.AreEqual("An established connection was aborted by the server.", connectionException.Message); } } finally { if (vmNetworkConnectionDisabled) { - EnableVirtualMachineNetworkConnection(); - ResetVirtualMachineNetworkConnection(); + disruptor.ResumeSshd(); } + disruptor?.Dispose(); } } @@ -230,15 +253,14 @@ public void Common_DetectLossOfNetworkConnectivityThroughSftpInvocation() ManualResetEvent errorOccurredSignaled = new ManualResetEvent(false); Exception errorOccurred = null; client.ErrorOccurred += (sender, args) => - { - errorOccurred = args.Exception; - errorOccurredSignaled.Set(); - }; + { + errorOccurred = args.Exception; + errorOccurredSignaled.Set(); + }; client.Connect(); - DisableVirtualMachineNetworkConnection(); - ResetVirtualMachineNetworkConnection(); - + var disruptor = _processDisruptor.BreakConnections(); + Thread.Sleep(100); try { client.ListDirectory("/"); @@ -246,30 +268,37 @@ public void Common_DetectLossOfNetworkConnectivityThroughSftpInvocation() } catch (SshConnectionException ex) { - Assert.AreEqual(DisconnectReason.ConnectionLost, ex.DisconnectReason); Assert.IsNull(ex.InnerException); - Assert.AreEqual("An established connection was aborted by the server.", ex.Message); + Assert.AreEqual("Client not connected.", ex.Message); + + Assert.IsNotNull(errorOccurred); + Assert.AreEqual(typeof(SshConnectionException), errorOccurred.GetType()); + + var connectionException = (SshConnectionException) errorOccurred; + Assert.AreEqual(DisconnectReason.ConnectionLost, connectionException.DisconnectReason); + Assert.IsNull(connectionException.InnerException); + Assert.AreEqual("An established connection was aborted by the server.", connectionException.Message); } finally { - EnableVirtualMachineNetworkConnection(); - ResetVirtualMachineNetworkConnection(); + disruptor.ResumeSshd(); + disruptor.Dispose(); } } } [TestMethod] - public void Common_DetectSessionKilledOnServer() + public void Common_DetectSessionKilledOnServer() { using (var client = new SftpClient(_connectionInfoFactory.Create())) { ManualResetEvent errorOccurredSignaled = new ManualResetEvent(false); Exception errorOccurred = null; client.ErrorOccurred += (sender, args) => - { - errorOccurred = args.Exception; - errorOccurredSignaled.Set(); - }; + { + errorOccurred = args.Exception; + errorOccurredSignaled.Set(); + }; client.Connect(); // Kill the server session @@ -326,25 +355,25 @@ public void Common_HostKeyValidation_Success() using (var client = new SshClient(_connectionInfoFactory.Create())) { client.HostKeyReceived += (sender, e) => + { + if (host_rsa_key_openssh_fingerprint.Length == e.FingerPrint.Length) { - if (host_rsa_key_openssh_fingerprint.Length == e.FingerPrint.Length) + for (var i = 0; i < host_rsa_key_openssh_fingerprint.Length; i++) { - for (var i = 0; i < host_rsa_key_openssh_fingerprint.Length; i++) + if (host_rsa_key_openssh_fingerprint[i] != e.FingerPrint[i]) { - if (host_rsa_key_openssh_fingerprint[i] != e.FingerPrint[i]) - { - e.CanTrust = false; - break; - } + e.CanTrust = false; + break; } - - hostValidationSuccessful = e.CanTrust; } - else - { - e.CanTrust = false; - } - }; + + hostValidationSuccessful = e.CanTrust; + } + else + { + e.CanTrust = false; + } + }; client.Connect(); } @@ -382,230 +411,5 @@ public void Common_ServerRejectsConnection() } } - - [TestMethod] - [WorkItem(1140)] - [Description("Test whether IsConnected is false after disconnect.")] - [Owner("Kenneth_aa")] - public void Test_BaseClient_IsConnected_True_After_Disconnect() - { - // 2012-04-29 - Kenneth_aa - // The problem with this test, is that after SSH Net calls .Disconnect(), the library doesn't wait - // for the server to confirm disconnect before IsConnected is checked. And now I'm not mentioning - // anything about Socket's either. - - var connectionInfo = new PasswordAuthenticationMethod(User.UserName, User.Password); - - using (SftpClient client = new SftpClient(SshServerHostName, SshServerPort, User.UserName, User.Password)) - { - client.Connect(); - Assert.AreEqual(true, client.IsConnected, "IsConnected is not true after Connect() was called."); - - client.Disconnect(); - - Assert.AreEqual(false, client.IsConnected, "IsConnected is true after Disconnect() was called."); - } - } - - private static void DisableHostNetworkConnection(string networkConnection) - { - SelectQuery wmiQuery = new SelectQuery("SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionId != NULL"); - ManagementObjectSearcher searchProcedure = new ManagementObjectSearcher(wmiQuery); - foreach (ManagementObject item in searchProcedure.Get()) - { - var netConnectionId = (string)item["NetConnectionId"]; - - if (netConnectionId == networkConnection) - { - var returnValue = item.InvokeMethod("Disable", null); - if (returnValue is uint retValue) - { - if (retValue == 0) - { - return; - } - - throw new ApplicationException($"Failed to disable '{networkConnection}' network connection. Return value is {retValue}.{Environment.NewLine}Make sure you're running the tests with elevated priviliges."); - } - else if (returnValue == null) - { - throw new ApplicationException($"Failed to disable '{networkConnection}' network connection. Return value is null."); - } - else - { - throw new ApplicationException($"Failed to disable '{networkConnection}' network connection. Unexpected return value {returnValue} ({returnValue.GetType()})."); - } - } - } - - throw new ApplicationException($"Failed to disable '{networkConnection}' network connection. Network connection not found."); - } - - private static void EnableHostNetworkConnection(string networkConnection) - { - SelectQuery wmiQuery = new SelectQuery("SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionId != NULL"); - ManagementObjectSearcher searchProcedure = new ManagementObjectSearcher(wmiQuery); - foreach (ManagementObject item in searchProcedure.Get()) - { - var netConnectionId = (string)item["NetConnectionId"]; - - if (netConnectionId == networkConnection) - { - var returnValue = item.InvokeMethod("Enable", null); - if (returnValue is uint retValue) - { - if (retValue == 0u) - { - Console.WriteLine($"Enable host network connection for '{networkConnection}'."); - Thread.Sleep(5000); - return; - } - - throw new ApplicationException($"Failed to enable '{networkConnection}' network connection. Return value is {retValue}..{Environment.NewLine}Make sure you're running the tests with elevated priviliges."); - } - else if (returnValue == null) - { - throw new ApplicationException($"Failed to enable '{networkConnection}' network connection. Return value is null."); - } - else - { - throw new ApplicationException($"Failed to enable '{networkConnection}' network connection. Unexpected return value {returnValue} ({returnValue.GetType()})."); - } - } - } - - throw new ApplicationException($"Failed to enable '{networkConnection}' network connection. Network connection not found."); - } - - private static string VirtualBoxFolder - { - get - { - if (Environment.Is64BitOperatingSystem) - { - if (!Environment.Is64BitProcess) - { - // dotnet test runs tests in a 32-bit process (no watter what I f***in' try), so let's hard-code the - // path to VirtualBox - return Path.Combine("c:\\Program Files", "Oracle", "VirtualBox"); - } - } - - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Oracle", "VirtualBox"); - } - } - - private static List GetRunningVMs() - { - var runningVmRegex = new Regex("\"(?.+?)\"\\s?(?{.+?})"); - - var startInfo = new ProcessStartInfo - { - FileName = Path.Combine(VirtualBoxFolder, "VBoxManage.exe"), - Arguments = "list runningvms", - RedirectStandardOutput = true, - UseShellExecute = false - }; - - var process = Process.Start(startInfo); - process.WaitForExit(); - - if (process.ExitCode != 0) - { - throw new ApplicationException($"Failed to get list of running VMs. Exit code is {process.ExitCode}."); - } - - var runningVms = new List(); - - string line; - - while ((line = process.StandardOutput.ReadLine()) != null) - { - var match = runningVmRegex.Match(line); - if (match != null) - { - runningVms.Add(match.Groups["name"].Value); - } - } - - return runningVms; - } - - private static void SetLinkState(string vmName, bool on) - { - var linkStateValue = (on ? "on" : "off"); - - var startInfo = new ProcessStartInfo - { - FileName = Path.Combine(VirtualBoxFolder, "VBoxManage.exe"), - Arguments = $"controlvm \"{vmName}\" setlinkstate1 {linkStateValue}", - RedirectStandardOutput = true, - UseShellExecute = false - }; - - var process = Process.Start(startInfo); - process.WaitForExit(); - - if (process.ExitCode != 0) - { - throw new ApplicationException($"Failed to set linkstate for VM '{vmName}' to '{linkStateValue}'. Exit code is {process.ExitCode}."); - } - else - { - Console.WriteLine($"Changed linkstate for VM '{vmName}' to '{linkStateValue}."); - } - } - - private static void SetPromiscuousMode(string vmName, string value) - { - var startInfo = new ProcessStartInfo - { - FileName = Path.Combine(VirtualBoxFolder, "VBoxManage.exe"), - Arguments = $"controlvm \"{vmName}\" nicpromisc1 {value}", - RedirectStandardOutput = true, - UseShellExecute = false - }; - - var process = Process.Start(startInfo); - process.WaitForExit(); - - if (process.ExitCode != 0) - { - throw new ApplicationException($"Failed to set promiscuous for VM '{vmName}' to '{value}'. Exit code is {process.ExitCode}."); - } - else - { - Console.WriteLine($"Changed promiscuous for VM '{vmName}' to '{value}'."); - } - } - - private static void DisableVirtualMachineNetworkConnection() - { - var runningVMs = GetRunningVMs(); - Assert.AreEqual(1, runningVMs.Count); - - SetLinkState(runningVMs[0], false); - Thread.Sleep(1000); - } - - private static void EnableVirtualMachineNetworkConnection() - { - var runningVMs = GetRunningVMs(); - Assert.AreEqual(1, runningVMs.Count); - - SetLinkState(runningVMs[0], true); - Thread.Sleep(1000); - } - - private static void ResetVirtualMachineNetworkConnection() - { - var runningVMs = GetRunningVMs(); - Assert.AreEqual(1, runningVMs.Count); - - SetPromiscuousMode(runningVMs[0], "allow-all"); - Thread.Sleep(1000); - SetPromiscuousMode(runningVMs[0], "deny"); - Thread.Sleep(1000); - } } } diff --git a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs index 10197bf73..91c248bd3 100644 --- a/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs +++ b/src/Renci.SshNet.IntegrationTests/OldIntegrationTests/SftpClientTest.Upload.cs @@ -200,6 +200,11 @@ public void Test_Sftp_Multiple_Async_Upload_And_Download_10Files_5MB_Each() testInfo.DownloadedHash = CalculateMD5(testInfo.DownloadedFileName); + Console.WriteLine(remoteFile); + Console.WriteLine("UploadedBytes: "+ testInfo.UploadResult.UploadedBytes); + Console.WriteLine("DownloadedBytes: " + testInfo.DownloadResult.DownloadedBytes); + Console.WriteLine("UploadedHash: " + testInfo.UploadedHash); + Console.WriteLine("DownloadedHash: " + testInfo.DownloadedHash); if (!(testInfo.UploadResult.UploadedBytes > 0 && testInfo.DownloadResult.DownloadedBytes > 0 && testInfo.DownloadResult.DownloadedBytes == testInfo.UploadResult.UploadedBytes)) { uploadDownloadSizeOk = false; diff --git a/src/Renci.SshNet.IntegrationTests/ProcessDisruptor.cs b/src/Renci.SshNet.IntegrationTests/ProcessDisruptor.cs new file mode 100644 index 000000000..825696260 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/ProcessDisruptor.cs @@ -0,0 +1,41 @@ +namespace Renci.SshNet.IntegrationTests +{ + internal class ProcessDisruptor + { + private readonly IConnectionInfoFactory _connectionInfoFactory; + + public ProcessDisruptor(IConnectionInfoFactory connectionInfoFactory) + { + _connectionInfoFactory = connectionInfoFactory; + } + + public ProcessDisruptorOperation BreakConnections() + { + var client = new SshClient(_connectionInfoFactory.Create()); + + client.Connect(); + + PauseSshd(client); + + return new ProcessDisruptorOperation(client); + } + + private static void PauseSshd(SshClient client) + { + var command = client.CreateCommand("sudo echo 'DenyUsers sshnet' >> /etc/ssh/sshd_config"); + var output = command.Execute(); + if (command.ExitStatus != 0) + { + throw new ApplicationException( + $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + } + command = client.CreateCommand("sudo pkill -9 -U sshnet -f sshd.pam"); + output = command.Execute(); + if (command.ExitStatus != 0) + { + throw new ApplicationException( + $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + } + } + } +} diff --git a/src/Renci.SshNet.IntegrationTests/ProcessDisruptorOperation.cs b/src/Renci.SshNet.IntegrationTests/ProcessDisruptorOperation.cs new file mode 100644 index 000000000..230fcaf49 --- /dev/null +++ b/src/Renci.SshNet.IntegrationTests/ProcessDisruptorOperation.cs @@ -0,0 +1,36 @@ +namespace Renci.SshNet.IntegrationTests +{ + internal class ProcessDisruptorOperation : IDisposable + { + private SshClient _sshClient; + + public ProcessDisruptorOperation(SshClient sshClient) + { + _sshClient = sshClient; + } + + public void ResumeSshd() + { + var command = _sshClient.CreateCommand("sudo sed -i '/DenyUsers sshnet/d' /etc/ssh/sshd_config"); + var output = command.Execute(); + if (command.ExitStatus != 0) + { + throw new ApplicationException( + $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + } + command = _sshClient.CreateCommand("sudo /usr/sbin/sshd.pam"); + output = command.Execute(); + if (command.ExitStatus != 0) + { + throw new ApplicationException( + $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + } + } + + public void Dispose() + { + _sshClient?.Dispose(); + _sshClient = null; + } + } +} From e78383e30984cdd67997bf467e51c3b852baf356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Tue, 12 Sep 2023 09:34:14 +0200 Subject: [PATCH 12/15] Rename --- .../ConnectivityTests.cs | 30 +++++++++---------- ...Disruptor.cs => SshConnectionDisruptor.cs} | 12 ++++---- ...rOperation.cs => SshConnectionRestorer.cs} | 8 ++--- 3 files changed, 25 insertions(+), 25 deletions(-) rename src/Renci.SshNet.IntegrationTests/{ProcessDisruptor.cs => SshConnectionDisruptor.cs} (70%) rename src/Renci.SshNet.IntegrationTests/{ProcessDisruptorOperation.cs => SshConnectionRestorer.cs} (76%) diff --git a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs index 60ab2a74d..5401e6b6b 100644 --- a/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs +++ b/src/Renci.SshNet.IntegrationTests/ConnectivityTests.cs @@ -11,7 +11,7 @@ public class ConnectivityTests : IntegrationTestBase private IConnectionInfoFactory _connectionInfoFactory; private IConnectionInfoFactory _adminConnectionInfoFactory; private RemoteSshdConfig _remoteSshdConfig; - private ProcessDisruptor _processDisruptor; + private SshConnectionDisruptor _sshConnectionDisruptor; [TestInitialize] public void SetUp() @@ -20,7 +20,7 @@ public void SetUp() _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort, _authenticationMethodFactory); _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort); _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig(); - _processDisruptor = new ProcessDisruptor(_adminConnectionInfoFactory); + _sshConnectionDisruptor = new SshConnectionDisruptor(_adminConnectionInfoFactory); } [TestCleanup] @@ -57,7 +57,7 @@ public void Common_CreateMoreChannelsThanMaxSessions() public void Common_DisposeAfterLossOfNetworkConnectivity() { var hostNetworkConnectionDisabled = false; - ProcessDisruptorOperation disruptor = null; + SshConnectionRestorer disruptor = null; try { Exception errorOccurred = null; @@ -72,7 +72,7 @@ public void Common_DisposeAfterLossOfNetworkConnectivity() }; client.Connect(); - disruptor = _processDisruptor.BreakConnections(); + disruptor = _sshConnectionDisruptor.BreakConnections(); hostNetworkConnectionDisabled = true; WaitForConnectionInterruption(client); } @@ -89,7 +89,7 @@ public void Common_DisposeAfterLossOfNetworkConnectivity() { if (hostNetworkConnectionDisabled) { - disruptor?.ResumeSshd(); + disruptor?.RestoreConnections(); disruptor?.Dispose(); } } @@ -111,7 +111,7 @@ public void Common_DetectLossOfNetworkConnectivityThroughKeepAlive() client.KeepAliveInterval = new TimeSpan(0, 0, 0, 0, 50); client.Connect(); - var disruptor = _processDisruptor.BreakConnections(); + var disruptor = _sshConnectionDisruptor.BreakConnections(); try { @@ -129,7 +129,7 @@ public void Common_DetectLossOfNetworkConnectivityThroughKeepAlive() } finally { - disruptor?.ResumeSshd(); + disruptor?.RestoreConnections(); disruptor?.Dispose(); } } @@ -167,7 +167,7 @@ public void Common_DetectConnectionResetThroughSftpInvocation() }; client.Connect(); - var disruptor = _processDisruptor.BreakConnections(); + var disruptor = _sshConnectionDisruptor.BreakConnections(); try { @@ -189,7 +189,7 @@ public void Common_DetectConnectionResetThroughSftpInvocation() } finally { - disruptor.ResumeSshd(); + disruptor.RestoreConnections(); disruptor.Dispose(); } } @@ -199,7 +199,7 @@ public void Common_DetectConnectionResetThroughSftpInvocation() public void Common_LossOfNetworkConnectivityDisconnectAndConnect() { bool vmNetworkConnectionDisabled = false; - ProcessDisruptorOperation disruptor = null; + SshConnectionRestorer disruptor = null; try { using (var client = new SftpClient(_connectionInfoFactory.Create())) @@ -209,7 +209,7 @@ public void Common_LossOfNetworkConnectivityDisconnectAndConnect() client.Connect(); - disruptor = _processDisruptor.BreakConnections(); + disruptor = _sshConnectionDisruptor.BreakConnections(); vmNetworkConnectionDisabled = true; WaitForConnectionInterruption(client); @@ -218,7 +218,7 @@ public void Common_LossOfNetworkConnectivityDisconnectAndConnect() Assert.IsFalse(client.IsConnected); - disruptor.ResumeSshd(); + disruptor.RestoreConnections(); vmNetworkConnectionDisabled = false; // connect when network connectivity is restored @@ -239,7 +239,7 @@ public void Common_LossOfNetworkConnectivityDisconnectAndConnect() { if (vmNetworkConnectionDisabled) { - disruptor.ResumeSshd(); + disruptor.RestoreConnections(); } disruptor?.Dispose(); } @@ -259,7 +259,7 @@ public void Common_DetectLossOfNetworkConnectivityThroughSftpInvocation() }; client.Connect(); - var disruptor = _processDisruptor.BreakConnections(); + var disruptor = _sshConnectionDisruptor.BreakConnections(); Thread.Sleep(100); try { @@ -281,7 +281,7 @@ public void Common_DetectLossOfNetworkConnectivityThroughSftpInvocation() } finally { - disruptor.ResumeSshd(); + disruptor.RestoreConnections(); disruptor.Dispose(); } } diff --git a/src/Renci.SshNet.IntegrationTests/ProcessDisruptor.cs b/src/Renci.SshNet.IntegrationTests/SshConnectionDisruptor.cs similarity index 70% rename from src/Renci.SshNet.IntegrationTests/ProcessDisruptor.cs rename to src/Renci.SshNet.IntegrationTests/SshConnectionDisruptor.cs index 825696260..741134114 100644 --- a/src/Renci.SshNet.IntegrationTests/ProcessDisruptor.cs +++ b/src/Renci.SshNet.IntegrationTests/SshConnectionDisruptor.cs @@ -1,15 +1,15 @@ namespace Renci.SshNet.IntegrationTests { - internal class ProcessDisruptor + internal class SshConnectionDisruptor { private readonly IConnectionInfoFactory _connectionInfoFactory; - public ProcessDisruptor(IConnectionInfoFactory connectionInfoFactory) + public SshConnectionDisruptor(IConnectionInfoFactory connectionInfoFactory) { _connectionInfoFactory = connectionInfoFactory; } - public ProcessDisruptorOperation BreakConnections() + public SshConnectionRestorer BreakConnections() { var client = new SshClient(_connectionInfoFactory.Create()); @@ -17,7 +17,7 @@ public ProcessDisruptorOperation BreakConnections() PauseSshd(client); - return new ProcessDisruptorOperation(client); + return new SshConnectionRestorer(client); } private static void PauseSshd(SshClient client) @@ -27,14 +27,14 @@ private static void PauseSshd(SshClient client) if (command.ExitStatus != 0) { throw new ApplicationException( - $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + $"Blocking user sshnet failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); } command = client.CreateCommand("sudo pkill -9 -U sshnet -f sshd.pam"); output = command.Execute(); if (command.ExitStatus != 0) { throw new ApplicationException( - $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + $"Killing sshd.pam service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); } } } diff --git a/src/Renci.SshNet.IntegrationTests/ProcessDisruptorOperation.cs b/src/Renci.SshNet.IntegrationTests/SshConnectionRestorer.cs similarity index 76% rename from src/Renci.SshNet.IntegrationTests/ProcessDisruptorOperation.cs rename to src/Renci.SshNet.IntegrationTests/SshConnectionRestorer.cs index 230fcaf49..d7c6437db 100644 --- a/src/Renci.SshNet.IntegrationTests/ProcessDisruptorOperation.cs +++ b/src/Renci.SshNet.IntegrationTests/SshConnectionRestorer.cs @@ -1,22 +1,22 @@ namespace Renci.SshNet.IntegrationTests { - internal class ProcessDisruptorOperation : IDisposable + internal class SshConnectionRestorer : IDisposable { private SshClient _sshClient; - public ProcessDisruptorOperation(SshClient sshClient) + public SshConnectionRestorer(SshClient sshClient) { _sshClient = sshClient; } - public void ResumeSshd() + public void RestoreConnections() { var command = _sshClient.CreateCommand("sudo sed -i '/DenyUsers sshnet/d' /etc/ssh/sshd_config"); var output = command.Execute(); if (command.ExitStatus != 0) { throw new ApplicationException( - $"Resuming ssh service failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); + $"Unblocking user sshnet failed with exit code {command.ExitStatus}.\r\n{output}\r\n{command.Error}"); } command = _sshClient.CreateCommand("sudo /usr/sbin/sshd.pam"); output = command.Execute(); From f39a0ff576d650913949ea04cc978774e17a11d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Tue, 12 Sep 2023 10:37:24 +0200 Subject: [PATCH 13/15] Some fixes --- .../Common/AsyncSocketListener.cs | 299 +++++++-- .../Common/ByteExtensions.cs | 81 --- .../Common/LinkedListStream.cs | 118 ---- .../Common/SocketAbstraction.cs | 565 ------------------ .../Common/Socks5Handler.cs | 1 + 5 files changed, 265 insertions(+), 799 deletions(-) delete mode 100644 src/Renci.SshNet.IntegrationTests/Common/ByteExtensions.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Common/LinkedListStream.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Common/SocketAbstraction.cs diff --git a/src/Renci.SshNet.IntegrationTests/Common/AsyncSocketListener.cs b/src/Renci.SshNet.IntegrationTests/Common/AsyncSocketListener.cs index 4a455af4c..753385582 100644 --- a/src/Renci.SshNet.IntegrationTests/Common/AsyncSocketListener.cs +++ b/src/Renci.SshNet.IntegrationTests/Common/AsyncSocketListener.cs @@ -1,7 +1,5 @@ using System.Net; using System.Net.Sockets; -#if !FEATURE_SOCKET_DISPOSE -#endif // !FEATURE_SOCKET_DISPOSE namespace Renci.SshNet.IntegrationTests.Common { @@ -9,9 +7,12 @@ public class AsyncSocketListener : IDisposable { private readonly IPEndPoint _endPoint; private readonly ManualResetEvent _acceptCallbackDone; + private readonly List _connectedClients; + private readonly object _syncLock; private Socket _listener; private Thread _receiveThread; private bool _started; + private string _stackTrace; public delegate void BytesReceivedHandler(byte[] bytesReceived, Socket socket); public delegate void ConnectedHandler(Socket socket); @@ -24,8 +25,22 @@ public AsyncSocketListener(IPEndPoint endPoint) { _endPoint = endPoint; _acceptCallbackDone = new ManualResetEvent(false); + _connectedClients = new List(); + _syncLock = new object(); + ShutdownRemoteCommunicationSocket = true; } + /// + /// Gets a value indicating whether the is invoked on the + /// that is used to handle the communication with the remote host, when the remote host has closed the connection. + /// + /// + /// to invoke on the that is used + /// to handle the communication with the remote host, when the remote host has closed the connection; otherwise, + /// . The default is . + /// + public bool ShutdownRemoteCommunicationSocket { get; set; } + public void Start() { _listener = new Socket(_endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); @@ -36,16 +51,38 @@ public void Start() _receiveThread = new Thread(StartListener); _receiveThread.Start(_listener); + + _stackTrace = Environment.StackTrace; } public void Stop() { _started = false; - if (_listener != null) + + lock (_syncLock) { - _listener.Dispose(); - _listener = null; + foreach (var connectedClient in _connectedClients) + { + try + { + connectedClient.Shutdown(SocketShutdown.Send); + } + catch (Exception ex) + { + Console.Error.WriteLine("[{0}] Failure shutting down socket: {1}", + typeof(AsyncSocketListener).FullName, + ex); + } + + DrainSocket(connectedClient); + connectedClient.Dispose(); + } + + _connectedClients.Clear(); } + + _listener?.Dispose(); + if (_receiveThread != null) { _receiveThread.Join(); @@ -61,45 +98,130 @@ public void Dispose() private void StartListener(object state) { - var listener = (Socket)state; - while (_started) + try + { + var listener = (Socket) state; + while (_started) + { + _ = _acceptCallbackDone.Reset(); + _ = listener.BeginAccept(AcceptCallback, listener); + _ = _acceptCallbackDone.WaitOne(); + } + } + catch (Exception ex) { - _acceptCallbackDone.Reset(); - listener.BeginAccept(AcceptCallback, listener); - _acceptCallbackDone.WaitOne(); + // On .NET framework when Thread throws an exception then unit tests + // were executed without any problem. + // On new .NET exceptions from Thread breaks unit tests session. + Console.Error.WriteLine("[{0}] Failure in StartListener: {1}", + typeof(AsyncSocketListener).FullName, + ex); } } private void AcceptCallback(IAsyncResult ar) { - // Signal the main thread to continue. - _acceptCallbackDone.Set(); + // Signal the main thread to continue + _ = _acceptCallbackDone.Set(); + + // Get the socket that listens for inbound connections + var listener = (Socket) ar.AsyncState; + + // Get the socket that handles the client request + Socket handler; - // Get the socket that handles the client request. - var listener = (Socket)ar.AsyncState; try { - var handler = listener.EndAccept(ar); - SignalConnected(handler); - var state = new SocketStateObject(handler); - handler.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReadCallback, state); + handler = listener.EndAccept(ar); } - catch (SocketException) + catch (SocketException ex) + { + // The listener is stopped through a Dispose() call, which in turn causes + // Socket.EndAccept(...) to throw a SocketException or + // ObjectDisposedException + // + // Since we consider such an exception normal when the listener is being + // stopped, we only write a message to stderr if the listener is considered + // to be up and running + if (_started) + { + Console.Error.WriteLine("[{0}] Failure accepting new connection: {1}", + typeof(AsyncSocketListener).FullName, + ex); + } + return; + } + catch (ObjectDisposedException ex) + { + // The listener is stopped through a Dispose() call, which in turn causes + // Socket.EndAccept(IAsyncResult) to throw a SocketException or + // ObjectDisposedException + // + // Since we consider such an exception normal when the listener is being + // stopped, we only write a message to stderr if the listener is considered + // to be up and running + if (_started) + { + Console.Error.WriteLine("[{0}] Failure accepting new connection: {1}", + typeof(AsyncSocketListener).FullName, + ex); + } + return; + } + + // Signal new connection + SignalConnected(handler); + + lock (_syncLock) + { + // Register client socket + _connectedClients.Add(handler); + } + + var state = new SocketStateObject(handler); + + try { - // when the socket is closed, an SocketException is thrown since .NET 5 - // by Socket.EndAccept(IAsyncResult) + _ = handler.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReadCallback, state); } - catch (ObjectDisposedException) + catch (SocketException ex) { - // when the socket is closed, an ObjectDisposedException is thrown on old .NET Framework - // by Socket.EndAccept(IAsyncResult) + // The listener is stopped through a Dispose() call, which in turn causes + // Socket.BeginReceive(...) to throw a SocketException or + // ObjectDisposedException + // + // Since we consider such an exception normal when the listener is being + // stopped, we only write a message to stderr if the listener is considered + // to be up and running + if (_started) + { + Console.Error.WriteLine("[{0}] Failure receiving new data: {1}", + typeof(AsyncSocketListener).FullName, + ex); + } + } + catch (ObjectDisposedException ex) + { + // The listener is stopped through a Dispose() call, which in turn causes + // Socket.BeginReceive(...) to throw a SocketException or + // ObjectDisposedException + // + // Since we consider such an exception normal when the listener is being + // stopped, we only write a message to stderr if the listener is considered + // to be up and running + if (_started) + { + Console.Error.WriteLine("[{0}] Failure receiving new data: {1}", + typeof(AsyncSocketListener).FullName, + ex); + } } } private void ReadCallback(IAsyncResult ar) { // Retrieve the state object and the handler socket - // from the asynchronous state object. + // from the asynchronous state object var state = (SocketStateObject) ar.AsyncState; var handler = state.Socket; @@ -107,30 +229,113 @@ private void ReadCallback(IAsyncResult ar) try { // Read data from the client socket. - bytesRead = handler.EndReceive(ar); + bytesRead = handler.EndReceive(ar, out var errorCode); + if (errorCode != SocketError.Success) + { + bytesRead = 0; + } + } + catch (SocketException ex) + { + // The listener is stopped through a Dispose() call, which in turn causes + // Socket.EndReceive(...) to throw a SocketException or + // ObjectDisposedException + // + // Since we consider such an exception normal when the listener is being + // stopped, we only write a message to stderr if the listener is considered + // to be up and running + if (_started) + { + Console.Error.WriteLine("[{0}] Failure receiving new data: {1}", + typeof(AsyncSocketListener).FullName, + ex); + } + return; } - catch (ObjectDisposedException) + catch (ObjectDisposedException ex) { - // when the socket is closed, the callback will be invoked for any pending BeginReceive - // we could use the Socket.Connected property to detect this here, but the proper thing - // to do is invoke EndReceive knowing that it will throw an ObjectDisposedException + // The listener is stopped through a Dispose() call, which in turn causes + // Socket.EndReceive(...) to throw a SocketException or + // ObjectDisposedException + // + // Since we consider such an exception normal when the listener is being + // stopped, we only write a message to stderr if the listener is considered + // to be up and running + if (_started) + { + Console.Error.WriteLine("[{0}] Failure receiving new data: {1}", + typeof(AsyncSocketListener).FullName, + ex); + } return; } + void ConnectionDisconnected() + { + SignalDisconnected(handler); + + if (ShutdownRemoteCommunicationSocket) + { + lock (_syncLock) + { + if (!_started) + { + return; + } + + try + { + handler.Shutdown(SocketShutdown.Send); + handler.Close(); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionReset) + { + // On .NET 7 we got Socker Exception with ConnectionReset from Shutdown method + // when the socket is disposed + } + catch (SocketException ex) + { + throw new Exception("Exception in ReadCallback: " + ex.SocketErrorCode + " " + _stackTrace, ex); + } + catch (Exception ex) + { + throw new Exception("Exception in ReadCallback: " + _stackTrace, ex); + } + + _ = _connectedClients.Remove(handler); + } + } + } + if (bytesRead > 0) { var bytesReceived = new byte[bytesRead]; Array.Copy(state.Buffer, bytesReceived, bytesRead); SignalBytesReceived(bytesReceived, handler); - // prepare to receive more bytes - handler.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReadCallback, state); + try + { + _ = handler.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReadCallback, state); + } + catch (ObjectDisposedException) + { + // TODO On .NET 7, sometimes we get ObjectDisposedException when _started but only on appveyor, locally it works + ConnectionDisconnected(); + } + catch (SocketException ex) + { + if (!_started) + { + throw new Exception("BeginReceive while stopping!", ex); + } + + throw new Exception("BeginReceive while started!: " + ex.SocketErrorCode + " " + _stackTrace, ex); + } + } else { - SignalDisconnected(handler); - handler.Shutdown(SocketShutdown.Both); - handler.Close(); + ConnectionDisconnected(); } } @@ -149,6 +354,30 @@ private void SignalDisconnected(Socket client) Disconnected?.Invoke(client); } + private static void DrainSocket(Socket socket) + { + var buffer = new byte[128]; + + try + { + while (true && socket.Connected) + { + var bytesRead = socket.Receive(buffer); + if (bytesRead == 0) + { + break; + } + } + } + catch (SocketException ex) + { + Console.Error.WriteLine("[{0}] Failure draining socket ({1}): {2}", + typeof(AsyncSocketListener).FullName, + ex.SocketErrorCode.ToString("G"), + ex); + } + } + private class SocketStateObject { public Socket Socket { get; private set; } diff --git a/src/Renci.SshNet.IntegrationTests/Common/ByteExtensions.cs b/src/Renci.SshNet.IntegrationTests/Common/ByteExtensions.cs deleted file mode 100644 index 720a8edf0..000000000 --- a/src/Renci.SshNet.IntegrationTests/Common/ByteExtensions.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Globalization; - -namespace Renci.SshNet.IntegrationTests.Common -{ - public static class ByteExtensions - { - public static byte[] HexToByteArray(string hexString) - { - var bytes = new byte[hexString.Length / 2]; - - for (var i = 0; i < hexString.Length; i += 2) - { - var s = hexString.Substring(i, 2); - bytes[i / 2] = byte.Parse(s, NumberStyles.HexNumber, null); - } - - return bytes; - } - - public static string ToHex(byte[] bytes) - { - var builder = new StringBuilder(bytes.Length * 2); - - foreach (byte b in bytes) - { - builder.Append(b.ToString("X2")); - } - - return builder.ToString(); - } - - public static byte[] Repeat(byte b, int count) - { - var value = new byte[count]; - - for (var i = 0; i < count; i++) - { - value[i] = b; - } - - return value; - } - - /// - /// Returns a specified number of contiguous bytes from a given offset. - /// - /// The array to return a number of bytes from. - /// The zero-based offset in at which to begin taking bytes. - /// The number of bytes to take from . - /// - /// A array that contains the specified number of bytes at the specified offset - /// of the input array. - /// - /// is null. - /// - /// When is zero and equals the length of , - /// then is returned. - /// - public static byte[] Take(byte[] value, int offset, int count) - { - if (value == null) - { - throw new ArgumentNullException("value"); - } - - if (count == 0) - { - return new byte[0]; - } - - if (offset == 0 && value.Length == count) - { - return value; - } - - var taken = new byte[count]; - Buffer.BlockCopy(value, offset, taken, 0, count); - return taken; - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Common/LinkedListStream.cs b/src/Renci.SshNet.IntegrationTests/Common/LinkedListStream.cs deleted file mode 100644 index 9e6878a4f..000000000 --- a/src/Renci.SshNet.IntegrationTests/Common/LinkedListStream.cs +++ /dev/null @@ -1,118 +0,0 @@ -namespace Renci.SshNet.IntegrationTests.Common -{ - internal class LinkedListStream : Stream - { - private PipeEntry _first; - private PipeEntry _last; - - public override void Flush() - { - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - var totalBytesRead = 0; - - while (count > 0 && _first != null) - { - var bytesRead = _first.Read(buffer, offset, count); - if (_first.IsEmpty) - { - _first = _first.Next; - } - - count -= bytesRead; - totalBytesRead += bytesRead; - offset += bytesRead; - } - - return totalBytesRead; - } - - public override void Write(byte[] buffer, int offset, int count) - { - var last = new PipeEntry(buffer, offset, count); - if (_last == null) - { - _last = last; - } - else - { - _last = _last.Next = last; - } - - _first ??= _last; - } - - public override bool CanRead - { - get { return true; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanWrite - { - get { return true; } - } - - public override long Length - { - get - { - throw new NotSupportedException(); - } - } - - public override long Position { get; set; } - } - - internal class PipeEntry - { - private readonly byte[] _data; - public int Position; - public int Length; - - public PipeEntry(byte[] data, int offset, int count) - { - _data = data; - Position = offset; - Length = count; - } - - public int Read(byte[] dst, int offset, int count) - { - var bytesToCopy = count; - var bytesAvailable = Length - Position; - - if (count > bytesAvailable) - { - bytesToCopy = bytesAvailable; - } - - Buffer.BlockCopy(_data, Position, dst, offset, bytesToCopy); - Position += bytesToCopy; - return bytesToCopy; - } - - public bool IsEmpty - { - get { return Position == Length; } - } - - public PipeEntry Next { get; set; } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Common/SocketAbstraction.cs b/src/Renci.SshNet.IntegrationTests/Common/SocketAbstraction.cs deleted file mode 100644 index 1f96f7af3..000000000 --- a/src/Renci.SshNet.IntegrationTests/Common/SocketAbstraction.cs +++ /dev/null @@ -1,565 +0,0 @@ -using System.Globalization; -using System.Net; -using System.Net.Sockets; - -using Renci.SshNet.Common; -using Renci.SshNet.Messages.Transport; - -namespace Renci.SshNet.IntegrationTests.Common -{ - internal static class SocketAbstraction - { - public static bool CanRead(Socket socket) - { - if (socket.Connected) - { -#if FEATURE_SOCKET_POLL - return socket.Poll(-1, SelectMode.SelectRead) && socket.Available > 0; -#else - return true; -#endif // FEATURE_SOCKET_POLL - } - - return false; - - } - - /// - /// Returns a value indicating whether the specified can be used - /// to send data. - /// - /// The to check. - /// - /// true if can be written to; otherwise, false. - /// - public static bool CanWrite(Socket socket) - { - if (socket != null && socket.Connected) - { -#if FEATURE_SOCKET_POLL - return socket.Poll(-1, SelectMode.SelectWrite); -#else - return true; -#endif // FEATURE_SOCKET_POLL - } - - return false; - } - - public static Socket Connect(IPEndPoint remoteEndpoint, TimeSpan connectTimeout) - { - var socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) {NoDelay = true}; - - var connectCompleted = new ManualResetEvent(false); - var args = new SocketAsyncEventArgs - { - UserToken = connectCompleted, - RemoteEndPoint = remoteEndpoint - }; - args.Completed += ConnectCompleted; - - if (socket.ConnectAsync(args)) - { - if (!connectCompleted.WaitOne(connectTimeout)) - { - // avoid ObjectDisposedException in ConnectCompleted - args.Completed -= ConnectCompleted; - // dispose Socket - socket.Dispose(); - // dispose ManualResetEvent - connectCompleted.Dispose(); - // dispose SocketAsyncEventArgs - args.Dispose(); - - throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture, - "Connection failed to establish within {0:F0} milliseconds.", - connectTimeout.TotalMilliseconds)); - } - } - - // dispose ManualResetEvent - connectCompleted.Dispose(); - - if (args.SocketError != SocketError.Success) - { - var socketError = (int) args.SocketError; - - // dispose Socket - socket.Dispose(); - // dispose SocketAsyncEventArgs - args.Dispose(); - - throw new SocketException(socketError); - } - - // dispose SocketAsyncEventArgs - args.Dispose(); - - return socket; - } - - public static void ClearReadBuffer(Socket socket) - { - var timeout = TimeSpan.FromMilliseconds(500); - var buffer = new byte[256]; - int bytesReceived; - - do - { - bytesReceived = ReadPartial(socket, buffer, 0, buffer.Length, timeout); - } - while (bytesReceived > 0); - } - - public static int ReadPartial(Socket socket, byte[] buffer, int offset, int size, TimeSpan timeout) - { - var receiveCompleted = new ManualResetEvent(false); - var sendReceiveToken = new PartialSendReceiveToken(socket, receiveCompleted); - var args = new SocketAsyncEventArgs - { - RemoteEndPoint = socket.RemoteEndPoint, - UserToken = sendReceiveToken - }; - args.Completed += ReceiveCompleted; - args.SetBuffer(buffer, offset, size); - - try - { - if (socket.ReceiveAsync(args)) - { - if (!receiveCompleted.WaitOne(timeout)) - { - throw new SshOperationTimeoutException( - string.Format( - CultureInfo.InvariantCulture, - "Socket read operation has timed out after {0:F0} milliseconds.", - timeout.TotalMilliseconds)); - } - } - else - { - sendReceiveToken.Process(args); - } - - if (args.SocketError != SocketError.Success) - { - throw new SocketException((int) args.SocketError); - } - - return args.BytesTransferred; - } - finally - { - // initialize token to avoid the waithandle getting used after it's disposed - args.UserToken = null; - args.Dispose(); - receiveCompleted.Dispose(); - } - } - - public static void ReadContinuous(Socket socket, byte[] buffer, int offset, int size, Action processReceivedBytesAction) - { - var completionWaitHandle = new ManualResetEvent(false); - var readToken = new ContinuousReceiveToken(socket, processReceivedBytesAction, completionWaitHandle); - var args = new SocketAsyncEventArgs - { - RemoteEndPoint = socket.RemoteEndPoint, - UserToken = readToken - }; - args.Completed += ReceiveCompleted; - args.SetBuffer(buffer, offset, size); - - if (!socket.ReceiveAsync(args)) - { - ReceiveCompleted(null, args); - } - - completionWaitHandle.WaitOne(); - completionWaitHandle.Dispose(); - - if (readToken.Exception != null) - { - throw readToken.Exception; - } - } - - /// - /// Reads a byte from the specified . - /// - /// The to read from. - /// Specifies the amount of time after which the call will time out. - /// - /// The byte read, or -1 if the socket was closed. - /// - /// The read operation timed out. - /// The read failed. - public static int ReadByte(Socket socket, TimeSpan timeout) - { - var buffer = new byte[1]; - if (Read(socket, buffer, 0, 1, timeout) == 0) - { - return -1; - } - - return buffer[0]; - } - - /// - /// Sends a byte using the specified . - /// - /// The to write to. - /// The value to send. - /// The write failed. - public static void SendByte(Socket socket, byte value) - { - var buffer = new[] {value}; - Send(socket, buffer, 0, 1); - } - - /// - /// Receives data from a bound . - /// - /// - /// The number of bytes to receive. - /// Specifies the amount of time after which the call will time out. - /// - /// The bytes received. - /// - /// - /// If no data is available for reading, the method will - /// block until data is available or the time-out value is exceeded. If the time-out value is exceeded, the - /// call will throw a . - /// If you are in non-blocking mode, and there is no data available in the in the protocol stack buffer, the - /// method will complete immediately and throw a . - /// - public static byte[] Read(Socket socket, int size, TimeSpan timeout) - { - var buffer = new byte[size]; - Read(socket, buffer, 0, size, timeout); - return buffer; - } - - /// - /// Receives data from a bound into a receive buffer. - /// - /// - /// An array of type that is the storage location for the received data. - /// The position in parameter to store the received data. - /// The number of bytes to receive. - /// Specifies the amount of time after which the call will time out. - /// - /// The number of bytes received. - /// - /// - /// If no data is available for reading, the method will - /// block until data is available or the time-out value is exceeded. If the time-out value is exceeded, the - /// call will throw a . - /// If you are in non-blocking mode, and there is no data available in the in the protocol stack buffer, the - /// method will complete immediately and throw a . - /// - public static int Read(Socket socket, byte[] buffer, int offset, int size, TimeSpan timeout) - { - var receiveCompleted = new ManualResetEvent(false); - var sendReceiveToken = new BlockingSendReceiveToken(socket, buffer, offset, size, receiveCompleted); - - var args = new SocketAsyncEventArgs - { - UserToken = sendReceiveToken, - RemoteEndPoint = socket.RemoteEndPoint - }; - args.Completed += ReceiveCompleted; - args.SetBuffer(buffer, offset, size); - - try - { - if (socket.ReceiveAsync(args)) - { - if (!receiveCompleted.WaitOne(timeout)) - { - throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture, - "Socket read operation has timed out after {0:F0} milliseconds.", timeout.TotalMilliseconds)); - } - } - else - { - sendReceiveToken.Process(args); - } - - if (args.SocketError != SocketError.Success) - { - throw new SocketException((int) args.SocketError); - } - - return sendReceiveToken.TotalBytesTransferred; - } - finally - { - // initialize token to avoid the waithandle getting used after it's disposed - args.UserToken = null; - args.Dispose(); - receiveCompleted.Dispose(); - } - } - - public static void Send(Socket socket, byte[] data) - { - Send(socket, data, 0, data.Length); - } - - public static void Send(Socket socket, byte[] data, int offset, int size) - { - var sendCompleted = new ManualResetEvent(false); - var sendReceiveToken = new BlockingSendReceiveToken(socket, data, offset, size, sendCompleted); - var socketAsyncSendArgs = new SocketAsyncEventArgs - { - RemoteEndPoint = socket.RemoteEndPoint, - UserToken = sendReceiveToken - }; - socketAsyncSendArgs.SetBuffer(data, offset, size); - socketAsyncSendArgs.Completed += SendCompleted; - - try - { - if (socket.SendAsync(socketAsyncSendArgs)) - { - if (!sendCompleted.WaitOne()) - { - throw new SocketException((int) SocketError.TimedOut); - } - } - else - { - sendReceiveToken.Process(socketAsyncSendArgs); - } - - if (socketAsyncSendArgs.SocketError != SocketError.Success) - { - throw new SocketException((int) socketAsyncSendArgs.SocketError); - } - - if (sendReceiveToken.TotalBytesTransferred == 0) - { - throw new SshConnectionException("An established connection was aborted by the server.", - DisconnectReason.ConnectionLost); - } - } - finally - { - // initialize token to avoid the completion waithandle getting used after it's disposed - socketAsyncSendArgs.UserToken = null; - socketAsyncSendArgs.Dispose(); - sendCompleted.Dispose(); - } - } - - public static bool IsErrorResumable(SocketError socketError) - { - switch (socketError) - { - case SocketError.WouldBlock: - case SocketError.IOPending: - case SocketError.NoBufferSpaceAvailable: - return true; - default: - return false; - } - } - - private static void ConnectCompleted(object sender, SocketAsyncEventArgs e) - { - var eventWaitHandle = (ManualResetEvent) e.UserToken; - eventWaitHandle?.Set(); - } - - private static void ReceiveCompleted(object sender, SocketAsyncEventArgs e) - { - var sendReceiveToken = (IToken) e.UserToken; - sendReceiveToken?.Process(e); - } - - private static void SendCompleted(object sender, SocketAsyncEventArgs e) - { - var sendReceiveToken = (IToken) e.UserToken; - sendReceiveToken?.Process(e); - } - - private interface IToken - { - void Process(SocketAsyncEventArgs args); - } - - private class BlockingSendReceiveToken : IToken - { - public BlockingSendReceiveToken(Socket socket, byte[] buffer, int offset, int size, EventWaitHandle completionWaitHandle) - { - _socket = socket; - _buffer = buffer; - _offset = offset; - _bytesToTransfer = size; - _completionWaitHandle = completionWaitHandle; - } - - public void Process(SocketAsyncEventArgs args) - { - if (args.SocketError == SocketError.Success) - { - TotalBytesTransferred += args.BytesTransferred; - - if (TotalBytesTransferred == _bytesToTransfer) - { - // finished transferring specified bytes - _completionWaitHandle.Set(); - return; - } - - if (args.BytesTransferred == 0) - { - // remote server closed the connection - _completionWaitHandle.Set(); - return; - } - - _offset += args.BytesTransferred; - args.SetBuffer(_buffer, _offset, _bytesToTransfer - TotalBytesTransferred); - ResumeOperation(args); - return; - } - - if (IsErrorResumable(args.SocketError)) - { - Thread.Sleep(30); - ResumeOperation(args); - return; - } - - // we're dealing with a (fatal) error - _completionWaitHandle.Set(); - } - - private void ResumeOperation(SocketAsyncEventArgs args) - { - switch (args.LastOperation) - { - case SocketAsyncOperation.Receive: - _socket.ReceiveAsync(args); - break; - case SocketAsyncOperation.Send: - _socket.SendAsync(args); - break; - } - } - - private readonly int _bytesToTransfer; - public int TotalBytesTransferred { get; private set; } - private readonly EventWaitHandle _completionWaitHandle; - private readonly Socket _socket; - private readonly byte[] _buffer; - private int _offset; - } - - private class PartialSendReceiveToken : IToken - { - public PartialSendReceiveToken(Socket socket, EventWaitHandle completionWaitHandle) - { - _socket = socket; - _completionWaitHandle = completionWaitHandle; - } - - public void Process(SocketAsyncEventArgs args) - { - if (args.SocketError == SocketError.Success) - { - _completionWaitHandle.Set(); - return; - } - - if (IsErrorResumable(args.SocketError)) - { - Thread.Sleep(30); - ResumeOperation(args); - return; - } - - // we're dealing with a (fatal) error - _completionWaitHandle.Set(); - } - - private void ResumeOperation(SocketAsyncEventArgs args) - { - switch (args.LastOperation) - { - case SocketAsyncOperation.Receive: - _socket.ReceiveAsync(args); - break; - case SocketAsyncOperation.Send: - _socket.SendAsync(args); - break; - } - } - - private readonly EventWaitHandle _completionWaitHandle; - private readonly Socket _socket; - } - - private class ContinuousReceiveToken : IToken - { - public ContinuousReceiveToken(Socket socket, Action processReceivedBytesAction, EventWaitHandle completionWaitHandle) - { - _socket = socket; - _processReceivedBytesAction = processReceivedBytesAction; - _completionWaitHandle = completionWaitHandle; - } - - public Exception Exception { get; private set; } - - public void Process(SocketAsyncEventArgs args) - { - if (args.SocketError == SocketError.Success) - { - if (args.BytesTransferred == 0) - { - // remote socket was closed - _completionWaitHandle.Set(); - return; - } - - _processReceivedBytesAction(args.Buffer, args.Offset, args.BytesTransferred); - ResumeOperation(args); - return; - } - - if (IsErrorResumable(args.SocketError)) - { - Thread.Sleep(30); - ResumeOperation(args); - return; - } - - if (args.SocketError != SocketError.OperationAborted) - { - Exception = new SocketException((int) args.SocketError); - } - - // we're dealing with a (fatal) error - _completionWaitHandle.Set(); - } - - private void ResumeOperation(SocketAsyncEventArgs args) - { - switch (args.LastOperation) - { - case SocketAsyncOperation.Receive: - _socket.ReceiveAsync(args); - break; - case SocketAsyncOperation.Send: - _socket.SendAsync(args); - break; - } - } - - private readonly EventWaitHandle _completionWaitHandle; - private readonly Socket _socket; - private readonly Action _processReceivedBytesAction; - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs b/src/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs index 7a29d58a3..e50858c33 100644 --- a/src/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs +++ b/src/Renci.SshNet.IntegrationTests/Common/Socks5Handler.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Sockets; +using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages.Transport; From 796cc0af10a1ec41ce5918123e06286088badb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Nag=C3=B3rski?= Date: Tue, 12 Sep 2023 10:44:17 +0200 Subject: [PATCH 14/15] Remove performance tests --- .../Issue67/ISshStream.cs | 11 -- .../Issue67/Issue67Program.cs | 64 ------- .../Issue67/MySshClient.cs | 163 ----------------- .../Issue67/SharpSshStream.cs | 68 ------- .../Issue67/SshNetStream.cs | 63 ------- .../Issue67/SshStreamFactory.cs | 20 --- .../Issue67/UnblockStreamReader.cs | 168 ------------------ .../Issue67/UnblockStreamUtility.cs | 103 ----------- .../Issue67/UntilInfo.cs | 37 ---- .../Renci.SshNet.IntegrationTests.csproj | 2 +- 10 files changed, 1 insertion(+), 698 deletions(-) delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/ISshStream.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/Issue67Program.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/MySshClient.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/SharpSshStream.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/SshNetStream.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/SshStreamFactory.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamReader.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamUtility.cs delete mode 100644 src/Renci.SshNet.IntegrationTests/Issue67/UntilInfo.cs diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/ISshStream.cs b/src/Renci.SshNet.IntegrationTests/Issue67/ISshStream.cs deleted file mode 100644 index 5e78bd433..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/ISshStream.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Renci.SshNet.IntegrationTests.Issue67 -{ - public interface ISshStream - { - void Connect(string host, string userName, string password); - void Close(); - void Write(string data); - StreamReader GetStreamReader(); - StreamWriter GetStreamWriter(); - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/Issue67Program.cs b/src/Renci.SshNet.IntegrationTests/Issue67/Issue67Program.cs deleted file mode 100644 index 0b9c6fd9d..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/Issue67Program.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Diagnostics; - -namespace Renci.SshNet.IntegrationTests.Issue67 -{ - class Issue67Program - { - private const string Host = "192.168.1.122"; - - public static void Start() - { - Stopwatch stopwatch = new Stopwatch(); - - SshClient sshNet = new SshClient(Host, Users.Regular.UserName, Users.Regular.Password); - stopwatch.Restart(); - sshNet.Connect(); - stopwatch.Stop(); - Console.Write("sshNet.Connect() "); - Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); - stopwatch.Restart(); - SshCommand sshCommand = sshNet.RunCommand("free -m"); - stopwatch.Stop(); - Console.Write("sshNet.RunCommand() "); - Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); - -#if NETFRAMEWORK - Tamir.SharpSsh.SshExec sharpSsh = new Tamir.SharpSsh.SshExec(Host, Users.Regular.UserName, Users.Regular.Password); - stopwatch.Restart(); - sharpSsh.Connect(); - stopwatch.Stop(); - Console.Write("sharpSsh.Connect() "); - Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); - stopwatch.Restart(); - string result = sharpSsh.RunCommand("free -m"); - stopwatch.Stop(); - Console.Write("sharpSsh.RunCommand() "); - Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); -#endif // NETFRAMEWORK - - MySshClient mySshClient_SshNet = new MySshClient(Host, Users.Regular.UserName, Users.Regular.Password, "sshnet"); - stopwatch.Restart(); - mySshClient_SshNet.Connect(); - stopwatch.Stop(); - Console.Write("mySshClient_SshNet.Connect() "); - Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); - stopwatch.Restart(); - string[] results1 = mySshClient_SshNet.RunCommand("free -m"); - stopwatch.Stop(); - Console.Write("mySshClient_SshNet.RunCommand() "); - Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); - - MySshClient mySshClient_SharpSsh = new MySshClient(Host, Users.Regular.UserName, Users.Regular.Password, "sharpssh"); - stopwatch.Restart(); - mySshClient_SharpSsh.Connect(); - stopwatch.Stop(); - Console.Write("mySshClient_SharpSsh.Connect() "); - Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); - stopwatch.Restart(); - string[] results2 = mySshClient_SharpSsh.RunCommand("free -m"); - stopwatch.Stop(); - Console.Write("mySshClient_SharpSsh.RunCommand() "); - Console.WriteLine(stopwatch.ElapsedMilliseconds + " milliseconds"); - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/MySshClient.cs b/src/Renci.SshNet.IntegrationTests/Issue67/MySshClient.cs deleted file mode 100644 index 748c9e219..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/MySshClient.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System.ComponentModel; - -namespace Renci.SshNet.IntegrationTests.Issue67 -{ - public class MySshClient : IDisposable - { - public MySshClient(string host, string userName, string password, string sshStreamType) - { - _host = host; - _userName = userName; - _password = password; - _sshStreamType = sshStreamType; - } - - private readonly string _host; - private readonly string _userName; - private readonly string _password; - private readonly string _sshStreamType; - private readonly int _noResponseTimeoutSeconds = 60; - - private readonly Component _component = new Component(); - private bool _disposed = false; - - ~MySshClient() - { - Dispose(false); - } - - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - _component.Dispose(); - } - - Close(); - - _disposed = true; - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private ISshStream SshStream - { - get; - set; - } - - public void Connect() - { - SshStream = SshStreamFactory.CreateSshStream(_sshStreamType); - SshStream.Connect(_host, _userName, _password); - - try - { - UnblockStreamReader = new UnblockStreamReader(SshStream.GetStreamReader()); - InitEnv(); - } - catch (Exception ex) - { - Close(); - throw ex; - } - } - - protected virtual void InitEnv() - { - SshStream.Write("set +o vi"); - SshStream.Write("set +o viraw"); - SshStream.Write("export PROMPT_COMMAND="); - SshStream.Write("export PS1=" + Prompt); - var response = ReadResponse("export PS1=" + Prompt + "\r\n", _noResponseTimeoutSeconds); - response = ReadResponse(Prompt, _noResponseTimeoutSeconds); - - string helloMessage = "Hello this is test message!"; - SshStream.Write("echo '" + helloMessage + "'"); - response = ReadResponse(helloMessage + "\r\n", _noResponseTimeoutSeconds); - response = ReadResponse(Prompt, _noResponseTimeoutSeconds); - - SshStream.Write("stty columns 512"); - response = ReadResponse(Prompt, _noResponseTimeoutSeconds); - - SshStream.Write("stty rows 24"); - response = ReadResponse(Prompt, _noResponseTimeoutSeconds); - - SshStream.Write("export LANG=en_US.UTF-8"); - response = ReadResponse(Prompt, _noResponseTimeoutSeconds); - - SshStream.Write("export NLS_LANG=American_America.ZHS16GBK"); - response = ReadResponse(Prompt, _noResponseTimeoutSeconds); - - SshStream.Write("unalias grep"); - response = ReadResponse(Prompt, _noResponseTimeoutSeconds); - } - - protected virtual void Close() - { - if (UnblockStreamReader != null) - { - UnblockStreamReader.Close(); - UnblockStreamReader = null; - } - if (SshStream != null) - { - SshStream.Close(); - SshStream = null; - } - } - - protected UnblockStreamReader UnblockStreamReader - { - get; - private set; - } - - public string Prompt - { - get - { - return "[SHINE_COMMAND_PROMPT]"; - } - } - - public void Write(string data) - { - if (SshStream == null) - { - Connect(); - } - if (UnblockStreamReader.GetUnreadBufferLength() > 0) - { - UnblockStreamReader.ReadToEnd(); - } - SshStream.Write(data); - } - - public string[] ReadResponse(string prompt, int noResponseTimeoutSeconds) - { - List untilInfoList = new List() { new UntilInfo(prompt) }; - string[] response = UnblockStreamUtility.ReadUntil(UnblockStreamReader, untilInfoList, noResponseTimeoutSeconds); - return response; - } - - public string[] ReadResponse(List untilInfoList, int noResponseTimeoutSeconds) - { - string[] response = UnblockStreamUtility.ReadUntil(UnblockStreamReader, untilInfoList, noResponseTimeoutSeconds); - return response; - } - - public string[] RunCommand(string command) - { - SshStream.Write(command); - return ReadResponse(Prompt, _noResponseTimeoutSeconds); - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/SharpSshStream.cs b/src/Renci.SshNet.IntegrationTests/Issue67/SharpSshStream.cs deleted file mode 100644 index 118746d2e..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/SharpSshStream.cs +++ /dev/null @@ -1,68 +0,0 @@ -#if NETFRAMEWORK - -using System.IO; - -namespace SshNetTests.Issue67 -{ - internal class SharpSshStream : ISshStream - { - Tamir.SharpSsh.SshStream _sshStream; - - public void Connect(string host, string userName, string password) - { - _sshStream = new Tamir.SharpSsh.SshStream(host, userName, password); - } - - public void Close() - { - if (_sshStream != null) - _sshStream.Close(); - } - - public void Write(string data) - { - StreamWriter streamWriter = GetStreamWriter(); - streamWriter.Write(data + "\r"); - streamWriter.Flush(); - } - - StreamReader _streamReader = null; - public StreamReader GetStreamReader() - { - if (_streamReader != null) - { - return _streamReader; - } - else - { - if (_sshStream == null) - { - return null; - } - _streamReader = new StreamReader(_sshStream); - return _streamReader; - } - } - - StreamWriter _streamWriter = null; - public StreamWriter GetStreamWriter() - { - if (_streamWriter != null) - { - return _streamWriter; - } - else - { - if (_sshStream == null) - { - return null; - } - - _streamWriter = new StreamWriter(_sshStream); - return _streamWriter; - } - } - } -} - -#endif // NETFRAMEWORK \ No newline at end of file diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/SshNetStream.cs b/src/Renci.SshNet.IntegrationTests/Issue67/SshNetStream.cs deleted file mode 100644 index ceec86c5c..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/SshNetStream.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Renci.SshNet.IntegrationTests.Issue67 -{ - internal class SshNetStream : ISshStream - { - Renci.SshNet.ShellStream _shellStream; - Renci.SshNet.SshClient _sshClinet; - - public void Connect(string host, string userName, string password) - { - _sshClinet = new Renci.SshNet.SshClient(host, userName, password); - _sshClinet.Connect(); - _shellStream = _sshClinet.CreateShellStream("ShellStream", 512, 24, 512, 512, 1024); - } - - public void Close() - { - _sshClinet?.Disconnect(); - } - - public void Write(string data) - { - StreamWriter streamWriter = GetStreamWriter(); - streamWriter.Write(data + "\r"); - streamWriter.Flush(); - } - - StreamReader _streamReader = null; - public StreamReader GetStreamReader() - { - if (_streamReader != null) - { - return _streamReader; - } - else - { - if (_shellStream == null) - { - return null; - } - _streamReader = new StreamReader(_shellStream); - return _streamReader; - } - } - - StreamWriter _streamWriter = null; - public StreamWriter GetStreamWriter() - { - if (_streamWriter != null) - { - return _streamWriter; - } - else - { - if (_shellStream == null) - { - return null; - } - _streamWriter = new StreamWriter(_shellStream); - return _streamWriter; - } - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/SshStreamFactory.cs b/src/Renci.SshNet.IntegrationTests/Issue67/SshStreamFactory.cs deleted file mode 100644 index e8efa56d8..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/SshStreamFactory.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Renci.SshNet.IntegrationTests.Issue67 -{ - internal static class SshStreamFactory - { - public static ISshStream CreateSshStream(string sshStreamType) - { - switch (sshStreamType) - { -#if NETFRAMEWORK - case "sharpssh": - return new SharpSshStream(); -#endif // NETFRAMEWORK - case "sshnet": - return new SshNetStream(); - default: - throw new Exception("Invalid SshStream type:" + sshStreamType); - } - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamReader.cs b/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamReader.cs deleted file mode 100644 index 1a9414b86..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamReader.cs +++ /dev/null @@ -1,168 +0,0 @@ -namespace Renci.SshNet.IntegrationTests.Issue67 -{ - class UnblockStreamReaderLockObject - { - public char[] buffer; - public int len; - public int pos; - }; - - public class UnblockStreamReader - { - const int BUFFER_SIZE = 65536; - readonly Thread _readThread; - private readonly UnblockStreamReaderLockObject _lockObject; - public int GetUnreadBufferLength() - { - return _lockObject.len; - } - public int GetUnreadBufferPosition() - { - return _lockObject.pos; - } - readonly StreamReader _streamReader; - - public UnblockStreamReader(StreamReader streamReader) - { - _lockObject = new UnblockStreamReaderLockObject - { - buffer = new char[BUFFER_SIZE + 1], - len = 0, - pos = 0 - }; - - _streamReader = streamReader; - _readThread = new Thread(ReadThreadProc) - { - Name = "UnblockStreamReader thread" - }; - _readThread.Start(); - } - - public void Close() - { - _readThread.Interrupt(); - lock (_lockObject) - { - _lockObject.len = 0; - _lockObject.pos = 0; - } - } - - private void ReadThreadProc(object param) - { - char[] buf = new char[1]; - int readLen = 0; - bool isSleep = false; - try - { - while (true) - { - lock (_lockObject) - { - if (_lockObject.len >= BUFFER_SIZE) - { - isSleep = true; - } - } - if (isSleep) - { - isSleep = false; - Thread.Sleep(10); - continue; - } - readLen = _streamReader.Read(buf, 0, 1); - if (readLen > 0) - { - lock (_lockObject) - { - if ((_lockObject.pos + _lockObject.len) >= BUFFER_SIZE) - { - for (int i = 0; i < _lockObject.len; i++) - { - _lockObject.buffer[i] = _lockObject.buffer[_lockObject.pos + i]; - } - _lockObject.pos = 0; - } - - _lockObject.buffer[_lockObject.pos + _lockObject.len] = buf[0]; - - _lockObject.len++; - } - } - else - { - Thread.Sleep(10); - } - } - } - catch (Exception e) - { - e.ToString(); - return; - } - } - - public int ReadChar(ref char buf) - { - lock (_lockObject) - { - if (_lockObject.len == 0) - { - return 0; - } - buf = _lockObject.buffer[_lockObject.pos]; - _lockObject.pos++; - _lockObject.len--; - } - return 1; - } - - public string ReadToEnd(bool isRemove = true) - { - string resultString; - lock (_lockObject) - { - if (_lockObject.len == 0) - { - return null; - } - resultString = new string(_lockObject.buffer, _lockObject.pos, _lockObject.len); - if (isRemove) - { - _lockObject.pos = 0; - _lockObject.len = 0; - } - return resultString; - } - } - - public string ReadLine(char lineEndFlag = '\n') - { - string resultString; - while (true) - { - lock (_lockObject) - { - if (_lockObject.len == 0) - { - Thread.Sleep(10); - continue; - } - - for (int i = 0; i < _lockObject.len; i++) - { - if (_lockObject.buffer[_lockObject.pos + i] == lineEndFlag) - { - resultString = new string(_lockObject.buffer, _lockObject.pos, i + 1); - _lockObject.pos = _lockObject.pos + i + 1; - _lockObject.len = _lockObject.len - i - 1; - return resultString; - } - } - } - Thread.Sleep(10); - } - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamUtility.cs b/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamUtility.cs deleted file mode 100644 index 6dd99c1fe..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/UnblockStreamUtility.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Diagnostics; - -namespace Renci.SshNet.IntegrationTests.Issue67 -{ - public class UnblockStreamUtility - { - internal static string[] ReadUntil(UnblockStreamReader reader, List untilInfoList, int noResponseTimeoutSeconds) - { - List resultList = new List(); - char[] buffer = new char[65536]; - int curBufferLen = 0; - int noResponseTimeoutMilliseconds = noResponseTimeoutSeconds * 1000; - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); - while (true) - { - char readChar = new char(); - int readCharLen = reader.ReadChar(ref readChar); - if (readCharLen == 0) - { - Thread.Sleep(10); - if (stopwatch.ElapsedMilliseconds >= noResponseTimeoutMilliseconds) - { - stopwatch.Stop(); - throw new Exception("No Response Timeout!"); - } - continue; - } - else { stopwatch.Restart(); } - buffer[curBufferLen] = readChar; - - foreach (UntilInfo untilInfo in untilInfoList) - { - if (readChar == untilInfo.UntilCharArray[untilInfo.CompareLen]) - { - untilInfo.CompareLen++; - if (untilInfo.CompareLen == untilInfo.UntilCharArray.Length) - { - untilInfo.CompareLen = 0; - string lineStr = new string(buffer, 0, curBufferLen + 1); - if (lineStr.EndsWith("\r\r\n")) - { - lineStr = lineStr.Substring(0, lineStr.Length - 3); - } - else if (lineStr.EndsWith("\r\n")) - { - lineStr = lineStr.Substring(0, lineStr.Length - 2); - } - else if (lineStr.EndsWith("\n")) - { - lineStr = lineStr.Substring(0, lineStr.Length - 1); - } - - resultList.Add(lineStr); - curBufferLen = 0; - - if (untilInfo.ExceptionMessage != null) - { - throw new Exception(untilInfo.ExceptionMessage); - } - else - { - return resultList.ToArray(); - } - } - } - else - { - untilInfo.CompareLen = 0; - } - } - - if (readChar == '\n') - { - string lineStr = new string(buffer, 0, curBufferLen + 1); - - if (lineStr.EndsWith("\r\r\n")) - { - lineStr = lineStr.Substring(0, lineStr.Length - 3); - } - else if (lineStr.EndsWith("\r\n")) - { - lineStr = lineStr.Substring(0, lineStr.Length - 2); - } - else if (lineStr.EndsWith("\n")) - { - lineStr = lineStr.Substring(0, lineStr.Length - 1); - } - - resultList.Add(lineStr); - - curBufferLen = 0; - foreach (UntilInfo untilInfo in untilInfoList) - { - untilInfo.CompareLen = 0; - } - continue; - } - curBufferLen++; - } - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Issue67/UntilInfo.cs b/src/Renci.SshNet.IntegrationTests/Issue67/UntilInfo.cs deleted file mode 100644 index 696679d11..000000000 --- a/src/Renci.SshNet.IntegrationTests/Issue67/UntilInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Renci.SshNet.IntegrationTests.Issue67 -{ - public class UntilInfo - { - public UntilInfo(string untilString, string exceptionMessage = null) - { - UntilString = untilString; - ExceptionMessage = exceptionMessage; - UntilCharArray = untilString.ToCharArray(); - CompareLen = 0; - } - - public string UntilString - { - get; - private set; - } - - public string ExceptionMessage - { - get; - private set; - } - - public char[] UntilCharArray - { - get; - private set; - } - - public int CompareLen - { - get; - set; - } - } -} diff --git a/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj index 175192709..ae3505f10 100644 --- a/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj +++ b/src/Renci.SshNet.IntegrationTests/Renci.SshNet.IntegrationTests.csproj @@ -27,7 +27,7 @@ - +