Public Access
Clear the SSH gate: window-change reaches the remote, and licence as MIT
Licence is MIT, set solution-wide rather than only on the packable project: DodoSSH.Contracts is published so clients can build against it, and a package with no licence expression is one a corporate policy scanner rejects outright. The SSH.NET spike is the M1 client gate and it passes. SSH.NET 2025.1.0 exposes ShellStream.ChangeWindowSize, but a method existing is not the remote observing it, so the tests read `stty size` back from a real sshd after resizing rather than asserting the call did not throw. Repeated resizes each take effect too, which matters because dragging a window edge produces a stream of them. The IChannelSession fallback is not needed. Also verified against a real sshd: password and public-key auth, that the host key arrives as a raw blob we can fingerprint ourselves rather than reading SSH.NET's MD5 property, and that refusing the key via CanTrust actually aborts the connection -- without which the TOFU dialog would be decoration. Kept as a permanent suite, not deleted after the spike. An upgrade that silently stopped sending the request would present as wrapped output only after a resize, which is easy to misattribute to the terminal emulator. Two bugs in the test itself, both worth naming because either would have been read as "resize does not work": - A PTY emits CRLF, and the anchored regex rejected the CR. The output visibly contained `24 80` while the match failed. - Each read can begin with output still buffered from the previous command, including its size line. Taking the first match would have reported the pre-resize size. platform-flags.md now records window-change as resolved rather than unverified -- a stale flag is worse than none -- plus the three real SSH.NET limits found on the way: ShellStream does not override ReadAsync so every idle session parks a pool thread, one connection cannot serve both SshClient and SftpClient, and agent forwarding needs an upstream change.
This commit is contained in:
@@ -37,6 +37,13 @@
|
|||||||
<Company>DodoTech</Company>
|
<Company>DodoTech</Company>
|
||||||
<Product>DodoSSH</Product>
|
<Product>DodoSSH</Product>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
|
<Copyright>Copyright (c) 2026 Jaap-Jan de Wit (DodoTech)</Copyright>
|
||||||
|
<!--
|
||||||
|
Set solution-wide rather than only on the packable project. DodoSSH.Contracts is
|
||||||
|
published to GitHub Packages so clients can build against it, and a package without a
|
||||||
|
licence expression is one a corporate consumer's policy scanner rejects outright.
|
||||||
|
-->
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup Label="Analyzers">
|
<ItemGroup Label="Analyzers">
|
||||||
|
|||||||
@@ -64,6 +64,17 @@
|
|||||||
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Label="Desktop client">
|
||||||
|
<!--
|
||||||
|
SSH.NET already covers PTY shells, all three auth methods, ed25519/RSA/ECDSA, encrypted
|
||||||
|
keys including PuTTY .ppk, SFTP, and local/remote/dynamic forwarding. The gaps are
|
||||||
|
agent forwarding (needs an upstream change; de-scoped from v1) and being handed a
|
||||||
|
pre-connected Stream — it performs its own socket connect, which is why the relay and
|
||||||
|
ProxyJump both go through a loopback TCP bridge. See docs/adr/.
|
||||||
|
-->
|
||||||
|
<PackageVersion Include="SSH.NET" Version="2025.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Label="Analyzers">
|
<ItemGroup Label="Analyzers">
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="5.6.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
|
||||||
@@ -86,6 +97,8 @@
|
|||||||
<PackageVersion Include="Shouldly" Version="4.3.0" />
|
<PackageVersion Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageVersion Include="NSubstitute" Version="6.0.0" />
|
<PackageVersion Include="NSubstitute" Version="6.0.0" />
|
||||||
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.13.0" />
|
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.13.0" />
|
||||||
|
<!-- Generic container, for the OpenSSH server the SSH suite talks to. -->
|
||||||
|
<PackageVersion Include="Testcontainers" Version="4.13.0" />
|
||||||
<PackageVersion Include="Respawn" Version="7.0.0" />
|
<PackageVersion Include="Respawn" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.10" />
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -16,10 +16,12 @@
|
|||||||
<Project Path="src/DodoSSH.Domain/DodoSSH.Domain.csproj" />
|
<Project Path="src/DodoSSH.Domain/DodoSSH.Domain.csproj" />
|
||||||
<Project Path="src/DodoSSH.Infrastructure/DodoSSH.Infrastructure.csproj" />
|
<Project Path="src/DodoSSH.Infrastructure/DodoSSH.Infrastructure.csproj" />
|
||||||
<Project Path="src/DodoSSH.Api/DodoSSH.Api.csproj" />
|
<Project Path="src/DodoSSH.Api/DodoSSH.Api.csproj" />
|
||||||
|
<Project Path="src/DodoSSH.Client.Ssh/DodoSSH.Client.Ssh.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
|
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/DodoSSH.Api.Tests/DodoSSH.Api.Tests.csproj" />
|
<Project Path="tests/DodoSSH.Api.Tests/DodoSSH.Api.Tests.csproj" />
|
||||||
|
<Project Path="tests/DodoSSH.Client.Ssh.Tests/DodoSSH.Client.Ssh.Tests.csproj" />
|
||||||
<Project Path="tests/DodoSSH.Contracts.Tests/DodoSSH.Contracts.Tests.csproj" />
|
<Project Path="tests/DodoSSH.Contracts.Tests/DodoSSH.Contracts.Tests.csproj" />
|
||||||
<Project Path="tests/DodoSSH.Crypto.Tests/DodoSSH.Crypto.Tests.csproj" />
|
<Project Path="tests/DodoSSH.Crypto.Tests/DodoSSH.Crypto.Tests.csproj" />
|
||||||
<Project Path="tests/DodoSSH.Domain.Tests/DodoSSH.Domain.Tests.csproj" />
|
<Project Path="tests/DodoSSH.Domain.Tests/DodoSSH.Domain.Tests.csproj" />
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Jaap-Jan de Wit (DodoTech)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -100,4 +100,4 @@ off-Windows.
|
|||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Not yet chosen.
|
[MIT](LICENSE).
|
||||||
|
|||||||
+21
-3
@@ -36,9 +36,27 @@ Ubuntu on both Wayland and X11, Fedora KDE, and macOS 15. This is why the termin
|
|||||||
`ITerminalHost` — that abstraction is what preserves the option to swap backends, and it should
|
`ITerminalHost` — that abstraction is what preserves the option to swap backends, and it should
|
||||||
not be collapsed away for convenience.
|
not be collapsed away for convenience.
|
||||||
|
|
||||||
**SSH.NET's `window-change` on `ShellStream` is unverified.** A terminal that cannot resize is
|
**SSH.NET's `window-change` is verified working** as of 2025.1.0 — resolved, not a flag.
|
||||||
unusable, so this gates M1's client work. Fall back to `IChannelSession` if it does not work, and
|
`ShellStream.ChangeWindowSize(columns, rows, width, height)` exists and the remote genuinely
|
||||||
prefer forking and upstreaming a fix over reflection as a long-term answer.
|
observes it: `PtyAndResizeSpikeTests` reads `stty size` back from a real sshd after resizing, and
|
||||||
|
repeated resizes each take effect. The `IChannelSession` fallback is not needed. That suite stays
|
||||||
|
in place as a regression guard, because an upgrade that silently stopped sending the request would
|
||||||
|
present as wrapped output only after a resize — easy to misattribute to the terminal emulator.
|
||||||
|
|
||||||
|
**`ShellStream` does not override `ReadAsync`.** The base `Stream` implementation therefore runs
|
||||||
|
the blocking `Read` on a thread-pool thread, so every open session parks one thread for as long as
|
||||||
|
it is idle. Fine for the handful of tabs M1 targets; revisit before advertising many concurrent
|
||||||
|
sessions, since the fix is either an upstream change or driving `IChannelSession` directly.
|
||||||
|
|
||||||
|
**SSH.NET cannot share one connection between `SshClient` and `SftpClient`.** A shell plus SFTP to
|
||||||
|
the same host means two TCP connections, two authentications and — later — two relay sockets.
|
||||||
|
Connect SFTP lazily and reuse the cached decrypted credential so the user is not prompted twice.
|
||||||
|
|
||||||
|
**Agent forwarding is de-scoped from v1.** It needs an upstream SSH.NET change. A vault-backed
|
||||||
|
agent of our own plus ProxyJump covers the real use cases.
|
||||||
|
|
||||||
|
**The SSH suite pulls `linuxserver/openssh-server` from Docker Hub**, which is rate-limited for
|
||||||
|
unauthenticated pulls. If CI starts failing on image pulls rather than on tests, that is why.
|
||||||
|
|
||||||
**MSIX packaging is ruled out, not merely deprioritised.** A packaged app runs WebView2 in an
|
**MSIX packaging is ruled out, not merely deprioritised.** A packaged app runs WebView2 in an
|
||||||
AppContainer where loopback connections are blocked without a `CheckNetIsolation` exemption. The
|
AppContainer where loopback connections are blocked without a `CheckNetIsolation` exemption. The
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The SSH layer. Avalonia-free on purpose: this is the seam that makes connection,
|
||||||
|
authentication, PTY handling and host-key trust unit-testable without a UI toolkit.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SSH.NET" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="DodoSSH.Client.Ssh.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace DodoSSH.Client.Ssh;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// OpenSSH-style SSH host key fingerprints. See docs/crypto.md §8.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// A different thing from a DodoSSH identity fingerprint, and deliberately kept in the format
|
||||||
|
/// users already recognise: <c>SHA256:</c> followed by unpadded standard base64, exactly what
|
||||||
|
/// <c>ssh-keygen -lf</c> prints. A user comparing our string against the one their server
|
||||||
|
/// operator sent them must not have to wonder whether the encodings match.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Computed from the raw host key blob. SSH.NET exposes an MD5 <c>FingerPrint</c> property;
|
||||||
|
/// that is not used — MD5 is banned repo-wide, and no modern operator publishes an MD5 host
|
||||||
|
/// key fingerprint to compare against.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static class SshHostKeyFingerprint
|
||||||
|
{
|
||||||
|
/// <summary>Prefix identifying the hash algorithm, as OpenSSH writes it.</summary>
|
||||||
|
public const string Prefix = "SHA256:";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats a host key blob as <c>SHA256:<base64></c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hostKeyBlob">
|
||||||
|
/// The raw key blob as it arrived on the wire — the same bytes OpenSSH hashes, not a parsed
|
||||||
|
/// or re-encoded form.
|
||||||
|
/// </param>
|
||||||
|
public static string Format(ReadOnlySpan<byte> hostKeyBlob)
|
||||||
|
{
|
||||||
|
if (hostKeyBlob.IsEmpty)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("A host key blob is required.", nameof(hostKeyBlob));
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> digest = stackalloc byte[SHA256.HashSizeInBytes];
|
||||||
|
SHA256.HashData(hostKeyBlob, digest);
|
||||||
|
|
||||||
|
// Standard base64, not base64url, and unpadded — OpenSSH's exact rendering. Using the
|
||||||
|
// URL alphabet here would produce a string that looks right and never matches.
|
||||||
|
return Prefix + Convert.ToBase64String(digest).TrimEnd('=');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares two fingerprints in constant time.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A fingerprint is not a secret, so this is not about a timing oracle. It is about refusing
|
||||||
|
/// to let a mismatch be decided by ordinal string comparison somewhere that later gets
|
||||||
|
/// "optimised" into a case-insensitive or culture-aware comparison — base64 is
|
||||||
|
/// case-sensitive, and a case-insensitive match here would accept a different key.
|
||||||
|
/// </remarks>
|
||||||
|
public static bool Equal(string? left, string? right)
|
||||||
|
{
|
||||||
|
if (left is null || right is null || left.Length != right.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CryptographicOperations.FixedTimeEquals(
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(left),
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(right));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Meziantou.Analyzer": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.0.134, )",
|
||||||
|
"resolved": "3.0.134",
|
||||||
|
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[5.6.0, )",
|
||||||
|
"resolved": "5.6.0",
|
||||||
|
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||||
|
},
|
||||||
|
"SSH.NET": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2025.1.0, )",
|
||||||
|
"resolved": "2025.1.0",
|
||||||
|
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
|
||||||
|
"dependencies": {
|
||||||
|
"BouncyCastle.Cryptography": "2.6.2",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.0.2",
|
||||||
|
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.0.3",
|
||||||
|
"contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BouncyCastle.Cryptography": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.6.2, )",
|
||||||
|
"resolved": "2.6.2",
|
||||||
|
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1429,15 +1429,6 @@
|
|||||||
"resolved": "1.0.5",
|
"resolved": "1.0.5",
|
||||||
"contentHash": "LaSDYOJDh2WncgRboqiWtk/Igqoim/LV7v808qBeWY/f36Ol5oEKguEYpKrWw5ap8KYP0SRXf7/v3zil9koY6Q=="
|
"contentHash": "LaSDYOJDh2WncgRboqiWtk/Igqoim/LV7v808qBeWY/f36Ol5oEKguEYpKrWw5ap8KYP0SRXf7/v3zil9koY6Q=="
|
||||||
},
|
},
|
||||||
"SSH.NET": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2025.1.0",
|
|
||||||
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
|
|
||||||
"dependencies": {
|
|
||||||
"BouncyCastle.Cryptography": "2.6.2",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Stef.Validation": {
|
"Stef.Validation": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "0.3.0",
|
"resolved": "0.3.0",
|
||||||
@@ -1470,18 +1461,6 @@
|
|||||||
"System.CodeDom": "6.0.0"
|
"System.CodeDom": "6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Testcontainers": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.13.0",
|
|
||||||
"contentHash": "j8vi9jPBNSwaraGGx8w+2gtZyWrlbKxdhiGMS3nektg+KiwjFWx9ghCjs57EoQfvI+IAbzti0oQJupQChwgMog==",
|
|
||||||
"dependencies": {
|
|
||||||
"Docker.DotNet.Enhanced": "4.3.3",
|
|
||||||
"Docker.DotNet.Enhanced.X509": "4.3.3",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.3",
|
|
||||||
"SSH.NET": "2025.1.0",
|
|
||||||
"SharpZipLib": "1.4.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TinyMapper.Signed": {
|
"TinyMapper.Signed": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "4.0.0",
|
"resolved": "4.0.0",
|
||||||
@@ -1803,6 +1782,29 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libsodium": "[1.0.22, 1.0.23)"
|
"libsodium": "[1.0.22, 1.0.23)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"SSH.NET": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2025.1.0, )",
|
||||||
|
"resolved": "2025.1.0",
|
||||||
|
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
|
||||||
|
"dependencies": {
|
||||||
|
"BouncyCastle.Cryptography": "2.6.2",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Testcontainers": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[4.13.0, )",
|
||||||
|
"resolved": "4.13.0",
|
||||||
|
"contentHash": "j8vi9jPBNSwaraGGx8w+2gtZyWrlbKxdhiGMS3nektg+KiwjFWx9ghCjs57EoQfvI+IAbzti0oQJupQChwgMog==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced": "4.3.3",
|
||||||
|
"Docker.DotNet.Enhanced.X509": "4.3.3",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3",
|
||||||
|
"SSH.NET": "2025.1.0",
|
||||||
|
"SharpZipLib": "1.4.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Talks to a real OpenSSH server in a container. A mocked SSH library would prove nothing
|
||||||
|
about the two things that actually decide the terminal's design: whether a PTY resize
|
||||||
|
reaches the remote, and whether host keys arrive in a form we can fingerprint.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="../../src/DodoSSH.Client.Ssh/DodoSSH.Client.Ssh.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Referenced directly as well, so spike tests can drive SSH.NET without the wrapper. -->
|
||||||
|
<PackageReference Include="SSH.NET" />
|
||||||
|
<PackageReference Include="Testcontainers" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Renci.SshNet;
|
||||||
|
using Renci.SshNet.Common;
|
||||||
|
|
||||||
|
namespace DodoSSH.Client.Ssh.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The M1 client gate: does a PTY resize actually reach the remote?
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The plan flagged <c>window-change</c> on <c>ShellStream</c> as unverified, and it decides the
|
||||||
|
/// terminal's whole design — a terminal that cannot resize is unusable, and the fallback is
|
||||||
|
/// dropping to <c>IChannelSession</c> and driving the channel requests directly. SSH.NET 2025.1.0
|
||||||
|
/// does expose <c>ChangeWindowSize</c>, but a method existing is not the remote observing it, so
|
||||||
|
/// these read the size back from the server rather than asserting the call did not throw.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Kept as a permanent regression suite rather than deleted after the spike. An SSH.NET upgrade
|
||||||
|
/// that silently stopped sending the request would otherwise ship, and the symptom — wrapped
|
||||||
|
/// output only after the user resizes the window — is easy to blame on the terminal emulator.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[Collection(SshCollection.Name)]
|
||||||
|
public sealed class PtyAndResizeSpikeTests(SshServerFixture fixture)
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(20);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Matches busybox <c>stty size</c> output: rows then columns.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Surrounding horizontal whitespace is tolerated, and the caller normalises CR to LF first.
|
||||||
|
/// A PTY emits CRLF, so an anchored pattern that does not account for the CR fails on output
|
||||||
|
/// that is visibly correct — which reads as "the resize did not work" and sends you looking in
|
||||||
|
/// entirely the wrong place.
|
||||||
|
/// </remarks>
|
||||||
|
private static readonly Regex SizeLine = new(
|
||||||
|
@"^[ \t]*(?<rows>[0-9]{1,5})[ \t]+(?<columns>[0-9]{1,5})[ \t]*$",
|
||||||
|
RegexOptions.Multiline | RegexOptions.ExplicitCapture,
|
||||||
|
TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PasswordAuthentication_Connects()
|
||||||
|
{
|
||||||
|
using var client = CreatePasswordClient();
|
||||||
|
|
||||||
|
await client.ConnectAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
client.IsConnected.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PublicKeyAuthentication_Connects()
|
||||||
|
{
|
||||||
|
// PKCS#1 PEM ("BEGIN RSA PRIVATE KEY"), which SSH.NET has read for years. The vault stores
|
||||||
|
// key material in its own format, so this is only about SSH.NET accepting a standard PEM.
|
||||||
|
var pem = fixture.ClientKey.ExportRSAPrivateKeyPem();
|
||||||
|
using var keyStream = new MemoryStream(Encoding.ASCII.GetBytes(pem));
|
||||||
|
using var privateKey = new PrivateKeyFile(keyStream);
|
||||||
|
|
||||||
|
using var client = new SshClient(
|
||||||
|
new ConnectionInfo(
|
||||||
|
fixture.Host,
|
||||||
|
fixture.Port,
|
||||||
|
SshServerFixture.Username,
|
||||||
|
new PrivateKeyAuthenticationMethod(SshServerFixture.Username, privateKey)));
|
||||||
|
|
||||||
|
await client.ConnectAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
client.IsConnected.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TheHostKey_ArrivesAsARawBlobWeCanFingerprint()
|
||||||
|
{
|
||||||
|
// Known-hosts entries are a synced vault entity, so the fingerprint we store has to be
|
||||||
|
// computed from the wire blob rather than read off a library property. This confirms the
|
||||||
|
// blob is exposed at all, and that our formatting matches OpenSSH's shape.
|
||||||
|
using var client = CreatePasswordClient();
|
||||||
|
|
||||||
|
byte[]? blob = null;
|
||||||
|
string? algorithm = null;
|
||||||
|
|
||||||
|
client.HostKeyReceived += (_, e) =>
|
||||||
|
{
|
||||||
|
blob = e.HostKey;
|
||||||
|
algorithm = e.HostKeyName;
|
||||||
|
};
|
||||||
|
|
||||||
|
await client.ConnectAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
blob.ShouldNotBeNull();
|
||||||
|
blob.Length.ShouldBeGreaterThan(0);
|
||||||
|
algorithm.ShouldNotBeNullOrEmpty();
|
||||||
|
|
||||||
|
var fingerprint = SshHostKeyFingerprint.Format(blob);
|
||||||
|
|
||||||
|
fingerprint.ShouldStartWith(SshHostKeyFingerprint.Prefix);
|
||||||
|
fingerprint.ShouldNotEndWith("=");
|
||||||
|
|
||||||
|
// 32 bytes of SHA-256 render as 43 unpadded base64 characters.
|
||||||
|
fingerprint.Length.ShouldBe(SshHostKeyFingerprint.Prefix.Length + 43);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RefusingTheHostKey_AbortsTheConnection()
|
||||||
|
{
|
||||||
|
// The mismatch path has to be a hard block, not a warning. If CanTrust were advisory the
|
||||||
|
// TOFU dialog would be decoration.
|
||||||
|
using var client = CreatePasswordClient();
|
||||||
|
|
||||||
|
client.HostKeyReceived += (_, e) => e.CanTrust = false;
|
||||||
|
|
||||||
|
await Should.ThrowAsync<SshConnectionException>(
|
||||||
|
async () => await client.ConnectAsync(TestContext.Current.CancellationToken));
|
||||||
|
|
||||||
|
client.IsConnected.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task APtyShell_ReportsTheSizeItWasCreatedWith()
|
||||||
|
{
|
||||||
|
using var client = CreatePasswordClient();
|
||||||
|
await client.ConnectAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
using var shell = client.CreateShellStream("xterm-256color", 80, 24, 800, 600, 4096);
|
||||||
|
|
||||||
|
var (rows, columns) = await ReadSizeAsync(shell);
|
||||||
|
|
||||||
|
rows.ShouldBe(24);
|
||||||
|
columns.ShouldBe(80);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangeWindowSize_IsObservedByTheRemote()
|
||||||
|
{
|
||||||
|
// The gate. Reads the size back from the server after resizing, so this fails if
|
||||||
|
// ChangeWindowSize silently does nothing.
|
||||||
|
using var client = CreatePasswordClient();
|
||||||
|
await client.ConnectAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
using var shell = client.CreateShellStream("xterm-256color", 80, 24, 800, 600, 4096);
|
||||||
|
|
||||||
|
var initial = await ReadSizeAsync(shell);
|
||||||
|
initial.ShouldBe((24, 80));
|
||||||
|
|
||||||
|
shell.ChangeWindowSize(columns: 132, rows: 43, width: 1320, height: 1075);
|
||||||
|
|
||||||
|
var resized = await ReadSizeAsync(shell);
|
||||||
|
|
||||||
|
resized.ShouldBe(
|
||||||
|
(43, 132),
|
||||||
|
"The remote did not observe the window-change request. ShellStream.ChangeWindowSize is "
|
||||||
|
+ "not usable, and the terminal must drive IChannelSession directly instead.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangeWindowSize_CanBeCalledRepeatedly()
|
||||||
|
{
|
||||||
|
// A user dragging a window edge produces a stream of these. If only the first took effect,
|
||||||
|
// the terminal would settle on whatever size the drag started at.
|
||||||
|
using var client = CreatePasswordClient();
|
||||||
|
await client.ConnectAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
using var shell = client.CreateShellStream("xterm-256color", 80, 24, 800, 600, 4096);
|
||||||
|
await ReadSizeAsync(shell);
|
||||||
|
|
||||||
|
foreach (var (columns, rows) in new[] { (100u, 30u), (120u, 40u), (90u, 25u) })
|
||||||
|
{
|
||||||
|
shell.ChangeWindowSize(columns, rows, columns * 10, rows * 25);
|
||||||
|
|
||||||
|
var observed = await ReadSizeAsync(shell);
|
||||||
|
observed.ShouldBe(((int)rows, (int)columns));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Helpers ----
|
||||||
|
|
||||||
|
private SshClient CreatePasswordClient() =>
|
||||||
|
new(fixture.Host, fixture.Port, SshServerFixture.Username, SshServerFixture.Password);
|
||||||
|
|
||||||
|
/// <summary>Asks the remote what size it thinks the terminal is.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The marker is written split — <c>"MAR""KER"</c> — so the PTY's echo of the command line
|
||||||
|
/// does not contain it. Without that the read completes on the echo and never sees the output,
|
||||||
|
/// which looks exactly like a resize that did not take effect.
|
||||||
|
/// </remarks>
|
||||||
|
private static async Task<(int Rows, int Columns)> ReadSizeAsync(ShellStream shell)
|
||||||
|
{
|
||||||
|
shell.WriteLine("""stty size; echo "MAR""KER" """);
|
||||||
|
|
||||||
|
var output = (await ReadUntilAsync(shell, "MARKER")).Replace('\r', '\n');
|
||||||
|
|
||||||
|
var matches = SizeLine.Matches(output);
|
||||||
|
matches.Count.ShouldBeGreaterThan(0, $"No 'rows cols' line in output:\n{output}");
|
||||||
|
|
||||||
|
// The last match, not the first. A read can begin with output still buffered from the
|
||||||
|
// previous command, including its size line — taking the first match would report the size
|
||||||
|
// from before the resize and turn a working resize into a failing test.
|
||||||
|
var match = matches[^1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
int.Parse(match.Groups["rows"].Value, CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(match.Groups["columns"].Value, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Raced against a timeout rather than cancelled. <c>ShellStream</c> does not override
|
||||||
|
/// <c>ReadAsync</c>, so the base implementation runs the blocking read on a pool thread and
|
||||||
|
/// cannot observe a token once it has started — passing one would produce a test that hangs
|
||||||
|
/// instead of failing. The abandoned read dies with the process.
|
||||||
|
/// </remarks>
|
||||||
|
private static async Task<string> ReadUntilAsync(ShellStream shell, string marker)
|
||||||
|
{
|
||||||
|
var accumulated = new StringBuilder();
|
||||||
|
var deadline = TimeProvider.System.GetUtcNow() + ReadTimeout;
|
||||||
|
|
||||||
|
while (TimeProvider.System.GetUtcNow() < deadline)
|
||||||
|
{
|
||||||
|
var buffer = new byte[4096];
|
||||||
|
var read = shell.ReadAsync(buffer.AsMemory()).AsTask();
|
||||||
|
|
||||||
|
var remaining = deadline - TimeProvider.System.GetUtcNow();
|
||||||
|
if (remaining <= TimeSpan.Zero || await Task.WhenAny(read, Task.Delay(remaining)) != read)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = await read;
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulated.Append(Encoding.UTF8.GetString(buffer, 0, count));
|
||||||
|
|
||||||
|
if (accumulated.ToString().Contains(marker, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return accumulated.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TimeoutException(
|
||||||
|
$"Did not see '{marker}' within {ReadTimeout}. Output so far:\n{accumulated}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using DotNet.Testcontainers.Builders;
|
||||||
|
using DotNet.Testcontainers.Containers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace DodoSSH.Client.Ssh.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A real OpenSSH server in a container, with password and public-key auth both enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// One container per assembly. Everything the SSH layer needs to be right about — PTY
|
||||||
|
/// allocation, window-change requests, host key encoding, key formats — is behaviour of a real
|
||||||
|
/// sshd, and none of it can be established against a mock.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The image is Alpine-based, so <c>stty</c> comes from busybox. Its <c>stty size</c> prints
|
||||||
|
/// "rows cols", which is what the resize assertions read.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class SshServerFixture : IAsyncLifetime
|
||||||
|
{
|
||||||
|
/// <summary>The account tests authenticate as.</summary>
|
||||||
|
public const string Username = "dodo";
|
||||||
|
|
||||||
|
/// <summary>Password for password authentication.</summary>
|
||||||
|
public const string Password = "correct-horse-battery-staple";
|
||||||
|
|
||||||
|
private const int SshPort = 2222;
|
||||||
|
|
||||||
|
private IContainer? container;
|
||||||
|
|
||||||
|
/// <summary>Host port the container's sshd is published on.</summary>
|
||||||
|
public ushort Port => container!.GetMappedPublicPort(SshPort);
|
||||||
|
|
||||||
|
/// <summary>Host the container is reachable at.</summary>
|
||||||
|
public string Host => container!.Hostname;
|
||||||
|
|
||||||
|
/// <summary>An RSA key pair whose public half is authorized on the server.</summary>
|
||||||
|
public RSA ClientKey { get; } = RSA.Create(3072);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask InitializeAsync()
|
||||||
|
{
|
||||||
|
container = new ContainerBuilder("linuxserver/openssh-server:latest")
|
||||||
|
.WithEnvironment("PUID", "1000")
|
||||||
|
.WithEnvironment("PGID", "1000")
|
||||||
|
.WithEnvironment("USER_NAME", Username)
|
||||||
|
.WithEnvironment("USER_PASSWORD", Password)
|
||||||
|
.WithEnvironment("PASSWORD_ACCESS", "true")
|
||||||
|
.WithEnvironment("SUDO_ACCESS", "false")
|
||||||
|
.WithEnvironment("PUBLIC_KEY", ExportOpenSshPublicKey(ClientKey))
|
||||||
|
.WithPortBinding(SshPort, assignRandomHostPort: true)
|
||||||
|
|
||||||
|
// The entrypoint generates host keys, rewrites sshd_config and installs the authorized
|
||||||
|
// key before sshd is usable, so a published port is not readiness. Both conditions are
|
||||||
|
// needed: the log line proves the authorized key was installed (a connection accepted
|
||||||
|
// before that fails public-key auth), and the port check proves sshd is actually
|
||||||
|
// accepting. The message is this image's own wording — "Server listening on" is
|
||||||
|
// OpenSSH's and never appears here, which is a wait that hangs rather than fails.
|
||||||
|
.WithWaitStrategy(Wait.ForUnixContainer()
|
||||||
|
.UntilMessageIsLogged("Public key from env variable added")
|
||||||
|
.UntilCommandIsCompleted("sh", "-c", $"netstat -ltn | grep -q ':{SshPort}'"))
|
||||||
|
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await container.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (container is not null)
|
||||||
|
{
|
||||||
|
await container.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientKey.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders an RSA public key in the single-line <c>authorized_keys</c> format.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Hand-encoded because there is no BCL helper. The SSH wire format is a sequence of
|
||||||
|
/// length-prefixed strings: the algorithm name, then the exponent, then the modulus — both
|
||||||
|
/// as signed big-endian integers, which is why a leading zero byte is prepended when the
|
||||||
|
/// high bit is set. Getting that wrong yields a key sshd silently ignores.
|
||||||
|
/// </remarks>
|
||||||
|
private static string ExportOpenSshPublicKey(RSA rsa)
|
||||||
|
{
|
||||||
|
var parameters = rsa.ExportParameters(includePrivateParameters: false);
|
||||||
|
|
||||||
|
using var blob = new MemoryStream();
|
||||||
|
WriteSshString(blob, "ssh-rsa"u8.ToArray());
|
||||||
|
WriteSshMpint(blob, parameters.Exponent!);
|
||||||
|
WriteSshMpint(blob, parameters.Modulus!);
|
||||||
|
|
||||||
|
return $"ssh-rsa {Convert.ToBase64String(blob.ToArray())} dodossh-test";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteSshString(Stream destination, byte[] value)
|
||||||
|
{
|
||||||
|
Span<byte> length = stackalloc byte[4];
|
||||||
|
System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(length, (uint)value.Length);
|
||||||
|
destination.Write(length);
|
||||||
|
destination.Write(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteSshMpint(Stream destination, byte[] value)
|
||||||
|
{
|
||||||
|
// Signed big-endian: a high bit set would otherwise read as negative.
|
||||||
|
if (value.Length > 0 && (value[0] & 0x80) != 0)
|
||||||
|
{
|
||||||
|
var padded = new byte[value.Length + 1];
|
||||||
|
value.CopyTo(padded, 1);
|
||||||
|
WriteSshString(destination, padded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteSshString(destination, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shares one SSH server across every test class in the assembly.</summary>
|
||||||
|
[CollectionDefinition(Name)]
|
||||||
|
public sealed class SshCollection : ICollectionFixture<SshServerFixture>
|
||||||
|
{
|
||||||
|
/// <summary>Collection name.</summary>
|
||||||
|
public const string Name = "ssh";
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Meziantou.Analyzer": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.0.134, )",
|
||||||
|
"resolved": "3.0.134",
|
||||||
|
"contentHash": "tTYCcYKyOko3TMNxmxmA9nakbcHVUgglENmCMIhzIjl9y9FBZO/0tWSxTGC74Sp198FmWih5S5KkjQRBg5ePkQ=="
|
||||||
|
},
|
||||||
|
"Microsoft.CodeAnalysis.BannedApiAnalyzers": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[5.6.0, )",
|
||||||
|
"resolved": "5.6.0",
|
||||||
|
"contentHash": "Kcobt3pnOdO0A+6CKiMHZdTEluJpsfxiV20axtZdmfBQnDmiWTKPJADlgAfdTuKNAnVarrkJa0UEGwuOo91muw=="
|
||||||
|
},
|
||||||
|
"NSubstitute": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[6.0.0, )",
|
||||||
|
"resolved": "6.0.0",
|
||||||
|
"contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"Castle.Core": "5.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Shouldly": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[4.3.0, )",
|
||||||
|
"resolved": "4.3.0",
|
||||||
|
"contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"DiffEngine": "11.3.0",
|
||||||
|
"EmptyFiles": "4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SSH.NET": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2025.1.0, )",
|
||||||
|
"resolved": "2025.1.0",
|
||||||
|
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
|
||||||
|
"dependencies": {
|
||||||
|
"BouncyCastle.Cryptography": "2.6.2",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Testcontainers": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[4.13.0, )",
|
||||||
|
"resolved": "4.13.0",
|
||||||
|
"contentHash": "j8vi9jPBNSwaraGGx8w+2gtZyWrlbKxdhiGMS3nektg+KiwjFWx9ghCjs57EoQfvI+IAbzti0oQJupQChwgMog==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced": "4.3.3",
|
||||||
|
"Docker.DotNet.Enhanced.X509": "4.3.3",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3",
|
||||||
|
"SSH.NET": "2025.1.0",
|
||||||
|
"SharpZipLib": "1.4.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.v3": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[3.2.2, )",
|
||||||
|
"resolved": "3.2.2",
|
||||||
|
"contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.v3.mtp-v1": "[3.2.2]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Castle.Core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "5.1.1",
|
||||||
|
"contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"System.Diagnostics.EventLog": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DiffEngine": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.0",
|
||||||
|
"contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"EmptyFiles": "4.4.0",
|
||||||
|
"System.Management": "6.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Docker.DotNet.Enhanced": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.3.3",
|
||||||
|
"contentHash": "nGicLwvd42FhRk+khY5uS6cx49ErNdwYKnYBg0F4m4BDKLp/R77AVmmN9xAiqI3W/wN5ZCHkdUhgxf5ORkZuFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3",
|
||||||
|
"Docker.DotNet.Enhanced.LegacyHttp": "4.3.3",
|
||||||
|
"Docker.DotNet.Enhanced.NPipe": "4.3.3",
|
||||||
|
"Docker.DotNet.Enhanced.NativeHttp": "4.3.3",
|
||||||
|
"Docker.DotNet.Enhanced.Unix": "4.3.3",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Docker.DotNet.Enhanced.Handler.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.3.3",
|
||||||
|
"contentHash": "9Cp8hOgtynixcDoAs9lnEaQosluojSYmiW3fsLsLIVfZjlq/fznSIZNUhnmyT4Xo1Iyuok/y49WL/25O47u0Pw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Docker.DotNet.Enhanced.LegacyHttp": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.3.3",
|
||||||
|
"contentHash": "7j3M16emv9PAQN7VwFn23xLYNj8GJmwPOcogveHkaWnOCqiC+anRaNKQwqIBNApM1AuwZKivehTKTPmmrjUUnw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Docker.DotNet.Enhanced.NativeHttp": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.3.3",
|
||||||
|
"contentHash": "iNzK+jRFeEMobSA7l/h4ARwCKOOefOWtVN5/RB0ft6/6H6IQXvVUuOgGyZAjYLBT7TsyClRYno2B904f3dtBuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Docker.DotNet.Enhanced.NPipe": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.3.3",
|
||||||
|
"contentHash": "ZTLYufuEfY0e6qLOgeH9QgXx2KYuoABRVaY5A8rsggyLgYqbDj9rCRfVAhHPCUv83S7pVxDHy+Tvm/BnxjWVpg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Docker.DotNet.Enhanced.Unix": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.3.3",
|
||||||
|
"contentHash": "ypo8qNbmvHw1t9VfpRTMogCw2vht6VjkXzlGYUUeP2H2bf83USURdla1maW1njn2oq2rfLUFOGMfmt+A37QU2w==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Docker.DotNet.Enhanced.X509": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.3.3",
|
||||||
|
"contentHash": "oBDibWezEv4hgj3RIQxI3DVcxkNV1MdrD0d/jhjUu+h3DL+qc0wlkQva15kkwMatXmC/hWp1VP0DMoFXe+BmEw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced.Handler.Abstractions": "4.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EmptyFiles": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "4.4.0",
|
||||||
|
"contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw=="
|
||||||
|
},
|
||||||
|
"Microsoft.ApplicationInsights": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw=="
|
||||||
|
},
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "6.0.0",
|
||||||
|
"contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg=="
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.0.2",
|
||||||
|
"contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
|
||||||
|
},
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.0.3",
|
||||||
|
"contentHash": "dL0QGToTxggRLMYY4ZYX5AMwBb+byQBd/5dMiZE07Nv73o6I5Are3C7eQTh7K2+A4ct0PVISSr7TZANbiNb2yQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Testing.Extensions.Telemetry": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.9.1",
|
||||||
|
"contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.ApplicationInsights": "2.23.0",
|
||||||
|
"Microsoft.Testing.Platform": "1.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Testing.Extensions.TrxReport.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.9.1",
|
||||||
|
"contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Testing.Platform": "1.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Testing.Platform": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.9.1",
|
||||||
|
"contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA=="
|
||||||
|
},
|
||||||
|
"Microsoft.Testing.Platform.MSBuild": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.9.1",
|
||||||
|
"contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Testing.Platform": "1.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Microsoft.Win32.Registry": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "5.0.0",
|
||||||
|
"contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg=="
|
||||||
|
},
|
||||||
|
"SharpZipLib": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.4.2",
|
||||||
|
"contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A=="
|
||||||
|
},
|
||||||
|
"System.CodeDom": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "6.0.0",
|
||||||
|
"contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA=="
|
||||||
|
},
|
||||||
|
"System.Diagnostics.EventLog": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "6.0.0",
|
||||||
|
"contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw=="
|
||||||
|
},
|
||||||
|
"System.Management": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "6.0.1",
|
||||||
|
"contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==",
|
||||||
|
"dependencies": {
|
||||||
|
"System.CodeDom": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.analyzers": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "1.27.0",
|
||||||
|
"contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g=="
|
||||||
|
},
|
||||||
|
"xunit.v3.assert": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.2.2",
|
||||||
|
"contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA=="
|
||||||
|
},
|
||||||
|
"xunit.v3.common": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.2.2",
|
||||||
|
"contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Bcl.AsyncInterfaces": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.v3.core.mtp-v1": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.2.2",
|
||||||
|
"contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Testing.Extensions.Telemetry": "1.9.1",
|
||||||
|
"Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1",
|
||||||
|
"Microsoft.Testing.Platform": "1.9.1",
|
||||||
|
"Microsoft.Testing.Platform.MSBuild": "1.9.1",
|
||||||
|
"xunit.v3.extensibility.core": "[3.2.2]",
|
||||||
|
"xunit.v3.runner.inproc.console": "[3.2.2]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.v3.extensibility.core": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.2.2",
|
||||||
|
"contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.v3.common": "[3.2.2]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.v3.mtp-v1": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.2.2",
|
||||||
|
"contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.analyzers": "1.27.0",
|
||||||
|
"xunit.v3.assert": "[3.2.2]",
|
||||||
|
"xunit.v3.core.mtp-v1": "[3.2.2]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.v3.runner.common": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.2.2",
|
||||||
|
"contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Win32.Registry": "[5.0.0]",
|
||||||
|
"xunit.v3.common": "[3.2.2]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xunit.v3.runner.inproc.console": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.2.2",
|
||||||
|
"contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==",
|
||||||
|
"dependencies": {
|
||||||
|
"xunit.v3.extensibility.core": "[3.2.2]",
|
||||||
|
"xunit.v3.runner.common": "[3.2.2]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dodossh.client.ssh": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SSH.NET": "[2025.1.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"BouncyCastle.Cryptography": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2.6.2, )",
|
||||||
|
"resolved": "2.6.2",
|
||||||
|
"contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -286,15 +286,6 @@
|
|||||||
"resolved": "1.4.2",
|
"resolved": "1.4.2",
|
||||||
"contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A=="
|
"contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A=="
|
||||||
},
|
},
|
||||||
"SSH.NET": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2025.1.0",
|
|
||||||
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
|
|
||||||
"dependencies": {
|
|
||||||
"BouncyCastle.Cryptography": "2.6.2",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System.CodeDom": {
|
"System.CodeDom": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "6.0.0",
|
"resolved": "6.0.0",
|
||||||
@@ -313,18 +304,6 @@
|
|||||||
"System.CodeDom": "6.0.0"
|
"System.CodeDom": "6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Testcontainers": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "4.13.0",
|
|
||||||
"contentHash": "j8vi9jPBNSwaraGGx8w+2gtZyWrlbKxdhiGMS3nektg+KiwjFWx9ghCjs57EoQfvI+IAbzti0oQJupQChwgMog==",
|
|
||||||
"dependencies": {
|
|
||||||
"Docker.DotNet.Enhanced": "4.3.3",
|
|
||||||
"Docker.DotNet.Enhanced.X509": "4.3.3",
|
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "8.0.3",
|
|
||||||
"SSH.NET": "2025.1.0",
|
|
||||||
"SharpZipLib": "1.4.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"xunit.analyzers": {
|
"xunit.analyzers": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "1.27.0",
|
"resolved": "1.27.0",
|
||||||
@@ -443,6 +422,29 @@
|
|||||||
"Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
|
"Microsoft.Extensions.Configuration.Abstractions": "10.0.10",
|
||||||
"Microsoft.Extensions.Logging": "10.0.10"
|
"Microsoft.Extensions.Logging": "10.0.10"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"SSH.NET": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[2025.1.0, )",
|
||||||
|
"resolved": "2025.1.0",
|
||||||
|
"contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==",
|
||||||
|
"dependencies": {
|
||||||
|
"BouncyCastle.Cryptography": "2.6.2",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Testcontainers": {
|
||||||
|
"type": "CentralTransitive",
|
||||||
|
"requested": "[4.13.0, )",
|
||||||
|
"resolved": "4.13.0",
|
||||||
|
"contentHash": "j8vi9jPBNSwaraGGx8w+2gtZyWrlbKxdhiGMS3nektg+KiwjFWx9ghCjs57EoQfvI+IAbzti0oQJupQChwgMog==",
|
||||||
|
"dependencies": {
|
||||||
|
"Docker.DotNet.Enhanced": "4.3.3",
|
||||||
|
"Docker.DotNet.Enhanced.X509": "4.3.3",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.3",
|
||||||
|
"SSH.NET": "2025.1.0",
|
||||||
|
"SharpZipLib": "1.4.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user