diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 022af640..8573ad31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: fetch-depth: 0 # needed for Nerdbank.GitVersioning - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 - name: Build Unit Tests .NET run: dotnet build -f net10.0 test/Renci.SshNet.Tests/ @@ -62,7 +62,7 @@ jobs: fetch-depth: 0 # needed for Nerdbank.GitVersioning - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 - name: Build Solution run: dotnet build Renci.SshNet.sln @@ -103,8 +103,8 @@ jobs: -p:CoverletOutput=../../coverlet/windows_unit_test_net_4_6_2_coverage.xml ` test/Renci.SshNet.Tests/ - Windows-Integration-Tests: - name: Windows Integration Tests + Windows-Integration-Tests-NetFramework: + name: Windows Integration Tests .NET Framework runs-on: windows-2025 steps: - name: Checkout @@ -113,7 +113,7 @@ jobs: fetch-depth: 0 # needed for Nerdbank.GitVersioning - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 - name: Setup WSL2 uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0 @@ -142,7 +142,49 @@ jobs: - name: Archive Coverlet Results uses: actions/upload-artifact@v4 with: - name: Coverlet Results Windows + name: Coverlet Results Windows .NET Framework + path: coverlet + + Windows-Integration-Tests-Net: + name: Windows Integration Tests .NET + runs-on: windows-2025 + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 # needed for Nerdbank.GitVersioning + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + + - name: Setup WSL2 + uses: Vampire/setup-wsl@6a8db447be7ed35f2f499c02c6e60ff77ef11278 # v6.0.0 + with: + distribution: Ubuntu-24.04 + + - name: Setup SSH Server + shell: wsl-bash {0} + run: | + apt-get update && apt-get upgrade -y + apt-get install -y podman + podman build -t renci-ssh-tests-server-image -f test/Renci.SshNet.IntegrationTests/Dockerfile test/Renci.SshNet.IntegrationTests/ + podman run --rm -h renci-ssh-tests-server -d -p 2222:22 renci-ssh-tests-server-image + + - name: Run Integration Tests .NET + run: + dotnet test ` + -f net10.0 ` + --logger "console;verbosity=normal" ` + --logger GitHubActions ` + -p:CollectCoverage=true ` + -p:CoverletOutputFormat=cobertura ` + -p:CoverletOutput=..\..\coverlet\windows_integration_test_net_9_coverage.xml ` + test\Renci.SshNet.IntegrationTests\ + + - name: Archive Coverlet Results + uses: actions/upload-artifact@v4 + with: + name: Coverlet Results Windows .NET path: coverlet Publish: @@ -153,6 +195,8 @@ jobs: needs: - Windows - Linux + - Windows-Integration-Tests-NetFramework + - Windows-Integration-Tests-Net steps: - name: Download NuGet Package uses: actions/download-artifact@v5 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b989c225..d33343dc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: uses: actions/configure-pages@v5 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 - name: Setup docfx run: dotnet tool update -g docfx diff --git a/src/Renci.SshNet/Abstractions/SocketAbstraction.cs b/src/Renci.SshNet/Abstractions/SocketAbstraction.cs index 69ec38b2..63dc2bf5 100644 --- a/src/Renci.SshNet/Abstractions/SocketAbstraction.cs +++ b/src/Renci.SshNet/Abstractions/SocketAbstraction.cs @@ -12,34 +12,6 @@ namespace Renci.SshNet.Abstractions { internal static partial class SocketAbstraction { - public static bool CanRead(Socket socket) - { - if (socket.Connected) - { - return socket.Poll(-1, SelectMode.SelectRead) && socket.Available > 0; - } - - return false; - } - - /// - /// Returns a value indicating whether the specified can be used - /// to send data. - /// - /// The to check. - /// - /// if can be written to; otherwise, . - /// - public static bool CanWrite(Socket socket) - { - if (socket != null && socket.Connected) - { - return socket.Poll(-1, SelectMode.SelectWrite); - } - - return false; - } - public static Socket Connect(IPEndPoint remoteEndpoint, TimeSpan connectTimeout) { var socket = new Socket(remoteEndpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; diff --git a/src/Renci.SshNet/Common/Extensions.cs b/src/Renci.SshNet/Common/Extensions.cs index f6449f68..fa0d584e 100644 --- a/src/Renci.SshNet/Common/Extensions.cs +++ b/src/Renci.SshNet/Common/Extensions.cs @@ -12,7 +12,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Threading; -using Renci.SshNet.Abstractions; using Renci.SshNet.Messages; namespace Renci.SshNet.Common @@ -321,16 +320,6 @@ namespace Renci.SshNet.Common return concat; } - internal static bool CanRead(this Socket socket) - { - return SocketAbstraction.CanRead(socket); - } - - internal static bool CanWrite(this Socket socket) - { - return SocketAbstraction.CanWrite(socket); - } - internal static bool IsConnected(this Socket socket) { if (socket is null) diff --git a/src/Renci.SshNet/Security/KeyExchangeEC.cs b/src/Renci.SshNet/Security/KeyExchangeEC.cs index a36bb7bc..f0269a6b 100644 --- a/src/Renci.SshNet/Security/KeyExchangeEC.cs +++ b/src/Renci.SshNet/Security/KeyExchangeEC.cs @@ -1,4 +1,6 @@ -using Renci.SshNet.Messages.Transport; +using System; + +using Renci.SshNet.Messages.Transport; namespace Renci.SshNet.Security { @@ -76,5 +78,23 @@ namespace Renci.SshNet.Security _serverPayload = message.GetBytes(); _clientPayload = Session.ClientInitMessage.GetBytes(); } + + protected abstract class Impl : IDisposable + { + public abstract byte[] GenerateClientPublicKey(); + + public abstract byte[] CalculateAgreement(byte[] serverPublicKey); + + protected virtual void Dispose(bool disposing) + { + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } } } diff --git a/src/Renci.SshNet/Security/KeyExchangeECCurve25519.BclImpl.cs b/src/Renci.SshNet/Security/KeyExchangeECCurve25519.BclImpl.cs new file mode 100644 index 00000000..de3ab55e --- /dev/null +++ b/src/Renci.SshNet/Security/KeyExchangeECCurve25519.BclImpl.cs @@ -0,0 +1,57 @@ +#if NET +using System.Security.Cryptography; + +namespace Renci.SshNet.Security +{ + internal partial class KeyExchangeECCurve25519 + { + protected sealed class BclImpl : Impl + { + private readonly ECCurve _curve; + private readonly ECDiffieHellman _clientECDH; + + public BclImpl() + { + _curve = ECCurve.CreateFromFriendlyName("Curve25519"); + _clientECDH = ECDiffieHellman.Create(); + } + + public override byte[] GenerateClientPublicKey() + { + _clientECDH.GenerateKey(_curve); + + var q = _clientECDH.PublicKey.ExportParameters().Q; + + return q.X; + } + + public override byte[] CalculateAgreement(byte[] serverPublicKey) + { + var parameters = new ECParameters + { + Curve = _curve, + Q = new ECPoint + { + X = serverPublicKey, + Y = new byte[serverPublicKey.Length] + }, + }; + + using var serverECDH = ECDiffieHellman.Create(parameters); + + return _clientECDH.DeriveRawSecretAgreement(serverECDH.PublicKey); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _clientECDH.Dispose(); + } + } + } + } +} +#endif diff --git a/src/Renci.SshNet/Security/KeyExchangeECCurve25519.BouncyCastleImpl.cs b/src/Renci.SshNet/Security/KeyExchangeECCurve25519.BouncyCastleImpl.cs new file mode 100644 index 00000000..0e58a47c --- /dev/null +++ b/src/Renci.SshNet/Security/KeyExchangeECCurve25519.BouncyCastleImpl.cs @@ -0,0 +1,38 @@ +using Org.BouncyCastle.Crypto.Agreement; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; + +using Renci.SshNet.Abstractions; + +namespace Renci.SshNet.Security +{ + internal partial class KeyExchangeECCurve25519 + { + protected sealed class BouncyCastleImpl : Impl + { + private X25519Agreement _keyAgreement; + + public override byte[] GenerateClientPublicKey() + { + var g = new X25519KeyPairGenerator(); + g.Init(new X25519KeyGenerationParameters(CryptoAbstraction.SecureRandom)); + + var aKeyPair = g.GenerateKeyPair(); + _keyAgreement = new X25519Agreement(); + _keyAgreement.Init(aKeyPair.Private); + + return ((X25519PublicKeyParameters)aKeyPair.Public).GetEncoded(); + } + + public override byte[] CalculateAgreement(byte[] serverPublicKey) + { + var publicKey = new X25519PublicKeyParameters(serverPublicKey); + + var k1 = new byte[_keyAgreement.AgreementSize]; + _keyAgreement.CalculateAgreement(publicKey, k1, 0); + + return k1; + } + } + } +} diff --git a/src/Renci.SshNet/Security/KeyExchangeECCurve25519.cs b/src/Renci.SshNet/Security/KeyExchangeECCurve25519.cs index b7f13cfb..3ce8d1f8 100644 --- a/src/Renci.SshNet/Security/KeyExchangeECCurve25519.cs +++ b/src/Renci.SshNet/Security/KeyExchangeECCurve25519.cs @@ -1,18 +1,19 @@ -using System.Security.Cryptography; +using System.Security.Cryptography; -using Org.BouncyCastle.Crypto.Agreement; -using Org.BouncyCastle.Crypto.Generators; -using Org.BouncyCastle.Crypto.Parameters; - -using Renci.SshNet.Abstractions; using Renci.SshNet.Common; using Renci.SshNet.Messages.Transport; namespace Renci.SshNet.Security { - internal sealed class KeyExchangeECCurve25519 : KeyExchangeEC + internal partial class KeyExchangeECCurve25519 : KeyExchangeEC { - private X25519Agreement _keyAgreement; +#pragma warning disable SA1401 // Fields should be private +#if NET + protected Impl _impl; +#else + protected BouncyCastleImpl _impl; +#endif +#pragma warning restore SA1401 // Fields should be private /// /// Gets algorithm name. @@ -37,29 +38,46 @@ namespace Renci.SshNet.Security public override void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage) { base.Start(session, message, sendClientInitMessage); +#if NET + if (System.OperatingSystem.IsWindowsVersionAtLeast(10)) + { + _impl = new BclImpl(); + } + else +#endif + { + _impl = new BouncyCastleImpl(); + } + StartImpl(); + } + + /// + /// The implementation of start key exchange algorithm. + /// + protected virtual void StartImpl() + { Session.RegisterMessage("SSH_MSG_KEX_ECDH_REPLY"); Session.KeyExchangeEcdhReplyMessageReceived += Session_KeyExchangeEcdhReplyMessageReceived; - var g = new X25519KeyPairGenerator(); - g.Init(new X25519KeyGenerationParameters(CryptoAbstraction.SecureRandom)); - - var aKeyPair = g.GenerateKeyPair(); - _keyAgreement = new X25519Agreement(); - _keyAgreement.Init(aKeyPair.Private); - _clientExchangeValue = ((X25519PublicKeyParameters)aKeyPair.Public).GetEncoded(); + _clientExchangeValue = _impl.GenerateClientPublicKey(); SendMessage(new KeyExchangeEcdhInitMessage(_clientExchangeValue)); } - /// - /// Finishes key exchange algorithm. - /// + /// public override void Finish() { base.Finish(); + FinishImpl(); + } + /// + /// The implementation of finish key exchange algorithm. + /// + protected virtual void FinishImpl() + { Session.KeyExchangeEcdhReplyMessageReceived -= Session_KeyExchangeEcdhReplyMessageReceived; } @@ -100,11 +118,19 @@ namespace Renci.SshNet.Security _hostKey = hostKey; _signature = signature; - var publicKey = new X25519PublicKeyParameters(serverExchangeValue); - - var k1 = new byte[_keyAgreement.AgreementSize]; - _keyAgreement.CalculateAgreement(publicKey, k1, 0); + var k1 = _impl.CalculateAgreement(serverExchangeValue); SharedKey = k1.ToBigInteger2().ToByteArray(isBigEndian: true); } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _impl?.Dispose(); + } + } } } diff --git a/src/Renci.SshNet/Security/KeyExchangeECDH.BclImpl.cs b/src/Renci.SshNet/Security/KeyExchangeECDH.BclImpl.cs index 0cd1dd34..508f0a2a 100644 --- a/src/Renci.SshNet/Security/KeyExchangeECDH.BclImpl.cs +++ b/src/Renci.SshNet/Security/KeyExchangeECDH.BclImpl.cs @@ -17,7 +17,7 @@ namespace Renci.SshNet.Security _clientECDH = ECDiffieHellman.Create(); } - public override byte[] GenerateClientECPoint() + public override byte[] GenerateClientPublicKey() { _clientECDH.GenerateKey(_curve); @@ -26,9 +26,9 @@ namespace Renci.SshNet.Security return EncodeECPoint(q); } - public override byte[] CalculateAgreement(byte[] serverECPoint) + public override byte[] CalculateAgreement(byte[] serverPublicKey) { - var q = DecodeECPoint(serverECPoint); + var q = DecodeECPoint(serverPublicKey); var parameters = new ECParameters { diff --git a/src/Renci.SshNet/Security/KeyExchangeECDH.BouncyCastleImpl.cs b/src/Renci.SshNet/Security/KeyExchangeECDH.BouncyCastleImpl.cs index 4d0208ef..9f38c4ab 100644 --- a/src/Renci.SshNet/Security/KeyExchangeECDH.BouncyCastleImpl.cs +++ b/src/Renci.SshNet/Security/KeyExchangeECDH.BouncyCastleImpl.cs @@ -20,7 +20,7 @@ namespace Renci.SshNet.Security _keyAgreement = new ECDHCBasicAgreement(); } - public override byte[] GenerateClientECPoint() + public override byte[] GenerateClientPublicKey() { var g = new ECKeyPairGenerator(); g.Init(new ECKeyGenerationParameters(_domainParameters, CryptoAbstraction.SecureRandom)); @@ -31,10 +31,10 @@ namespace Renci.SshNet.Security return ((ECPublicKeyParameters)aKeyPair.Public).Q.GetEncoded(); } - public override byte[] CalculateAgreement(byte[] serverECPoint) + public override byte[] CalculateAgreement(byte[] serverPublicKey) { var c = _domainParameters.Curve; - var q = c.DecodePoint(serverECPoint); + var q = c.DecodePoint(serverPublicKey); var publicKey = new ECPublicKeyParameters("ECDH", q, _domainParameters); return _keyAgreement.CalculateAgreement(publicKey).ToByteArray(); diff --git a/src/Renci.SshNet/Security/KeyExchangeECDH.cs b/src/Renci.SshNet/Security/KeyExchangeECDH.cs index 35ef3b5c..c697ee1e 100644 --- a/src/Renci.SshNet/Security/KeyExchangeECDH.cs +++ b/src/Renci.SshNet/Security/KeyExchangeECDH.cs @@ -1,6 +1,4 @@ -using System; - -using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Asn1.X9; using Renci.SshNet.Common; using Renci.SshNet.Messages.Transport; @@ -41,7 +39,7 @@ namespace Renci.SshNet.Security Session.KeyExchangeEcdhReplyMessageReceived += Session_KeyExchangeEcdhReplyMessageReceived; #if NET - if (!OperatingSystem.IsWindows() || OperatingSystem.IsWindowsVersionAtLeast(10)) + if (!System.OperatingSystem.IsWindows() || System.OperatingSystem.IsWindowsVersionAtLeast(10)) { _impl = new BclImpl(Curve); } @@ -51,7 +49,7 @@ namespace Renci.SshNet.Security _impl = new BouncyCastleImpl(CurveParameter); } - _clientExchangeValue = _impl.GenerateClientECPoint(); + _clientExchangeValue = _impl.GenerateClientPublicKey(); SendMessage(new KeyExchangeEcdhInitMessage(_clientExchangeValue)); } @@ -106,23 +104,5 @@ namespace Renci.SshNet.Security _impl?.Dispose(); } } - - private abstract class Impl : IDisposable - { - public abstract byte[] GenerateClientECPoint(); - - public abstract byte[] CalculateAgreement(byte[] serverECPoint); - - protected virtual void Dispose(bool disposing) - { - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - } } } diff --git a/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs index d860c922..87614f8e 100644 --- a/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs +++ b/src/Renci.SshNet/Security/KeyExchangeMLKem768X25519Sha256.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Security.Cryptography; -using Org.BouncyCastle.Crypto.Agreement; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Kems; using Org.BouncyCastle.Crypto.Parameters; @@ -13,10 +12,9 @@ using Renci.SshNet.Messages.Transport; namespace Renci.SshNet.Security { - internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeEC + internal sealed class KeyExchangeMLKem768X25519Sha256 : KeyExchangeECCurve25519 { private MLKemDecapsulator _mlkemDecapsulator; - private X25519Agreement _x25519Agreement; /// /// Gets algorithm name. @@ -38,10 +36,8 @@ namespace Renci.SshNet.Security } /// - public override void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage) + protected override void StartImpl() { - base.Start(session, message, sendClientInitMessage); - Session.RegisterMessage("SSH_MSG_KEX_HYBRID_REPLY"); Session.KeyExchangeHybridReplyMessageReceived += Session_KeyExchangeHybridReplyMessageReceived; @@ -53,28 +49,18 @@ namespace Renci.SshNet.Security _mlkemDecapsulator = new MLKemDecapsulator(MLKemParameters.ml_kem_768); _mlkemDecapsulator.Init(mlkem768KeyPair.Private); - var x25519KeyPairGenerator = new X25519KeyPairGenerator(); - x25519KeyPairGenerator.Init(new X25519KeyGenerationParameters(CryptoAbstraction.SecureRandom)); - var x25519KeyPair = x25519KeyPairGenerator.GenerateKeyPair(); - - _x25519Agreement = new X25519Agreement(); - _x25519Agreement.Init(x25519KeyPair.Private); - var mlkem768PublicKey = ((MLKemPublicKeyParameters)mlkem768KeyPair.Public).GetEncoded(); - var x25519PublicKey = ((X25519PublicKeyParameters)x25519KeyPair.Public).GetEncoded(); + + var x25519PublicKey = _impl.GenerateClientPublicKey(); _clientExchangeValue = mlkem768PublicKey.Concat(x25519PublicKey); SendMessage(new KeyExchangeHybridInitMessage(_clientExchangeValue)); } - /// - /// Finishes key exchange algorithm. - /// - public override void Finish() + /// + protected override void FinishImpl() { - base.Finish(); - Session.KeyExchangeHybridReplyMessageReceived -= Session_KeyExchangeHybridReplyMessageReceived; } @@ -115,21 +101,20 @@ namespace Renci.SshNet.Security _hostKey = hostKey; _signature = signature; - if (serverExchangeValue.Length != _mlkemDecapsulator.EncapsulationLength + _x25519Agreement.AgreementSize) + if (serverExchangeValue.Length != _mlkemDecapsulator.EncapsulationLength + X25519PublicKeyParameters.KeySize) { throw new SshConnectionException( string.Format(CultureInfo.CurrentCulture, "Bad S_Reply length: {0}.", serverExchangeValue.Length), DisconnectReason.KeyExchangeFailed); } - var secret = new byte[_mlkemDecapsulator.SecretLength + _x25519Agreement.AgreementSize]; + var mlkemSecret = new byte[_mlkemDecapsulator.SecretLength]; - _mlkemDecapsulator.Decapsulate(serverExchangeValue, 0, _mlkemDecapsulator.EncapsulationLength, secret, 0, _mlkemDecapsulator.SecretLength); + _mlkemDecapsulator.Decapsulate(serverExchangeValue, 0, _mlkemDecapsulator.EncapsulationLength, mlkemSecret, 0, _mlkemDecapsulator.SecretLength); - var x25519PublicKey = new X25519PublicKeyParameters(serverExchangeValue, _mlkemDecapsulator.EncapsulationLength); - _x25519Agreement.CalculateAgreement(x25519PublicKey, secret, _mlkemDecapsulator.SecretLength); + var x25519Agreement = _impl.CalculateAgreement(serverExchangeValue.Take(_mlkemDecapsulator.EncapsulationLength, X25519PublicKeyParameters.KeySize)); - SharedKey = SHA256.HashData(secret); + SharedKey = SHA256.HashData(mlkemSecret.Concat(x25519Agreement)); } } } diff --git a/src/Renci.SshNet/Security/KeyExchangeSNtruP761X25519Sha512.cs b/src/Renci.SshNet/Security/KeyExchangeSNtruP761X25519Sha512.cs index b7db0116..6fbb2ac7 100644 --- a/src/Renci.SshNet/Security/KeyExchangeSNtruP761X25519Sha512.cs +++ b/src/Renci.SshNet/Security/KeyExchangeSNtruP761X25519Sha512.cs @@ -1,10 +1,7 @@ -using System; -using System.Globalization; +using System.Globalization; using System.Linq; using System.Security.Cryptography; -using Org.BouncyCastle.Crypto.Agreement; -using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Pqc.Crypto.NtruPrime; @@ -14,10 +11,9 @@ using Renci.SshNet.Messages.Transport; namespace Renci.SshNet.Security { - internal sealed class KeyExchangeSNtruP761X25519Sha512 : KeyExchangeEC + internal sealed class KeyExchangeSNtruP761X25519Sha512 : KeyExchangeECCurve25519 { private SNtruPrimeKemExtractor _sntrup761Extractor; - private X25519Agreement _x25519Agreement; /// /// Gets algorithm name. @@ -39,10 +35,8 @@ namespace Renci.SshNet.Security } /// - public override void Start(Session session, KeyExchangeInitMessage message, bool sendClientInitMessage) + protected override void StartImpl() { - base.Start(session, message, sendClientInitMessage); - Session.RegisterMessage("SSH_MSG_KEX_ECDH_REPLY"); Session.KeyExchangeEcdhReplyMessageReceived += Session_KeyExchangeEcdhReplyMessageReceived; @@ -53,28 +47,18 @@ namespace Renci.SshNet.Security _sntrup761Extractor = new SNtruPrimeKemExtractor((SNtruPrimePrivateKeyParameters)sntrup761KeyPair.Private); - var x25519KeyPairGenerator = new X25519KeyPairGenerator(); - x25519KeyPairGenerator.Init(new X25519KeyGenerationParameters(CryptoAbstraction.SecureRandom)); - var x25519KeyPair = x25519KeyPairGenerator.GenerateKeyPair(); - - _x25519Agreement = new X25519Agreement(); - _x25519Agreement.Init(x25519KeyPair.Private); - var sntrup761PublicKey = ((SNtruPrimePublicKeyParameters)sntrup761KeyPair.Public).GetEncoded(); - var x25519PublicKey = ((X25519PublicKeyParameters)x25519KeyPair.Public).GetEncoded(); + + var x25519PublicKey = _impl.GenerateClientPublicKey(); _clientExchangeValue = sntrup761PublicKey.Concat(x25519PublicKey); SendMessage(new KeyExchangeEcdhInitMessage(_clientExchangeValue)); } - /// - /// Finishes key exchange algorithm. - /// - public override void Finish() + /// + protected override void FinishImpl() { - base.Finish(); - Session.KeyExchangeEcdhReplyMessageReceived -= Session_KeyExchangeEcdhReplyMessageReceived; } @@ -123,14 +107,12 @@ namespace Renci.SshNet.Security } var sntrup761CipherText = serverExchangeValue.Take(_sntrup761Extractor.EncapsulationLength); - var secret = _sntrup761Extractor.ExtractSecret(sntrup761CipherText); - var sntrup761SecretLength = secret.Length; - var x25519PublicKey = new X25519PublicKeyParameters(serverExchangeValue, _sntrup761Extractor.EncapsulationLength); - Array.Resize(ref secret, sntrup761SecretLength + _x25519Agreement.AgreementSize); - _x25519Agreement.CalculateAgreement(x25519PublicKey, secret, sntrup761SecretLength); + var sntrup761Secret = _sntrup761Extractor.ExtractSecret(sntrup761CipherText); - SharedKey = SHA512.HashData(secret); + var x25519Agreement = _impl.CalculateAgreement(serverExchangeValue.Take(_sntrup761Extractor.EncapsulationLength, X25519PublicKeyParameters.KeySize)); + + SharedKey = SHA512.HashData(sntrup761Secret.Concat(x25519Agreement)); } } } diff --git a/src/Renci.SshNet/Session.cs b/src/Renci.SshNet/Session.cs index edcd3af3..1b61728c 100644 --- a/src/Renci.SshNet/Session.cs +++ b/src/Renci.SshNet/Session.cs @@ -78,12 +78,6 @@ namespace Renci.SshNet private readonly ISocketFactory _socketFactory; private readonly ILogger _logger; - /// - /// Holds an object that is used to ensure only a single thread can read from - /// at any given time. - /// - private readonly Lock _socketReadLock = new Lock(); - /// /// Holds an object that is used to ensure only a single thread can write to /// at any given time. @@ -102,7 +96,7 @@ namespace Renci.SshNet /// This is also used to ensure that will not be disposed /// while performing a given operation or set of operations on . /// - private readonly SemaphoreSlim _socketDisposeLock = new SemaphoreSlim(1, 1); + private readonly Lock _socketDisposeLock = new Lock(); /// /// Holds an object that is used to ensure only a single thread can connect @@ -276,17 +270,11 @@ namespace Renci.SshNet { get { - if (_disposed || _isDisconnectMessageSent || !_isAuthenticated) - { - return false; - } - - if (_messageListenerCompleted is null || _messageListenerCompleted.WaitOne(0)) - { - return false; - } - - return IsSocketConnected(); + return !_disposed && + !_isDisconnectMessageSent && + _isAuthenticated && + _messageListenerCompleted?.WaitOne(0) == false && + _socket.IsConnected(); } } @@ -1043,7 +1031,7 @@ namespace Renci.SshNet /// The size of the packet exceeds the maximum size defined by the protocol. internal void SendMessage(Message message) { - if (!_socket.CanWrite()) + if (!_socket.IsConnected()) { throw new SshConnectionException("Client not connected."); } @@ -1158,9 +1146,7 @@ namespace Renci.SshNet /// private void SendPacket(byte[] packet, int offset, int length) { - _socketDisposeLock.Wait(); - - try + lock (_socketDisposeLock) { if (!_socket.IsConnected()) { @@ -1169,10 +1155,6 @@ namespace Renci.SshNet SocketAbstraction.Send(_socket, packet, offset, length); } - finally - { - _ = _socketDisposeLock.Release(); - } } /// @@ -1256,77 +1238,71 @@ namespace Renci.SshNet byte[] data; uint packetLength; - // avoid reading from socket while IsSocketConnected is attempting to determine whether the - // socket is still connected by invoking Socket.Poll(...) and subsequently verifying value of - // Socket.Available - lock (_socketReadLock) + // Read first block - which starts with the packet length + var firstBlock = new byte[blockSize]; + if (TrySocketRead(socket, firstBlock, 0, blockSize) == 0) { - // Read first block - which starts with the packet length - var firstBlock = new byte[blockSize]; - if (TrySocketRead(socket, firstBlock, 0, blockSize) == 0) + // connection with SSH server was closed + return null; + } + + var plainFirstBlock = firstBlock; + + // First block is not encrypted in AES GCM mode. + if (_serverCipher is not null and not Security.Cryptography.Ciphers.AesGcmCipher) + { + _serverCipher.SetSequenceNumber(_inboundPacketSequence); + + // First block is not encrypted in ETM mode. + if (_serverMac == null || !_serverEtm) + { + plainFirstBlock = _serverCipher.Decrypt(firstBlock); + } + } + + packetLength = BinaryPrimitives.ReadUInt32BigEndian(plainFirstBlock); + + // Test packet minimum and maximum boundaries + if (packetLength < Math.Max((byte)8, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4) + { + throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Bad packet length: {0}.", packetLength), + DisconnectReason.ProtocolError); + } + + // Determine the number of bytes left to read; We've already read "blockSize" bytes, but the + // "packet length" field itself - which is 4 bytes - is not included in the length of the packet + var bytesToRead = (int)(packetLength - (blockSize - packetLengthFieldLength)) + serverMacLength; + + // Construct buffer for holding the payload and the inbound packet sequence as we need both in order + // to generate the hash. + // + // The total length of the "data" buffer is an addition of: + // - inboundPacketSequenceLength (4 bytes) + // - packetLength + // - serverMacLength + // + // We include the inbound packet sequence to allow us to have the the full SSH packet in a single + // byte[] for the purpose of calculating the client hash. Room for the server MAC is foreseen + // to read the packet including server MAC in a single pass (except for the initial block). + data = new byte[bytesToRead + blockSize + inboundPacketSequenceLength]; + BinaryPrimitives.WriteUInt32BigEndian(data, _inboundPacketSequence); + + // Use raw packet length field to calculate the mac in AEAD mode. + if (_serverAead) + { + Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, blockSize); + } + else + { + Buffer.BlockCopy(plainFirstBlock, 0, data, inboundPacketSequenceLength, blockSize); + } + + if (bytesToRead > 0) + { + if (TrySocketRead(socket, data, blockSize + inboundPacketSequenceLength, bytesToRead) == 0) { - // connection with SSH server was closed return null; } - - var plainFirstBlock = firstBlock; - - // First block is not encrypted in AES GCM mode. - if (_serverCipher is not null and not Security.Cryptography.Ciphers.AesGcmCipher) - { - _serverCipher.SetSequenceNumber(_inboundPacketSequence); - - // First block is not encrypted in ETM mode. - if (_serverMac == null || !_serverEtm) - { - plainFirstBlock = _serverCipher.Decrypt(firstBlock); - } - } - - packetLength = BinaryPrimitives.ReadUInt32BigEndian(plainFirstBlock); - - // Test packet minimum and maximum boundaries - if (packetLength < Math.Max((byte)8, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4) - { - throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Bad packet length: {0}.", packetLength), - DisconnectReason.ProtocolError); - } - - // Determine the number of bytes left to read; We've already read "blockSize" bytes, but the - // "packet length" field itself - which is 4 bytes - is not included in the length of the packet - var bytesToRead = (int)(packetLength - (blockSize - packetLengthFieldLength)) + serverMacLength; - - // Construct buffer for holding the payload and the inbound packet sequence as we need both in order - // to generate the hash. - // - // The total length of the "data" buffer is an addition of: - // - inboundPacketSequenceLength (4 bytes) - // - packetLength - // - serverMacLength - // - // We include the inbound packet sequence to allow us to have the the full SSH packet in a single - // byte[] for the purpose of calculating the client hash. Room for the server MAC is foreseen - // to read the packet including server MAC in a single pass (except for the initial block). - data = new byte[bytesToRead + blockSize + inboundPacketSequenceLength]; - BinaryPrimitives.WriteUInt32BigEndian(data, _inboundPacketSequence); - - // Use raw packet length field to calculate the mac in AEAD mode. - if (_serverAead) - { - Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, blockSize); - } - else - { - Buffer.BlockCopy(plainFirstBlock, 0, data, inboundPacketSequenceLength, blockSize); - } - - if (bytesToRead > 0) - { - if (TrySocketRead(socket, data, blockSize + inboundPacketSequenceLength, bytesToRead) == 0) - { - return null; - } - } } // validate encrypted message against MAC @@ -1858,84 +1834,6 @@ namespace Renci.SshNet return message; } - /// - /// Gets a value indicating whether the socket is connected. - /// - /// - /// if the socket is connected; otherwise, . - /// - /// - /// - /// As a first check we verify whether is - /// . However, this only returns the state of the socket as of - /// the last I/O operation. - /// - /// - /// Therefore we use the combination of with mode - /// and to verify if the socket is still connected. - /// - /// - /// The MSDN doc mention the following on the return value of - /// with mode : - /// - /// - /// if data is available for reading; - /// - /// - /// if the connection has been closed, reset, or terminated; otherwise, returns . - /// - /// - /// - /// - /// Conclusion: when the return value is - but no data is available for reading - then - /// the socket is no longer connected. - /// - /// - /// When a is used from multiple threads, there's a race condition - /// between the invocation of and the moment - /// when the value of is obtained. To workaround this issue - /// we synchronize reads from the . - /// - /// - /// We assume the socket is still connected if the read lock cannot be acquired immediately. - /// In this case, we just return without actually waiting to acquire - /// the lock. We don't want to wait for the read lock if another thread already has it because - /// there are cases where the other thread holding the lock can be waiting indefinitely for - /// a socket read operation to complete. - /// - /// - private bool IsSocketConnected() - { - _socketDisposeLock.Wait(); - - try - { - if (!_socket.IsConnected()) - { - return false; - } - - if (!_socketReadLock.TryEnter()) - { - return true; - } - - try - { - var connectionClosedOrDataAvailable = _socket.Poll(0, SelectMode.SelectRead); - return !(connectionClosedOrDataAvailable && _socket.Available == 0); - } - finally - { - _socketReadLock.Exit(); - } - } - finally - { - _ = _socketDisposeLock.Release(); - } - } - /// /// Performs a blocking read on the socket until bytes are received. /// @@ -1958,44 +1856,37 @@ namespace Renci.SshNet /// private void SocketDisconnectAndDispose() { - if (_socket != null) + lock (_socketDisposeLock) { - _socketDisposeLock.Wait(); - - try + if (_socket is null) { - if (_socket != null) + return; + } + + if (_socket.Connected) + { + try { - if (_socket.Connected) - { - try - { - _logger.LogDebug("[{SessionId}] Shutting down socket.", SessionIdHex); + _logger.LogDebug("[{SessionId}] Shutting down socket.", SessionIdHex); - // Interrupt any pending reads; should be done outside of socket read lock as we - // actually want shutdown the socket to make sure blocking reads are interrupted. - // - // This may result in a SocketException (eg. An existing connection was forcibly - // closed by the remote host) which we'll log and ignore as it means the socket - // was already shut down. - _socket.Shutdown(SocketShutdown.Send); - } - catch (SocketException ex) - { - _logger.LogInformation(ex, "Failure shutting down socket"); - } - } - - _logger.LogDebug("[{SessionId}] Disposing socket.", SessionIdHex); - _socket.Dispose(); - _logger.LogDebug("[{SessionId}] Disposed socket.", SessionIdHex); - _socket = null; + // Interrupt any pending reads; should be done outside of socket read lock as we + // actually want shutdown the socket to make sure blocking reads are interrupted. + // + // This may result in a SocketException (eg. An existing connection was forcibly + // closed by the remote host) which we'll log and ignore as it means the socket + // was already shut down. + _socket.Shutdown(SocketShutdown.Both); + } + catch (SocketException ex) + { + _logger.LogInformation(ex, "Failure shutting down socket"); } } - finally - { - _ = _socketDisposeLock.Release(); - } + + _logger.LogDebug("[{SessionId}] Disposing socket.", SessionIdHex); + _socket.Dispose(); + _logger.LogDebug("[{SessionId}] Disposed socket.", SessionIdHex); + _socket = null; } } @@ -2016,25 +1907,6 @@ namespace Renci.SshNet break; } - try - { - // Block until either data is available or the socket is closed - var connectionClosedOrDataAvailable = socket.Poll(-1, SelectMode.SelectRead); - if (connectionClosedOrDataAvailable && socket.Available == 0) - { - // connection with SSH server was closed or connection was reset - break; - } - } - catch (ObjectDisposedException) - { - // The socket was disposed by either: - // * a call to Disconnect() - // * a call to Dispose() - // * a SSH_MSG_DISCONNECT received from server - break; - } - var message = ReceiveMessage(socket); if (message is null) { @@ -2070,25 +1942,12 @@ namespace Renci.SshNet /// The . private void RaiseError(Exception exp) { - var connectionException = exp as SshConnectionException; - _logger.LogInformation(exp, "[{SessionId}] Raised exception", SessionIdHex); - if (_isDisconnecting) + if (_isDisconnecting && exp is SshConnectionException or ObjectDisposedException) { - // a connection exception which is raised while isDisconnecting is normal and - // should be ignored - if (connectionException != null) - { - return; - } - - // any timeout while disconnecting can be caused by loss of connectivity - // altogether and should be ignored - if (exp is SocketException socketException && socketException.SocketErrorCode == SocketError.TimedOut) - { - return; - } + // Such an exception raised while isDisconnecting is expected and can be ignored. + return; } // "save" exception and set exception wait handle to ensure any waits are interrupted @@ -2097,10 +1956,10 @@ namespace Renci.SshNet ErrorOccured?.Invoke(this, new ExceptionEventArgs(exp)); - if (connectionException != null) + if (exp is SshConnectionException connectionException) { _logger.LogInformation(exp, "[{SessionId}] Disconnecting after exception", SessionIdHex); - Disconnect(connectionException.DisconnectReason, exp.ToString()); + Disconnect(connectionException.DisconnectReason, exp.Message); } } diff --git a/src/Renci.SshNet/Sftp/Requests/SftpMkDirRequest.cs b/src/Renci.SshNet/Sftp/Requests/SftpMkDirRequest.cs index fafd481f..a04a896c 100644 --- a/src/Renci.SshNet/Sftp/Requests/SftpMkDirRequest.cs +++ b/src/Renci.SshNet/Sftp/Requests/SftpMkDirRequest.cs @@ -8,7 +8,6 @@ namespace Renci.SshNet.Sftp.Requests internal sealed class SftpMkDirRequest : SftpRequest { private byte[] _path; - private byte[] _attributesBytes; public override SftpMessageTypes SftpMessageType { @@ -23,18 +22,6 @@ namespace Renci.SshNet.Sftp.Requests public Encoding Encoding { get; private set; } - private SftpFileAttributes Attributes { get; set; } - - private byte[] AttributesBytes - { - get - { - _attributesBytes ??= Attributes.GetBytes(); - - return _attributesBytes; - } - } - /// /// Gets the size of the message in bytes. /// @@ -48,36 +35,30 @@ namespace Renci.SshNet.Sftp.Requests var capacity = base.BufferCapacity; capacity += 4; // Path length capacity += _path.Length; // Path - capacity += AttributesBytes.Length; // Attributes + capacity += 4; // Attributes return capacity; } } public SftpMkDirRequest(uint protocolVersion, uint requestId, string path, Encoding encoding, Action statusAction) - : this(protocolVersion, requestId, path, encoding, SftpFileAttributes.Empty, statusAction) - { - } - - private SftpMkDirRequest(uint protocolVersion, uint requestId, string path, Encoding encoding, SftpFileAttributes attributes, Action statusAction) : base(protocolVersion, requestId, statusAction) { Encoding = encoding; Path = path; - Attributes = attributes; } protected override void LoadData() { base.LoadData(); _path = ReadBinary(); - Attributes = ReadAttributes(); + _ = ReadAttributes(); } protected override void SaveData() { base.SaveData(); WriteBinaryString(_path); - Write(AttributesBytes); + Write(0u); // empty attributes } } } diff --git a/src/Renci.SshNet/Sftp/Requests/SftpOpenRequest.cs b/src/Renci.SshNet/Sftp/Requests/SftpOpenRequest.cs index 6a86b4da..45065f68 100644 --- a/src/Renci.SshNet/Sftp/Requests/SftpOpenRequest.cs +++ b/src/Renci.SshNet/Sftp/Requests/SftpOpenRequest.cs @@ -9,7 +9,6 @@ namespace Renci.SshNet.Sftp.Requests { private readonly Action _handleAction; private byte[] _fileName; - private byte[] _attributes; public override SftpMessageTypes SftpMessageType { @@ -24,12 +23,6 @@ namespace Renci.SshNet.Sftp.Requests public Flags Flags { get; } - public SftpFileAttributes Attributes - { - get { return SftpFileAttributes.FromBytes(_attributes); } - private set { _attributes = value.GetBytes(); } - } - public Encoding Encoding { get; } /// @@ -46,23 +39,17 @@ namespace Renci.SshNet.Sftp.Requests capacity += 4; // FileName length capacity += _fileName.Length; // FileName capacity += 4; // Flags - capacity += _attributes.Length; // Attributes + capacity += 4; // Attributes return capacity; } } public SftpOpenRequest(uint protocolVersion, uint requestId, string fileName, Encoding encoding, Flags flags, Action handleAction, Action statusAction) - : this(protocolVersion, requestId, fileName, encoding, flags, SftpFileAttributes.Empty, handleAction, statusAction) - { - } - - private SftpOpenRequest(uint protocolVersion, uint requestId, string fileName, Encoding encoding, Flags flags, SftpFileAttributes attributes, Action handleAction, Action statusAction) : base(protocolVersion, requestId, statusAction) { Encoding = encoding; Filename = fileName; Flags = flags; - Attributes = attributes; _handleAction = handleAction; } @@ -79,7 +66,7 @@ namespace Renci.SshNet.Sftp.Requests WriteBinaryString(_fileName); Write((uint)Flags); - Write(_attributes); + Write(0u); // empty attributes } public override void Complete(SftpResponse response) diff --git a/src/Renci.SshNet/Sftp/Responses/SftpVersionResponse.cs b/src/Renci.SshNet/Sftp/Responses/SftpVersionResponse.cs index 1ae8e6d6..8c2ff2c4 100644 --- a/src/Renci.SshNet/Sftp/Responses/SftpVersionResponse.cs +++ b/src/Renci.SshNet/Sftp/Responses/SftpVersionResponse.cs @@ -11,14 +11,14 @@ namespace Renci.SshNet.Sftp.Responses public uint Version { get; set; } - public IDictionary Extentions { get; set; } + public IDictionary Extensions { get; set; } protected override void LoadData() { base.LoadData(); Version = ReadUInt32(); - Extentions = ReadExtensionPair(); + Extensions = ReadExtensionPair(); } protected override void SaveData() @@ -27,9 +27,9 @@ namespace Renci.SshNet.Sftp.Responses Write(Version); - if (Extentions != null) + if (Extensions != null) { - Write(Extentions); + Write(Extensions); } } } diff --git a/src/Renci.SshNet/Sftp/SftpFileAttributes.cs b/src/Renci.SshNet/Sftp/SftpFileAttributes.cs index 503e1470..70ca0ddc 100644 --- a/src/Renci.SshNet/Sftp/SftpFileAttributes.cs +++ b/src/Renci.SshNet/Sftp/SftpFileAttributes.cs @@ -1,7 +1,10 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; -using System.Globalization; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using Renci.SshNet.Common; @@ -23,8 +26,8 @@ namespace Renci.SshNet.Sftp private const uint S_IFCHR = 0x2000; // character device private const uint S_IFIFO = 0x1000; // FIFO private const uint S_ISUID = 0x0800; // set UID bit - private const uint S_ISGID = 0x0400; // set-group-ID bit (see below) - private const uint S_ISVTX = 0x0200; // sticky bit (see below) + private const uint S_ISGID = 0x0400; // set-group-ID bit + private const uint S_ISVTX = 0x0200; // sticky bit private const uint S_IRUSR = 0x0100; // owner has read permission private const uint S_IWUSR = 0x0080; // owner has write permission private const uint S_IXUSR = 0x0040; // owner has execute permission @@ -43,12 +46,7 @@ namespace Renci.SshNet.Sftp private readonly int _originalUserId; private readonly int _originalGroupId; private readonly uint _originalPermissions; - private readonly IDictionary _originalExtensions; - - private bool _isBitFiledsBitSet; - private bool _isUIDBitSet; - private bool _isGroupIDBitSet; - private bool _isStickyBitSet; + private readonly Dictionary? _originalExtensions; internal bool IsLastAccessTimeChanged { @@ -82,6 +80,7 @@ namespace Renci.SshNet.Sftp internal bool IsExtensionsChanged { + [MemberNotNullWhen(true, nameof(Extensions))] get { return _originalExtensions != null && Extensions != null && !_originalExtensions.SequenceEqual(Extensions); } } @@ -169,7 +168,13 @@ namespace Renci.SshNet.Sftp /// /// if file represents a socket; otherwise, . /// - public bool IsSocket { get; private set; } + public bool IsSocket + { + get + { + return (Permissions & S_IFMT) == S_IFSOCK; + } + } /// /// Gets a value indicating whether file represents a symbolic link. @@ -177,7 +182,13 @@ namespace Renci.SshNet.Sftp /// /// if file represents a symbolic link; otherwise, . /// - public bool IsSymbolicLink { get; private set; } + public bool IsSymbolicLink + { + get + { + return (Permissions & S_IFMT) == S_IFLNK; + } + } /// /// Gets a value indicating whether file represents a regular file. @@ -185,7 +196,13 @@ namespace Renci.SshNet.Sftp /// /// if file represents a regular file; otherwise, . /// - public bool IsRegularFile { get; private set; } + public bool IsRegularFile + { + get + { + return (Permissions & S_IFMT) == S_IFREG; + } + } /// /// Gets a value indicating whether file represents a block device. @@ -193,7 +210,13 @@ namespace Renci.SshNet.Sftp /// /// if file represents a block device; otherwise, . /// - public bool IsBlockDevice { get; private set; } + public bool IsBlockDevice + { + get + { + return (Permissions & S_IFMT) == S_IFBLK; + } + } /// /// Gets a value indicating whether file represents a directory. @@ -201,7 +224,13 @@ namespace Renci.SshNet.Sftp /// /// if file represents a directory; otherwise, . /// - public bool IsDirectory { get; private set; } + public bool IsDirectory + { + get + { + return (Permissions & S_IFMT) == S_IFDIR; + } + } /// /// Gets a value indicating whether file represents a character device. @@ -209,7 +238,13 @@ namespace Renci.SshNet.Sftp /// /// if file represents a character device; otherwise, . /// - public bool IsCharacterDevice { get; private set; } + public bool IsCharacterDevice + { + get + { + return (Permissions & S_IFMT) == S_IFCHR; + } + } /// /// Gets a value indicating whether file represents a named pipe. @@ -217,7 +252,88 @@ namespace Renci.SshNet.Sftp /// /// if file represents a named pipe; otherwise, . /// - public bool IsNamedPipe { get; private set; } + public bool IsNamedPipe + { + get + { + return (Permissions & S_IFMT) == S_IFIFO; + } + } + + /// + /// Gets or sets a value indicating whether the setuid bit is set. + /// + /// + /// if the setuid bit is set; otherwise, . + /// + public bool IsUIDBitSet + { + get + { + return (Permissions & S_ISUID) == S_ISUID; + } + set + { + if (value) + { + Permissions |= S_ISUID; + } + else + { + Permissions &= ~S_ISUID; + } + } + } + + /// + /// Gets or sets a value indicating whether the setgid bit is set. + /// + /// + /// if the setgid bit is set; otherwise, . + /// + public bool IsGroupIDBitSet + { + get + { + return (Permissions & S_ISGID) == S_ISGID; + } + set + { + if (value) + { + Permissions |= S_ISGID; + } + else + { + Permissions &= ~S_ISGID; + } + } + } + + /// + /// Gets or sets a value indicating whether the sticky bit is set. + /// + /// + /// if the sticky bit is set; otherwise, . + /// + public bool IsStickyBitSet + { + get + { + return (Permissions & S_ISVTX) == S_ISVTX; + } + set + { + if (value) + { + Permissions |= S_ISVTX; + } + else + { + Permissions &= ~S_ISVTX; + } + } + } /// /// Gets or sets a value indicating whether the owner can read from this file. @@ -225,7 +341,24 @@ namespace Renci.SshNet.Sftp /// /// if owner can read from this file; otherwise, . /// - public bool OwnerCanRead { get; set; } + public bool OwnerCanRead + { + get + { + return (Permissions & S_IRUSR) == S_IRUSR; + } + set + { + if (value) + { + Permissions |= S_IRUSR; + } + else + { + Permissions &= ~S_IRUSR; + } + } + } /// /// Gets or sets a value indicating whether the owner can write into this file. @@ -233,7 +366,24 @@ namespace Renci.SshNet.Sftp /// /// if owner can write into this file; otherwise, . /// - public bool OwnerCanWrite { get; set; } + public bool OwnerCanWrite + { + get + { + return (Permissions & S_IWUSR) == S_IWUSR; + } + set + { + if (value) + { + Permissions |= S_IWUSR; + } + else + { + Permissions &= ~S_IWUSR; + } + } + } /// /// Gets or sets a value indicating whether the owner can execute this file. @@ -241,7 +391,24 @@ namespace Renci.SshNet.Sftp /// /// if owner can execute this file; otherwise, . /// - public bool OwnerCanExecute { get; set; } + public bool OwnerCanExecute + { + get + { + return (Permissions & S_IXUSR) == S_IXUSR; + } + set + { + if (value) + { + Permissions |= S_IXUSR; + } + else + { + Permissions &= ~S_IXUSR; + } + } + } /// /// Gets or sets a value indicating whether the group members can read from this file. @@ -249,7 +416,24 @@ namespace Renci.SshNet.Sftp /// /// if group members can read from this file; otherwise, . /// - public bool GroupCanRead { get; set; } + public bool GroupCanRead + { + get + { + return (Permissions & S_IRGRP) == S_IRGRP; + } + set + { + if (value) + { + Permissions |= S_IRGRP; + } + else + { + Permissions &= ~S_IRGRP; + } + } + } /// /// Gets or sets a value indicating whether the group members can write into this file. @@ -257,7 +441,24 @@ namespace Renci.SshNet.Sftp /// /// if group members can write into this file; otherwise, . /// - public bool GroupCanWrite { get; set; } + public bool GroupCanWrite + { + get + { + return (Permissions & S_IWGRP) == S_IWGRP; + } + set + { + if (value) + { + Permissions |= S_IWGRP; + } + else + { + Permissions &= ~S_IWGRP; + } + } + } /// /// Gets or sets a value indicating whether the group members can execute this file. @@ -265,7 +466,24 @@ namespace Renci.SshNet.Sftp /// /// if group members can execute this file; otherwise, . /// - public bool GroupCanExecute { get; set; } + public bool GroupCanExecute + { + get + { + return (Permissions & S_IXGRP) == S_IXGRP; + } + set + { + if (value) + { + Permissions |= S_IXGRP; + } + else + { + Permissions &= ~S_IXGRP; + } + } + } /// /// Gets or sets a value indicating whether the others can read from this file. @@ -273,7 +491,24 @@ namespace Renci.SshNet.Sftp /// /// if others can read from this file; otherwise, . /// - public bool OthersCanRead { get; set; } + public bool OthersCanRead + { + get + { + return (Permissions & S_IROTH) == S_IROTH; + } + set + { + if (value) + { + Permissions |= S_IROTH; + } + else + { + Permissions &= ~S_IROTH; + } + } + } /// /// Gets or sets a value indicating whether the others can write into this file. @@ -281,7 +516,24 @@ namespace Renci.SshNet.Sftp /// /// if others can write into this file; otherwise, . /// - public bool OthersCanWrite { get; set; } + public bool OthersCanWrite + { + get + { + return (Permissions & S_IWOTH) == S_IWOTH; + } + set + { + if (value) + { + Permissions |= S_IWOTH; + } + else + { + Permissions &= ~S_IWOTH; + } + } + } /// /// Gets or sets a value indicating whether the others can execute this file. @@ -289,7 +541,24 @@ namespace Renci.SshNet.Sftp /// /// if others can execute this file; otherwise, . /// - public bool OthersCanExecute { get; set; } + public bool OthersCanExecute + { + get + { + return (Permissions & S_IXOTH) == S_IXOTH; + } + set + { + if (value) + { + Permissions |= S_IXOTH; + } + else + { + Permissions &= ~S_IXOTH; + } + } + } /// /// Gets the extensions. @@ -297,165 +566,11 @@ namespace Renci.SshNet.Sftp /// /// The extensions. /// - public IDictionary Extensions { get; private set; } + public IDictionary? Extensions { get; } - internal uint Permissions - { - get - { - uint permission = 0; + internal uint Permissions { get; private set; } - if (_isBitFiledsBitSet) - { - permission |= S_IFMT; - } - - if (IsSocket) - { - permission |= S_IFSOCK; - } - - if (IsSymbolicLink) - { - permission |= S_IFLNK; - } - - if (IsRegularFile) - { - permission |= S_IFREG; - } - - if (IsBlockDevice) - { - permission |= S_IFBLK; - } - - if (IsDirectory) - { - permission |= S_IFDIR; - } - - if (IsCharacterDevice) - { - permission |= S_IFCHR; - } - - if (IsNamedPipe) - { - permission |= S_IFIFO; - } - - if (_isUIDBitSet) - { - permission |= S_ISUID; - } - - if (_isGroupIDBitSet) - { - permission |= S_ISGID; - } - - if (_isStickyBitSet) - { - permission |= S_ISVTX; - } - - if (OwnerCanRead) - { - permission |= S_IRUSR; - } - - if (OwnerCanWrite) - { - permission |= S_IWUSR; - } - - if (OwnerCanExecute) - { - permission |= S_IXUSR; - } - - if (GroupCanRead) - { - permission |= S_IRGRP; - } - - if (GroupCanWrite) - { - permission |= S_IWGRP; - } - - if (GroupCanExecute) - { - permission |= S_IXGRP; - } - - if (OthersCanRead) - { - permission |= S_IROTH; - } - - if (OthersCanWrite) - { - permission |= S_IWOTH; - } - - if (OthersCanExecute) - { - permission |= S_IXOTH; - } - - return permission; - } - private set - { - _isBitFiledsBitSet = (value & S_IFMT) == S_IFMT; - - IsSocket = (value & S_IFSOCK) == S_IFSOCK; - - IsSymbolicLink = (value & S_IFLNK) == S_IFLNK; - - IsRegularFile = (value & S_IFREG) == S_IFREG; - - IsBlockDevice = (value & S_IFBLK) == S_IFBLK; - - IsDirectory = (value & S_IFDIR) == S_IFDIR; - - IsCharacterDevice = (value & S_IFCHR) == S_IFCHR; - - IsNamedPipe = (value & S_IFIFO) == S_IFIFO; - - _isUIDBitSet = (value & S_ISUID) == S_ISUID; - - _isGroupIDBitSet = (value & S_ISGID) == S_ISGID; - - _isStickyBitSet = (value & S_ISVTX) == S_ISVTX; - - OwnerCanRead = (value & S_IRUSR) == S_IRUSR; - - OwnerCanWrite = (value & S_IWUSR) == S_IWUSR; - - OwnerCanExecute = (value & S_IXUSR) == S_IXUSR; - - GroupCanRead = (value & S_IRGRP) == S_IRGRP; - - GroupCanWrite = (value & S_IWGRP) == S_IWGRP; - - GroupCanExecute = (value & S_IXGRP) == S_IXGRP; - - OthersCanRead = (value & S_IROTH) == S_IROTH; - - OthersCanWrite = (value & S_IWOTH) == S_IWOTH; - - OthersCanExecute = (value & S_IXOTH) == S_IXOTH; - } - } - - private SftpFileAttributes() - { - } - - internal SftpFileAttributes(DateTime lastAccessTimeUtc, DateTime lastWriteTimeUtc, long size, int userId, int groupId, uint permissions, IDictionary extensions) + internal SftpFileAttributes(DateTime lastAccessTimeUtc, DateTime lastWriteTimeUtc, long size, int userId, int groupId, uint permissions, Dictionary? extensions) { LastAccessTimeUtc = _originalLastAccessTimeUtc = lastAccessTimeUtc; LastWriteTimeUtc = _originalLastWriteTimeUtc = lastWriteTimeUtc; @@ -467,31 +582,118 @@ namespace Renci.SshNet.Sftp } /// - /// Sets the permissions. + /// Sets the POSIX permissions for this file. /// - /// The mode. + /// + /// The permission mode as an octal number (e.g., 755, 644, 1777). + /// + /// + /// has more than 4 digits or cannot be interpreted as an octal number. + /// public void SetPermissions(short mode) { - if (mode is < 0 or > 999) + var special = (uint)Math.DivRem(mode, 1000, out var userGroupOther); + + var user = (uint)Math.DivRem(userGroupOther, 100, out var groupOther); + + var group = (uint)Math.DivRem(groupOther, 10, out var iOther); + + var other = (uint)iOther; + + if ((special & ~7u) != 0 || (user & ~7u) != 0 || (group & ~7u) != 0 || (other & ~7u) != 0) { throw new ArgumentOutOfRangeException(nameof(mode)); } - var modeBytes = mode.ToString(CultureInfo.InvariantCulture).PadLeft(3, '0').ToCharArray(); + Permissions = (Permissions & ~0xFFFu) | (special << 9) | (user << 6) | (group << 3) | other; + } - var permission = ((modeBytes[0] & 0x0F) * 8 * 8) + ((modeBytes[1] & 0x0F) * 8) + (modeBytes[2] & 0x0F); + /// + public override string? ToString() + { + var sb = new StringBuilder(); - OwnerCanRead = (permission & S_IRUSR) == S_IRUSR; - OwnerCanWrite = (permission & S_IWUSR) == S_IWUSR; - OwnerCanExecute = (permission & S_IXUSR) == S_IXUSR; + if (Permissions != default) + { + AppendPermissionsString(sb); + sb.Append(' '); + } - GroupCanRead = (permission & S_IRGRP) == S_IRGRP; - GroupCanWrite = (permission & S_IWGRP) == S_IWGRP; - GroupCanExecute = (permission & S_IXGRP) == S_IXGRP; + if (Size != -1) + { + sb.AppendFormat("Size: {0} ", Size); + } - OthersCanRead = (permission & S_IROTH) == S_IROTH; - OthersCanWrite = (permission & S_IWOTH) == S_IWOTH; - OthersCanExecute = (permission & S_IXOTH) == S_IXOTH; + if (LastWriteTime != default) + { + sb.AppendFormat("LastWriteTime: {0:s} ", LastWriteTime); + } + + if (sb.Length > 0) + { + if (sb[sb.Length - 1] == ' ') + { + sb.Length--; + } + + Debug.Assert(sb.Length > 0); + Debug.Assert(sb[^1] != ' '); + + return sb.ToString(); + } + + return base.ToString(); + } + + private void AppendPermissionsString(StringBuilder sb) + { + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ls.html + + sb.Append( + IsRegularFile ? '-' : + IsDirectory ? 'd' : + IsSymbolicLink ? 'l' : + IsNamedPipe ? 'p' : + IsSocket ? 's' : + IsCharacterDevice ? 'c' : + IsBlockDevice ? 'b' : + '-'); + + sb.Append(OwnerCanRead ? 'r' : '-'); + sb.Append(OwnerCanWrite ? 'w' : '-'); + + if (OwnerCanExecute) + { + sb.Append(IsUIDBitSet ? 's' : 'x'); + } + else + { + sb.Append(IsUIDBitSet ? 'S' : '-'); + } + + sb.Append(GroupCanRead ? 'r' : '-'); + sb.Append(GroupCanWrite ? 'w' : '-'); + + if (GroupCanExecute) + { + sb.Append(IsGroupIDBitSet ? 's' : 'x'); + } + else + { + sb.Append(IsGroupIDBitSet ? 'S' : '-'); + } + + sb.Append(OthersCanRead ? 'r' : '-'); + sb.Append(OthersCanWrite ? 'w' : '-'); + + if (OthersCanExecute) + { + sb.Append(IsStickyBitSet ? 't' : 'x'); + } + else + { + sb.Append(IsStickyBitSet ? 'T' : '-'); + } } /// @@ -506,7 +708,7 @@ namespace Renci.SshNet.Sftp { uint flag = 0; - if (IsSizeChanged && IsRegularFile) + if (IsSizeChanged) { flag |= 0x00000001; } @@ -533,7 +735,7 @@ namespace Renci.SshNet.Sftp stream.Write(flag); - if (IsSizeChanged && IsRegularFile) + if (IsSizeChanged) { stream.Write((ulong)Size); } @@ -551,9 +753,9 @@ namespace Renci.SshNet.Sftp if (IsLastAccessTimeChanged || IsLastWriteTimeChanged) { - var time = (uint)((LastAccessTimeUtc.ToFileTimeUtc() / 10000000) - 11644473600); + var time = (uint)((DateTimeOffset)DateTime.SpecifyKind(LastAccessTimeUtc, DateTimeKind.Utc)).ToUnixTimeSeconds(); stream.Write(time); - time = (uint)((LastWriteTimeUtc.ToFileTimeUtc() / 10000000) - 11644473600); + time = (uint)((DateTimeOffset)DateTime.SpecifyKind(LastWriteTimeUtc, DateTimeKind.Utc)).ToUnixTimeSeconds(); stream.Write(time); } @@ -561,12 +763,8 @@ namespace Renci.SshNet.Sftp { foreach (var item in Extensions) { - /* - * TODO: we write as ASCII but read as UTF8 !!! - */ - - stream.Write(item.Key, SshData.Ascii); - stream.Write(item.Value, SshData.Ascii); + stream.Write(item.Key, Encoding.UTF8); + stream.Write(item.Value, Encoding.UTF8); } } @@ -574,8 +772,6 @@ namespace Renci.SshNet.Sftp } } - internal static readonly SftpFileAttributes Empty = new SftpFileAttributes(); - internal static SftpFileAttributes FromBytes(SshDataStream stream) { const uint SSH_FILEXFER_ATTR_SIZE = 0x00000001; @@ -592,7 +788,7 @@ namespace Renci.SshNet.Sftp uint permissions = 0; DateTime accessTime; DateTime modifyTime; - Dictionary extensions = null; + Dictionary? extensions = null; if ((flag & SSH_FILEXFER_ATTR_SIZE) == SSH_FILEXFER_ATTR_SIZE) { @@ -613,12 +809,8 @@ namespace Renci.SshNet.Sftp if ((flag & SSH_FILEXFER_ATTR_ACMODTIME) == SSH_FILEXFER_ATTR_ACMODTIME) { - // The incoming times are "Unix times", so they're already in UTC. We need to preserve that - // to avoid losing information in a local time conversion during the "fall back" hour in DST. - var time = stream.ReadUInt32(); - accessTime = DateTime.FromFileTimeUtc((time + 11644473600) * 10000000); - time = stream.ReadUInt32(); - modifyTime = DateTime.FromFileTimeUtc((time + 11644473600) * 10000000); + accessTime = DateTimeOffset.FromUnixTimeSeconds(stream.ReadUInt32()).UtcDateTime; + modifyTime = DateTimeOffset.FromUnixTimeSeconds(stream.ReadUInt32()).UtcDateTime; } else { @@ -632,8 +824,8 @@ namespace Renci.SshNet.Sftp extensions = new Dictionary(extendedCount); for (var i = 0; i < extendedCount; i++) { - var extensionName = stream.ReadString(SshData.Utf8); - var extensionData = stream.ReadString(SshData.Utf8); + var extensionName = stream.ReadString(Encoding.UTF8); + var extensionData = stream.ReadString(Encoding.UTF8); extensions.Add(extensionName, extensionData); } } diff --git a/src/Renci.SshNet/Sftp/SftpSession.cs b/src/Renci.SshNet/Sftp/SftpSession.cs index cd18dabf..3cfcec6c 100644 --- a/src/Renci.SshNet/Sftp/SftpSession.cs +++ b/src/Renci.SshNet/Sftp/SftpSession.cs @@ -371,7 +371,7 @@ namespace Renci.SshNet.Sftp if (response is SftpVersionResponse versionResponse) { ProtocolVersion = versionResponse.Version; - _supportedExtensions = versionResponse.Extentions; + _supportedExtensions = versionResponse.Extensions; _ = _sftpVersionConfirmed.Set(); } diff --git a/test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs b/test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs index 2a2ccaa4..9309be40 100644 --- a/test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs +++ b/test/Renci.SshNet.IntegrationTests/Common/RemoteSshdConfigExtensions.cs @@ -23,6 +23,7 @@ namespace Renci.SshNet.IntegrationTests.Common .ClearHostKeyAlgorithms() .ClearPublicKeyAcceptedAlgorithms() .ClearMessageAuthenticationCodeAlgorithms() + .PermitTTY(true) .WithUsePAM(true) .Update() .Restart(); diff --git a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs index bcdb00d7..304f294a 100644 --- a/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs +++ b/test/Renci.SshNet.IntegrationTests/TestsFixtures/InfrastructureFixture.cs @@ -1,4 +1,6 @@ -using DotNet.Testcontainers.Builders; +using System.Runtime.InteropServices; + +using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Images; @@ -28,26 +30,27 @@ namespace Renci.SshNet.IntegrationTests.TestsFixtures private IFutureDockerImage _sshServerImage; - public string SshServerHostName { get; set; } + public string SshServerHostName { get; private set; } - public ushort SshServerPort { get; set; } + public ushort SshServerPort { get; private set; } - public SshUser AdminUser = new SshUser("sshnetadm", "ssh4ever"); + public SshUser AdminUser { get; } = new SshUser("sshnetadm", "ssh4ever"); - public SshUser User = new SshUser("sshnet", "ssh4ever"); + public SshUser User { get; } = new SshUser("sshnet", "ssh4ever"); public async Task InitializeAsync() { - // for the .NET Framework Tests in CI, the Container is set up in WSL2 with Podman -#if NETFRAMEWORK - if (Environment.GetEnvironmentVariable("CI") == "true") +#pragma warning disable MA0144 // use System.OperatingSystem to check the current OS + // for the Windows Tests in CI, the Container is set up in WSL2 with Podman + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + Environment.GetEnvironmentVariable("CI") == "true") +#pragma warning restore MA0144 // use System.OperatingSystem to check the current OS { SshServerPort = 2222; - SshServerHostName = "localhost"; + SshServerHostName = "127.0.0.1"; await Task.Delay(1_000); return; } -#endif var containerLogger = _loggerFactory.CreateLogger("testcontainers"); diff --git a/test/Renci.SshNet.Tests/Classes/AbstractionsTest.cs b/test/Renci.SshNet.Tests/Classes/AbstractionsTest.cs index 97f8e688..c7328485 100644 --- a/test/Renci.SshNet.Tests/Classes/AbstractionsTest.cs +++ b/test/Renci.SshNet.Tests/Classes/AbstractionsTest.cs @@ -9,12 +9,6 @@ namespace Renci.SshNet.Tests.Classes [TestClass] public class AbstractionsTest { - [TestMethod] - public void SocketAbstraction_CanWrite_ShouldReturnFalseWhenSocketIsNull() - { - Assert.IsFalse(SocketAbstraction.CanWrite(null)); - } - [TestMethod] public void CryptoAbstraction_GenerateRandom_ShouldPerformNoOpWhenDataIsZeroLength() { diff --git a/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ConnectionReset.cs b/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ConnectionReset.cs index 1b11e702..a09999b3 100644 --- a/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ConnectionReset.cs +++ b/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ConnectionReset.cs @@ -64,8 +64,6 @@ namespace Renci.SshNet.Tests.Classes var connectionException = (SshConnectionException)exception; Assert.AreEqual(DisconnectReason.ConnectionLost, connectionException.DisconnectReason); - Assert.IsNull(connectionException.InnerException); - Assert.AreEqual("An established connection was aborted by the server.", connectionException.Message); } [TestMethod] @@ -137,45 +135,29 @@ namespace Renci.SshNet.Tests.Classes public void ISession_WaitOnHandle_WaitHandle_ShouldThrowSshConnectionException() { var session = (ISession)Session; - var waitHandle = new ManualResetEvent(false); + using var waitHandle = new ManualResetEvent(false); - try - { - session.WaitOnHandle(waitHandle); - Assert.Fail(); - } - catch (SshConnectionException ex) - { - Assert.AreEqual("An established connection was aborted by the server.", ex.Message); - Assert.IsNull(ex.InnerException); - Assert.AreEqual(DisconnectReason.ConnectionLost, ex.DisconnectReason); - } + var ex = Assert.ThrowsExactly(() => session.WaitOnHandle(waitHandle)); + + Assert.AreEqual(DisconnectReason.ConnectionLost, ex.DisconnectReason); } [TestMethod] public void ISession_WaitOnHandle_WaitHandleAndTimeout_ShouldThrowSshConnectionException() { var session = (ISession)Session; - var waitHandle = new ManualResetEvent(false); + using var waitHandle = new ManualResetEvent(false); - try - { - session.WaitOnHandle(waitHandle, Timeout.InfiniteTimeSpan); - 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); - } + var ex = Assert.ThrowsExactly(() => session.WaitOnHandle(waitHandle)); + + Assert.AreEqual(DisconnectReason.ConnectionLost, ex.DisconnectReason); } [TestMethod] public void ISession_TryWait_WaitHandleAndTimeout_ShouldReturnDisconnected() { var session = (ISession)Session; - var waitHandle = new ManualResetEvent(false); + using var waitHandle = new ManualResetEvent(false); var result = session.TryWait(waitHandle, Timeout.InfiniteTimeSpan); @@ -186,7 +168,7 @@ namespace Renci.SshNet.Tests.Classes public void ISession_TryWait_WaitHandleAndTimeoutAndException_ShouldReturnDisconnected() { var session = (ISession)Session; - var waitHandle = new ManualResetEvent(false); + using var waitHandle = new ManualResetEvent(false); var result = session.TryWait(waitHandle, Timeout.InfiniteTimeSpan, out var exception); diff --git a/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerSendsBadPacket.cs b/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerSendsBadPacket.cs index 5dd53a29..61f1b494 100644 --- a/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerSendsBadPacket.cs +++ b/test/Renci.SshNet.Tests/Classes/SessionTest_Connected_ServerSendsBadPacket.cs @@ -91,13 +91,20 @@ namespace Renci.SshNet.Tests.Classes } [TestMethod] - public void ReceiveOnServerSocketShouldReturnZero() + public void ServerShouldBeDisconnected() { - var buffer = new byte[1]; + try + { + var buffer = new byte[1]; - var actual = ServerSocket.Receive(buffer, 0, buffer.Length, SocketFlags.None); + var actual = ServerSocket.Receive(buffer, 0, buffer.Length, SocketFlags.None); - Assert.AreEqual(0, actual); + Assert.AreEqual(0, actual); // FIN + } + catch (SocketException sx) + { + Assert.AreEqual(SocketError.ConnectionReset, sx.SocketErrorCode); // RST + } } [TestMethod] diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpFSetStatRequestTest.cs b/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpFSetStatRequestTest.cs index 69c0096e..2fd3bd1e 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpFSetStatRequestTest.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpFSetStatRequestTest.cs @@ -8,6 +8,7 @@ using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Requests; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp.Requests { @@ -29,7 +30,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp.Requests _requestId = (uint)random.Next(0, int.MaxValue); _handle = new byte[random.Next(1, 10)]; random.NextBytes(_handle); - _attributes = SftpFileAttributes.Empty; + _attributes = SftpFileAttributesBuilder.Empty; _attributesBytes = _attributes.GetBytes(); } diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpMkDirRequestTest.cs b/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpMkDirRequestTest.cs index ec0cf1af..e01d3f7f 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpMkDirRequestTest.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpMkDirRequestTest.cs @@ -10,6 +10,7 @@ using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Requests; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp.Requests { @@ -34,7 +35,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp.Requests _encoding = Encoding.Unicode; _path = random.Next().ToString(CultureInfo.InvariantCulture); _pathBytes = _encoding.GetBytes(_path); - _attributes = SftpFileAttributes.Empty; + _attributes = SftpFileAttributesBuilder.Empty; _attributesBytes = _attributes.GetBytes(); } diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpOpenRequestTest.cs b/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpOpenRequestTest.cs index 20838c86..dd7e407a 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpOpenRequestTest.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpOpenRequestTest.cs @@ -10,6 +10,7 @@ using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Requests; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp.Requests { @@ -36,7 +37,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp.Requests _filename = random.Next().ToString(CultureInfo.InvariantCulture); _filenameBytes = _encoding.GetBytes(_filename); _flags = Flags.Read; - _attributes = SftpFileAttributes.Empty; + _attributes = SftpFileAttributesBuilder.Empty; _attributesBytes = _attributes.GetBytes(); } diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpSetStatRequestTest.cs b/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpSetStatRequestTest.cs index c7111dd1..ae2e2231 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpSetStatRequestTest.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/Requests/SftpSetStatRequestTest.cs @@ -10,6 +10,7 @@ using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Requests; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp.Requests { @@ -34,7 +35,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp.Requests _encoding = Encoding.Unicode; _path = random.Next().ToString(CultureInfo.InvariantCulture); _pathBytes = _encoding.GetBytes(_path); - _attributes = SftpFileAttributes.Empty; + _attributes = SftpFileAttributesBuilder.Empty; _attributesBytes = _attributes.GetBytes(); } diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/Responses/SftpAttrsResponseTest.cs b/test/Renci.SshNet.Tests/Classes/Sftp/Responses/SftpAttrsResponseTest.cs index ed899651..4598b537 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/Responses/SftpAttrsResponseTest.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/Responses/SftpAttrsResponseTest.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp.Responses { @@ -61,7 +62,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp.Responses private SftpFileAttributes CreateSftpFileAttributes() { - var attributes = SftpFileAttributes.Empty; + var attributes = SftpFileAttributesBuilder.Empty; attributes.GroupId = _random.Next(); attributes.LastAccessTime = new DateTime(2014, 8, 23, 17, 43, 50, DateTimeKind.Local); attributes.LastWriteTime = new DateTime(2013, 7, 22, 16, 40, 42, DateTimeKind.Local); diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileAttributesTest.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileAttributesTest.cs new file mode 100644 index 00000000..566379a7 --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileAttributesTest.cs @@ -0,0 +1,223 @@ +using System; +using System.Buffers.Binary; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Renci.SshNet.Common; +using Renci.SshNet.Sftp; + +namespace Renci.SshNet.Tests.Classes.Sftp +{ + [TestClass] + public class SftpFileAttributesTest + { + [TestMethod] + [DataRow(0xC000u, true, false, false, false, false, false, false)] // Socket + [DataRow(0xA000u, false, true, false, false, false, false, false)] // Symbolic link + [DataRow(0x8000u, false, false, true, false, false, false, false)] // Regular file + [DataRow(0x6000u, false, false, false, true, false, false, false)] // Block device + [DataRow(0x4000u, false, false, false, false, true, false, false)] // Directory + [DataRow(0x2000u, false, false, false, false, false, true, false)] // Character device + [DataRow(0x1000u, false, false, false, false, false, false, true)] // Named pipe + public void FileTypePropertiesAreMutuallyExclusive( + uint permissions, + bool isSocket, + bool isSymbolicLink, + bool isRegularFile, + bool isBlockDevice, + bool isDirectory, + bool isCharacterDevice, + bool isNamedPipe) + { + var attributeBytes = new byte[8]; + attributeBytes[3] = 0x4; // SSH_FILEXFER_ATTR_PERMISSIONS + BinaryPrimitives.WriteUInt32BigEndian(attributeBytes.AsSpan(4), permissions); + + var attributes = SftpFileAttributes.FromBytes(attributeBytes); + + Assert.AreEqual(isSocket, attributes.IsSocket); + Assert.AreEqual(isSymbolicLink, attributes.IsSymbolicLink); + Assert.AreEqual(isRegularFile, attributes.IsRegularFile); + Assert.AreEqual(isBlockDevice, attributes.IsBlockDevice); + Assert.AreEqual(isDirectory, attributes.IsDirectory); + Assert.AreEqual(isCharacterDevice, attributes.IsCharacterDevice); + Assert.AreEqual(isNamedPipe, attributes.IsNamedPipe); + } + + [TestMethod] + public void FromBytesGetBytes() + { + // 81a4 in hex = 100644 in octal + var attributes = SftpFileAttributes.FromBytes([0, 0, 0, 0x4, 0, 0, 0x81, 0xa4]); + + Assert.IsTrue(attributes.IsRegularFile); + + Assert.IsFalse(attributes.IsUIDBitSet); + Assert.IsFalse(attributes.IsGroupIDBitSet); + Assert.IsFalse(attributes.IsStickyBitSet); + Assert.IsTrue(attributes.OwnerCanRead); + Assert.IsTrue(attributes.OwnerCanWrite); + Assert.IsFalse(attributes.OwnerCanExecute); + Assert.IsTrue(attributes.GroupCanRead); + Assert.IsFalse(attributes.GroupCanWrite); + Assert.IsFalse(attributes.GroupCanExecute); + Assert.IsTrue(attributes.OthersCanRead); + Assert.IsFalse(attributes.OthersCanWrite); + Assert.IsFalse(attributes.OthersCanExecute); + + Assert.AreEqual(-1, attributes.Size); // Erm, OK? + Assert.AreEqual(-1, attributes.UserId); + Assert.AreEqual(-1, attributes.GroupId); + + Assert.AreEqual(default, attributes.LastAccessTimeUtc); + Assert.AreEqual(DateTimeKind.Utc, attributes.LastAccessTimeUtc.Kind); + + Assert.AreEqual(default, attributes.LastWriteTimeUtc); + Assert.AreEqual(DateTimeKind.Utc, attributes.LastWriteTimeUtc.Kind); + + Assert.AreEqual("-rw-r--r--", attributes.ToString()); + + // No changes + CollectionAssert.AreEqual( + new byte[] { 0, 0, 0, 0 }, + attributes.GetBytes()); + + + // Permissions change + attributes.IsUIDBitSet = true; + attributes.OwnerCanExecute = true; + + CollectionAssert.AreEqual( + new byte[] { 0, 0, 0, 0x4, 0, 0, 0x89, 0xe4 }, + attributes.GetBytes()); + + Assert.AreEqual("-rwsr--r--", attributes.ToString()); + + // Size change + attributes.Size = 123; + + CollectionAssert.AreEqual( + new byte[] { + 0, 0, 0, 0x1 | 0x4, + 0, 0, 0, 0, 0, 0, 0, 123, + 0, 0, 0x89, 0xe4 }, + attributes.GetBytes()); + + Assert.IsTrue(attributes.ToString().StartsWith("-rwsr--r-- Size: ", StringComparison.Ordinal)); + + // Uid/gid change + attributes.UserId = 99; + attributes.GroupId = 66; + + CollectionAssert.AreEqual( + new byte[] { + 0, 0, 0, 0x1 | 0x2 | 0x4, + 0, 0, 0, 0, 0, 0, 0, 123, + 0, 0, 0, 99, 0, 0, 0, 66, + 0, 0, 0x89, 0xe4 }, + attributes.GetBytes()); + + + // Access/mod time change + attributes.LastAccessTimeUtc = new DateTime(2025, 08, 10, 17, 51, 37, DateTimeKind.Unspecified); + attributes.LastWriteTime = new DateTimeOffset(2016, 12, 02, 13, 18, 20, TimeSpan.FromHours(3)).LocalDateTime; + + var expectedTimeBytes = new byte[8]; + BinaryPrimitives.WriteUInt32BigEndian(expectedTimeBytes, 1754848297); + BinaryPrimitives.WriteUInt32BigEndian(expectedTimeBytes.AsSpan(4), 1480673900); + + CollectionAssert.AreEqual( + new byte[] { + 0, 0, 0, 0x1 | 0x2 | 0x4 | 0x8, + 0, 0, 0, 0, 0, 0, 0, 123, + 0, 0, 0, 99, 0, 0, 0, 66, + 0, 0, 0x89, 0xe4 + }.Concat(expectedTimeBytes), + attributes.GetBytes()); + + Assert.AreEqual(new DateTime(2016, 12, 02, 10, 18, 20, DateTimeKind.Utc), attributes.LastWriteTimeUtc); + Assert.AreEqual(DateTimeKind.Utc, attributes.LastWriteTimeUtc.Kind); + + var attributesString = attributes.ToString(); + Assert.IsTrue(attributesString.StartsWith("-rwsr--r-- Size: ", StringComparison.Ordinal)); + Assert.Contains(" LastWriteTime: ", attributesString, StringComparison.CurrentCulture); + } + + [TestMethod] + [DataRow((short)8888)] + [DataRow((short)10000)] + [DataRow((short)8000)] + [DataRow((short)0080)] + [DataRow((short)0008)] + [DataRow((short)1797)] + [DataRow((short)-1)] + [DataRow(short.MaxValue)] + public void SetPermissions_InvalidMode_ThrowsArgumentOutOfRangeException(short mode) + { + var attributes = SftpFileAttributes.FromBytes([0, 0, 0, 0]); + + var ex = Assert.Throws(() => attributes.SetPermissions(mode)); + Assert.AreEqual("mode", ex.ParamName); + } + + [TestMethod] + [DataRow((short)0777, false, false, false, true, true, true, true, true, true, true, true, true)] + [DataRow((short)0755, false, false, false, true, true, true, true, false, true, true, false, true)] + [DataRow((short)0644, false, false, false, true, true, false, true, false, false, true, false, false)] + [DataRow((short)0444, false, false, false, true, false, false, true, false, false, true, false, false)] + [DataRow((short)0000, false, false, false, false, false, false, false, false, false, false, false, false)] + [DataRow((short)4700, true, false, false, true, true, true, false, false, false, false, false, false)] + [DataRow((short)3001, false, true, true, false, false, false, false, false, false, false, false, true)] + [DataRow((short)7777, true, true, true, true, true, true, true, true, true, true, true, true)] + public void SetPermissions_ValidMode( + short mode, + bool setUid, bool setGid, bool sticky, + bool ownerRead, bool ownerWrite, bool ownerExec, + bool groupRead, bool groupWrite, bool groupExec, + bool othersRead, bool othersWrite, bool othersExec) + { + var attributes = SftpFileAttributes.FromBytes([0, 0, 0, 0]); + + attributes.SetPermissions(mode); + + Assert.AreEqual(setUid, attributes.IsUIDBitSet); + Assert.AreEqual(setGid, attributes.IsGroupIDBitSet); + Assert.AreEqual(sticky, attributes.IsStickyBitSet); + Assert.AreEqual(ownerRead, attributes.OwnerCanRead); + Assert.AreEqual(ownerWrite, attributes.OwnerCanWrite); + Assert.AreEqual(ownerExec, attributes.OwnerCanExecute); + Assert.AreEqual(groupRead, attributes.GroupCanRead); + Assert.AreEqual(groupWrite, attributes.GroupCanWrite); + Assert.AreEqual(groupExec, attributes.GroupCanExecute); + Assert.AreEqual(othersRead, attributes.OthersCanRead); + Assert.AreEqual(othersWrite, attributes.OthersCanWrite); + Assert.AreEqual(othersExec, attributes.OthersCanExecute); + } + + [TestMethod] + [DataRow(0xC000u, (short)1770, "srwxrwx--T")] // Socket + [DataRow(0xA000u, (short)2707, "lrwx--Srwx")] // Symbolic link + [DataRow(0x8000u, (short)4755, "-rwsr-xr-x")] // Regular file + [DataRow(0x8000u, (short)4644, "-rwSr--r--")] // Regular file + [DataRow(0x6000u, (short)2711, "brwx--s--x")] // Block device + [DataRow(0x4000u, (short)1777, "drwxrwxrwt")] // Directory + [DataRow(0x4000u, (short)1776, "drwxrwxrwT")] // Directory + [DataRow(0x2000u, (short)0660, "crw-rw----")] // Character device + [DataRow(0x1000u, (short)0022, "p----w--w-")] // Named pipe + public void ToStringWithPermissions( + uint fileType, + short permissions, + string expected) + { + var attributeBytes = new byte[8]; + attributeBytes[3] = 0x4; // SSH_FILEXFER_ATTR_PERMISSIONS + BinaryPrimitives.WriteUInt32BigEndian(attributeBytes.AsSpan(4), fileType); + + var attributes = SftpFileAttributes.FromBytes(attributeBytes); + + attributes.SetPermissions(permissions); + + Assert.AreEqual(expected, attributes.ToString()); + } + } +} diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTestBase.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTestBase.cs index c48836ba..1cb1207b 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTestBase.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpFileReaderTestBase.cs @@ -41,12 +41,6 @@ namespace Renci.SshNet.Tests.Classes.Sftp protected abstract void Act(); - protected static SftpFileAttributes CreateSftpFileAttributes(long size) - { - var utcDefault = DateTime.SpecifyKind(default, DateTimeKind.Utc); - return new SftpFileAttributes(utcDefault, utcDefault, size, default, default, default, null); - } - protected static byte[] CreateByteArray(Random random, int length) { var chunk = new byte[length]; diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestRead.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestRead.cs index 5a7fd28a..b0bc6157 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestRead.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestRead.cs @@ -12,6 +12,7 @@ using Renci.SshNet.Channels; using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp { @@ -73,7 +74,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp _sftpNameResponse = new SftpNameResponseBuilder().WithProtocolVersion(_protocolVersion) .WithResponseId(1) .WithEncoding(_encoding) - .WithFile("XYZ", SftpFileAttributes.Empty) + .WithFile("XYZ", SftpFileAttributesBuilder.Empty) .Build(); #endregion SftpSession.Connect() diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestStatVfs.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestStatVfs.cs index a80c744d..a5086d3d 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestStatVfs.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_Connected_RequestStatVfs.cs @@ -10,6 +10,7 @@ using Renci.SshNet.Channels; using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp { @@ -70,7 +71,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp _sftpNameResponse = new SftpNameResponseBuilder().WithProtocolVersion(_protocolVersion) .WithResponseId(1U) .WithEncoding(_encoding) - .WithFile("ABC", SftpFileAttributes.Empty) + .WithFile("ABC", SftpFileAttributesBuilder.Empty) .Build(); #endregion SftpSession.Connect() diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_MultipleSftpMessagesInSingleSshDataMessage.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_MultipleSftpMessagesInSingleSshDataMessage.cs index 762436eb..6bee762b 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_MultipleSftpMessagesInSingleSshDataMessage.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_MultipleSftpMessagesInSingleSshDataMessage.cs @@ -11,6 +11,7 @@ using Renci.SshNet.Channels; using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp { @@ -75,7 +76,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp _sftpNameResponse = new SftpNameResponseBuilder().WithProtocolVersion(_protocolVersion) .WithResponseId(1) .WithEncoding(_encoding) - .WithFile("/ABC", SftpFileAttributes.Empty) + .WithFile("/ABC", SftpFileAttributesBuilder.Empty) .Build(); #endregion SftpSession.Connect() diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_MultipleSftpMessagesSplitOverMultipleSshDataMessages.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_MultipleSftpMessagesSplitOverMultipleSshDataMessages.cs index e2a6cf58..37b7d4b8 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_MultipleSftpMessagesSplitOverMultipleSshDataMessages.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_MultipleSftpMessagesSplitOverMultipleSshDataMessages.cs @@ -11,6 +11,7 @@ using Renci.SshNet.Channels; using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp { @@ -75,7 +76,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp _sftpNameResponse = new SftpNameResponseBuilder().WithProtocolVersion(_protocolVersion) .WithResponseId(1) .WithEncoding(_encoding) - .WithFile("/ABC", SftpFileAttributes.Empty) + .WithFile("/ABC", SftpFileAttributesBuilder.Empty) .Build(); #endregion SftpSession.Connect() diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_SingleSftpMessageInSshDataMessage.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_SingleSftpMessageInSshDataMessage.cs index 3e8e2890..f5f570e6 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_SingleSftpMessageInSshDataMessage.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpSessionTest_DataReceived_SingleSftpMessageInSshDataMessage.cs @@ -11,6 +11,7 @@ using Renci.SshNet.Channels; using Renci.SshNet.Common; using Renci.SshNet.Sftp; using Renci.SshNet.Sftp.Responses; +using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes.Sftp { @@ -71,7 +72,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp _sftpNameResponse = new SftpNameResponseBuilder().WithProtocolVersion(_protocolVersion) .WithResponseId(1) .WithEncoding(_encoding) - .WithFile("/ABC", SftpFileAttributes.Empty) + .WithFile("/ABC", SftpFileAttributesBuilder.Empty) .Build(); #endregion SftpSession.Connect() diff --git a/test/Renci.SshNet.Tests/Classes/Sftp/SftpVersionResponseBuilder.cs b/test/Renci.SshNet.Tests/Classes/Sftp/SftpVersionResponseBuilder.cs index f69af49b..0590c65e 100644 --- a/test/Renci.SshNet.Tests/Classes/Sftp/SftpVersionResponseBuilder.cs +++ b/test/Renci.SshNet.Tests/Classes/Sftp/SftpVersionResponseBuilder.cs @@ -31,7 +31,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp var sftpVersionResponse = new SftpVersionResponse() { Version = _version, - Extentions = _extensions + Extensions = _extensions }; return sftpVersionResponse; } diff --git a/test/Renci.SshNet.Tests/Classes/ShellStreamTest_Write_WriteBufferNotEmptyAndWriteMoreBytesThanBufferCanContain.cs b/test/Renci.SshNet.Tests/Classes/ShellStreamTest_Write_WriteBufferNotEmptyAndWriteMoreBytesThanBufferCanContain.cs index 3aa6b65c..47c2c870 100644 --- a/test/Renci.SshNet.Tests/Classes/ShellStreamTest_Write_WriteBufferNotEmptyAndWriteMoreBytesThanBufferCanContain.cs +++ b/test/Renci.SshNet.Tests/Classes/ShellStreamTest_Write_WriteBufferNotEmptyAndWriteMoreBytesThanBufferCanContain.cs @@ -10,7 +10,6 @@ using Moq; using Renci.SshNet.Channels; using Renci.SshNet.Common; -using Renci.SshNet.Tests.Common; namespace Renci.SshNet.Tests.Classes { @@ -60,9 +59,7 @@ namespace Renci.SshNet.Tests.Classes _offset = 0; _count = _data.Length; - _expectedBytesSent = new ArrayBuilder().Add(_bufferData) - .Add(_data, 0, _bufferSize - _bufferData.Length) - .Build(); + _expectedBytesSent = [.. _bufferData, .. _data.Take(0, _bufferSize - _bufferData.Length)]; } private void CreateMocks() diff --git a/test/Renci.SshNet.Tests/Common/ArrayBuilder`1.cs b/test/Renci.SshNet.Tests/Common/ArrayBuilder`1.cs deleted file mode 100644 index 9f9f7d65..00000000 --- a/test/Renci.SshNet.Tests/Common/ArrayBuilder`1.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; - -namespace Renci.SshNet.Tests.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/test/Renci.SshNet.Tests/Common/Extensions.cs b/test/Renci.SshNet.Tests/Common/Extensions.cs index 5f8b1e31..977c182d 100644 --- a/test/Renci.SshNet.Tests/Common/Extensions.cs +++ b/test/Renci.SshNet.Tests/Common/Extensions.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Renci.SshNet.Common; -using Renci.SshNet.Sftp; namespace Renci.SshNet.Tests.Common { @@ -23,44 +21,5 @@ namespace Renci.SshNet.Tests.Common return reportedExceptions; } - - public static byte[] Copy(this byte[] buffer) - { - var copy = new byte[buffer.Length]; - Buffer.BlockCopy(buffer, 0, copy, 0, buffer.Length); - return copy; - } - - /// - /// Creates a deep clone of the current instance. - /// - /// - /// A deep clone of the current instance. - /// - internal static SftpFileAttributes Clone(this SftpFileAttributes value) - { - Dictionary clonedExtensions; - - if (value.Extensions != null) - { - clonedExtensions = new Dictionary(value.Extensions.Count); - foreach (var entry in value.Extensions) - { - clonedExtensions.Add(entry.Key, entry.Value); - } - } - else - { - clonedExtensions = null; - } - - return new SftpFileAttributes(value.LastAccessTimeUtc, - value.LastWriteTimeUtc, - value.Size, - value.UserId, - value.GroupId, - value.Permissions, - clonedExtensions); - } } } diff --git a/test/Renci.SshNet.Tests/Common/SftpFileAttributesBuilder.cs b/test/Renci.SshNet.Tests/Common/SftpFileAttributesBuilder.cs index ca14b16c..50e772b9 100644 --- a/test/Renci.SshNet.Tests/Common/SftpFileAttributesBuilder.cs +++ b/test/Renci.SshNet.Tests/Common/SftpFileAttributesBuilder.cs @@ -1,4 +1,5 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using Renci.SshNet.Sftp; @@ -7,18 +8,21 @@ namespace Renci.SshNet.Tests.Common { public class SftpFileAttributesBuilder { + public static SftpFileAttributes Empty + { + get + { + return new SftpFileAttributesBuilder().Build(); + } + } + private DateTime? _lastAccessTime; private DateTime? _lastWriteTime; private long? _size; private int? _userId; private int? _groupId; private uint? _permissions; - private readonly IDictionary _extensions; - - public SftpFileAttributesBuilder() - { - _extensions = new Dictionary(); - } + private Dictionary? _extensions; public SftpFileAttributesBuilder WithLastAccessTime(DateTime lastAccessTime) { @@ -58,6 +62,7 @@ namespace Renci.SshNet.Tests.Common public SftpFileAttributesBuilder WithExtension(string name, string value) { + _extensions ??= []; _extensions.Add(name, value); return this; }