mirror of
https://github.com/sshnet/SSH.NET.git
synced 2026-09-10 09:15:47 +00:00
Merge remote-tracking branch 'upstream/develop' into net10
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the specified <see cref="Socket"/> can be used
|
||||
/// to send data.
|
||||
/// </summary>
|
||||
/// <param name="socket">The <see cref="Socket"/> to check.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if <paramref name="socket"/> can be written to; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
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 };
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The implementation of start key exchange algorithm.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes key exchange algorithm.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public override void Finish()
|
||||
{
|
||||
base.Finish();
|
||||
FinishImpl();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The implementation of finish key exchange algorithm.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_impl?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets algorithm name.
|
||||
@@ -38,10 +36,8 @@ namespace Renci.SshNet.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes key exchange algorithm.
|
||||
/// </summary>
|
||||
public override void Finish()
|
||||
/// <inheritdoc/>
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets algorithm name.
|
||||
@@ -39,10 +35,8 @@ namespace Renci.SshNet.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finishes key exchange algorithm.
|
||||
/// </summary>
|
||||
public override void Finish()
|
||||
/// <inheritdoc/>
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+99
-240
@@ -78,12 +78,6 @@ namespace Renci.SshNet
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Holds an object that is used to ensure only a single thread can read from
|
||||
/// <see cref="_socket"/> at any given time.
|
||||
/// </summary>
|
||||
private readonly Lock _socketReadLock = new Lock();
|
||||
|
||||
/// <summary>
|
||||
/// Holds an object that is used to ensure only a single thread can write to
|
||||
/// <see cref="_socket"/> at any given time.
|
||||
@@ -102,7 +96,7 @@ namespace Renci.SshNet
|
||||
/// This is also used to ensure that <see cref="_socket"/> will not be disposed
|
||||
/// while performing a given operation or set of operations on <see cref="_socket"/>.
|
||||
/// </remarks>
|
||||
private readonly SemaphoreSlim _socketDisposeLock = new SemaphoreSlim(1, 1);
|
||||
private readonly Lock _socketDisposeLock = new Lock();
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <exception cref="InvalidOperationException">The size of the packet exceeds the maximum size defined by the protocol.</exception>
|
||||
internal void SendMessage(Message message)
|
||||
{
|
||||
if (!_socket.CanWrite())
|
||||
if (!_socket.IsConnected())
|
||||
{
|
||||
throw new SshConnectionException("Client not connected.");
|
||||
}
|
||||
@@ -1158,9 +1146,7 @@ namespace Renci.SshNet
|
||||
/// </remarks>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the socket is connected.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the socket is connected; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// As a first check we verify whether <see cref="Socket.Connected"/> is
|
||||
/// <see langword="true"/>. However, this only returns the state of the socket as of
|
||||
/// the last I/O operation.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Therefore we use the combination of <see cref="Socket.Poll(int, SelectMode)"/> with mode <see cref="SelectMode.SelectRead"/>
|
||||
/// and <see cref="Socket.Available"/> to verify if the socket is still connected.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The MSDN doc mention the following on the return value of <see cref="Socket.Poll(int, SelectMode)"/>
|
||||
/// with mode <see cref="SelectMode.SelectRead"/>:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// <description><see langword="true"/> if data is available for reading;</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description><see langword="true"/> if the connection has been closed, reset, or terminated; otherwise, returns <see langword="false"/>.</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <c>Conclusion:</c> when the return value is <see langword="true"/> - but no data is available for reading - then
|
||||
/// the socket is no longer connected.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When a <see cref="Socket"/> is used from multiple threads, there's a race condition
|
||||
/// between the invocation of <see cref="Socket.Poll(int, SelectMode)"/> and the moment
|
||||
/// when the value of <see cref="Socket.Available"/> is obtained. To workaround this issue
|
||||
/// we synchronize reads from the <see cref="Socket"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// We assume the socket is still connected if the read lock cannot be acquired immediately.
|
||||
/// In this case, we just return <see langword="true"/> 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a blocking read on the socket until <paramref name="length"/> bytes are received.
|
||||
/// </summary>
|
||||
@@ -1958,44 +1856,37 @@ namespace Renci.SshNet
|
||||
/// </summary>
|
||||
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
|
||||
/// <param name="exp">The <see cref="Exception"/>.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the message in bytes.
|
||||
/// </summary>
|
||||
@@ -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<SftpStatusResponse> statusAction)
|
||||
: this(protocolVersion, requestId, path, encoding, SftpFileAttributes.Empty, statusAction)
|
||||
{
|
||||
}
|
||||
|
||||
private SftpMkDirRequest(uint protocolVersion, uint requestId, string path, Encoding encoding, SftpFileAttributes attributes, Action<SftpStatusResponse> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace Renci.SshNet.Sftp.Requests
|
||||
{
|
||||
private readonly Action<SftpHandleResponse> _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; }
|
||||
|
||||
/// <summary>
|
||||
@@ -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<SftpHandleResponse> handleAction, Action<SftpStatusResponse> 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<SftpHandleResponse> handleAction, Action<SftpStatusResponse> 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)
|
||||
|
||||
@@ -11,14 +11,14 @@ namespace Renci.SshNet.Sftp.Responses
|
||||
|
||||
public uint Version { get; set; }
|
||||
|
||||
public IDictionary<string, string> Extentions { get; set; }
|
||||
public IDictionary<string, string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, string> _originalExtensions;
|
||||
|
||||
private bool _isBitFiledsBitSet;
|
||||
private bool _isUIDBitSet;
|
||||
private bool _isGroupIDBitSet;
|
||||
private bool _isStickyBitSet;
|
||||
private readonly Dictionary<string, string>? _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
|
||||
/// <value>
|
||||
/// <see langword="true"/> if file represents a socket; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsSocket { get; private set; }
|
||||
public bool IsSocket
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_IFMT) == S_IFSOCK;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether file represents a symbolic link.
|
||||
@@ -177,7 +182,13 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if file represents a symbolic link; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsSymbolicLink { get; private set; }
|
||||
public bool IsSymbolicLink
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_IFMT) == S_IFLNK;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether file represents a regular file.
|
||||
@@ -185,7 +196,13 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if file represents a regular file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsRegularFile { get; private set; }
|
||||
public bool IsRegularFile
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_IFMT) == S_IFREG;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether file represents a block device.
|
||||
@@ -193,7 +210,13 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if file represents a block device; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsBlockDevice { get; private set; }
|
||||
public bool IsBlockDevice
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_IFMT) == S_IFBLK;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether file represents a directory.
|
||||
@@ -201,7 +224,13 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if file represents a directory; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsDirectory { get; private set; }
|
||||
public bool IsDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_IFMT) == S_IFDIR;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether file represents a character device.
|
||||
@@ -209,7 +238,13 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if file represents a character device; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsCharacterDevice { get; private set; }
|
||||
public bool IsCharacterDevice
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_IFMT) == S_IFCHR;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether file represents a named pipe.
|
||||
@@ -217,7 +252,88 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if file represents a named pipe; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsNamedPipe { get; private set; }
|
||||
public bool IsNamedPipe
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_IFMT) == S_IFIFO;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the setuid bit is set.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> if the setuid bit is set; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsUIDBitSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_ISUID) == S_ISUID;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Permissions |= S_ISUID;
|
||||
}
|
||||
else
|
||||
{
|
||||
Permissions &= ~S_ISUID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the setgid bit is set.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> if the setgid bit is set; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsGroupIDBitSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_ISGID) == S_ISGID;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Permissions |= S_ISGID;
|
||||
}
|
||||
else
|
||||
{
|
||||
Permissions &= ~S_ISGID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the sticky bit is set.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> if the sticky bit is set; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool IsStickyBitSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return (Permissions & S_ISVTX) == S_ISVTX;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Permissions |= S_ISVTX;
|
||||
}
|
||||
else
|
||||
{
|
||||
Permissions &= ~S_ISVTX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the owner can read from this file.
|
||||
@@ -225,7 +341,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if owner can read from this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the owner can write into this file.
|
||||
@@ -233,7 +366,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if owner can write into this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the owner can execute this file.
|
||||
@@ -241,7 +391,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if owner can execute this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the group members can read from this file.
|
||||
@@ -249,7 +416,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if group members can read from this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the group members can write into this file.
|
||||
@@ -257,7 +441,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if group members can write into this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the group members can execute this file.
|
||||
@@ -265,7 +466,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if group members can execute this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the others can read from this file.
|
||||
@@ -273,7 +491,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if others can read from this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the others can write into this file.
|
||||
@@ -281,7 +516,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if others can write into this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the others can execute this file.
|
||||
@@ -289,7 +541,24 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// <see langword="true"/> if others can execute this file; otherwise, <see langword="false"/>.
|
||||
/// </value>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the extensions.
|
||||
@@ -297,165 +566,11 @@ namespace Renci.SshNet.Sftp
|
||||
/// <value>
|
||||
/// The extensions.
|
||||
/// </value>
|
||||
public IDictionary<string, string> Extensions { get; private set; }
|
||||
public IDictionary<string, string>? 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<string, string> extensions)
|
||||
internal SftpFileAttributes(DateTime lastAccessTimeUtc, DateTime lastWriteTimeUtc, long size, int userId, int groupId, uint permissions, Dictionary<string, string>? extensions)
|
||||
{
|
||||
LastAccessTimeUtc = _originalLastAccessTimeUtc = lastAccessTimeUtc;
|
||||
LastWriteTimeUtc = _originalLastWriteTimeUtc = lastWriteTimeUtc;
|
||||
@@ -467,31 +582,118 @@ namespace Renci.SshNet.Sftp
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the permissions.
|
||||
/// Sets the POSIX permissions for this file.
|
||||
/// </summary>
|
||||
/// <param name="mode">The mode.</param>
|
||||
/// <param name="mode">
|
||||
/// The permission mode as an octal number (e.g., <c>755</c>, <c>644</c>, <c>1777</c>).
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// <paramref name="mode"/> has more than 4 digits or cannot be interpreted as an octal number.
|
||||
/// </exception>
|
||||
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);
|
||||
/// <inheritdoc/>
|
||||
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' : '-');
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<string, string> extensions = null;
|
||||
Dictionary<string, string>? 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<string, string>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ namespace Renci.SshNet.Sftp
|
||||
if (response is SftpVersionResponse versionResponse)
|
||||
{
|
||||
ProtocolVersion = versionResponse.Version;
|
||||
_supportedExtensions = versionResponse.Extentions;
|
||||
_supportedExtensions = versionResponse.Extensions;
|
||||
|
||||
_ = _sftpVersionConfirmed.Set();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Renci.SshNet.IntegrationTests.Common
|
||||
.ClearHostKeyAlgorithms()
|
||||
.ClearPublicKeyAcceptedAlgorithms()
|
||||
.ClearMessageAuthenticationCodeAlgorithms()
|
||||
.PermitTTY(true)
|
||||
.WithUsePAM(true)
|
||||
.Update()
|
||||
.Restart();
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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<SshConnectionException>(() => 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<SshConnectionException>(() => 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);
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ArgumentOutOfRangeException>(() => 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Renci.SshNet.Tests.Classes.Sftp
|
||||
var sftpVersionResponse = new SftpVersionResponse()
|
||||
{
|
||||
Version = _version,
|
||||
Extentions = _extensions
|
||||
Extensions = _extensions
|
||||
};
|
||||
return sftpVersionResponse;
|
||||
}
|
||||
|
||||
+1
-4
@@ -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<byte>().Add(_bufferData)
|
||||
.Add(_data, 0, _bufferSize - _bufferData.Length)
|
||||
.Build();
|
||||
_expectedBytesSent = [.. _bufferData, .. _data.Take(0, _bufferSize - _bufferData.Length)];
|
||||
}
|
||||
|
||||
private void CreateMocks()
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Renci.SshNet.Tests.Common
|
||||
{
|
||||
public class ArrayBuilder<T>
|
||||
{
|
||||
private readonly List<T> _buffer;
|
||||
|
||||
public ArrayBuilder()
|
||||
{
|
||||
_buffer = new List<T>();
|
||||
}
|
||||
|
||||
public ArrayBuilder<T> Add(T[] array)
|
||||
{
|
||||
return Add(array, 0, array.Length);
|
||||
}
|
||||
|
||||
public ArrayBuilder<T> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a deep clone of the current instance.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A deep clone of the current instance.
|
||||
/// </returns>
|
||||
internal static SftpFileAttributes Clone(this SftpFileAttributes value)
|
||||
{
|
||||
Dictionary<string, string> clonedExtensions;
|
||||
|
||||
if (value.Extensions != null)
|
||||
{
|
||||
clonedExtensions = new Dictionary<string, string>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, string> _extensions;
|
||||
|
||||
public SftpFileAttributesBuilder()
|
||||
{
|
||||
_extensions = new Dictionary<string, string>();
|
||||
}
|
||||
private Dictionary<string, string>? _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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user