From 5181dcb8845365646bafe54c066891c248696768 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Wed, 8 Oct 2025 09:46:02 +0200 Subject: [PATCH 01/50] split handler into sub projects add native http handler add WriteClosableStreamWrapper for native http handler move WriteClosableStream into base namespace add HijackStreamHelper for reflection (conflict with System member) remove namedPipeConnectTimeout from config and hardcoded to 10s instead of 100ms --- Docker.DotNet.sln | 73 ++++ .../Docker.DotNet.LegacyHttp.csproj | 20 + .../LegacyHttpHandlerFactory.cs | 21 + .../BufferedReadStream.cs | 240 +++++++++++ .../ChunkedReadStream.cs | 152 +++++++ .../ChunkedWriteStream.cs | 92 ++++ .../ContentLengthReadStream.cs | 166 ++++++++ .../HttpConnection.cs | 185 ++++++++ .../HttpConnectionResponseContent.cs | 77 ++++ .../ManagedHandler.cs | 397 ++++++++++++++++++ .../Microsoft.Net.Http.Client/ProxyMode.cs | 8 + .../Microsoft.Net.Http.Client/RedirectMode.cs | 19 + .../RequestExtensions.cs | 102 +++++ .../Docker.DotNet.NPipe.csproj | 20 + .../BufferedReadStream.cs | 240 +++++++++++ .../ChunkedReadStream.cs | 152 +++++++ .../ChunkedWriteStream.cs | 92 ++++ .../ContentLengthReadStream.cs | 166 ++++++++ .../HttpConnection.cs | 185 ++++++++ .../HttpConnectionResponseContent.cs | 76 ++++ .../ManagedHandler.cs | 397 ++++++++++++++++++ .../Microsoft.Net.Http.Client/ProxyMode.cs | 8 + .../Microsoft.Net.Http.Client/RedirectMode.cs | 19 + .../RequestExtensions.cs | 102 +++++ .../NpipeHandlerFactory.cs | 43 ++ .../Docker.DotNet.NativeHttp.csproj | 20 + .../NativeHttpHandlerFactory.cs | 35 ++ .../Docker.DotNet.Unix.csproj | 20 + .../BufferedReadStream.cs | 240 +++++++++++ .../ChunkedReadStream.cs | 152 +++++++ .../ChunkedWriteStream.cs | 92 ++++ .../ContentLengthReadStream.cs | 166 ++++++++ .../HttpConnection.cs | 185 ++++++++ .../HttpConnectionResponseContent.cs | 76 ++++ .../ManagedHandler.cs | 397 ++++++++++++++++++ .../Microsoft.Net.Http.Client/ProxyMode.cs | 8 + .../Microsoft.Net.Http.Client/RedirectMode.cs | 19 + .../RequestExtensions.cs | 102 +++++ .../UnixDomainSocketEndPoint.cs | 88 ++++ src/Docker.DotNet.Unix/UnixHandlerFactory.cs | 26 ++ .../CertificateCredentials.cs | 59 ++- .../Docker.DotNet.X509.csproj | 1 - src/Docker.DotNet/Docker.DotNet.csproj | 1 - src/Docker.DotNet/DockerClient.cs | 103 ++--- .../DockerClientConfiguration.cs | 44 +- src/Docker.DotNet/DockerPipeStream.cs | 2 +- src/Docker.DotNet/HijackStreamHelper.cs | 7 + src/Docker.DotNet/IDockerHandlerFactory.cs | 7 + src/Docker.DotNet/WriteClosableStream.cs | 8 + .../WriteClosableStreamWrapper.cs | 54 +++ 50 files changed, 4869 insertions(+), 95 deletions(-) create mode 100644 src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj create mode 100644 src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/BufferedReadStream.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedReadStream.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedWriteStream.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ContentLengthReadStream.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnection.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ManagedHandler.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ProxyMode.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RedirectMode.cs create mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RequestExtensions.cs create mode 100644 src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/BufferedReadStream.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedReadStream.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedWriteStream.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ContentLengthReadStream.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnection.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ManagedHandler.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ProxyMode.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RedirectMode.cs create mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RequestExtensions.cs create mode 100644 src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs create mode 100644 src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj create mode 100644 src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs create mode 100644 src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/BufferedReadStream.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedReadStream.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedWriteStream.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ContentLengthReadStream.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnection.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ManagedHandler.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ProxyMode.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RedirectMode.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RequestExtensions.cs create mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs create mode 100644 src/Docker.DotNet.Unix/UnixHandlerFactory.cs create mode 100644 src/Docker.DotNet/HijackStreamHelper.cs create mode 100644 src/Docker.DotNet/IDockerHandlerFactory.cs create mode 100644 src/Docker.DotNet/WriteClosableStream.cs create mode 100644 src/Docker.DotNet/WriteClosableStreamWrapper.cs diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln index 158d90e7..30cefbd1 100644 --- a/Docker.DotNet.sln +++ b/Docker.DotNet.sln @@ -15,6 +15,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.X509", "src\D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" EndProject +Project("{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}") = "Docker.DotNet.NPipe", "src\Docker.DotNet.NPipe\Docker.DotNet.NPipe.csproj", "{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}" +EndProject +Project("{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}") = "Docker.DotNet.Unix", "src\Docker.DotNet.Unix\Docker.DotNet.Unix.csproj", "{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}" +EndProject +Project("{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}") = "Docker.DotNet.LegacyHttp", "src\Docker.DotNet.LegacyHttp\Docker.DotNet.LegacyHttp.csproj", "{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}" +EndProject +Project("{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}") = "Docker.DotNet.NativeHttp", "src\Docker.DotNet.NativeHttp\Docker.DotNet.NativeHttp.csproj", "{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}" +EndProject + Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,6 +34,66 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x86.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x64.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x64.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x86.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x86.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x64.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x86.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|x64.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|x86.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|Any CPU.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|x64.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|x64.Build.0 = Release|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|x86.ActiveCfg = Release|Any CPU + {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|x86.Build.0 = Release|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -78,9 +147,13 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {E1F24B25-E027-45E0-A6E1-E08138F1F95D} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} EndGlobalSection EndGlobal diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj new file mode 100644 index 00000000..f7a0da9d --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj @@ -0,0 +1,20 @@ + + + Docker.DotNet.LegacyHttp + Docker.DotNet.LegacyHttp + Docker.DotNet.LegacyHttp is a library that allows you to connect via http(s) with a Docker engine programmatically in your .NET applications. + + + + + + + + + + + + + + + diff --git a/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs new file mode 100644 index 00000000..1faabe68 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Docker.DotNet.LegacyHttp +{ + public class LegacyHttpHandlerFactory : IDockerHandlerFactory + { + public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + { + var builder = new UriBuilder(uri) + { + Scheme = configuration.Credentials.IsTlsCredentials() ? "https" : "http" + }; + uri = builder.Uri; + return new Tuple( + new Microsoft.Net.Http.Client.ManagedHandler(logger), + uri + ); + } + } +} diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/BufferedReadStream.cs new file mode 100644 index 00000000..b98aa284 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -0,0 +1,240 @@ +using System.Buffers; +using System.IO; +using System.Net.Sockets; +using Docker.DotNet; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Net.Http.Client; + +internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream +{ + private readonly Stream _inner; + private readonly Socket _socket; + private readonly byte[] _buffer; + private readonly ILogger _logger; + private int _bufferRefCount; + private int _bufferOffset; + private int _bufferCount; + + public BufferedReadStream(Stream inner, Socket socket, ILogger logger) + : this(inner, socket, 8192, logger) + { + } + + public BufferedReadStream(Stream inner, Socket socket, int bufferLength, ILogger logger) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + _socket = socket; + _buffer = ArrayPool.Shared.Rent(bufferLength); + _logger = logger; + _bufferRefCount = 1; + } + + public override bool CanRead + { + get { return _inner.CanRead || _bufferCount > 0; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return _inner.CanWrite; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override bool CanCloseWrite => _socket != null || _inner is WriteClosableStream; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Exchange(ref _bufferRefCount, 0) == 1) + { + ArrayPool.Shared.Return(_buffer); + } + + _inner.Dispose(); + } + + base.Dispose(disposing); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int read = ReadBuffer(buffer, offset, count); + if (read > 0) + { + return read; + } + + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int read = ReadBuffer(buffer, offset, count); + if (read > 0) + { + return Task.FromResult(read); + } + + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override void CloseWrite() + { + if (_socket != null) + { + _socket.Shutdown(SocketShutdown.Send); + return; + } + + if (_inner is WriteClosableStream writeClosableStream) + { + writeClosableStream.CloseWrite(); + return; + } + + throw new NotSupportedException("Cannot shutdown write on this transport"); + } + + public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) + { + int read = PeekBuffer(buffer, toPeek, out peeked, out available, out remaining); + if (read > 0) + { + return true; + } + + if (_inner is IPeekableStream peekableStream) + { + return peekableStream.Peek(buffer, toPeek, out peeked, out available, out remaining); + } + + throw new NotSupportedException("_inner stream isn't a peekable stream"); + } + + public async Task ReadLineAsync(CancellationToken cancellationToken) + { + var line = new StringBuilder(_buffer.Length); + + var crIndex = -1; + + var lfIndex = -1; + + bool crlfFound; + + do + { + if (_bufferCount == 0) + { + _bufferOffset = 0; + + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) + .ConfigureAwait(false); + } + + var c = (char)_buffer[_bufferOffset]; + line.Append(c); + + _bufferOffset++; + _bufferCount--; + + switch (c) + { + case '\r': + crIndex = line.Length; + break; + case '\n': + lfIndex = line.Length; + break; + } + + crlfFound = crIndex + 1 == lfIndex; + } + while (!crlfFound); + + return line.ToString(0, line.Length - 2); + } + + private int ReadBuffer(byte[] buffer, int offset, int count) + { + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return 0; + } + + private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) + { + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, (int)toPeek); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); + peeked = (uint) toCopy; + available = (uint)_bufferCount; + remaining = available - peeked; + return toCopy; + } + + peeked = 0; + available = 0; + remaining = 0; + return 0; + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedReadStream.cs new file mode 100644 index 00000000..dc2e552d --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedReadStream.cs @@ -0,0 +1,152 @@ +using System.Globalization; +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal sealed class ChunkedReadStream : Stream +{ + private readonly BufferedReadStream _inner; + private int _chunkBytesRemaining; + private bool _done; + + public ChunkedReadStream(BufferedReadStream stream) + { + _inner = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override bool CanRead + { + get { return _inner.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int ReadTimeout + { + get + { + return _inner.ReadTimeout; + } + set + { + _inner.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + return _inner.WriteTimeout; + } + set + { + _inner.WriteTimeout = value; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_done) + { + return 0; + } + + if (_chunkBytesRemaining == 0) + { + var headerLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) + { + throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); + } + } + + var readBytesCount = 0; + + if (_chunkBytesRemaining > 0) + { + var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); + + readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) + .ConfigureAwait(false); + + if (readBytesCount == 0) + { + throw new EndOfStreamException(); + } + + _chunkBytesRemaining -= readBytesCount; + } + + if (_chunkBytesRemaining == 0) + { + var emptyLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (!string.IsNullOrEmpty(emptyLine)) + { + throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); + } + + _done = readBytesCount == 0; + } + + return readBytesCount; + } + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + _inner.Flush(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedWriteStream.cs new file mode 100644 index 00000000..b63fe3c6 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedWriteStream.cs @@ -0,0 +1,92 @@ +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal sealed class ChunkedWriteStream : Stream +{ + private static readonly byte[] EndOfContentBytes = Encoding.ASCII.GetBytes("0\r\n\r\n"); + + private readonly Stream _inner; + + public ChunkedWriteStream(Stream stream) + { + _inner = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get { throw new NotImplementedException(); } + } + + public override long Position + { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public override void Flush() + { + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (count == 0) + { + return; + } + + const string crlf = "\r\n"; + + var chunkHeader = count.ToString("X") + crlf; + var headerBytes = Encoding.ASCII.GetBytes(chunkHeader); + + // Write the chunk header + await _inner.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken) + .ConfigureAwait(false); + + // Write the chunk data + await _inner.WriteAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + + // Write the chunk footer (CRLF) + await _inner.WriteAsync(headerBytes, headerBytes.Length - 2, 2, cancellationToken) + .ConfigureAwait(false); + } + + public Task EndContentAsync(CancellationToken cancellationToken) + { + return _inner.WriteAsync(EndOfContentBytes, 0, EndOfContentBytes.Length, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ContentLengthReadStream.cs new file mode 100644 index 00000000..c0ba7ef3 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ContentLengthReadStream.cs @@ -0,0 +1,166 @@ +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal class ContentLengthReadStream : Stream +{ + private readonly Stream _inner; + private long _bytesRemaining; + private bool _disposed; + + public ContentLengthReadStream(Stream inner, long contentLength) + { + _inner = inner; + _bytesRemaining = contentLength; + } + + public override bool CanRead + { + get { return !_disposed; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int ReadTimeout + { + get + { + CheckDisposed(); + return _inner.ReadTimeout; + } + set + { + CheckDisposed(); + _inner.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + CheckDisposed(); + return _inner.WriteTimeout; + } + set + { + CheckDisposed(); + _inner.WriteTimeout = value; + } + } + + private void UpdateBytesRemaining(int read) + { + _bytesRemaining -= read; + if (_bytesRemaining <= 0) + { + _disposed = true; + } + System.Diagnostics.Debug.Assert(_bytesRemaining >= 0, "Negative bytes remaining? " + _bytesRemaining); + } + + public override int Read(byte[] buffer, int offset, int count) + { + // TODO: Validate buffer + if (_disposed) + { + return 0; + } + + if (_bytesRemaining == 0) + { + return 0; + } + + int toRead = (int)Math.Min(count, _bytesRemaining); + int read = _inner.Read(buffer, offset, toRead); + UpdateBytesRemaining(read); + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // TODO: Validate args + if (_disposed) + { + return 0; + } + + if (_bytesRemaining == 0) + { + return 0; + } + + cancellationToken.ThrowIfCancellationRequested(); + int toRead = (int)Math.Min(count, _bytesRemaining); + int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken); + UpdateBytesRemaining(read); + return read; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // TODO: Sync drain with timeout if small number of bytes remaining? This will let us re-use the connection. + _inner.Dispose(); + } + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(typeof(ContentLengthReadStream).FullName); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnection.cs new file mode 100644 index 00000000..2f20e3dc --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnection.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; + +namespace Microsoft.Net.Http.Client; + +internal sealed class HttpConnection : IDisposable +{ + private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; + + public HttpConnection(BufferedReadStream transport) + { + Transport = transport; + } + + public BufferedReadStream Transport { get; } + + public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + // Serialize headers & send + string rawRequest = SerializeRequest(request); + byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest); + await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken); + + if (request.Content != null) + { + if (request.Content.Headers.ContentLength.HasValue) + { + await request.Content.CopyToAsync(Transport); + } + else + { + // The length of the data is unknown. Send it in chunked mode. + using (var chunkedStream = new ChunkedWriteStream(Transport)) + { + await request.Content.CopyToAsync(chunkedStream); + await chunkedStream.EndContentAsync(cancellationToken); + } + } + } + + // Receive headers + List responseLines = await ReadResponseLinesAsync(cancellationToken); + + // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) + return CreateResponseMessage(responseLines); + } + catch (Exception ex) + { + Dispose(); // Any errors at this layer abort the connection. + throw new HttpRequestException("The requested failed, see inner exception for details.", ex); + } + } + + private string SerializeRequest(HttpRequestMessage request) + { + StringBuilder builder = new StringBuilder(); + builder.Append(request.Method); + builder.Append(' '); + builder.Append(request.GetAddressLineProperty()); + builder.Append(" HTTP/"); + builder.Append(request.Version.ToString(2)); + builder.Append("\r\n"); + + builder.Append(request.Headers); + + if (request.Content != null) + { + // Force the content to compute its content length if it has not already. + var contentLength = request.Content.Headers.ContentLength; + if (contentLength.HasValue) + { + request.Content.Headers.ContentLength = contentLength.Value; + } + + builder.Append(request.Content.Headers); + if (!contentLength.HasValue) + { + // Add header for chunked mode. + builder.Append("Transfer-Encoding: chunked\r\n"); + } + } + // Headers end with an empty line + builder.Append("\r\n"); + return builder.ToString(); + } + + private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) + { + var lines = new List(12); + + do + { + var line = await Transport.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (string.IsNullOrEmpty(line)) + { + break; + } + + lines.Add(line); + } + while (true); + + return lines; + } + + private HttpResponseMessage CreateResponseMessage(List responseLines) + { + string responseLine = responseLines.First(); + // HTTP/1.1 200 OK + string[] responseLineParts = responseLine.Split(new[] { ' ' }, 3); + // TODO: Verify HTTP/1.0 or 1.1. + if (responseLineParts.Length < 2) + { + throw new HttpRequestException("Invalid response line: " + responseLine); + } + + if (int.TryParse(responseLineParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode)) + { + // TODO: Validate range + } + else + { + throw new HttpRequestException("Invalid status code: " + responseLineParts[1]); + } + HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)statusCode); + if (responseLineParts.Length >= 3) + { + response.ReasonPhrase = responseLineParts[2]; + } + var content = new HttpConnectionResponseContent(this); + response.Content = content; + + foreach (var rawHeader in responseLines.Skip(1)) + { + int colonOffset = rawHeader.IndexOf(':'); + if (colonOffset <= 0) + { + throw new HttpRequestException("The given header line format is invalid: " + rawHeader); + } + string headerName = rawHeader.Substring(0, colonOffset); + string headerValue = rawHeader.Substring(colonOffset + 2); + if (!response.Headers.TryAddWithoutValidation(headerName, headerValue)) + { + bool success = response.Content.Headers.TryAddWithoutValidation(headerName, headerValue); + System.Diagnostics.Debug.Assert(success, "Failed to add response header: " + rawHeader); + } + } + + // TODO: We'll need to refactor this in the future. + // + // Depending on the request and response (headers), we need to handle the response + // differently. We need to distinguish between four types of responses: + // + // 1. Chunked transfer encoding + // 2. HTTP with a `Content-Length` header + // 3. Hijacked TCP connections (using the connection upgrade headers) + // - `/containers/{id}/attach` + // - `/exec/{id}/start` + // 4. Streams without the connection upgrade headers + // - `/containers/{id}/logs` + + var isConnectionUpgrade = response.Headers.TryGetValues("Upgrade", out var responseHeaderValues) + && responseHeaderValues.Any(header => "tcp".Equals(header)); + + var isStream = content.Headers.TryGetValues("Content-Type", out var contentHeaderValues) + && contentHeaderValues.Any(header => DockerStreamHeaders.Contains(header)); + + var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isStream) || (isStream && !isConnectionUpgrade); + + content.ResolveResponseStream(chunked: isChunkedTransferEncoding); + + return response; + } + + public void Dispose() + { + Transport.Dispose(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs new file mode 100644 index 00000000..f6f99a0f --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs @@ -0,0 +1,77 @@ +using System.IO; +using Docker.DotNet; + +namespace Microsoft.Net.Http.Client; + +public class HttpConnectionResponseContent : HttpContent +{ + private readonly HttpConnection _connection; + private Stream _responseStream; + + internal HttpConnectionResponseContent(HttpConnection connection) + { + _connection = connection; + } + + internal void ResolveResponseStream(bool chunked) + { + if (_responseStream != null) + { + throw new InvalidOperationException("Called multiple times"); + } + if (chunked) + { + _responseStream = new ChunkedReadStream(_connection.Transport); + } + else if (Headers.ContentLength.HasValue) + { + _responseStream = new ContentLengthReadStream(_connection.Transport, Headers.ContentLength.Value); + } + else + { + _responseStream = _connection.Transport; + } + } + + public WriteClosableStream HijackStream() + { + if (_responseStream != _connection.Transport) + { + throw new InvalidOperationException("cannot hijack chunked or content length stream"); + } + + return _connection.Transport; + } + + protected override Task SerializeToStreamAsync(Stream stream, System.Net.TransportContext context) + { + return _responseStream.CopyToAsync(stream); + } + + protected override Task CreateContentReadStreamAsync() + { + return Task.FromResult(_responseStream); + } + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + _responseStream.Dispose(); + _connection.Dispose(); + } + } + finally + { + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ManagedHandler.cs new file mode 100644 index 00000000..bc17c436 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ManagedHandler.cs @@ -0,0 +1,397 @@ +namespace Microsoft.Net.Http.Client; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; + +public class ManagedHandler : HttpMessageHandler +{ + private readonly ILogger _logger; + + private readonly StreamOpener _streamOpener; + + private readonly SocketOpener _socketOpener; + + private IWebProxy _proxy; + + public delegate Task StreamOpener(string host, int port, CancellationToken cancellationToken); + + public delegate Task SocketOpener(string host, int port, CancellationToken cancellationToken); + + public ManagedHandler(ILogger logger) + { + _logger = logger; + _socketOpener = TcpSocketOpenerAsync; + } + + public ManagedHandler(StreamOpener opener, ILogger logger) + { + _logger = logger; + _streamOpener = opener ?? throw new ArgumentNullException(nameof(opener)); + } + + public ManagedHandler(SocketOpener opener, ILogger logger) + { + _logger = logger; + _socketOpener = opener ?? throw new ArgumentNullException(nameof(opener)); + } + + public IWebProxy Proxy + { + get + { + if (_proxy == null) + { + _proxy = WebRequest.DefaultWebProxy; + } + + return _proxy; + } + set + { + _proxy = value; + } + } + + public bool UseProxy { get; set; } = true; + + public int MaxAutomaticRedirects { get; set; } = 20; + + public RedirectMode RedirectMode { get; set; } = RedirectMode.NoDowngrade; + + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + + public X509CertificateCollection ClientCertificates { get; set; } = new X509Certificate2Collection(); + + protected override async Task SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) + { + if (httpRequestMessage == null) + { + throw new ArgumentNullException(nameof(httpRequestMessage)); + } + + HttpResponseMessage httpResponseMessage = null; + + for (var i = 0; i < MaxAutomaticRedirects; i++) + { + httpResponseMessage?.Dispose(); + + httpResponseMessage = await ProcessRequestAsync(httpRequestMessage, cancellationToken) + .ConfigureAwait(false); + + if (!IsRedirectResponse(httpRequestMessage, httpResponseMessage)) + { + return httpResponseMessage; + } + } + + return httpResponseMessage; + } + + private bool IsRedirectResponse(HttpRequestMessage request, HttpResponseMessage response) + { + if (response.StatusCode < HttpStatusCode.MovedPermanently || response.StatusCode >= HttpStatusCode.BadRequest) + { + return false; + } + + if (RedirectMode == RedirectMode.None) + { + return false; + } + + var location = response.Headers.Location; + + if (location == null) + { + return false; + } + + if (!location.IsAbsoluteUri) + { + request.RequestUri = location; + request.Headers.Authorization = null; + request.SetAddressLineProperty(null); + request.SetPathAndQueryProperty(null); + return true; + } + + // Check if redirect from https to http is allowed + if (request.IsHttps() && string.Equals("http", location.Scheme, StringComparison.OrdinalIgnoreCase) + && RedirectMode == RedirectMode.NoDowngrade) + { + return false; + } + + // Reset fields calculated from the URI. + request.RequestUri = location; + request.Headers.Authorization = null; + request.Headers.Host = null; + request.SetConnectionHostProperty(null); + request.SetConnectionPortProperty(null); + request.SetSchemeProperty(null); + request.SetHostProperty(null); + request.SetPortProperty(null); + request.SetAddressLineProperty(null); + request.SetPathAndQueryProperty(null); + return true; + } + + private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + ProcessUrl(request); + ProcessHostHeader(request); + request.Headers.ConnectionClose = !request.Headers.Contains("Connection"); // TODO: Connection reuse is not supported. + + ProxyMode proxyMode = DetermineProxyModeAndAddressLine(request); + Socket socket; + Stream transport; + + try + { + if (_socketOpener != null) + { + socket = await _socketOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); + transport = new NetworkStream(socket, true); + } + else + { + socket = null; + transport = await _streamOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException e) + { + throw new HttpRequestException("Connection failed.", e); + } + + if (proxyMode == ProxyMode.Tunnel) + { + await TunnelThroughProxyAsync(request, transport, cancellationToken); + } + + if (request.IsHttps()) + { + SslStream sslStream = new SslStream(transport, false, ServerCertificateValidationCallback); + await sslStream.AuthenticateAsClientAsync(request.GetHostProperty(), ClientCertificates, SslProtocols.Tls12, false); + transport = sslStream; + } + + var bufferedReadStream = new BufferedReadStream(transport, socket, _logger); + var connection = new HttpConnection(bufferedReadStream); + return await connection.SendAsync(request, cancellationToken); + } + + // Data comes from either the request.RequestUri or from the request.Properties + private static void ProcessUrl(HttpRequestMessage request) + { + string scheme = request.GetSchemeProperty(); + if (string.IsNullOrWhiteSpace(scheme)) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + scheme = request.RequestUri.Scheme; + request.SetSchemeProperty(scheme); + } + + if (!request.IsHttp() && !request.IsHttps()) + { + throw new InvalidOperationException("Only HTTP or HTTPS are supported, not: " + request.RequestUri.Scheme); + } + + string host = request.GetHostProperty(); + if (string.IsNullOrWhiteSpace(host)) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + host = request.RequestUri.DnsSafeHost; + request.SetHostProperty(host); + } + + string connectionHost = request.GetConnectionHostProperty(); + if (string.IsNullOrWhiteSpace(connectionHost)) + { + request.SetConnectionHostProperty(host); + } + + int? port = request.GetPortProperty(); + if (!port.HasValue) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + port = request.RequestUri.Port; + request.SetPortProperty(port); + } + + int? connectionPort = request.GetConnectionPortProperty(); + if (!connectionPort.HasValue) + { + request.SetConnectionPortProperty(port); + } + + string pathAndQuery = request.GetPathAndQueryProperty(); + if (string.IsNullOrWhiteSpace(pathAndQuery)) + { + if (request.RequestUri.IsAbsoluteUri) + { + pathAndQuery = request.RequestUri.PathAndQuery; + } + else + { + pathAndQuery = Uri.EscapeDataString(request.RequestUri.ToString()); + } + request.SetPathAndQueryProperty(pathAndQuery); + } + } + + private static void ProcessHostHeader(HttpRequestMessage request) + { + if (string.IsNullOrWhiteSpace(request.Headers.Host)) + { + string host = request.GetHostProperty(); + int port = request.GetPortProperty().Value; + if (host.Contains(':')) + { + // IPv6 + host = '[' + host + ']'; + } + + request.Headers.Host = host + ":" + port.ToString(CultureInfo.InvariantCulture); + } + } + + private ProxyMode DetermineProxyModeAndAddressLine(HttpRequestMessage request) + { + string scheme = request.GetSchemeProperty(); + string host = request.GetHostProperty(); + int? port = request.GetPortProperty(); + string pathAndQuery = request.GetPathAndQueryProperty(); + string addressLine = request.GetAddressLineProperty(); + + if (string.IsNullOrEmpty(addressLine)) + { + request.SetAddressLineProperty(pathAndQuery); + } + + try + { + if (!UseProxy || Proxy == null || Proxy.IsBypassed(request.RequestUri)) + { + return ProxyMode.None; + } + } + catch (PlatformNotSupportedException) + { + return ProxyMode.None; + } + + var proxyUri = Proxy.GetProxy(request.RequestUri); + if (proxyUri == null) + { + return ProxyMode.None; + } + + if (request.IsHttp()) + { + if (string.IsNullOrEmpty(addressLine)) + { + addressLine = scheme + "://" + host + ":" + port.Value + pathAndQuery; + request.SetAddressLineProperty(addressLine); + } + request.SetConnectionHostProperty(proxyUri.DnsSafeHost); + request.SetConnectionPortProperty(proxyUri.Port); + return ProxyMode.Http; + } + + // Tunneling generates a completely separate request, don't alter the original, just the connection address. + request.SetConnectionHostProperty(proxyUri.DnsSafeHost); + request.SetConnectionPortProperty(proxyUri.Port); + return ProxyMode.Tunnel; + } + + private static async Task TcpSocketOpenerAsync(string host, int port, CancellationToken cancellationToken) + { + var addresses = await Dns.GetHostAddressesAsync(host) + .ConfigureAwait(false); + + if (addresses.Length == 0) + { + throw new Exception($"Unable to resolve any IP addresses for the host '{host}'."); + } + + var exceptions = new List(); + + foreach (var address in addresses) + { + var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + try + { + await socket.ConnectAsync(address, port) + .ConfigureAwait(false); + + return socket; + } + catch (Exception e) + { + socket.Dispose(); + exceptions.Add(e); + } + } + + throw new AggregateException(exceptions); + } + + private async Task TunnelThroughProxyAsync(HttpRequestMessage request, Stream transport, CancellationToken cancellationToken) + { + // Send a Connect request: + // CONNECT server.example.com:80 HTTP / 1.1 + // Host: server.example.com:80 + var connectRequest = new HttpRequestMessage(); + connectRequest.Version = new Version(1, 1); + + connectRequest.Headers.ProxyAuthorization = request.Headers.ProxyAuthorization; + connectRequest.Method = new HttpMethod("CONNECT"); + // TODO: IPv6 hosts + string authority = request.GetHostProperty() + ":" + request.GetPortProperty().Value; + connectRequest.SetAddressLineProperty(authority); + connectRequest.Headers.Host = authority; + + HttpConnection connection = new HttpConnection(new BufferedReadStream(transport, null, _logger)); + HttpResponseMessage connectResponse; + try + { + connectResponse = await connection.SendAsync(connectRequest, cancellationToken); + // TODO:? await connectResponse.Content.LoadIntoBufferAsync(); // Drain any body + // There's no danger of accidentally consuming real response data because the real request hasn't been sent yet. + } + catch (Exception ex) + { + transport.Dispose(); + throw new HttpRequestException("SSL Tunnel failed to initialize", ex); + } + + // Listen for a response. Any 2XX is considered success, anything else is considered a failure. + if ((int)connectResponse.StatusCode < 200 || 300 <= (int)connectResponse.StatusCode) + { + transport.Dispose(); + throw new HttpRequestException("Failed to negotiate the proxy tunnel: " + connectResponse); + } + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ProxyMode.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ProxyMode.cs new file mode 100644 index 00000000..05a823c9 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ProxyMode.cs @@ -0,0 +1,8 @@ +namespace Microsoft.Net.Http.Client; + +public enum ProxyMode +{ + None, + Http, + Tunnel +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RedirectMode.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RedirectMode.cs new file mode 100644 index 00000000..f6c1e2e7 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RedirectMode.cs @@ -0,0 +1,19 @@ +namespace Microsoft.Net.Http.Client; + +public enum RedirectMode +{ + /// + /// Do not follow redirects. + /// + None, + + /// + /// Disallows redirecting from HTTPS to HTTP + /// + NoDowngrade, + + /// + /// Follow all redirects + /// + All, +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RequestExtensions.cs new file mode 100644 index 00000000..4c998bc3 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RequestExtensions.cs @@ -0,0 +1,102 @@ +namespace Microsoft.Net.Http.Client; + +internal static class RequestExtensions +{ + public static bool IsHttp(this HttpRequestMessage request) + { + return string.Equals("http", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); + } + + public static bool IsHttps(this HttpRequestMessage request) + { + return string.Equals("https", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); + } + + public static string GetSchemeProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Scheme"); + } + + public static void SetSchemeProperty(this HttpRequestMessage request, string scheme) + { + request.SetProperty("url.Scheme", scheme); + } + + public static string GetHostProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Host"); + } + + public static void SetHostProperty(this HttpRequestMessage request, string host) + { + request.SetProperty("url.Host", host); + } + + public static int? GetPortProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Port"); + } + + public static void SetPortProperty(this HttpRequestMessage request, int? port) + { + request.SetProperty("url.Port", port); + } + + public static string GetConnectionHostProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.ConnectionHost"); + } + + public static void SetConnectionHostProperty(this HttpRequestMessage request, string host) + { + request.SetProperty("url.ConnectionHost", host); + } + + public static int? GetConnectionPortProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.ConnectionPort"); + } + + public static void SetConnectionPortProperty(this HttpRequestMessage request, int? port) + { + request.SetProperty("url.ConnectionPort", port); + } + + public static string GetPathAndQueryProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.PathAndQuery"); + } + + public static void SetPathAndQueryProperty(this HttpRequestMessage request, string pathAndQuery) + { + request.SetProperty("url.PathAndQuery", pathAndQuery); + } + + public static string GetAddressLineProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.AddressLine"); + } + + public static void SetAddressLineProperty(this HttpRequestMessage request, string addressLine) + { + request.SetProperty("url.AddressLine", addressLine); + } + + public static T GetProperty(this HttpRequestMessage request, string key) + { +#if NET6_0_OR_GREATER + return request.Options.TryGetValue(new HttpRequestOptionsKey(key), out var obj) ? obj : default; +#else + return request.Properties.TryGetValue(key, out var obj) ? (T)obj : default; +#endif + } + + public static void SetProperty(this HttpRequestMessage request, string key, T value) + { +#if NET6_0_OR_GREATER + request.Options.Set(new HttpRequestOptionsKey(key), value); +#else + request.Properties[key] = value; +#endif + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj new file mode 100644 index 00000000..dbecb65c --- /dev/null +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -0,0 +1,20 @@ + + + Docker.DotNet.NPipe + Docker.DotNet.NPipe + Docker.DotNet.NPipe is a library that allows you to connect via windows npipe with a Docker engine programmatically in your .NET applications. + + + + + + + + + + + + + + + diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/BufferedReadStream.cs new file mode 100644 index 00000000..b98aa284 --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -0,0 +1,240 @@ +using System.Buffers; +using System.IO; +using System.Net.Sockets; +using Docker.DotNet; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Net.Http.Client; + +internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream +{ + private readonly Stream _inner; + private readonly Socket _socket; + private readonly byte[] _buffer; + private readonly ILogger _logger; + private int _bufferRefCount; + private int _bufferOffset; + private int _bufferCount; + + public BufferedReadStream(Stream inner, Socket socket, ILogger logger) + : this(inner, socket, 8192, logger) + { + } + + public BufferedReadStream(Stream inner, Socket socket, int bufferLength, ILogger logger) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + _socket = socket; + _buffer = ArrayPool.Shared.Rent(bufferLength); + _logger = logger; + _bufferRefCount = 1; + } + + public override bool CanRead + { + get { return _inner.CanRead || _bufferCount > 0; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return _inner.CanWrite; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override bool CanCloseWrite => _socket != null || _inner is WriteClosableStream; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Exchange(ref _bufferRefCount, 0) == 1) + { + ArrayPool.Shared.Return(_buffer); + } + + _inner.Dispose(); + } + + base.Dispose(disposing); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int read = ReadBuffer(buffer, offset, count); + if (read > 0) + { + return read; + } + + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int read = ReadBuffer(buffer, offset, count); + if (read > 0) + { + return Task.FromResult(read); + } + + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override void CloseWrite() + { + if (_socket != null) + { + _socket.Shutdown(SocketShutdown.Send); + return; + } + + if (_inner is WriteClosableStream writeClosableStream) + { + writeClosableStream.CloseWrite(); + return; + } + + throw new NotSupportedException("Cannot shutdown write on this transport"); + } + + public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) + { + int read = PeekBuffer(buffer, toPeek, out peeked, out available, out remaining); + if (read > 0) + { + return true; + } + + if (_inner is IPeekableStream peekableStream) + { + return peekableStream.Peek(buffer, toPeek, out peeked, out available, out remaining); + } + + throw new NotSupportedException("_inner stream isn't a peekable stream"); + } + + public async Task ReadLineAsync(CancellationToken cancellationToken) + { + var line = new StringBuilder(_buffer.Length); + + var crIndex = -1; + + var lfIndex = -1; + + bool crlfFound; + + do + { + if (_bufferCount == 0) + { + _bufferOffset = 0; + + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) + .ConfigureAwait(false); + } + + var c = (char)_buffer[_bufferOffset]; + line.Append(c); + + _bufferOffset++; + _bufferCount--; + + switch (c) + { + case '\r': + crIndex = line.Length; + break; + case '\n': + lfIndex = line.Length; + break; + } + + crlfFound = crIndex + 1 == lfIndex; + } + while (!crlfFound); + + return line.ToString(0, line.Length - 2); + } + + private int ReadBuffer(byte[] buffer, int offset, int count) + { + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return 0; + } + + private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) + { + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, (int)toPeek); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); + peeked = (uint) toCopy; + available = (uint)_bufferCount; + remaining = available - peeked; + return toCopy; + } + + peeked = 0; + available = 0; + remaining = 0; + return 0; + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedReadStream.cs new file mode 100644 index 00000000..dc2e552d --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedReadStream.cs @@ -0,0 +1,152 @@ +using System.Globalization; +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal sealed class ChunkedReadStream : Stream +{ + private readonly BufferedReadStream _inner; + private int _chunkBytesRemaining; + private bool _done; + + public ChunkedReadStream(BufferedReadStream stream) + { + _inner = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override bool CanRead + { + get { return _inner.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int ReadTimeout + { + get + { + return _inner.ReadTimeout; + } + set + { + _inner.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + return _inner.WriteTimeout; + } + set + { + _inner.WriteTimeout = value; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_done) + { + return 0; + } + + if (_chunkBytesRemaining == 0) + { + var headerLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) + { + throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); + } + } + + var readBytesCount = 0; + + if (_chunkBytesRemaining > 0) + { + var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); + + readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) + .ConfigureAwait(false); + + if (readBytesCount == 0) + { + throw new EndOfStreamException(); + } + + _chunkBytesRemaining -= readBytesCount; + } + + if (_chunkBytesRemaining == 0) + { + var emptyLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (!string.IsNullOrEmpty(emptyLine)) + { + throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); + } + + _done = readBytesCount == 0; + } + + return readBytesCount; + } + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + _inner.Flush(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedWriteStream.cs new file mode 100644 index 00000000..b63fe3c6 --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedWriteStream.cs @@ -0,0 +1,92 @@ +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal sealed class ChunkedWriteStream : Stream +{ + private static readonly byte[] EndOfContentBytes = Encoding.ASCII.GetBytes("0\r\n\r\n"); + + private readonly Stream _inner; + + public ChunkedWriteStream(Stream stream) + { + _inner = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get { throw new NotImplementedException(); } + } + + public override long Position + { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public override void Flush() + { + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (count == 0) + { + return; + } + + const string crlf = "\r\n"; + + var chunkHeader = count.ToString("X") + crlf; + var headerBytes = Encoding.ASCII.GetBytes(chunkHeader); + + // Write the chunk header + await _inner.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken) + .ConfigureAwait(false); + + // Write the chunk data + await _inner.WriteAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + + // Write the chunk footer (CRLF) + await _inner.WriteAsync(headerBytes, headerBytes.Length - 2, 2, cancellationToken) + .ConfigureAwait(false); + } + + public Task EndContentAsync(CancellationToken cancellationToken) + { + return _inner.WriteAsync(EndOfContentBytes, 0, EndOfContentBytes.Length, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ContentLengthReadStream.cs new file mode 100644 index 00000000..c0ba7ef3 --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ContentLengthReadStream.cs @@ -0,0 +1,166 @@ +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal class ContentLengthReadStream : Stream +{ + private readonly Stream _inner; + private long _bytesRemaining; + private bool _disposed; + + public ContentLengthReadStream(Stream inner, long contentLength) + { + _inner = inner; + _bytesRemaining = contentLength; + } + + public override bool CanRead + { + get { return !_disposed; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int ReadTimeout + { + get + { + CheckDisposed(); + return _inner.ReadTimeout; + } + set + { + CheckDisposed(); + _inner.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + CheckDisposed(); + return _inner.WriteTimeout; + } + set + { + CheckDisposed(); + _inner.WriteTimeout = value; + } + } + + private void UpdateBytesRemaining(int read) + { + _bytesRemaining -= read; + if (_bytesRemaining <= 0) + { + _disposed = true; + } + System.Diagnostics.Debug.Assert(_bytesRemaining >= 0, "Negative bytes remaining? " + _bytesRemaining); + } + + public override int Read(byte[] buffer, int offset, int count) + { + // TODO: Validate buffer + if (_disposed) + { + return 0; + } + + if (_bytesRemaining == 0) + { + return 0; + } + + int toRead = (int)Math.Min(count, _bytesRemaining); + int read = _inner.Read(buffer, offset, toRead); + UpdateBytesRemaining(read); + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // TODO: Validate args + if (_disposed) + { + return 0; + } + + if (_bytesRemaining == 0) + { + return 0; + } + + cancellationToken.ThrowIfCancellationRequested(); + int toRead = (int)Math.Min(count, _bytesRemaining); + int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken); + UpdateBytesRemaining(read); + return read; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // TODO: Sync drain with timeout if small number of bytes remaining? This will let us re-use the connection. + _inner.Dispose(); + } + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(typeof(ContentLengthReadStream).FullName); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnection.cs new file mode 100644 index 00000000..2f20e3dc --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnection.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; + +namespace Microsoft.Net.Http.Client; + +internal sealed class HttpConnection : IDisposable +{ + private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; + + public HttpConnection(BufferedReadStream transport) + { + Transport = transport; + } + + public BufferedReadStream Transport { get; } + + public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + // Serialize headers & send + string rawRequest = SerializeRequest(request); + byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest); + await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken); + + if (request.Content != null) + { + if (request.Content.Headers.ContentLength.HasValue) + { + await request.Content.CopyToAsync(Transport); + } + else + { + // The length of the data is unknown. Send it in chunked mode. + using (var chunkedStream = new ChunkedWriteStream(Transport)) + { + await request.Content.CopyToAsync(chunkedStream); + await chunkedStream.EndContentAsync(cancellationToken); + } + } + } + + // Receive headers + List responseLines = await ReadResponseLinesAsync(cancellationToken); + + // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) + return CreateResponseMessage(responseLines); + } + catch (Exception ex) + { + Dispose(); // Any errors at this layer abort the connection. + throw new HttpRequestException("The requested failed, see inner exception for details.", ex); + } + } + + private string SerializeRequest(HttpRequestMessage request) + { + StringBuilder builder = new StringBuilder(); + builder.Append(request.Method); + builder.Append(' '); + builder.Append(request.GetAddressLineProperty()); + builder.Append(" HTTP/"); + builder.Append(request.Version.ToString(2)); + builder.Append("\r\n"); + + builder.Append(request.Headers); + + if (request.Content != null) + { + // Force the content to compute its content length if it has not already. + var contentLength = request.Content.Headers.ContentLength; + if (contentLength.HasValue) + { + request.Content.Headers.ContentLength = contentLength.Value; + } + + builder.Append(request.Content.Headers); + if (!contentLength.HasValue) + { + // Add header for chunked mode. + builder.Append("Transfer-Encoding: chunked\r\n"); + } + } + // Headers end with an empty line + builder.Append("\r\n"); + return builder.ToString(); + } + + private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) + { + var lines = new List(12); + + do + { + var line = await Transport.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (string.IsNullOrEmpty(line)) + { + break; + } + + lines.Add(line); + } + while (true); + + return lines; + } + + private HttpResponseMessage CreateResponseMessage(List responseLines) + { + string responseLine = responseLines.First(); + // HTTP/1.1 200 OK + string[] responseLineParts = responseLine.Split(new[] { ' ' }, 3); + // TODO: Verify HTTP/1.0 or 1.1. + if (responseLineParts.Length < 2) + { + throw new HttpRequestException("Invalid response line: " + responseLine); + } + + if (int.TryParse(responseLineParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode)) + { + // TODO: Validate range + } + else + { + throw new HttpRequestException("Invalid status code: " + responseLineParts[1]); + } + HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)statusCode); + if (responseLineParts.Length >= 3) + { + response.ReasonPhrase = responseLineParts[2]; + } + var content = new HttpConnectionResponseContent(this); + response.Content = content; + + foreach (var rawHeader in responseLines.Skip(1)) + { + int colonOffset = rawHeader.IndexOf(':'); + if (colonOffset <= 0) + { + throw new HttpRequestException("The given header line format is invalid: " + rawHeader); + } + string headerName = rawHeader.Substring(0, colonOffset); + string headerValue = rawHeader.Substring(colonOffset + 2); + if (!response.Headers.TryAddWithoutValidation(headerName, headerValue)) + { + bool success = response.Content.Headers.TryAddWithoutValidation(headerName, headerValue); + System.Diagnostics.Debug.Assert(success, "Failed to add response header: " + rawHeader); + } + } + + // TODO: We'll need to refactor this in the future. + // + // Depending on the request and response (headers), we need to handle the response + // differently. We need to distinguish between four types of responses: + // + // 1. Chunked transfer encoding + // 2. HTTP with a `Content-Length` header + // 3. Hijacked TCP connections (using the connection upgrade headers) + // - `/containers/{id}/attach` + // - `/exec/{id}/start` + // 4. Streams without the connection upgrade headers + // - `/containers/{id}/logs` + + var isConnectionUpgrade = response.Headers.TryGetValues("Upgrade", out var responseHeaderValues) + && responseHeaderValues.Any(header => "tcp".Equals(header)); + + var isStream = content.Headers.TryGetValues("Content-Type", out var contentHeaderValues) + && contentHeaderValues.Any(header => DockerStreamHeaders.Contains(header)); + + var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isStream) || (isStream && !isConnectionUpgrade); + + content.ResolveResponseStream(chunked: isChunkedTransferEncoding); + + return response; + } + + public void Dispose() + { + Transport.Dispose(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs new file mode 100644 index 00000000..01de9b77 --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs @@ -0,0 +1,76 @@ +using System.IO; + +namespace Microsoft.Net.Http.Client; + +public class HttpConnectionResponseContent : HttpContent +{ + private readonly HttpConnection _connection; + private Stream _responseStream; + + internal HttpConnectionResponseContent(HttpConnection connection) + { + _connection = connection; + } + + internal void ResolveResponseStream(bool chunked) + { + if (_responseStream != null) + { + throw new InvalidOperationException("Called multiple times"); + } + if (chunked) + { + _responseStream = new ChunkedReadStream(_connection.Transport); + } + else if (Headers.ContentLength.HasValue) + { + _responseStream = new ContentLengthReadStream(_connection.Transport, Headers.ContentLength.Value); + } + else + { + _responseStream = _connection.Transport; + } + } + + public Docker.DotNet.WriteClosableStream HijackStream() + { + if (_responseStream != _connection.Transport) + { + throw new InvalidOperationException("cannot hijack chunked or content length stream"); + } + + return _connection.Transport; + } + + protected override Task SerializeToStreamAsync(Stream stream, System.Net.TransportContext context) + { + return _responseStream.CopyToAsync(stream); + } + + protected override Task CreateContentReadStreamAsync() + { + return Task.FromResult(_responseStream); + } + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + _responseStream.Dispose(); + _connection.Dispose(); + } + } + finally + { + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ManagedHandler.cs new file mode 100644 index 00000000..bc17c436 --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ManagedHandler.cs @@ -0,0 +1,397 @@ +namespace Microsoft.Net.Http.Client; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; + +public class ManagedHandler : HttpMessageHandler +{ + private readonly ILogger _logger; + + private readonly StreamOpener _streamOpener; + + private readonly SocketOpener _socketOpener; + + private IWebProxy _proxy; + + public delegate Task StreamOpener(string host, int port, CancellationToken cancellationToken); + + public delegate Task SocketOpener(string host, int port, CancellationToken cancellationToken); + + public ManagedHandler(ILogger logger) + { + _logger = logger; + _socketOpener = TcpSocketOpenerAsync; + } + + public ManagedHandler(StreamOpener opener, ILogger logger) + { + _logger = logger; + _streamOpener = opener ?? throw new ArgumentNullException(nameof(opener)); + } + + public ManagedHandler(SocketOpener opener, ILogger logger) + { + _logger = logger; + _socketOpener = opener ?? throw new ArgumentNullException(nameof(opener)); + } + + public IWebProxy Proxy + { + get + { + if (_proxy == null) + { + _proxy = WebRequest.DefaultWebProxy; + } + + return _proxy; + } + set + { + _proxy = value; + } + } + + public bool UseProxy { get; set; } = true; + + public int MaxAutomaticRedirects { get; set; } = 20; + + public RedirectMode RedirectMode { get; set; } = RedirectMode.NoDowngrade; + + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + + public X509CertificateCollection ClientCertificates { get; set; } = new X509Certificate2Collection(); + + protected override async Task SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) + { + if (httpRequestMessage == null) + { + throw new ArgumentNullException(nameof(httpRequestMessage)); + } + + HttpResponseMessage httpResponseMessage = null; + + for (var i = 0; i < MaxAutomaticRedirects; i++) + { + httpResponseMessage?.Dispose(); + + httpResponseMessage = await ProcessRequestAsync(httpRequestMessage, cancellationToken) + .ConfigureAwait(false); + + if (!IsRedirectResponse(httpRequestMessage, httpResponseMessage)) + { + return httpResponseMessage; + } + } + + return httpResponseMessage; + } + + private bool IsRedirectResponse(HttpRequestMessage request, HttpResponseMessage response) + { + if (response.StatusCode < HttpStatusCode.MovedPermanently || response.StatusCode >= HttpStatusCode.BadRequest) + { + return false; + } + + if (RedirectMode == RedirectMode.None) + { + return false; + } + + var location = response.Headers.Location; + + if (location == null) + { + return false; + } + + if (!location.IsAbsoluteUri) + { + request.RequestUri = location; + request.Headers.Authorization = null; + request.SetAddressLineProperty(null); + request.SetPathAndQueryProperty(null); + return true; + } + + // Check if redirect from https to http is allowed + if (request.IsHttps() && string.Equals("http", location.Scheme, StringComparison.OrdinalIgnoreCase) + && RedirectMode == RedirectMode.NoDowngrade) + { + return false; + } + + // Reset fields calculated from the URI. + request.RequestUri = location; + request.Headers.Authorization = null; + request.Headers.Host = null; + request.SetConnectionHostProperty(null); + request.SetConnectionPortProperty(null); + request.SetSchemeProperty(null); + request.SetHostProperty(null); + request.SetPortProperty(null); + request.SetAddressLineProperty(null); + request.SetPathAndQueryProperty(null); + return true; + } + + private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + ProcessUrl(request); + ProcessHostHeader(request); + request.Headers.ConnectionClose = !request.Headers.Contains("Connection"); // TODO: Connection reuse is not supported. + + ProxyMode proxyMode = DetermineProxyModeAndAddressLine(request); + Socket socket; + Stream transport; + + try + { + if (_socketOpener != null) + { + socket = await _socketOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); + transport = new NetworkStream(socket, true); + } + else + { + socket = null; + transport = await _streamOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException e) + { + throw new HttpRequestException("Connection failed.", e); + } + + if (proxyMode == ProxyMode.Tunnel) + { + await TunnelThroughProxyAsync(request, transport, cancellationToken); + } + + if (request.IsHttps()) + { + SslStream sslStream = new SslStream(transport, false, ServerCertificateValidationCallback); + await sslStream.AuthenticateAsClientAsync(request.GetHostProperty(), ClientCertificates, SslProtocols.Tls12, false); + transport = sslStream; + } + + var bufferedReadStream = new BufferedReadStream(transport, socket, _logger); + var connection = new HttpConnection(bufferedReadStream); + return await connection.SendAsync(request, cancellationToken); + } + + // Data comes from either the request.RequestUri or from the request.Properties + private static void ProcessUrl(HttpRequestMessage request) + { + string scheme = request.GetSchemeProperty(); + if (string.IsNullOrWhiteSpace(scheme)) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + scheme = request.RequestUri.Scheme; + request.SetSchemeProperty(scheme); + } + + if (!request.IsHttp() && !request.IsHttps()) + { + throw new InvalidOperationException("Only HTTP or HTTPS are supported, not: " + request.RequestUri.Scheme); + } + + string host = request.GetHostProperty(); + if (string.IsNullOrWhiteSpace(host)) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + host = request.RequestUri.DnsSafeHost; + request.SetHostProperty(host); + } + + string connectionHost = request.GetConnectionHostProperty(); + if (string.IsNullOrWhiteSpace(connectionHost)) + { + request.SetConnectionHostProperty(host); + } + + int? port = request.GetPortProperty(); + if (!port.HasValue) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + port = request.RequestUri.Port; + request.SetPortProperty(port); + } + + int? connectionPort = request.GetConnectionPortProperty(); + if (!connectionPort.HasValue) + { + request.SetConnectionPortProperty(port); + } + + string pathAndQuery = request.GetPathAndQueryProperty(); + if (string.IsNullOrWhiteSpace(pathAndQuery)) + { + if (request.RequestUri.IsAbsoluteUri) + { + pathAndQuery = request.RequestUri.PathAndQuery; + } + else + { + pathAndQuery = Uri.EscapeDataString(request.RequestUri.ToString()); + } + request.SetPathAndQueryProperty(pathAndQuery); + } + } + + private static void ProcessHostHeader(HttpRequestMessage request) + { + if (string.IsNullOrWhiteSpace(request.Headers.Host)) + { + string host = request.GetHostProperty(); + int port = request.GetPortProperty().Value; + if (host.Contains(':')) + { + // IPv6 + host = '[' + host + ']'; + } + + request.Headers.Host = host + ":" + port.ToString(CultureInfo.InvariantCulture); + } + } + + private ProxyMode DetermineProxyModeAndAddressLine(HttpRequestMessage request) + { + string scheme = request.GetSchemeProperty(); + string host = request.GetHostProperty(); + int? port = request.GetPortProperty(); + string pathAndQuery = request.GetPathAndQueryProperty(); + string addressLine = request.GetAddressLineProperty(); + + if (string.IsNullOrEmpty(addressLine)) + { + request.SetAddressLineProperty(pathAndQuery); + } + + try + { + if (!UseProxy || Proxy == null || Proxy.IsBypassed(request.RequestUri)) + { + return ProxyMode.None; + } + } + catch (PlatformNotSupportedException) + { + return ProxyMode.None; + } + + var proxyUri = Proxy.GetProxy(request.RequestUri); + if (proxyUri == null) + { + return ProxyMode.None; + } + + if (request.IsHttp()) + { + if (string.IsNullOrEmpty(addressLine)) + { + addressLine = scheme + "://" + host + ":" + port.Value + pathAndQuery; + request.SetAddressLineProperty(addressLine); + } + request.SetConnectionHostProperty(proxyUri.DnsSafeHost); + request.SetConnectionPortProperty(proxyUri.Port); + return ProxyMode.Http; + } + + // Tunneling generates a completely separate request, don't alter the original, just the connection address. + request.SetConnectionHostProperty(proxyUri.DnsSafeHost); + request.SetConnectionPortProperty(proxyUri.Port); + return ProxyMode.Tunnel; + } + + private static async Task TcpSocketOpenerAsync(string host, int port, CancellationToken cancellationToken) + { + var addresses = await Dns.GetHostAddressesAsync(host) + .ConfigureAwait(false); + + if (addresses.Length == 0) + { + throw new Exception($"Unable to resolve any IP addresses for the host '{host}'."); + } + + var exceptions = new List(); + + foreach (var address in addresses) + { + var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + try + { + await socket.ConnectAsync(address, port) + .ConfigureAwait(false); + + return socket; + } + catch (Exception e) + { + socket.Dispose(); + exceptions.Add(e); + } + } + + throw new AggregateException(exceptions); + } + + private async Task TunnelThroughProxyAsync(HttpRequestMessage request, Stream transport, CancellationToken cancellationToken) + { + // Send a Connect request: + // CONNECT server.example.com:80 HTTP / 1.1 + // Host: server.example.com:80 + var connectRequest = new HttpRequestMessage(); + connectRequest.Version = new Version(1, 1); + + connectRequest.Headers.ProxyAuthorization = request.Headers.ProxyAuthorization; + connectRequest.Method = new HttpMethod("CONNECT"); + // TODO: IPv6 hosts + string authority = request.GetHostProperty() + ":" + request.GetPortProperty().Value; + connectRequest.SetAddressLineProperty(authority); + connectRequest.Headers.Host = authority; + + HttpConnection connection = new HttpConnection(new BufferedReadStream(transport, null, _logger)); + HttpResponseMessage connectResponse; + try + { + connectResponse = await connection.SendAsync(connectRequest, cancellationToken); + // TODO:? await connectResponse.Content.LoadIntoBufferAsync(); // Drain any body + // There's no danger of accidentally consuming real response data because the real request hasn't been sent yet. + } + catch (Exception ex) + { + transport.Dispose(); + throw new HttpRequestException("SSL Tunnel failed to initialize", ex); + } + + // Listen for a response. Any 2XX is considered success, anything else is considered a failure. + if ((int)connectResponse.StatusCode < 200 || 300 <= (int)connectResponse.StatusCode) + { + transport.Dispose(); + throw new HttpRequestException("Failed to negotiate the proxy tunnel: " + connectResponse); + } + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ProxyMode.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ProxyMode.cs new file mode 100644 index 00000000..05a823c9 --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ProxyMode.cs @@ -0,0 +1,8 @@ +namespace Microsoft.Net.Http.Client; + +public enum ProxyMode +{ + None, + Http, + Tunnel +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RedirectMode.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RedirectMode.cs new file mode 100644 index 00000000..f6c1e2e7 --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RedirectMode.cs @@ -0,0 +1,19 @@ +namespace Microsoft.Net.Http.Client; + +public enum RedirectMode +{ + /// + /// Do not follow redirects. + /// + None, + + /// + /// Disallows redirecting from HTTPS to HTTP + /// + NoDowngrade, + + /// + /// Follow all redirects + /// + All, +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RequestExtensions.cs new file mode 100644 index 00000000..4c998bc3 --- /dev/null +++ b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RequestExtensions.cs @@ -0,0 +1,102 @@ +namespace Microsoft.Net.Http.Client; + +internal static class RequestExtensions +{ + public static bool IsHttp(this HttpRequestMessage request) + { + return string.Equals("http", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); + } + + public static bool IsHttps(this HttpRequestMessage request) + { + return string.Equals("https", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); + } + + public static string GetSchemeProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Scheme"); + } + + public static void SetSchemeProperty(this HttpRequestMessage request, string scheme) + { + request.SetProperty("url.Scheme", scheme); + } + + public static string GetHostProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Host"); + } + + public static void SetHostProperty(this HttpRequestMessage request, string host) + { + request.SetProperty("url.Host", host); + } + + public static int? GetPortProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Port"); + } + + public static void SetPortProperty(this HttpRequestMessage request, int? port) + { + request.SetProperty("url.Port", port); + } + + public static string GetConnectionHostProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.ConnectionHost"); + } + + public static void SetConnectionHostProperty(this HttpRequestMessage request, string host) + { + request.SetProperty("url.ConnectionHost", host); + } + + public static int? GetConnectionPortProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.ConnectionPort"); + } + + public static void SetConnectionPortProperty(this HttpRequestMessage request, int? port) + { + request.SetProperty("url.ConnectionPort", port); + } + + public static string GetPathAndQueryProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.PathAndQuery"); + } + + public static void SetPathAndQueryProperty(this HttpRequestMessage request, string pathAndQuery) + { + request.SetProperty("url.PathAndQuery", pathAndQuery); + } + + public static string GetAddressLineProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.AddressLine"); + } + + public static void SetAddressLineProperty(this HttpRequestMessage request, string addressLine) + { + request.SetProperty("url.AddressLine", addressLine); + } + + public static T GetProperty(this HttpRequestMessage request, string key) + { +#if NET6_0_OR_GREATER + return request.Options.TryGetValue(new HttpRequestOptionsKey(key), out var obj) ? obj : default; +#else + return request.Properties.TryGetValue(key, out var obj) ? (T)obj : default; +#endif + } + + public static void SetProperty(this HttpRequestMessage request, string key, T value) + { +#if NET6_0_OR_GREATER + request.Options.Set(new HttpRequestOptionsKey(key), value); +#else + request.Properties[key] = value; +#endif + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs b/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs new file mode 100644 index 00000000..98b3537f --- /dev/null +++ b/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs @@ -0,0 +1,43 @@ +using System; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Docker.DotNet.NPipe +{ + public class NpipeHandlerFactory : IDockerHandlerFactory + { + public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + { + if (configuration.Credentials.IsTlsCredentials()) + { + throw new Exception("TLS not supported over npipe"); + } + var segments = uri.Segments; + if (segments.Length != 3 || !segments[1].Equals("pipe/", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException($"{configuration.EndpointBaseUri} is not a valid npipe URI"); + } + var serverName = uri.Host; + if (string.Equals(serverName, "localhost", StringComparison.OrdinalIgnoreCase)) + { + serverName = "."; + } + var pipeName = uri.Segments[2]; + uri = new UriBuilder("http", pipeName).Uri; + + return new Tuple( + new Microsoft.Net.Http.Client.ManagedHandler(async (host, port, cancellationToken) => + { + var timeout = (int)TimeSpan.FromSeconds(10).TotalMilliseconds; + var stream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + var dockerStream = new DockerPipeStream(stream); + await stream.ConnectAsync(timeout, cancellationToken).ConfigureAwait(false); + return dockerStream; + }, logger), + uri + ); + } + } +} diff --git a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj new file mode 100644 index 00000000..4f837cc0 --- /dev/null +++ b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj @@ -0,0 +1,20 @@ + + + Docker.DotNet.NativeHttp + Docker.DotNet.NativeHttp + Docker.DotNet.NativeHttp is a library that allows you to connect via http(s) with a Docker engine programmatically in your .NET applications. + + + + + + + + + + + + + + + diff --git a/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs b/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs new file mode 100644 index 00000000..05921aaa --- /dev/null +++ b/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace Docker.DotNet.NativeHttp +{ + public class NativeHttpHandlerFactory : IDockerHandlerFactory + { + public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + { + + var builder = new UriBuilder(uri) + { + Scheme = configuration.Credentials.IsTlsCredentials() ? "https" : "http" + }; + uri = builder.Uri; + +#if NET6_0_OR_GREATER + return new Tuple( + new SocketsHttpHandler() + { + PooledConnectionLifetime = TimeSpan.FromMinutes(5), + PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), + MaxConnectionsPerServer = 10 + }, + uri + ); +#else + return new Tuple( + new HttpClientHandler(), + uri + ); +#endif + } + } +} diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj new file mode 100644 index 00000000..40f453be --- /dev/null +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -0,0 +1,20 @@ + + + Docker.DotNet.Unix + Docker.DotNet.Unix + Docker.DotNet.Unix is a library that allows you to connect via unix socket with a Docker engine programmatically in your .NET applications. + + + + + + + + + + + + + + + diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/BufferedReadStream.cs new file mode 100644 index 00000000..b98aa284 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -0,0 +1,240 @@ +using System.Buffers; +using System.IO; +using System.Net.Sockets; +using Docker.DotNet; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Net.Http.Client; + +internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream +{ + private readonly Stream _inner; + private readonly Socket _socket; + private readonly byte[] _buffer; + private readonly ILogger _logger; + private int _bufferRefCount; + private int _bufferOffset; + private int _bufferCount; + + public BufferedReadStream(Stream inner, Socket socket, ILogger logger) + : this(inner, socket, 8192, logger) + { + } + + public BufferedReadStream(Stream inner, Socket socket, int bufferLength, ILogger logger) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + _socket = socket; + _buffer = ArrayPool.Shared.Rent(bufferLength); + _logger = logger; + _bufferRefCount = 1; + } + + public override bool CanRead + { + get { return _inner.CanRead || _bufferCount > 0; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return _inner.CanWrite; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override bool CanCloseWrite => _socket != null || _inner is WriteClosableStream; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Interlocked.Exchange(ref _bufferRefCount, 0) == 1) + { + ArrayPool.Shared.Return(_buffer); + } + + _inner.Dispose(); + } + + base.Dispose(disposing); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int read = ReadBuffer(buffer, offset, count); + if (read > 0) + { + return read; + } + + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int read = ReadBuffer(buffer, offset, count); + if (read > 0) + { + return Task.FromResult(read); + } + + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override void CloseWrite() + { + if (_socket != null) + { + _socket.Shutdown(SocketShutdown.Send); + return; + } + + if (_inner is WriteClosableStream writeClosableStream) + { + writeClosableStream.CloseWrite(); + return; + } + + throw new NotSupportedException("Cannot shutdown write on this transport"); + } + + public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) + { + int read = PeekBuffer(buffer, toPeek, out peeked, out available, out remaining); + if (read > 0) + { + return true; + } + + if (_inner is IPeekableStream peekableStream) + { + return peekableStream.Peek(buffer, toPeek, out peeked, out available, out remaining); + } + + throw new NotSupportedException("_inner stream isn't a peekable stream"); + } + + public async Task ReadLineAsync(CancellationToken cancellationToken) + { + var line = new StringBuilder(_buffer.Length); + + var crIndex = -1; + + var lfIndex = -1; + + bool crlfFound; + + do + { + if (_bufferCount == 0) + { + _bufferOffset = 0; + + _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) + .ConfigureAwait(false); + } + + var c = (char)_buffer[_bufferOffset]; + line.Append(c); + + _bufferOffset++; + _bufferCount--; + + switch (c) + { + case '\r': + crIndex = line.Length; + break; + case '\n': + lfIndex = line.Length; + break; + } + + crlfFound = crIndex + 1 == lfIndex; + } + while (!crlfFound); + + return line.ToString(0, line.Length - 2); + } + + private int ReadBuffer(byte[] buffer, int offset, int count) + { + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; + } + + return 0; + } + + private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) + { + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, (int)toPeek); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); + peeked = (uint) toCopy; + available = (uint)_bufferCount; + remaining = available - peeked; + return toCopy; + } + + peeked = 0; + available = 0; + remaining = 0; + return 0; + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedReadStream.cs new file mode 100644 index 00000000..dc2e552d --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedReadStream.cs @@ -0,0 +1,152 @@ +using System.Globalization; +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal sealed class ChunkedReadStream : Stream +{ + private readonly BufferedReadStream _inner; + private int _chunkBytesRemaining; + private bool _done; + + public ChunkedReadStream(BufferedReadStream stream) + { + _inner = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override bool CanRead + { + get { return _inner.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int ReadTimeout + { + get + { + return _inner.ReadTimeout; + } + set + { + _inner.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + return _inner.WriteTimeout; + } + set + { + _inner.WriteTimeout = value; + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_done) + { + return 0; + } + + if (_chunkBytesRemaining == 0) + { + var headerLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) + { + throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); + } + } + + var readBytesCount = 0; + + if (_chunkBytesRemaining > 0) + { + var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); + + readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) + .ConfigureAwait(false); + + if (readBytesCount == 0) + { + throw new EndOfStreamException(); + } + + _chunkBytesRemaining -= readBytesCount; + } + + if (_chunkBytesRemaining == 0) + { + var emptyLine = await _inner.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (!string.IsNullOrEmpty(emptyLine)) + { + throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); + } + + _done = readBytesCount == 0; + } + + return readBytesCount; + } + + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + _inner.Flush(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedWriteStream.cs new file mode 100644 index 00000000..b63fe3c6 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedWriteStream.cs @@ -0,0 +1,92 @@ +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal sealed class ChunkedWriteStream : Stream +{ + private static readonly byte[] EndOfContentBytes = Encoding.ASCII.GetBytes("0\r\n\r\n"); + + private readonly Stream _inner; + + public ChunkedWriteStream(Stream stream) + { + _inner = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get { throw new NotImplementedException(); } + } + + public override long Position + { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + public override void Flush() + { + _inner.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (count == 0) + { + return; + } + + const string crlf = "\r\n"; + + var chunkHeader = count.ToString("X") + crlf; + var headerBytes = Encoding.ASCII.GetBytes(chunkHeader); + + // Write the chunk header + await _inner.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken) + .ConfigureAwait(false); + + // Write the chunk data + await _inner.WriteAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + + // Write the chunk footer (CRLF) + await _inner.WriteAsync(headerBytes, headerBytes.Length - 2, 2, cancellationToken) + .ConfigureAwait(false); + } + + public Task EndContentAsync(CancellationToken cancellationToken) + { + return _inner.WriteAsync(EndOfContentBytes, 0, EndOfContentBytes.Length, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ContentLengthReadStream.cs new file mode 100644 index 00000000..c0ba7ef3 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ContentLengthReadStream.cs @@ -0,0 +1,166 @@ +using System.IO; + +namespace Microsoft.Net.Http.Client; + +internal class ContentLengthReadStream : Stream +{ + private readonly Stream _inner; + private long _bytesRemaining; + private bool _disposed; + + public ContentLengthReadStream(Stream inner, long contentLength) + { + _inner = inner; + _bytesRemaining = contentLength; + } + + public override bool CanRead + { + get { return !_disposed; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override int ReadTimeout + { + get + { + CheckDisposed(); + return _inner.ReadTimeout; + } + set + { + CheckDisposed(); + _inner.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + CheckDisposed(); + return _inner.WriteTimeout; + } + set + { + CheckDisposed(); + _inner.WriteTimeout = value; + } + } + + private void UpdateBytesRemaining(int read) + { + _bytesRemaining -= read; + if (_bytesRemaining <= 0) + { + _disposed = true; + } + System.Diagnostics.Debug.Assert(_bytesRemaining >= 0, "Negative bytes remaining? " + _bytesRemaining); + } + + public override int Read(byte[] buffer, int offset, int count) + { + // TODO: Validate buffer + if (_disposed) + { + return 0; + } + + if (_bytesRemaining == 0) + { + return 0; + } + + int toRead = (int)Math.Min(count, _bytesRemaining); + int read = _inner.Read(buffer, offset, toRead); + UpdateBytesRemaining(read); + return read; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + // TODO: Validate args + if (_disposed) + { + return 0; + } + + if (_bytesRemaining == 0) + { + return 0; + } + + cancellationToken.ThrowIfCancellationRequested(); + int toRead = (int)Math.Min(count, _bytesRemaining); + int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken); + UpdateBytesRemaining(read); + return read; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // TODO: Sync drain with timeout if small number of bytes remaining? This will let us re-use the connection. + _inner.Dispose(); + } + } + + private void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(typeof(ContentLengthReadStream).FullName); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnection.cs new file mode 100644 index 00000000..2f20e3dc --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnection.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; + +namespace Microsoft.Net.Http.Client; + +internal sealed class HttpConnection : IDisposable +{ + private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; + + public HttpConnection(BufferedReadStream transport) + { + Transport = transport; + } + + public BufferedReadStream Transport { get; } + + public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + // Serialize headers & send + string rawRequest = SerializeRequest(request); + byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest); + await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken); + + if (request.Content != null) + { + if (request.Content.Headers.ContentLength.HasValue) + { + await request.Content.CopyToAsync(Transport); + } + else + { + // The length of the data is unknown. Send it in chunked mode. + using (var chunkedStream = new ChunkedWriteStream(Transport)) + { + await request.Content.CopyToAsync(chunkedStream); + await chunkedStream.EndContentAsync(cancellationToken); + } + } + } + + // Receive headers + List responseLines = await ReadResponseLinesAsync(cancellationToken); + + // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) + return CreateResponseMessage(responseLines); + } + catch (Exception ex) + { + Dispose(); // Any errors at this layer abort the connection. + throw new HttpRequestException("The requested failed, see inner exception for details.", ex); + } + } + + private string SerializeRequest(HttpRequestMessage request) + { + StringBuilder builder = new StringBuilder(); + builder.Append(request.Method); + builder.Append(' '); + builder.Append(request.GetAddressLineProperty()); + builder.Append(" HTTP/"); + builder.Append(request.Version.ToString(2)); + builder.Append("\r\n"); + + builder.Append(request.Headers); + + if (request.Content != null) + { + // Force the content to compute its content length if it has not already. + var contentLength = request.Content.Headers.ContentLength; + if (contentLength.HasValue) + { + request.Content.Headers.ContentLength = contentLength.Value; + } + + builder.Append(request.Content.Headers); + if (!contentLength.HasValue) + { + // Add header for chunked mode. + builder.Append("Transfer-Encoding: chunked\r\n"); + } + } + // Headers end with an empty line + builder.Append("\r\n"); + return builder.ToString(); + } + + private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) + { + var lines = new List(12); + + do + { + var line = await Transport.ReadLineAsync(cancellationToken) + .ConfigureAwait(false); + + if (string.IsNullOrEmpty(line)) + { + break; + } + + lines.Add(line); + } + while (true); + + return lines; + } + + private HttpResponseMessage CreateResponseMessage(List responseLines) + { + string responseLine = responseLines.First(); + // HTTP/1.1 200 OK + string[] responseLineParts = responseLine.Split(new[] { ' ' }, 3); + // TODO: Verify HTTP/1.0 or 1.1. + if (responseLineParts.Length < 2) + { + throw new HttpRequestException("Invalid response line: " + responseLine); + } + + if (int.TryParse(responseLineParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode)) + { + // TODO: Validate range + } + else + { + throw new HttpRequestException("Invalid status code: " + responseLineParts[1]); + } + HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)statusCode); + if (responseLineParts.Length >= 3) + { + response.ReasonPhrase = responseLineParts[2]; + } + var content = new HttpConnectionResponseContent(this); + response.Content = content; + + foreach (var rawHeader in responseLines.Skip(1)) + { + int colonOffset = rawHeader.IndexOf(':'); + if (colonOffset <= 0) + { + throw new HttpRequestException("The given header line format is invalid: " + rawHeader); + } + string headerName = rawHeader.Substring(0, colonOffset); + string headerValue = rawHeader.Substring(colonOffset + 2); + if (!response.Headers.TryAddWithoutValidation(headerName, headerValue)) + { + bool success = response.Content.Headers.TryAddWithoutValidation(headerName, headerValue); + System.Diagnostics.Debug.Assert(success, "Failed to add response header: " + rawHeader); + } + } + + // TODO: We'll need to refactor this in the future. + // + // Depending on the request and response (headers), we need to handle the response + // differently. We need to distinguish between four types of responses: + // + // 1. Chunked transfer encoding + // 2. HTTP with a `Content-Length` header + // 3. Hijacked TCP connections (using the connection upgrade headers) + // - `/containers/{id}/attach` + // - `/exec/{id}/start` + // 4. Streams without the connection upgrade headers + // - `/containers/{id}/logs` + + var isConnectionUpgrade = response.Headers.TryGetValues("Upgrade", out var responseHeaderValues) + && responseHeaderValues.Any(header => "tcp".Equals(header)); + + var isStream = content.Headers.TryGetValues("Content-Type", out var contentHeaderValues) + && contentHeaderValues.Any(header => DockerStreamHeaders.Contains(header)); + + var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isStream) || (isStream && !isConnectionUpgrade); + + content.ResolveResponseStream(chunked: isChunkedTransferEncoding); + + return response; + } + + public void Dispose() + { + Transport.Dispose(); + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs new file mode 100644 index 00000000..01de9b77 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs @@ -0,0 +1,76 @@ +using System.IO; + +namespace Microsoft.Net.Http.Client; + +public class HttpConnectionResponseContent : HttpContent +{ + private readonly HttpConnection _connection; + private Stream _responseStream; + + internal HttpConnectionResponseContent(HttpConnection connection) + { + _connection = connection; + } + + internal void ResolveResponseStream(bool chunked) + { + if (_responseStream != null) + { + throw new InvalidOperationException("Called multiple times"); + } + if (chunked) + { + _responseStream = new ChunkedReadStream(_connection.Transport); + } + else if (Headers.ContentLength.HasValue) + { + _responseStream = new ContentLengthReadStream(_connection.Transport, Headers.ContentLength.Value); + } + else + { + _responseStream = _connection.Transport; + } + } + + public Docker.DotNet.WriteClosableStream HijackStream() + { + if (_responseStream != _connection.Transport) + { + throw new InvalidOperationException("cannot hijack chunked or content length stream"); + } + + return _connection.Transport; + } + + protected override Task SerializeToStreamAsync(Stream stream, System.Net.TransportContext context) + { + return _responseStream.CopyToAsync(stream); + } + + protected override Task CreateContentReadStreamAsync() + { + return Task.FromResult(_responseStream); + } + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + _responseStream.Dispose(); + _connection.Dispose(); + } + } + finally + { + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ManagedHandler.cs new file mode 100644 index 00000000..bc17c436 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ManagedHandler.cs @@ -0,0 +1,397 @@ +namespace Microsoft.Net.Http.Client; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.Logging; + +public class ManagedHandler : HttpMessageHandler +{ + private readonly ILogger _logger; + + private readonly StreamOpener _streamOpener; + + private readonly SocketOpener _socketOpener; + + private IWebProxy _proxy; + + public delegate Task StreamOpener(string host, int port, CancellationToken cancellationToken); + + public delegate Task SocketOpener(string host, int port, CancellationToken cancellationToken); + + public ManagedHandler(ILogger logger) + { + _logger = logger; + _socketOpener = TcpSocketOpenerAsync; + } + + public ManagedHandler(StreamOpener opener, ILogger logger) + { + _logger = logger; + _streamOpener = opener ?? throw new ArgumentNullException(nameof(opener)); + } + + public ManagedHandler(SocketOpener opener, ILogger logger) + { + _logger = logger; + _socketOpener = opener ?? throw new ArgumentNullException(nameof(opener)); + } + + public IWebProxy Proxy + { + get + { + if (_proxy == null) + { + _proxy = WebRequest.DefaultWebProxy; + } + + return _proxy; + } + set + { + _proxy = value; + } + } + + public bool UseProxy { get; set; } = true; + + public int MaxAutomaticRedirects { get; set; } = 20; + + public RedirectMode RedirectMode { get; set; } = RedirectMode.NoDowngrade; + + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + + public X509CertificateCollection ClientCertificates { get; set; } = new X509Certificate2Collection(); + + protected override async Task SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) + { + if (httpRequestMessage == null) + { + throw new ArgumentNullException(nameof(httpRequestMessage)); + } + + HttpResponseMessage httpResponseMessage = null; + + for (var i = 0; i < MaxAutomaticRedirects; i++) + { + httpResponseMessage?.Dispose(); + + httpResponseMessage = await ProcessRequestAsync(httpRequestMessage, cancellationToken) + .ConfigureAwait(false); + + if (!IsRedirectResponse(httpRequestMessage, httpResponseMessage)) + { + return httpResponseMessage; + } + } + + return httpResponseMessage; + } + + private bool IsRedirectResponse(HttpRequestMessage request, HttpResponseMessage response) + { + if (response.StatusCode < HttpStatusCode.MovedPermanently || response.StatusCode >= HttpStatusCode.BadRequest) + { + return false; + } + + if (RedirectMode == RedirectMode.None) + { + return false; + } + + var location = response.Headers.Location; + + if (location == null) + { + return false; + } + + if (!location.IsAbsoluteUri) + { + request.RequestUri = location; + request.Headers.Authorization = null; + request.SetAddressLineProperty(null); + request.SetPathAndQueryProperty(null); + return true; + } + + // Check if redirect from https to http is allowed + if (request.IsHttps() && string.Equals("http", location.Scheme, StringComparison.OrdinalIgnoreCase) + && RedirectMode == RedirectMode.NoDowngrade) + { + return false; + } + + // Reset fields calculated from the URI. + request.RequestUri = location; + request.Headers.Authorization = null; + request.Headers.Host = null; + request.SetConnectionHostProperty(null); + request.SetConnectionPortProperty(null); + request.SetSchemeProperty(null); + request.SetHostProperty(null); + request.SetPortProperty(null); + request.SetAddressLineProperty(null); + request.SetPathAndQueryProperty(null); + return true; + } + + private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + ProcessUrl(request); + ProcessHostHeader(request); + request.Headers.ConnectionClose = !request.Headers.Contains("Connection"); // TODO: Connection reuse is not supported. + + ProxyMode proxyMode = DetermineProxyModeAndAddressLine(request); + Socket socket; + Stream transport; + + try + { + if (_socketOpener != null) + { + socket = await _socketOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); + transport = new NetworkStream(socket, true); + } + else + { + socket = null; + transport = await _streamOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException e) + { + throw new HttpRequestException("Connection failed.", e); + } + + if (proxyMode == ProxyMode.Tunnel) + { + await TunnelThroughProxyAsync(request, transport, cancellationToken); + } + + if (request.IsHttps()) + { + SslStream sslStream = new SslStream(transport, false, ServerCertificateValidationCallback); + await sslStream.AuthenticateAsClientAsync(request.GetHostProperty(), ClientCertificates, SslProtocols.Tls12, false); + transport = sslStream; + } + + var bufferedReadStream = new BufferedReadStream(transport, socket, _logger); + var connection = new HttpConnection(bufferedReadStream); + return await connection.SendAsync(request, cancellationToken); + } + + // Data comes from either the request.RequestUri or from the request.Properties + private static void ProcessUrl(HttpRequestMessage request) + { + string scheme = request.GetSchemeProperty(); + if (string.IsNullOrWhiteSpace(scheme)) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + scheme = request.RequestUri.Scheme; + request.SetSchemeProperty(scheme); + } + + if (!request.IsHttp() && !request.IsHttps()) + { + throw new InvalidOperationException("Only HTTP or HTTPS are supported, not: " + request.RequestUri.Scheme); + } + + string host = request.GetHostProperty(); + if (string.IsNullOrWhiteSpace(host)) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + host = request.RequestUri.DnsSafeHost; + request.SetHostProperty(host); + } + + string connectionHost = request.GetConnectionHostProperty(); + if (string.IsNullOrWhiteSpace(connectionHost)) + { + request.SetConnectionHostProperty(host); + } + + int? port = request.GetPortProperty(); + if (!port.HasValue) + { + if (!request.RequestUri.IsAbsoluteUri) + { + throw new InvalidOperationException("Missing URL Scheme"); + } + port = request.RequestUri.Port; + request.SetPortProperty(port); + } + + int? connectionPort = request.GetConnectionPortProperty(); + if (!connectionPort.HasValue) + { + request.SetConnectionPortProperty(port); + } + + string pathAndQuery = request.GetPathAndQueryProperty(); + if (string.IsNullOrWhiteSpace(pathAndQuery)) + { + if (request.RequestUri.IsAbsoluteUri) + { + pathAndQuery = request.RequestUri.PathAndQuery; + } + else + { + pathAndQuery = Uri.EscapeDataString(request.RequestUri.ToString()); + } + request.SetPathAndQueryProperty(pathAndQuery); + } + } + + private static void ProcessHostHeader(HttpRequestMessage request) + { + if (string.IsNullOrWhiteSpace(request.Headers.Host)) + { + string host = request.GetHostProperty(); + int port = request.GetPortProperty().Value; + if (host.Contains(':')) + { + // IPv6 + host = '[' + host + ']'; + } + + request.Headers.Host = host + ":" + port.ToString(CultureInfo.InvariantCulture); + } + } + + private ProxyMode DetermineProxyModeAndAddressLine(HttpRequestMessage request) + { + string scheme = request.GetSchemeProperty(); + string host = request.GetHostProperty(); + int? port = request.GetPortProperty(); + string pathAndQuery = request.GetPathAndQueryProperty(); + string addressLine = request.GetAddressLineProperty(); + + if (string.IsNullOrEmpty(addressLine)) + { + request.SetAddressLineProperty(pathAndQuery); + } + + try + { + if (!UseProxy || Proxy == null || Proxy.IsBypassed(request.RequestUri)) + { + return ProxyMode.None; + } + } + catch (PlatformNotSupportedException) + { + return ProxyMode.None; + } + + var proxyUri = Proxy.GetProxy(request.RequestUri); + if (proxyUri == null) + { + return ProxyMode.None; + } + + if (request.IsHttp()) + { + if (string.IsNullOrEmpty(addressLine)) + { + addressLine = scheme + "://" + host + ":" + port.Value + pathAndQuery; + request.SetAddressLineProperty(addressLine); + } + request.SetConnectionHostProperty(proxyUri.DnsSafeHost); + request.SetConnectionPortProperty(proxyUri.Port); + return ProxyMode.Http; + } + + // Tunneling generates a completely separate request, don't alter the original, just the connection address. + request.SetConnectionHostProperty(proxyUri.DnsSafeHost); + request.SetConnectionPortProperty(proxyUri.Port); + return ProxyMode.Tunnel; + } + + private static async Task TcpSocketOpenerAsync(string host, int port, CancellationToken cancellationToken) + { + var addresses = await Dns.GetHostAddressesAsync(host) + .ConfigureAwait(false); + + if (addresses.Length == 0) + { + throw new Exception($"Unable to resolve any IP addresses for the host '{host}'."); + } + + var exceptions = new List(); + + foreach (var address in addresses) + { + var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + + try + { + await socket.ConnectAsync(address, port) + .ConfigureAwait(false); + + return socket; + } + catch (Exception e) + { + socket.Dispose(); + exceptions.Add(e); + } + } + + throw new AggregateException(exceptions); + } + + private async Task TunnelThroughProxyAsync(HttpRequestMessage request, Stream transport, CancellationToken cancellationToken) + { + // Send a Connect request: + // CONNECT server.example.com:80 HTTP / 1.1 + // Host: server.example.com:80 + var connectRequest = new HttpRequestMessage(); + connectRequest.Version = new Version(1, 1); + + connectRequest.Headers.ProxyAuthorization = request.Headers.ProxyAuthorization; + connectRequest.Method = new HttpMethod("CONNECT"); + // TODO: IPv6 hosts + string authority = request.GetHostProperty() + ":" + request.GetPortProperty().Value; + connectRequest.SetAddressLineProperty(authority); + connectRequest.Headers.Host = authority; + + HttpConnection connection = new HttpConnection(new BufferedReadStream(transport, null, _logger)); + HttpResponseMessage connectResponse; + try + { + connectResponse = await connection.SendAsync(connectRequest, cancellationToken); + // TODO:? await connectResponse.Content.LoadIntoBufferAsync(); // Drain any body + // There's no danger of accidentally consuming real response data because the real request hasn't been sent yet. + } + catch (Exception ex) + { + transport.Dispose(); + throw new HttpRequestException("SSL Tunnel failed to initialize", ex); + } + + // Listen for a response. Any 2XX is considered success, anything else is considered a failure. + if ((int)connectResponse.StatusCode < 200 || 300 <= (int)connectResponse.StatusCode) + { + transport.Dispose(); + throw new HttpRequestException("Failed to negotiate the proxy tunnel: " + connectResponse); + } + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ProxyMode.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ProxyMode.cs new file mode 100644 index 00000000..05a823c9 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ProxyMode.cs @@ -0,0 +1,8 @@ +namespace Microsoft.Net.Http.Client; + +public enum ProxyMode +{ + None, + Http, + Tunnel +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RedirectMode.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RedirectMode.cs new file mode 100644 index 00000000..f6c1e2e7 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RedirectMode.cs @@ -0,0 +1,19 @@ +namespace Microsoft.Net.Http.Client; + +public enum RedirectMode +{ + /// + /// Do not follow redirects. + /// + None, + + /// + /// Disallows redirecting from HTTPS to HTTP + /// + NoDowngrade, + + /// + /// Follow all redirects + /// + All, +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RequestExtensions.cs new file mode 100644 index 00000000..4c998bc3 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RequestExtensions.cs @@ -0,0 +1,102 @@ +namespace Microsoft.Net.Http.Client; + +internal static class RequestExtensions +{ + public static bool IsHttp(this HttpRequestMessage request) + { + return string.Equals("http", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); + } + + public static bool IsHttps(this HttpRequestMessage request) + { + return string.Equals("https", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); + } + + public static string GetSchemeProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Scheme"); + } + + public static void SetSchemeProperty(this HttpRequestMessage request, string scheme) + { + request.SetProperty("url.Scheme", scheme); + } + + public static string GetHostProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Host"); + } + + public static void SetHostProperty(this HttpRequestMessage request, string host) + { + request.SetProperty("url.Host", host); + } + + public static int? GetPortProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.Port"); + } + + public static void SetPortProperty(this HttpRequestMessage request, int? port) + { + request.SetProperty("url.Port", port); + } + + public static string GetConnectionHostProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.ConnectionHost"); + } + + public static void SetConnectionHostProperty(this HttpRequestMessage request, string host) + { + request.SetProperty("url.ConnectionHost", host); + } + + public static int? GetConnectionPortProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.ConnectionPort"); + } + + public static void SetConnectionPortProperty(this HttpRequestMessage request, int? port) + { + request.SetProperty("url.ConnectionPort", port); + } + + public static string GetPathAndQueryProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.PathAndQuery"); + } + + public static void SetPathAndQueryProperty(this HttpRequestMessage request, string pathAndQuery) + { + request.SetProperty("url.PathAndQuery", pathAndQuery); + } + + public static string GetAddressLineProperty(this HttpRequestMessage request) + { + return request.GetProperty("url.AddressLine"); + } + + public static void SetAddressLineProperty(this HttpRequestMessage request, string addressLine) + { + request.SetProperty("url.AddressLine", addressLine); + } + + public static T GetProperty(this HttpRequestMessage request, string key) + { +#if NET6_0_OR_GREATER + return request.Options.TryGetValue(new HttpRequestOptionsKey(key), out var obj) ? obj : default; +#else + return request.Properties.TryGetValue(key, out var obj) ? (T)obj : default; +#endif + } + + public static void SetProperty(this HttpRequestMessage request, string key, T value) + { +#if NET6_0_OR_GREATER + request.Options.Set(new HttpRequestOptionsKey(key), value); +#else + request.Properties[key] = value; +#endif + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs new file mode 100644 index 00000000..74bd3fb1 --- /dev/null +++ b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs @@ -0,0 +1,88 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Net.Http.Client; + +internal sealed class UnixDomainSocketEndPoint : EndPoint +{ + private const AddressFamily EndPointAddressFamily = AddressFamily.Unix; + + private static readonly Encoding s_pathEncoding = Encoding.UTF8; + + private static readonly int s_nativePathOffset = 2; // = offsetof(struct sockaddr_un, sun_path). It's the same on Linux and OSX + + private static readonly int s_nativePathLength = 91; // sockaddr_un.sun_path at http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_un.h.html, -1 for terminator + + private static readonly int s_nativeAddressSize = s_nativePathOffset + s_nativePathLength; + + private readonly string _path; + + private readonly byte[] _encodedPath; + + public UnixDomainSocketEndPoint(string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + _path = path; + _encodedPath = s_pathEncoding.GetBytes(_path); + + if (path.Length == 0 || _encodedPath.Length > s_nativePathLength) + { + throw new ArgumentOutOfRangeException(nameof(path), path); + } + } + + internal UnixDomainSocketEndPoint(SocketAddress socketAddress) + { + if (socketAddress == null) + { + throw new ArgumentNullException(nameof(socketAddress)); + } + + if (socketAddress.Family != EndPointAddressFamily || + socketAddress.Size > s_nativeAddressSize) + { + throw new ArgumentOutOfRangeException(nameof(socketAddress)); + } + + if (socketAddress.Size > s_nativePathOffset) + { + _encodedPath = new byte[socketAddress.Size - s_nativePathOffset]; + for (int i = 0; i < _encodedPath.Length; i++) + { + _encodedPath[i] = socketAddress[s_nativePathOffset + i]; + } + + _path = s_pathEncoding.GetString(_encodedPath, 0, _encodedPath.Length); + } + else + { + _encodedPath = Array.Empty(); + _path = string.Empty; + } + } + + public override SocketAddress Serialize() + { + var result = new SocketAddress(AddressFamily.Unix, s_nativeAddressSize); + Debug.Assert(_encodedPath.Length + s_nativePathOffset <= result.Size, "Expected path to fit in address"); + + for (int index = 0; index < _encodedPath.Length; index++) + { + result[s_nativePathOffset + index] = _encodedPath[index]; + } + result[s_nativePathOffset + _encodedPath.Length] = 0; // path must be null-terminated + + return result; + } + + public override EndPoint Create(SocketAddress socketAddress) => new UnixDomainSocketEndPoint(socketAddress); + + public override AddressFamily AddressFamily => EndPointAddressFamily; + + public override string ToString() => _path; +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs b/src/Docker.DotNet.Unix/UnixHandlerFactory.cs new file mode 100644 index 00000000..6f62249a --- /dev/null +++ b/src/Docker.DotNet.Unix/UnixHandlerFactory.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Docker.DotNet.Unix +{ + public class UnixHandlerFactory : IDockerHandlerFactory + { + public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + { + var pipeString = uri.LocalPath; + uri = new UriBuilder("http", uri.Segments.Last()).Uri; + return new Tuple( + new Microsoft.Net.Http.Client.ManagedHandler(async (host, port, cancellationToken) => + { + var sock = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + await sock.ConnectAsync(new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(pipeString)).ConfigureAwait(false); + return sock; + }, logger), + uri + ); + } + } +} diff --git a/src/Docker.DotNet.X509/CertificateCredentials.cs b/src/Docker.DotNet.X509/CertificateCredentials.cs index e5deb654..a8d692c1 100644 --- a/src/Docker.DotNet.X509/CertificateCredentials.cs +++ b/src/Docker.DotNet.X509/CertificateCredentials.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.Security.Authentication; + namespace Docker.DotNet.X509; public class CertificateCredentials : Credentials @@ -24,19 +28,60 @@ public override bool IsTlsCredentials() public override HttpMessageHandler GetHandler(HttpMessageHandler handler) { - if (handler is not ManagedHandler managedHandler) +#if NET6_0_OR_GREATER + if (handler is SocketsHttpHandler nativeHandler) { - return handler; + nativeHandler.UseProxy = true; + nativeHandler.AllowAutoRedirect = true; + nativeHandler.MaxAutomaticRedirections = 20; + nativeHandler.Proxy = WebRequest.DefaultWebProxy; + nativeHandler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions + { + ClientCertificates = new X509CertificateCollection { _certificate }, + CertificateRevocationCheckMode = X509RevocationMode.NoCheck, + EnabledSslProtocols = SslProtocols.Tls12, + RemoteCertificateValidationCallback = (message, certificate, chain, errors) => ServerCertificateValidationCallback?.Invoke(message, certificate, chain, errors) ?? false + }; + return nativeHandler; } - - if (!managedHandler.ClientCertificates.Contains(_certificate)) +#else + if (handler is HttpClientHandler nativeHandler) { - managedHandler.ClientCertificates.Add(_certificate); + if (!nativeHandler.ClientCertificates.Contains(_certificate)) + { + nativeHandler.ClientCertificates.Add(_certificate); + } + + nativeHandler.UseProxy = true; + nativeHandler.AllowAutoRedirect = true; + nativeHandler.MaxAutomaticRedirections = 20; + nativeHandler.Proxy = WebRequest.DefaultWebProxy; + nativeHandler.ClientCertificateOptions = ClientCertificateOption.Manual; + nativeHandler.CheckCertificateRevocationList = false; + nativeHandler.SslProtocols = SslProtocols.Tls12; + nativeHandler.ServerCertificateCustomValidationCallback += (message, certificate, chain, errors) => ServerCertificateValidationCallback?.Invoke(message, certificate, chain, errors) ?? false; + return nativeHandler; } +#endif + else + { + // Use reflection to support different handler without direct reference + var handlerType = handler.GetType(); + var clientCertificatesProp = handlerType.GetProperty("ClientCertificates"); + var serverCertValidationProp = handlerType.GetProperty("ServerCertificateValidationCallback"); - managedHandler.ServerCertificateValidationCallback = ServerCertificateValidationCallback; + if (clientCertificatesProp != null && serverCertValidationProp != null) + { + var clientCertificates = clientCertificatesProp.GetValue(handler) as System.Collections.IList; + if (clientCertificates != null && !clientCertificates.Contains(_certificate)) + { + clientCertificates.Add(_certificate); + } - return handler; + serverCertValidationProp.SetValue(handler, ServerCertificateValidationCallback); + } + return handler; + } } protected virtual void Dispose(bool disposing) diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index 8c2dc7c0..c3c9cd52 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -21,6 +21,5 @@ - \ No newline at end of file diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index 97a59fa4..1fcd26ba 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -48,6 +48,5 @@ - \ No newline at end of file diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 2c32db03..1a395b6c 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -1,7 +1,7 @@ namespace Docker.DotNet; using System; -using System.IO.Pipes; +using System.Reflection; public sealed class DockerClient : IDockerClient { @@ -15,10 +15,11 @@ public sealed class DockerClient : IDockerClient private readonly Version _requestedApiVersion; - internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion, ILogger logger = null) + private readonly IDockerHandlerFactory _handlerFactory; + + internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion, IDockerHandlerFactory handlerFactory = null, ILogger logger = null) { _requestedApiVersion = requestedApiVersion; - Configuration = configuration; DefaultTimeout = configuration.DefaultTimeout; @@ -34,80 +35,12 @@ internal DockerClient(DockerClientConfiguration configuration, Version requested Plugin = new PluginOperations(this); Exec = new ExecOperations(this); - ManagedHandler handler; - var uri = Configuration.EndpointBaseUri; - switch (uri.Scheme.ToLowerInvariant()) - { - case "npipe": - if (Configuration.Credentials.IsTlsCredentials()) - { - throw new Exception("TLS not supported over npipe"); - } - - var segments = uri.Segments; - if (segments.Length != 3 || !segments[1].Equals("pipe/", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException($"{Configuration.EndpointBaseUri} is not a valid npipe URI"); - } - - var serverName = uri.Host; - if (string.Equals(serverName, "localhost", StringComparison.OrdinalIgnoreCase)) - { - // npipe schemes dont work with npipe://localhost/... and need npipe://./... so fix that for a client here. - serverName = "."; - } - - var pipeName = uri.Segments[2]; - - uri = new UriBuilder("http", pipeName).Uri; - handler = new ManagedHandler(async (host, port, cancellationToken) => - { - var timeout = (int)Configuration.NamedPipeConnectTimeout.TotalMilliseconds; - var stream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); - var dockerStream = new DockerPipeStream(stream); - - await stream.ConnectAsync(timeout, cancellationToken) - .ConfigureAwait(false); - - return dockerStream; - }, logger); - break; - - case "tcp": - case "http": - var builder = new UriBuilder(uri) - { - Scheme = configuration.Credentials.IsTlsCredentials() ? "https" : "http" - }; - uri = builder.Uri; - handler = new ManagedHandler(logger); - break; - - case "https": - handler = new ManagedHandler(logger); - break; - - case "unix": - var pipeString = uri.LocalPath; - handler = new ManagedHandler(async (host, port, cancellationToken) => - { - var sock = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - - await sock.ConnectAsync(new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(pipeString)) - .ConfigureAwait(false); - - return sock; - }, logger); - uri = new UriBuilder("http", uri.Segments.Last()).Uri; - break; - - default: - throw new Exception($"Unknown URL scheme {configuration.EndpointBaseUri.Scheme}"); - } + _handlerFactory = handlerFactory ?? throw new InvalidOperationException("No handler factory provided"); - _endpointBaseUri = uri; + var handlerAndUri = _handlerFactory.CreateHandler(Configuration.EndpointBaseUri, Configuration, logger); - _client = new HttpClient(Configuration.Credentials.GetHandler(handler), true); + _endpointBaseUri = handlerAndUri.Item2; + _client = new HttpClient(Configuration.Credentials.GetHandler(handlerAndUri.Item1), true); _client.Timeout = Timeout.InfiniteTimeSpan; } @@ -395,12 +328,26 @@ internal async Task MakeRequestForHijackedStreamAsync( await HandleIfErrorResponseAsync(response.StatusCode, response, errorHandlers) .ConfigureAwait(false); - if (response.Content is not HttpConnectionResponseContent content) + // Use reflection to support different handler without direct reference + var hijackMethod = HijackStreamHelper.GetHijackMethodFromType(response.Content.GetType()); + + if (hijackMethod == null) { - throw new NotSupportedException("message handler does not support hijacked streams"); + // Native http handler + var stream = await response.Content.ReadAsStreamAsync() + .ConfigureAwait(false); + return new WriteClosableStreamWrapper(stream); } + else + { + var hijackedStream = hijackMethod.Invoke(response.Content, null) as WriteClosableStream; + if (hijackedStream == null) + { + throw new NotSupportedException("HijackStream did not return a WriteClosableStream"); + } - return content.HijackStream(); + return hijackedStream; + } } private async Task PrivateMakeRequestAsync( diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index a1fb82d3..dda1c4b7 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -7,9 +7,8 @@ public class DockerClientConfiguration : IDisposable public DockerClientConfiguration( Credentials credentials = null, TimeSpan defaultTimeout = default, - TimeSpan namedPipeConnectTimeout = default, IReadOnlyDictionary defaultHttpRequestHeaders = null) - : this(GetLocalDockerEndpoint(), credentials, defaultTimeout, namedPipeConnectTimeout, defaultHttpRequestHeaders) + : this(GetLocalDockerEndpoint(), credentials, defaultTimeout, defaultHttpRequestHeaders) { } @@ -17,7 +16,6 @@ public DockerClientConfiguration( Uri endpoint, Credentials credentials = null, TimeSpan defaultTimeout = default, - TimeSpan namedPipeConnectTimeout = default, IReadOnlyDictionary defaultHttpRequestHeaders = null) { if (endpoint == null) @@ -33,7 +31,6 @@ public DockerClientConfiguration( EndpointBaseUri = endpoint; Credentials = credentials ?? new AnonymousCredentials(); DefaultTimeout = TimeSpan.Equals(TimeSpan.Zero, defaultTimeout) ? TimeSpan.FromSeconds(100) : defaultTimeout; - NamedPipeConnectTimeout = TimeSpan.Equals(TimeSpan.Zero, namedPipeConnectTimeout) ? TimeSpan.FromMilliseconds(100) : namedPipeConnectTimeout; DefaultHttpRequestHeaders = defaultHttpRequestHeaders ?? new Dictionary(); } @@ -48,11 +45,44 @@ public DockerClientConfiguration( public TimeSpan DefaultTimeout { get; } - public TimeSpan NamedPipeConnectTimeout { get; } - public DockerClient CreateClient(Version requestedApiVersion = null, ILogger logger = null) { - return new DockerClient(this, requestedApiVersion, logger); + var scheme = EndpointBaseUri.Scheme; + if (scheme.Equals("http", StringComparison.OrdinalIgnoreCase) || + scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + scheme = "Http"; + } + + // Try to find a loaded handler factory that matches the scheme and Docker.DotNet + var factoryType = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => a.FullName.Contains("Docker.DotNet", StringComparison.OrdinalIgnoreCase)) + .SelectMany(a => a.GetTypes()) + .FirstOrDefault(t => + typeof(IDockerHandlerFactory).IsAssignableFrom(t) && + !t.IsInterface && !t.IsAbstract && + (t.Name.Contains(scheme, StringComparison.OrdinalIgnoreCase) || + t.Namespace?.Contains(scheme, StringComparison.OrdinalIgnoreCase) == true) + ); + + if (factoryType == null) + { + throw new InvalidOperationException($"No Docker handler factory implementation found for scheme '{scheme}'. Please reference at least one handler package (e.g., NPipe, Unix, NativeHttp, LegacyHttp)."); + } + + var factory = (IDockerHandlerFactory)Activator.CreateInstance(factoryType); + + return new DockerClient(this, requestedApiVersion, factory, logger); + } + + public DockerClient CreateClient(Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) + { + if (handlerFactory == null) + { + throw new ArgumentNullException(nameof(handlerFactory)); + } + + return new DockerClient(this, requestedApiVersion, handlerFactory, logger); } public void Dispose() diff --git a/src/Docker.DotNet/DockerPipeStream.cs b/src/Docker.DotNet/DockerPipeStream.cs index 596ebb07..71010a18 100644 --- a/src/Docker.DotNet/DockerPipeStream.cs +++ b/src/Docker.DotNet/DockerPipeStream.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet; -internal class DockerPipeStream : WriteClosableStream, IPeekableStream +public sealed class DockerPipeStream : WriteClosableStream, IPeekableStream { private readonly EventWaitHandle _event = new EventWaitHandle(false, EventResetMode.AutoReset); private readonly PipeStream _stream; diff --git a/src/Docker.DotNet/HijackStreamHelper.cs b/src/Docker.DotNet/HijackStreamHelper.cs new file mode 100644 index 00000000..25f4b2d8 --- /dev/null +++ b/src/Docker.DotNet/HijackStreamHelper.cs @@ -0,0 +1,7 @@ +namespace Docker.DotNet; + +public class HijackStreamHelper +{ + public static MethodInfo GetHijackMethodFromType(Type type) => + type.GetMethod("HijackStream", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); +} \ No newline at end of file diff --git a/src/Docker.DotNet/IDockerHandlerFactory.cs b/src/Docker.DotNet/IDockerHandlerFactory.cs new file mode 100644 index 00000000..fef37eb1 --- /dev/null +++ b/src/Docker.DotNet/IDockerHandlerFactory.cs @@ -0,0 +1,7 @@ +using System; +using Microsoft.Extensions.Logging; + +public interface IDockerHandlerFactory +{ + Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger); +} diff --git a/src/Docker.DotNet/WriteClosableStream.cs b/src/Docker.DotNet/WriteClosableStream.cs new file mode 100644 index 00000000..52135e61 --- /dev/null +++ b/src/Docker.DotNet/WriteClosableStream.cs @@ -0,0 +1,8 @@ +namespace Docker.DotNet; + +public abstract class WriteClosableStream : Stream +{ + public abstract bool CanCloseWrite { get; } + + public abstract void CloseWrite(); +} \ No newline at end of file diff --git a/src/Docker.DotNet/WriteClosableStreamWrapper.cs b/src/Docker.DotNet/WriteClosableStreamWrapper.cs new file mode 100644 index 00000000..a2d27289 --- /dev/null +++ b/src/Docker.DotNet/WriteClosableStreamWrapper.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; + +namespace Docker.DotNet; + +public class WriteClosableStreamWrapper : WriteClosableStream +{ + private readonly Stream _baseStream; + + public WriteClosableStreamWrapper(Stream baseStream) + { + _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); + } + + public override void CloseWrite() + { + _baseStream.Close(); // Replace with half-close logic if available + } + + public override bool CanRead => _baseStream.CanRead; + public override bool CanSeek => _baseStream.CanSeek; + public override bool CanWrite => _baseStream.CanWrite; + public override bool CanCloseWrite => true; + public override long Length => _baseStream.Length; + + public override long Position + { + get => _baseStream.Position; + set => _baseStream.Position = value; + } + + public override void Flush() => _baseStream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _baseStream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => + _baseStream.Seek(offset, origin); + + public override void SetLength(long value) => + _baseStream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => + _baseStream.Write(buffer, offset, count); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _baseStream.Dispose(); + } + base.Dispose(disposing); + } +} \ No newline at end of file From 5eb5fa437889c0098806da005ae7d0d8d6755651 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Wed, 8 Oct 2025 10:07:19 +0200 Subject: [PATCH 02/50] test all docker clients modify TestFixture to support dind and different clients add dind for tls client tests to ci add parallel test add speed test check for com.docker.service in Docker_IsRunning too add 1s sleep in MonitorEventsFiltered_Succeeds test --- .github/workflows/ci.yml | 64 +++ test/Docker.DotNet.Tests/CommonCommands.cs | 4 +- .../Docker.DotNet.Tests.csproj | 5 + .../IConfigOperationsTests.cs | 20 +- .../IContainerOperationsTests.cs | 500 +++++++++++++----- .../IImageOperationsTests.cs | 32 +- .../ISwarmOperationsTests.cs | 93 ++-- .../ISystemOperations.Tests.cs | 94 ++-- .../IVolumeOperationsTests.cs | 26 +- test/Docker.DotNet.Tests/TestClientsEnum.cs | 11 + test/Docker.DotNet.Tests/TestDaemonsEnum.cs | 9 + test/Docker.DotNet.Tests/TestFixture.cs | 298 ++++++++--- 12 files changed, 835 insertions(+), 321 deletions(-) create mode 100644 test/Docker.DotNet.Tests/TestClientsEnum.cs create mode 100644 test/Docker.DotNet.Tests/TestDaemonsEnum.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f613c4a..6582de3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,29 @@ on: jobs: build: runs-on: ubuntu-22.04 + services: + # Docker without TLS (plain TCP) !DEPRECATED! with next docker release + docker-no-tls: + image: docker:28.1-dind + env: + DOCKER_TLS_CERTDIR: "" + ports: + - 2375:2375 + options: >- + --privileged + + # Docker with TLS (secure TCP) + docker-tls: + image: docker:28.1-dind + env: + DOCKER_TLS_CERTDIR: /certs + ports: + - 2376:2376 + options: >- + --privileged + volumes: + - ${{ github.workspace }}/certs:/certs + strategy: matrix: framework: @@ -16,6 +39,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + path: test fetch-depth: 0 - name: Setup .NET Core uses: actions/setup-dotnet@v4 @@ -23,5 +47,45 @@ jobs: dotnet-version: 9.x - name: Build run: dotnet build -c Release --framework ${{ matrix.framework }} + working-directory: test + + - name: Pack client cert, key, ca for C# docker client + run: | + mkdir -p ${{ github.workspace }}/certs + sudo chmod 777 ${{ github.workspace }}/certs + + # create pfx + openssl pkcs12 -export -out ${{ github.workspace }}/certs/client.pfx -inkey ${{ github.workspace }}/certs/client/key.pem -in ${{ github.workspace }}/certs/client/cert.pem -certfile ${{ github.workspace }}/certs/client/ca.pem -passout pass: + + - name: Wait for Docker (no TLS) to be healthy + run: | + for i in {1..10}; do + if docker --host=tcp://localhost:2375 version; then + echo "Docker (no TLS) is ready!" + exit 0 + fi + echo "Waiting for Docker (no TLS) to be ready..." + sleep 3 + done + echo "Docker (no TLS) did not become ready in time." + exit 1 + + - name: Wait for Docker (with TLS) to be healthy + run: | + for i in {1..10}; do + if docker --host=tcp://localhost:2376 --tlsverify \ + --tlscacert=${{ github.workspace }}/certs/client/ca.pem \ + --tlscert=${{ github.workspace }}/certs/client/cert.pem \ + --tlskey=${{ github.workspace }}/certs/client/key.pem version; then + echo "Docker (TLS) is ready!" + exit 0 + fi + echo "Waiting for Docker (TLS) to be ready..." + sleep 3 + done + echo "Docker (TLS) did not become ready in time." + exit 1 + - name: Test run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build --logger console + working-directory: test diff --git a/test/Docker.DotNet.Tests/CommonCommands.cs b/test/Docker.DotNet.Tests/CommonCommands.cs index b6219392..19db1d0d 100644 --- a/test/Docker.DotNet.Tests/CommonCommands.cs +++ b/test/Docker.DotNet.Tests/CommonCommands.cs @@ -4,5 +4,7 @@ public static class CommonCommands { public static readonly string[] SleepInfinity = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; sleep infinity"]; - public static readonly string[] EchoToStdoutAndStderr = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; while true; do echo \"stdout message\"; echo \"stderr message\" >&2; sleep 1; done"]; + public static readonly string[] EchoToStdoutAndStderr = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; RND=$RANDOM; while true; do echo \"stdout message $RND\"; echo \"stderr message $RND\" >&2; sleep 1; done"]; + + public static readonly string[] EchoToStdoutAndStderrFast = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; RND=$RANDOM; while true; do echo \"stdout message $RND\"; echo \"stderr message $RND\" >&2; done"]; } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index 470c3e68..f773cd36 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -12,7 +12,12 @@ + + + + + diff --git a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs index 533d5d25..db018547 100644 --- a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs @@ -12,10 +12,14 @@ public IConfigOperationsTests(TestFixture testFixture, ITestOutputHelper testOut _testOutputHelper = testOutputHelper; } - [Fact] - public async Task SwarmConfig_CanCreateAndRead() + public static IEnumerable GetDockerClientTypes() => + TestFixture.GetDockerClientTypes(); + + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task SwarmConfig_CanCreateAndRead(TestClientsEnum clientType) { - var currentConfigs = await _testFixture.DockerClient.Configs.ListConfigsAsync(); + var currentConfigs = await _testFixture.DockerClients[clientType].Configs.ListConfigsAsync(); _testOutputHelper.WriteLine($"Current Configs: {currentConfigs.Count}"); @@ -31,15 +35,15 @@ public async Task SwarmConfig_CanCreateAndRead() Config = testConfigSpec }; - var createdConfig = await _testFixture.DockerClient.Configs.CreateConfigAsync(configParameters); + var createdConfig = await _testFixture.DockerClients[clientType].Configs.CreateConfigAsync(configParameters); Assert.NotNull(createdConfig.ID); _testOutputHelper.WriteLine($"Config created: {createdConfig.ID}"); - var configs = await _testFixture.DockerClient.Configs.ListConfigsAsync(); + var configs = await _testFixture.DockerClients[clientType].Configs.ListConfigsAsync(); Assert.Contains(configs, c => c.ID == createdConfig.ID); _testOutputHelper.WriteLine($"Current Configs: {configs.Count}"); - var configResponse = await _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID); + var configResponse = await _testFixture.DockerClients[clientType].Configs.InspectConfigAsync(createdConfig.ID); Assert.NotNull(configResponse); @@ -51,8 +55,8 @@ public async Task SwarmConfig_CanCreateAndRead() _testOutputHelper.WriteLine("Config created is the same."); - await _testFixture.DockerClient.Configs.RemoveConfigAsync(createdConfig.ID); + await _testFixture.DockerClients[clientType].Configs.RemoveConfigAsync(createdConfig.ID); - await Assert.ThrowsAsync(() => _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID)); + await Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].Configs.InspectConfigAsync(createdConfig.ID)); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs index cc2ae303..3535064e 100644 --- a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs @@ -1,3 +1,7 @@ +using System.Collections.Concurrent; +using System.Net.NetworkInformation; + + namespace Docker.DotNet.Tests; [Collection(nameof(TestCollection))] @@ -12,13 +16,17 @@ public IContainerOperationsTests(TestFixture testFixture, ITestOutputHelper test _testOutputHelper = testOutputHelper; } - [Fact] - public async Task CreateContainerAsync_CreatesContainer() + public static IEnumerable GetDockerClientTypes() => + TestFixture.GetDockerClientTypes(); + + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task CreateContainerAsync_CreatesContainer(TestClientsEnum clientType) { - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr }, _testFixture.Cts.Token @@ -28,22 +36,23 @@ public async Task CreateContainerAsync_CreatesContainer() Assert.NotEmpty(createContainerResponse.ID); } - [Fact] - public async Task GetContainerLogs_Tty_False_Follow_True_TaskIsCompleted() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerLogs_Tty_False_Follow_True_TaskIsCompleted(TestClientsEnum clientType) { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -51,7 +60,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -63,7 +72,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( new Progress(m => _testOutputHelper.WriteLine(m)), containerLogsCts.Token); - await _testFixture.DockerClient.Containers.StopContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), _testFixture.Cts.Token @@ -73,22 +82,23 @@ await _testFixture.DockerClient.Containers.StopContainerAsync( Assert.True(containerLogsTask.IsCompletedSuccessfully); } - [Fact] - public async Task GetContainerLogs_Tty_False_Follow_False_ReadsLogs() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerLogs_Tty_False_Follow_False_ReadsLogs(TestClientsEnum clientType) { var logList = new List(); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -96,7 +106,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( await Task.Delay(TimeSpan.FromSeconds(5)); - await _testFixture.DockerClient.Containers.GetContainerLogsAsync( + await _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -109,7 +119,7 @@ await _testFixture.DockerClient.Containers.GetContainerLogsAsync( _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StopContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), _testFixture.Cts.Token @@ -120,22 +130,168 @@ await _testFixture.DockerClient.Containers.StopContainerAsync( Assert.NotEmpty(logList); } - [Fact] - public async Task GetContainerLogs_Tty_True_Follow_False_ReadsLogs() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs(TestClientsEnum clientType) + { + if (clientType == TestClientsEnum.ManagedHttps) + { + // Skip this test for ManagedHttps client type because something is blocking + // [xUnit.net 00:00:42.97] Docker.DotNet.Tests.IContainerOperationsTests.GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs(clientType: ManagedHttps) [FAIL] + // Failed Docker.DotNet.Tests.IContainerOperationsTests.GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs(clientType: ManagedHttps) [13 s] + // Error Message: + // Average line count 1.0 is less than expected 20 + // Stack Trace: + // at Docker.DotNet.Tests.IContainerOperationsTests.GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs(TestClientsEnum clientType) in /home/runner/work/TestContainers.Docker.DotNet/TestContainers.Docker.DotNet/test/test/Docker.DotNet.Tests/IContainerOperationsTests.cs:line 258 + // --- End of stack trace from previous location --- + // Standard Output Messages: + // ClientType ManagedHttps: avg. Line count: 1.0, cpu ticks: 55,100,000, mem usage: 19,343,368, sockets: -2 + // ClientType ManagedHttps: FirstLine: + return; + } + + using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + var parallelContainerCount = 3; + var parallelThreadCount = 100; + var runtimeInSeconds = 9; + + var containerIds = new string[parallelContainerCount]; + + long memoryUsageBefore = GC.GetTotalAllocatedBytes(true); + + long socketsBefore = IPGlobalProperties.GetIPGlobalProperties() + .GetTcpIPv4Statistics() + .CurrentConnections; + + Process process = Process.GetCurrentProcess(); + TimeSpan cpuTimeBefore = process.TotalProcessorTime; + + ParallelOptions parallelOptions = new ParallelOptions + { + MaxDegreeOfParallelism = parallelContainerCount, + CancellationToken = _testFixture.Cts.Token + }; + + await Parallel.ForEachAsync(Enumerable.Range(0, parallelContainerCount), parallelOptions, async (parallel, ct) => + { + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + new CreateContainerParameters + { + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderr, + Tty = false + }, + _testFixture.Cts.Token + ); + + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + createContainerResponse.ID, + new ContainerStartParameters(), + _testFixture.Cts.Token + ); + containerIds[parallel] = createContainerResponse.ID; + }); + + await Task.Delay(TimeSpan.FromSeconds(runtimeInSeconds)); + + await Parallel.ForEachAsync(Enumerable.Range(0, parallelContainerCount), parallelOptions, async (parallel, ct) => + { + await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( + containerIds[parallel], + new ContainerStopParameters(), + _testFixture.Cts.Token + ); + }); + + containerLogsCts.CancelAfter(TimeSpan.FromSeconds(1)); + + var logLists = new ConcurrentDictionary(); + var threads = new List(); + + for (int parallel = 0; parallel < parallelContainerCount * parallelThreadCount; parallel++) + { + int index = parallel; + string containerId = containerIds[parallel % parallelContainerCount]; + CancellationToken ct = containerLogsCts.Token; + + var thread = new Thread(() => + { + var logList = new StringBuilder(2000); + try + { + var task = _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( + containerId, + new ContainerLogsParameters + { + ShowStderr = true, + ShowStdout = true, + Timestamps = true, + Follow = false + }, + new Progress(m => logList.AppendLine(m)), + ct + ); + + task.GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + } + + Thread.Sleep(100); + + logLists.TryAdd(index, logList.ToString()); + logList.Clear(); + }); + + threads.Add(thread); + thread.Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + TimeSpan cpuTimeAfter = process.TotalProcessorTime; + + long socketsAfter = IPGlobalProperties.GetIPGlobalProperties() + .GetTcpIPv4Statistics() + .CurrentConnections; + + if (clientType == TestClientsEnum.ManagedPipe) + socketsAfter = socketsBefore = 0; + + long memoryUsageAfter = GC.GetTotalAllocatedBytes(true); + + var averageLineCount = logLists.Values.Average(logs => logs.Split('\n').Count()); + + _testOutputHelper.WriteLine($"ClientType {clientType}: avg. Line count: {averageLineCount:N1}, cpu ticks: {cpuTimeAfter.Ticks - cpuTimeBefore.Ticks:N0}, mem usage: {memoryUsageAfter - memoryUsageBefore:N0}, sockets: {socketsAfter - socketsBefore:N0}"); + _testOutputHelper.WriteLine($"ClientType {clientType}: FirstLine: {logLists.Values.FirstOrDefault()}"); + + // one container should produce 2 lines per second (stdout + stderr) plus 1 for last empty line of split + Assert.True(averageLineCount > (runtimeInSeconds + 1) * 2, $"Average line count {averageLineCount:N1} is less than expected {(runtimeInSeconds + 1) * 2}"); + GC.Collect(); + } + + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerLogs_Tty_True_Follow_False_ReadsLogs(TestClientsEnum clientType) { var logList = new List(); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -143,7 +299,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( await Task.Delay(TimeSpan.FromSeconds(5)); - await _testFixture.DockerClient.Containers.GetContainerLogsAsync( + await _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -156,7 +312,7 @@ await _testFixture.DockerClient.Containers.GetContainerLogsAsync( _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StopContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), _testFixture.Cts.Token @@ -167,22 +323,23 @@ await _testFixture.DockerClient.Containers.StopContainerAsync( Assert.NotEmpty(logList); } - [Fact] - public async Task GetContainerLogs_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerLogs_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled(TestClientsEnum clientType) { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -190,7 +347,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - await Assert.ThrowsAsync(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync( + await Assert.ThrowsAnyAsync(() => _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -204,22 +361,87 @@ await Assert.ThrowsAsync(() => _testFixture.DockerCl )); } - [Fact] - public async Task GetContainerLogs_Tty_True_Follow_True_Requires_Task_To_Be_Cancelled() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerLogs_SpeedTest_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled(TestClientsEnum clientType) + { + using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + var runtimeInSeconds = 15; + + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + new CreateContainerParameters + { + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Entrypoint = CommonCommands.EchoToStdoutAndStderrFast, + Tty = false + }, + _testFixture.Cts.Token + ); + + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + createContainerResponse.ID, + new ContainerStartParameters(), + _testFixture.Cts.Token + ); + + containerLogsCts.CancelAfter(TimeSpan.FromSeconds(runtimeInSeconds)); + + long memoryUsageBefore = GC.GetTotalAllocatedBytes(true); + + var counter = 0; + try + { + await _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( + createContainerResponse.ID, + new ContainerLogsParameters + { + ShowStderr = true, + ShowStdout = true, + Timestamps = true, + Follow = true + }, + new Progress(m => counter++), + containerLogsCts.Token); + } + catch (OperationCanceledException) + { + + } + + + long memoryUsageAfter = GC.GetTotalAllocatedBytes(true); + + await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( + createContainerResponse.ID, + new ContainerStopParameters(), + _testFixture.Cts.Token + ); + + _testOutputHelper.WriteLine($"ClientType {clientType}: Line count: {counter}, mem usage: {memoryUsageAfter - memoryUsageBefore:N0}"); + + Assert.True(counter > runtimeInSeconds * 25000, $"Line count {counter} is less than expected {runtimeInSeconds * 25000}"); + + GC.Collect(); + } + + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerLogs_Tty_True_Follow_True_Requires_Task_To_Be_Cancelled(TestClientsEnum clientType) { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -227,7 +449,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -240,26 +462,27 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.Token ); - await Assert.ThrowsAsync(() => containerLogsTask); + await Assert.ThrowsAnyAsync(() => containerLogsTask); } - [Fact] - public async Task GetContainerLogs_Tty_True_Follow_True_ReadsLogs_TaskIsCancelled() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerLogs_Tty_True_Follow_True_ReadsLogs_TaskIsCancelled(TestClientsEnum clientType) { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var logList = new List(); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -267,7 +490,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -282,35 +505,37 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( await Task.Delay(TimeSpan.FromSeconds(5)); - await _testFixture.DockerClient.Containers.StopContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), _testFixture.Cts.Token ); - await Assert.ThrowsAsync(() => containerLogsTask); + await Assert.ThrowsAnyAsync(() => containerLogsTask); + _testOutputHelper.WriteLine($"Line count: {logList.Count}"); Assert.NotEmpty(logList); } - [Fact] - public async Task GetContainerStatsAsync_Tty_False_Stream_False_ReadsStats() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerStatsAsync_Tty_False_Stream_False_ReadsStats(TestClientsEnum clientType) { using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var containerStatsList = new List(); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync( + _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -318,7 +543,7 @@ public async Task GetContainerStatsAsync_Tty_False_Stream_False_ReadsStats() tcs.CancelAfter(TimeSpan.FromSeconds(10)); - await _testFixture.DockerClient.Containers.GetContainerStatsAsync( + await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { @@ -332,11 +557,12 @@ await _testFixture.DockerClient.Containers.GetContainerStatsAsync( Assert.NotEmpty(containerStatsList); Assert.Single(containerStatsList); - _testOutputHelper.WriteLine($"ConntainerStats count: {containerStatsList.Count}"); + _testOutputHelper.WriteLine($"ContainerStats count: {containerStatsList.Count}"); } - [Fact] - public async Task GetContainerStatsAsync_Tty_False_StreamStats() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerStatsAsync_Tty_False_StreamStats(TestClientsEnum clientType) { using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); using (tcs.Token.Register(() => throw new TimeoutException("GetContainerStatsAsync_Tty_False_StreamStats"))) @@ -345,17 +571,17 @@ public async Task GetContainerStatsAsync_Tty_False_StreamStats() _testOutputHelper.WriteLine($"Running test '{method!.Module}' -> '{method!.Name}'"); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync( + _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -367,7 +593,7 @@ public async Task GetContainerStatsAsync_Tty_False_StreamStats() linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); try { - await _testFixture.DockerClient.Containers.GetContainerStatsAsync( + await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { @@ -387,23 +613,24 @@ await _testFixture.DockerClient.Containers.GetContainerStatsAsync( } } - [Fact] - public async Task GetContainerStatsAsync_Tty_True_Stream_False_ReadsStats() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerStatsAsync_Tty_True_Stream_False_ReadsStats(TestClientsEnum clientType) { using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var containerStatsList = new List(); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync( + _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -411,7 +638,7 @@ public async Task GetContainerStatsAsync_Tty_True_Stream_False_ReadsStats() tcs.CancelAfter(TimeSpan.FromSeconds(10)); - await _testFixture.DockerClient.Containers.GetContainerStatsAsync( + await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { @@ -425,11 +652,12 @@ await _testFixture.DockerClient.Containers.GetContainerStatsAsync( Assert.NotEmpty(containerStatsList); Assert.Single(containerStatsList); - _testOutputHelper.WriteLine($"ConntainerStats count: {containerStatsList.Count}"); + _testOutputHelper.WriteLine($"ContainerStats count: {containerStatsList.Count}"); } - [Fact] - public async Task GetContainerStatsAsync_Tty_True_StreamStats() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetContainerStatsAsync_Tty_True_StreamStats(TestClientsEnum clientType) { using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); @@ -437,17 +665,17 @@ public async Task GetContainerStatsAsync_Tty_True_StreamStats() { _testOutputHelper.WriteLine("Running test GetContainerStatsAsync_Tty_True_StreamStats"); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync( + _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -460,7 +688,7 @@ public async Task GetContainerStatsAsync_Tty_True_StreamStats() try { - await _testFixture.DockerClient.Containers.GetContainerStatsAsync( + await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { @@ -481,33 +709,34 @@ await _testFixture.DockerClient.Containers.GetContainerStatsAsync( } } - [Fact] - public async Task KillContainerAsync_ContainerRunning_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task KillContainerAsync_ContainerRunning_Succeeds(TestClientsEnum clientType) { - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr }, _testFixture.Cts.Token); - await _testFixture.DockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token ); - var inspectRunningContainerResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync( + var inspectRunningContainerResponse = await _testFixture.DockerClients[clientType].Containers.InspectContainerAsync( createContainerResponse.ID, _testFixture.Cts.Token); - await _testFixture.DockerClient.Containers.KillContainerAsync( + await _testFixture.DockerClients[clientType].Containers.KillContainerAsync( createContainerResponse.ID, new ContainerKillParameters(), _testFixture.Cts.Token); - var inspectKilledContainerResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync( + var inspectKilledContainerResponse = await _testFixture.DockerClients[clientType].Containers.InspectContainerAsync( createContainerResponse.ID, _testFixture.Cts.Token); @@ -519,25 +748,26 @@ await _testFixture.DockerClient.Containers.KillContainerAsync( _testOutputHelper.WriteLine(JsonSerializer.Instance.Serialize(inspectKilledContainerResponse)); } - [Fact] - public async Task ListContainersAsync_ContainerExists_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task ListContainersAsync_ContainerExists_Succeeds(TestClientsEnum clientType) { - await _testFixture.DockerClient.Containers.CreateContainerAsync( + await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, _testFixture.Cts.Token); - IList containerList = await _testFixture.DockerClient.Containers.ListContainersAsync( + IList containerList = await _testFixture.DockerClients[clientType].Containers.ListContainersAsync( new ContainersListParameters { Filters = new Dictionary> { ["ancestor"] = new Dictionary { - [_testFixture.Image.ID] = true + [_testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID] = true } }, All = true @@ -549,25 +779,26 @@ await _testFixture.DockerClient.Containers.CreateContainerAsync( Assert.NotEmpty(containerList); } - [Fact] - public async Task ListProcessesAsync_RunningContainer_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task ListProcessesAsync_RunningContainer_Succeeds(TestClientsEnum clientType) { - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr }, _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.StartContainerAsync( + await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token ); - var containerProcessesResponse = await _testFixture.DockerClient.Containers.ListProcessesAsync( + var containerProcessesResponse = await _testFixture.DockerClients[clientType].Containers.ListProcessesAsync( createContainerResponse.ID, new ContainerListProcessesParameters(), _testFixture.Cts.Token @@ -584,24 +815,25 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( Assert.NotEmpty(containerProcessesResponse.Processes); } - [Fact] - public async Task RemoveContainerAsync_ContainerExists_Succeedes() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task RemoveContainerAsync_ContainerExists_Succeedes(TestClientsEnum clientType) { - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, _testFixture.Cts.Token ); - ContainerInspectResponse inspectCreatedContainer = await _testFixture.DockerClient.Containers.InspectContainerAsync( + ContainerInspectResponse inspectCreatedContainer = await _testFixture.DockerClients[clientType].Containers.InspectContainerAsync( createContainerResponse.ID, _testFixture.Cts.Token ); - await _testFixture.DockerClient.Containers.RemoveContainerAsync( + await _testFixture.DockerClients[clientType].Containers.RemoveContainerAsync( createContainerResponse.ID, new ContainerRemoveParameters { @@ -610,7 +842,7 @@ await _testFixture.DockerClient.Containers.RemoveContainerAsync( _testFixture.Cts.Token ); - Task inspectRemovedContainerTask = _testFixture.DockerClient.Containers.InspectContainerAsync( + Task inspectRemovedContainerTask = _testFixture.DockerClients[clientType].Containers.InspectContainerAsync( createContainerResponse.ID, _testFixture.Cts.Token ); @@ -619,19 +851,20 @@ await _testFixture.DockerClient.Containers.RemoveContainerAsync( await Assert.ThrowsAsync(() => inspectRemovedContainerTask); } - [Fact] - public async Task StartContainerAsync_ContainerExists_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task StartContainerAsync_ContainerExists_Succeeds(TestClientsEnum clientType) { - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, _testFixture.Cts.Token ); - var startContainerResult = await _testFixture.DockerClient.Containers.StartContainerAsync( + var startContainerResult = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -640,10 +873,11 @@ public async Task StartContainerAsync_ContainerExists_Succeeds() Assert.True(startContainerResult); } - [Fact] - public async Task StartContainerAsync_ContainerNotExists_ThrowsException() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task StartContainerAsync_ContainerNotExists_ThrowsException(TestClientsEnum clientType) { - Task startContainerTask = _testFixture.DockerClient.Containers.StartContainerAsync( + Task startContainerTask = _testFixture.DockerClients[clientType].Containers.StartContainerAsync( Guid.NewGuid().ToString(), new ContainerStartParameters(), _testFixture.Cts.Token @@ -652,8 +886,9 @@ public async Task StartContainerAsync_ContainerNotExists_ThrowsException() await Assert.ThrowsAsync(() => startContainerTask); } - [Fact] - public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledException() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledException(TestClientsEnum clientType) { using var waitContainerCts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); @@ -661,10 +896,10 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio var delay = TimeSpan.FromSeconds(5); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Image.ID, + Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr }, waitContainerCts.Token @@ -672,7 +907,7 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio _testOutputHelper.WriteLine($"CreateContainerResponse: '{JsonSerializer.Instance.Serialize(createContainerResponse)}'"); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), waitContainerCts.Token); + _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), waitContainerCts.Token); _testOutputHelper.WriteLine("Starting timeout to cancel WaitContainer operation."); @@ -680,7 +915,7 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio stopWatch.Start(); // Will wait forever here if cancellation fails. - var waitContainerTask = _testFixture.DockerClient.Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token); + var waitContainerTask = _testFixture.DockerClients[clientType].Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token); _ = await Assert.ThrowsAsync(() => waitContainerTask); @@ -696,25 +931,27 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio Assert.True(waitContainerTask.IsCanceled); } - [Fact] - public async Task CreateImageAsync_NonExistingImage_ThrowsDockerImageNotFoundException() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task CreateImageAsync_NonExistingImage_ThrowsDockerImageNotFoundException(TestClientsEnum clientType) { var createContainerParameters = new CreateContainerParameters(); createContainerParameters.Image = Guid.NewGuid().ToString("D"); - Func op = () => _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); + Func op = () => _testFixture.DockerClients[clientType].Containers.CreateContainerAsync(createContainerParameters); await Assert.ThrowsAsync(op); } - [Fact] - public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToPid1Stdin_CompletesPid1Process() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToPid1Stdin_CompletesPid1Process(TestClientsEnum clientType) { // Given var linefeedByte = new byte[] { 10 }; var createContainerParameters = new CreateContainerParameters(); - createContainerParameters.Image = _testFixture.Image.ID; + createContainerParameters.Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID; createContainerParameters.Entrypoint = new[] { "/bin/sh", "-c" }; createContainerParameters.Cmd = new[] { "read line; echo Done" }; createContainerParameters.OpenStdin = true; @@ -727,31 +964,32 @@ public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToPid1Stdin_Comple containerAttachParameters.Stream = true; // When - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync(createContainerParameters); + _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); - using var stream = await _testFixture.DockerClient.Containers.AttachContainerAsync(createContainerResponse.ID, containerAttachParameters); + using var stream = await _testFixture.DockerClients[clientType].Containers.AttachContainerAsync(createContainerResponse.ID, containerAttachParameters); await stream.WriteAsync(linefeedByte, 0, linefeedByte.Length, _testFixture.Cts.Token); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var (stdout, _) = await stream.ReadOutputToEndAsync(cts.Token); - var containerInspectResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync(createContainerResponse.ID, _testFixture.Cts.Token); + var containerInspectResponse = await _testFixture.DockerClients[clientType].Containers.InspectContainerAsync(createContainerResponse.ID, _testFixture.Cts.Token); // Then Assert.Equal(0, containerInspectResponse.State.ExitCode); Assert.Equal("Done\n", stdout); } - [Fact] - public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToExecStdin_CompletesExecProcess() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToExecStdin_CompletesExecProcess(TestClientsEnum clientType) { // Given var linefeedByte = new byte[] { 10 }; var createContainerParameters = new CreateContainerParameters(); - createContainerParameters.Image = _testFixture.Image.ID; + createContainerParameters.Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID; createContainerParameters.Entrypoint = CommonCommands.SleepInfinity; var containerExecCreateParameters = new ContainerExecCreateParameters(); @@ -762,19 +1000,19 @@ public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToExecStdin_Comple var containerExecStartParameters = new ContainerExecStartParameters(); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); - _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync(createContainerParameters); + _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); // When - var containerExecCreateResponse = await _testFixture.DockerClient.Exec.CreateContainerExecAsync(createContainerResponse.ID, containerExecCreateParameters); - using var stream = await _testFixture.DockerClient.Exec.StartContainerExecAsync(containerExecCreateResponse.ID, containerExecStartParameters); + var containerExecCreateResponse = await _testFixture.DockerClients[clientType].Exec.CreateContainerExecAsync(createContainerResponse.ID, containerExecCreateParameters); + using var stream = await _testFixture.DockerClients[clientType].Exec.StartContainerExecAsync(containerExecCreateResponse.ID, containerExecStartParameters); await stream.WriteAsync(linefeedByte, 0, linefeedByte.Length, _testFixture.Cts.Token); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var (stdout, _) = await stream.ReadOutputToEndAsync(cts.Token); - var containerExecInspectResponse = await _testFixture.DockerClient.Exec.InspectContainerExecAsync(containerExecCreateResponse.ID, _testFixture.Cts.Token); + var containerExecInspectResponse = await _testFixture.DockerClients[clientType].Exec.InspectContainerExecAsync(containerExecCreateResponse.ID, _testFixture.Cts.Token); // Then Assert.Equal(0, containerExecInspectResponse.ExitCode); diff --git a/test/Docker.DotNet.Tests/IImageOperationsTests.cs b/test/Docker.DotNet.Tests/IImageOperationsTests.cs index 50d522f7..7b1061a8 100644 --- a/test/Docker.DotNet.Tests/IImageOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IImageOperationsTests.cs @@ -12,15 +12,19 @@ public IImageOperationsTests(TestFixture testFixture, ITestOutputHelper testOutp _testOutputHelper = testOutputHelper; } - [Fact] - public async Task CreateImageAsync_TaskCancelled_ThrowsTaskCanceledException() + public static IEnumerable GetDockerClientTypes() => + TestFixture.GetDockerClientTypes(); + + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task CreateImageAsync_TaskCancelled_ThrowsTaskCanceledException(TestClientsEnum clientType) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var newTag = Guid.NewGuid().ToString(); var newRepositoryName = Guid.NewGuid().ToString(); - await _testFixture.DockerClient.Images.TagImageAsync( + await _testFixture.DockerClients[clientType].Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -30,7 +34,7 @@ await _testFixture.DockerClient.Images.TagImageAsync( cts.Token ); - var createImageTask = _testFixture.DockerClient.Images.CreateImageAsync( + var createImageTask = _testFixture.DockerClients[clientType].Images.CreateImageAsync( new ImagesCreateParameters { FromImage = $"{newRepositoryName}:{newTag}" @@ -47,10 +51,11 @@ await _testFixture.DockerClient.Images.TagImageAsync( Assert.True(createImageTask.IsCanceled); } - [Fact] - public Task CreateImageAsync_ErrorResponse_ThrowsDockerApiException() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public Task CreateImageAsync_ErrorResponse_ThrowsDockerApiException(TestClientsEnum clientType) { - return Assert.ThrowsAsync(() => _testFixture.DockerClient.Images.CreateImageAsync( + return Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].Images.CreateImageAsync( new ImagesCreateParameters { FromImage = "1.2.3.Apparently&this$is+not-a_valid%repository//name", @@ -58,12 +63,13 @@ public Task CreateImageAsync_ErrorResponse_ThrowsDockerApiException() }, null, null)); } - [Fact] - public async Task DeleteImageAsync_RemovesImage() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task DeleteImageAsync_RemovesImage(TestClientsEnum clientType) { var newImageTag = Guid.NewGuid().ToString(); - await _testFixture.DockerClient.Images.TagImageAsync( + await _testFixture.DockerClients[clientType].Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -73,18 +79,18 @@ await _testFixture.DockerClient.Images.TagImageAsync( _testFixture.Cts.Token ); - var inspectExistingImageResponse = await _testFixture.DockerClient.Images.InspectImageAsync( + var inspectExistingImageResponse = await _testFixture.DockerClients[clientType].Images.InspectImageAsync( $"{_testFixture.Repository}:{newImageTag}", _testFixture.Cts.Token ); - await _testFixture.DockerClient.Images.DeleteImageAsync( + await _testFixture.DockerClients[clientType].Images.DeleteImageAsync( $"{_testFixture.Repository}:{newImageTag}", new ImageDeleteParameters(), _testFixture.Cts.Token ); - Task inspectDeletedImageTask = _testFixture.DockerClient.Images.InspectImageAsync( + Task inspectDeletedImageTask = _testFixture.DockerClients[clientType].Images.InspectImageAsync( $"{_testFixture.Repository}:{newImageTag}", _testFixture.Cts.Token ); diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index e620446c..58c5f44c 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -12,39 +12,43 @@ public ISwarmOperationsTests(TestFixture testFixture, ITestOutputHelper testOutp _testOutputHelper = testOutputHelper; } - [Fact] - public async Task GetFilteredServicesByName_Succeeds() + public static IEnumerable GetDockerClientTypes() => + TestFixture.GetDockerClientTypes(); + + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetFilteredServicesByName_Succeeds(TestClientsEnum clientType) { var serviceName = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}"; - var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = serviceName, - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(new ServiceListParameters + var services = await _testFixture.DockerClients[clientType].Swarm.ListServicesAsync(new ServiceListParameters { Filters = new Dictionary> { @@ -57,42 +61,43 @@ public async Task GetFilteredServicesByName_Succeeds() Assert.Single(services); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(thirdServiceId); } - [Fact] - public async Task GetFilteredServicesById_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetFilteredServicesById_Succeeds(TestClientsEnum clientType) { - var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(new ServiceListParameters + var services = await _testFixture.DockerClients[clientType].Swarm.ListServicesAsync(new ServiceListParameters { Filters = new Dictionary> { @@ -105,69 +110,71 @@ public async Task GetFilteredServicesById_Succeeds() Assert.Single(services); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(thirdServiceId); } - [Fact] - public async Task GetServices_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetServices_Succeeds(TestClientsEnum clientType) { - var initialServiceCount = (await _testFixture.DockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None)).Count(); + var initialServiceCount = (await _testFixture.DockerClients[clientType].Swarm.ListServicesAsync(cancellationToken: CancellationToken.None)).Count(); - var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } } })).ID; - var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); + var services = await _testFixture.DockerClients[clientType].Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); Assert.True(services.Count() > initialServiceCount); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(thirdServiceId); } - [Fact] - public async Task GetServiceLogs_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetServiceLogs_Succeeds(TestClientsEnum clientType) { var cts = new CancellationTokenSource(); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token, cts.Token); var serviceName = $"service-withLogs-{Guid.NewGuid().ToString().Substring(1, 10)}"; - var serviceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters + var serviceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = serviceName, - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID, Command = CommonCommands.EchoToStdoutAndStderr } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Command = CommonCommands.EchoToStdoutAndStderr } } } })).ID; - using var stream = await _testFixture.DockerClient.Swarm.GetServiceLogsAsync(serviceName, false, new ServiceLogsParameters + using var stream = await _testFixture.DockerClients[clientType].Swarm.GetServiceLogsAsync(serviceName, false, new ServiceLogsParameters { Follow = true, ShowStdout = true, @@ -246,6 +253,6 @@ public async Task GetServiceLogs_Succeeds() Assert.NotNull(logLines); Assert.NotEmpty(logLines); - await _testFixture.DockerClient.Swarm.RemoveServiceAsync(serviceId); + await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(serviceId); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs index 6c313945..5ddb8d4d 100644 --- a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs +++ b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs @@ -12,29 +12,39 @@ public ISystemOperationsTests(TestFixture testFixture, ITestOutputHelper testOut _testOutputHelper = testOutputHelper; } + public static IEnumerable GetDockerClientTypes() => + TestFixture.GetDockerClientTypes(); + [Fact] public void Docker_IsRunning() { - var dockerProcess = Process.GetProcesses().FirstOrDefault(process => process.ProcessName.Equals("docker", StringComparison.InvariantCultureIgnoreCase) || process.ProcessName.Equals("dockerd", StringComparison.InvariantCultureIgnoreCase)); + var processNames = Process.GetProcesses().Select(Process => Process.ProcessName); + var dockerProcess = processNames.FirstOrDefault( + name => name.Equals("docker", StringComparison.InvariantCultureIgnoreCase) + || name.Equals("com.docker.service", StringComparison.InvariantCultureIgnoreCase) + || name.Equals("dockerd", StringComparison.InvariantCultureIgnoreCase)); Assert.NotNull(dockerProcess); } - [Fact] - public async Task GetSystemInfoAsync_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetSystemInfoAsync_Succeeds(TestClientsEnum clientType) { - var info = await _testFixture.DockerClient.System.GetSystemInfoAsync(); + var info = await _testFixture.DockerClients[clientType].System.GetSystemInfoAsync(); Assert.NotNull(info.Architecture); } - [Fact] - public async Task GetVersionAsync_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task GetVersionAsync_Succeeds(TestClientsEnum clientType) { - var version = await _testFixture.DockerClient.System.GetVersionAsync(); + var version = await _testFixture.DockerClients[clientType].System.GetVersionAsync(); Assert.NotNull(version.APIVersion); } - [Fact] - public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled(TestClientsEnum clientType) { var progress = new Progress(); @@ -42,24 +52,27 @@ public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled() await cts.CancelAsync(); await Task.Delay(1); - await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token)); + await Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token)); } - [Fact] - public async Task MonitorEventsAsync_NullParameters_Throws() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task MonitorEventsAsync_NullParameters_Throws(TestClientsEnum clientType) { - await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(null, null)); + await Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].System.MonitorEventsAsync(null, null)); } - [Fact] - public async Task MonitorEventsAsync_NullProgress_Throws() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task MonitorEventsAsync_NullProgress_Throws(TestClientsEnum clientType) { - await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), null)); + await Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].System.MonitorEventsAsync(new ContainerEventsParameters(), null)); } - [Fact] - public async Task MonitorEventsAsync_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task MonitorEventsAsync_Succeeds(TestClientsEnum clientType) { var newTag = $"MonitorTests-{Guid.NewGuid().ToString().Substring(1, 10)}"; @@ -74,14 +87,14 @@ public async Task MonitorEventsAsync_Succeeds() using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); - var task = _testFixture.DockerClient.System.MonitorEventsAsync( + var task = _testFixture.DockerClients[clientType].System.MonitorEventsAsync( new ContainerEventsParameters(), progressMessage, cts.Token); - await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }, _testFixture.Cts.Token); + await _testFixture.DockerClients[clientType].Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }, _testFixture.Cts.Token); - await _testFixture.DockerClient.Images.DeleteImageAsync( + await _testFixture.DockerClients[clientType].Images.DeleteImageAsync( name: $"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters { @@ -99,8 +112,9 @@ await _testFixture.DockerClient.Images.DeleteImageAsync( Assert.True(wasProgressCalled); } - [Fact] - public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption(TestClientsEnum clientType) { var rand = new Random(); var sw = new Stopwatch(); @@ -114,7 +128,7 @@ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption() string newImageTag = Guid.NewGuid().ToString(); - var monitorTask = _testFixture.DockerClient.System.MonitorEventsAsync( + var monitorTask = _testFixture.DockerClients[clientType].System.MonitorEventsAsync( new ContainerEventsParameters(), new Progress(value => _testOutputHelper.WriteLine($"DockerSystemEvent: {JsonSerializer.Instance.Serialize(value)}")), cts.Token); @@ -123,7 +137,7 @@ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption() await Task.Delay(100, CancellationToken.None); // (3) Invoke another request that will attempt to grab the same buffer - var listImagesTask1 = _testFixture.DockerClient.Images.TagImageAsync( + var listImagesTask1 = _testFixture.DockerClients[clientType].Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -146,7 +160,7 @@ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption() await listImagesTask1; - await _testFixture.DockerClient.Images.TagImageAsync( + await _testFixture.DockerClients[clientType].Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -163,13 +177,14 @@ await _testFixture.DockerClient.Images.TagImageAsync( } } - [Fact] - public async Task MonitorEventsFiltered_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task MonitorEventsFiltered_Succeeds(TestClientsEnum clientType) { string newTag = $"MonitorTests-{Guid.NewGuid().ToString().Substring(1, 10)}"; string newImageRepositoryName = Guid.NewGuid().ToString(); - await _testFixture.DockerClient.Images.TagImageAsync( + await _testFixture.DockerClients[clientType].Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -179,7 +194,7 @@ await _testFixture.DockerClient.Images.TagImageAsync( _testFixture.Cts.Token ); - ImageInspectResponse image = await _testFixture.DockerClient.Images.InspectImageAsync( + ImageInspectResponse image = await _testFixture.DockerClients[clientType].Images.InspectImageAsync( $"{newImageRepositoryName}:{newTag}", _testFixture.Cts.Token ); @@ -228,13 +243,15 @@ await _testFixture.DockerClient.Images.TagImageAsync( }); using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); - var task = Task.Run(() => _testFixture.DockerClient.System.MonitorEventsAsync(eventsParams, progress, cts.Token)); + var task = Task.Run(() => _testFixture.DockerClients[clientType].System.MonitorEventsAsync(eventsParams, progress, cts.Token)); - await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }); - await _testFixture.DockerClient.Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters()); + await Task.Delay(TimeSpan.FromSeconds(1)); + + await _testFixture.DockerClients[clientType].Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }); + await _testFixture.DockerClients[clientType].Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters()); - var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_testFixture.Repository}:{_testFixture.Tag}", Entrypoint = CommonCommands.SleepInfinity }); - await _testFixture.DockerClient.Containers.RemoveContainerAsync(createContainerResponse.ID, new ContainerRemoveParameters(), cts.Token); + var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_testFixture.Repository}:{_testFixture.Tag}", Entrypoint = CommonCommands.SleepInfinity }); + await _testFixture.DockerClients[clientType].Containers.RemoveContainerAsync(createContainerResponse.ID, new ContainerRemoveParameters(), cts.Token); await Task.Delay(TimeSpan.FromSeconds(1)); await cts.CancelAsync(); @@ -245,9 +262,10 @@ await _testFixture.DockerClient.Images.TagImageAsync( Assert.True(task.IsCanceled); } - [Fact] - public async Task PingAsync_Succeeds() + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task PingAsync_Succeeds(TestClientsEnum clientType) { - await _testFixture.DockerClient.System.PingAsync(); + await _testFixture.DockerClients[clientType].System.PingAsync(); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs b/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs index e77ad7d1..9960f7a7 100644 --- a/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs @@ -12,30 +12,34 @@ public IVolumeOperationsTests(TestFixture testFixture, ITestOutputHelper testOut _testOutputHelper = testOutputHelper; } - [Fact] - public async Task ListAsync_VolumeExists_Succeeds() + public static IEnumerable GetDockerClientTypes() => + TestFixture.GetDockerClientTypes(); + + [Theory] + [MemberData(nameof(GetDockerClientTypes))] + public async Task ListAsync_VolumeExists_Succeeds(TestClientsEnum clientType) { const string volumeName = "docker-dotnet-test-volume"; - await _testFixture.DockerClient.Volumes.CreateAsync(new VolumesCreateParameters - { - Name = volumeName, - }, + await _testFixture.DockerClients[clientType].Volumes.CreateAsync(new VolumesCreateParameters + { + Name = volumeName, + }, _testFixture.Cts.Token); try { - var response = await _testFixture.DockerClient.Volumes.ListAsync(new VolumesListParameters - { - Filters = new Dictionary>(), - }, + var response = await _testFixture.DockerClients[clientType].Volumes.ListAsync(new VolumesListParameters + { + Filters = new Dictionary>(), + }, _testFixture.Cts.Token); Assert.Contains(volumeName, response.Volumes.Select(volume => volume.Name)); } finally { - await _testFixture.DockerClient.Volumes.RemoveAsync(volumeName, force: true, _testFixture.Cts.Token); + await _testFixture.DockerClients[clientType].Volumes.RemoveAsync(volumeName, force: true, _testFixture.Cts.Token); } } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestClientsEnum.cs b/test/Docker.DotNet.Tests/TestClientsEnum.cs new file mode 100644 index 00000000..b4af7ba5 --- /dev/null +++ b/test/Docker.DotNet.Tests/TestClientsEnum.cs @@ -0,0 +1,11 @@ +namespace Docker.DotNet.Tests +{ + public enum TestClientsEnum + { + ManagedPipe = 1, + ManagedHttp = 2, + NativeHttp = 3, + ManagedHttps = 4, + NativeHttps = 5, + } +} \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestDaemonsEnum.cs b/test/Docker.DotNet.Tests/TestDaemonsEnum.cs new file mode 100644 index 00000000..242a8c57 --- /dev/null +++ b/test/Docker.DotNet.Tests/TestDaemonsEnum.cs @@ -0,0 +1,9 @@ +namespace Docker.DotNet.Tests +{ + public enum TestDaemonsEnum + { + Local = 1, + DindHttp = 2, + DindHttps = 3 + } +} \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index b32ecd6e..2a031df3 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -1,3 +1,12 @@ +using System.IO; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Docker.DotNet.LegacyHttp; +using Docker.DotNet.NativeHttp; +using Docker.DotNet.NPipe; +using Docker.DotNet.Unix; +using Docker.DotNet.X509; + namespace Docker.DotNet.Tests; [CollectionDefinition(nameof(TestCollection))] @@ -11,7 +20,9 @@ public sealed class TestFixture : Progress, IAsyncLifetime, IDispos private readonly IMessageSink _messageSink; - private bool _hasInitializedSwarm; + private Dictionary _isInitialized = new(); + private Dictionary _isDisposed = new(); + private Dictionary _hasInitializedSwarm = new(); /// /// Initializes a new instance of the class. @@ -21,12 +32,51 @@ public sealed class TestFixture : Progress, IAsyncLifetime, IDispos public TestFixture(IMessageSink messageSink) { _messageSink = messageSink; - DockerClientConfiguration = new DockerClientConfiguration(); - DockerClient = DockerClientConfiguration.CreateClient(logger: this); - Cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + + DockerClients = new Dictionary + { + { TestClientsEnum.ManagedPipe, new DockerClientConfiguration().CreateClient(null, OperatingSystem.IsWindows() ? new NpipeHandlerFactory() : new UnixHandlerFactory(), logger: this) }, + { TestClientsEnum.ManagedHttp, new DockerClientConfiguration(endpoint: new Uri("http://localhost:2375")).CreateClient(null, new LegacyHttpHandlerFactory(), this) }, + { TestClientsEnum.NativeHttp, new DockerClientConfiguration(endpoint: new Uri("http://localhost:2375")).CreateClient(null, new NativeHttpHandlerFactory(), logger: this) }, + }; + + try + { + var tempDir = Environment.GetEnvironmentVariable("GITHUB_WORKSPACE"); +#if NET9_0_OR_GREATER + var credentials = new CertificateCredentials(X509CertificateLoader.LoadPkcs12FromFile(Path.Combine(tempDir, "certs", "client.pfx"), "")) + { + ServerCertificateValidationCallback = ValidateServerCertificate + }; +#else + var credentials = new CertificateCredentials(new X509Certificate2(Path.Combine(tempDir, "certs", "client.pfx"), "")) + { + ServerCertificateValidationCallback = ValidateServerCertificate + }; +#endif + DockerClients.Add(TestClientsEnum.ManagedHttps, new DockerClientConfiguration(endpoint: new Uri("http://localhost:2376"), credentials).CreateClient(null, new LegacyHttpHandlerFactory(), this)); + DockerClients.Add(TestClientsEnum.NativeHttps, new DockerClientConfiguration(endpoint: new Uri("http://localhost:2376"), credentials).CreateClient(null, new NativeHttpHandlerFactory(), logger: this)); + } + catch (Exception ex) + { + this.LogWarning(ex, "Couldn't init tls clients because of certificate errors."); + } + + + Images = new Dictionary(); + Cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); Cts.Token.Register(() => throw new TimeoutException("Docker.DotNet tests timed out.")); } + internal static bool ValidateServerCertificate( + object sender, + X509Certificate cert, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + return true; + } + /// /// Gets the Docker image repository. /// @@ -40,14 +90,9 @@ public TestFixture(IMessageSink messageSink) = Guid.NewGuid().ToString("N"); /// - /// Gets the Docker client configuration. - /// - public DockerClientConfiguration DockerClientConfiguration { get; } - - /// - /// Gets the Docker client. + /// Gets the Docker clients. /// - public DockerClient DockerClient { get; } + public Dictionary DockerClients { get; } /// /// Gets the cancellation token source. @@ -57,7 +102,7 @@ public TestFixture(IMessageSink messageSink) /// /// Gets or sets the Docker image. /// - public ImagesListResponse Image { get; private set; } + public Dictionary Images { get; private set; } /// public async Task InitializeAsync() @@ -66,94 +111,192 @@ public async Task InitializeAsync() const string tag = "3.20"; - // Create image - await DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = repository, Tag = tag }, null, this, Cts.Token) - .ConfigureAwait(false); + foreach (TestDaemonsEnum daemon in Enum.GetValues(typeof(TestDaemonsEnum))) + { + if (_isInitialized.TryGetValue(daemon, out var value) && value) + continue; + + // Create image + await DockerClients[GetClientForDaemon(daemon)].Images.CreateImageAsync(new ImagesCreateParameters { FromImage = repository, Tag = tag }, null, this, Cts.Token) + .ConfigureAwait(false); - // Get images - var images = await DockerClient.Images.ListImagesAsync( - new ImagesListParameters - { - Filters = new Dictionary> + // Get images + var images = await DockerClients[GetClientForDaemon(daemon)].Images.ListImagesAsync( + new ImagesListParameters { - ["reference"] = new Dictionary + Filters = new Dictionary> { - [repository + ":" + tag] = true + ["reference"] = new Dictionary + { + [repository + ":" + tag] = true + } } - } - }, Cts.Token) - .ConfigureAwait(false); + }, Cts.Token) + .ConfigureAwait(false); - // Set image - Image = images.Single(); + // Set image + Images.Add(daemon, images.Single()); - // Tag image - await DockerClient.Images.TagImageAsync(Image.ID, new ImageTagParameters { RepositoryName = Repository, Tag = Tag }, Cts.Token) - .ConfigureAwait(false); + // Tag image + await DockerClients[GetClientForDaemon(daemon)].Images.TagImageAsync(Images[daemon].ID, new ImageTagParameters { RepositoryName = Repository, Tag = Tag }, Cts.Token) + .ConfigureAwait(false); - // Init a new swarm, if not part of an existing one - try + // Init a new swarm, if not part of an existing one + try + { + _ = await DockerClients[GetClientForDaemon(daemon)].Swarm.InitSwarmAsync(new SwarmInitParameters { AdvertiseAddr = "10.10.10.10", ListenAddr = "127.0.0.1" }, Cts.Token) + .ConfigureAwait(false); + + _hasInitializedSwarm.Add(daemon, true); + } + catch + { + this.LogInformation("Couldn't init a new swarm, the node should take part of an existing one."); + + _hasInitializedSwarm.Add(daemon, false); + } + + _isInitialized.Add(daemon, false); + } + } + + public static TestDaemonsEnum GetDaemonForClient(TestClientsEnum client) + { + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true") { - _ = await DockerClient.Swarm.InitSwarmAsync(new SwarmInitParameters { AdvertiseAddr = "10.10.10.10", ListenAddr = "127.0.0.1" }, Cts.Token) - .ConfigureAwait(false); + return client switch + { + TestClientsEnum.ManagedPipe => TestDaemonsEnum.Local, + TestClientsEnum.ManagedHttp => TestDaemonsEnum.DindHttp, + TestClientsEnum.NativeHttp => TestDaemonsEnum.DindHttp, + TestClientsEnum.ManagedHttps => TestDaemonsEnum.DindHttps, + TestClientsEnum.NativeHttps => TestDaemonsEnum.DindHttps, + _ => throw new ArgumentOutOfRangeException(nameof(client), client, null) + }; + } + else + { + return client switch + { + TestClientsEnum.ManagedPipe => TestDaemonsEnum.Local, + TestClientsEnum.ManagedHttp => TestDaemonsEnum.Local, + TestClientsEnum.NativeHttp => TestDaemonsEnum.Local, + _ => throw new ArgumentOutOfRangeException(nameof(client), client, null) + }; + } + } - _hasInitializedSwarm = true; + public static TestClientsEnum GetClientForDaemon(TestDaemonsEnum daemon) + { + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true") + { + return daemon switch + { + TestDaemonsEnum.Local => TestClientsEnum.ManagedPipe, + TestDaemonsEnum.DindHttp => TestClientsEnum.ManagedHttp, + TestDaemonsEnum.DindHttps => TestClientsEnum.ManagedHttps, + _ => throw new ArgumentOutOfRangeException(nameof(daemon), daemon, null) + + }; } - catch + else { - this.LogInformation("Couldn't init a new swarm, the node should take part of an existing one."); + return daemon switch + { + TestDaemonsEnum.Local => TestClientsEnum.ManagedPipe, + TestDaemonsEnum.DindHttp => TestClientsEnum.ManagedPipe, + TestDaemonsEnum.DindHttps => TestClientsEnum.ManagedPipe, + _ => throw new ArgumentOutOfRangeException(nameof(daemon), daemon, null) + }; + } + } + + public static IEnumerable GetDockerClientTypes() + { + var allClients = Enum.GetValues(typeof(TestClientsEnum)) + .Cast(); - _hasInitializedSwarm = false; + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") != "true") + { + return allClients + .Where(t => t == TestClientsEnum.ManagedPipe || + t == TestClientsEnum.ManagedHttp || + t == TestClientsEnum.NativeHttp) + .Select(t => new object[] { t }); } + + return allClients.Select(t => new object[] { t }); + } + + public static IEnumerable GetDockerDaemonTypes() + { + var allDaemons = Enum.GetValues(typeof(TestDaemonsEnum)) + .Cast(); + + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") != "true") + { + return allDaemons + .Where(t => t == TestDaemonsEnum.Local); + } + + return allDaemons; } /// public async Task DisposeAsync() { - if (_hasInitializedSwarm) + foreach (TestDaemonsEnum daemon in GetDockerDaemonTypes()) { - await DockerClient.Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true }, Cts.Token) - .ConfigureAwait(false); - } + if (_isDisposed.TryGetValue(daemon, out var disposed) && disposed) + continue; - var containers = await DockerClient.Containers.ListContainersAsync( - new ContainersListParameters - { - Filters = new Dictionary> + if (_hasInitializedSwarm.TryGetValue(daemon, out var swarm) && swarm) + { + await DockerClients[GetClientForDaemon(daemon)].Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true }, Cts.Token) + .ConfigureAwait(false); + } + + var containers = await DockerClients[GetClientForDaemon(daemon)].Containers.ListContainersAsync( + new ContainersListParameters { - ["ancestor"] = new Dictionary + Filters = new Dictionary> { - [Image.ID] = true - } - }, - All = true - }, Cts.Token) - .ConfigureAwait(false); - - var images = await DockerClient.Images.ListImagesAsync( - new ImagesListParameters - { - Filters = new Dictionary> + ["ancestor"] = new Dictionary + { + [Images[daemon].ID] = true + } + }, + All = true + }, Cts.Token) + .ConfigureAwait(false); + + var images = await DockerClients[GetClientForDaemon(daemon)].Images.ListImagesAsync( + new ImagesListParameters { - ["reference"] = new Dictionary + Filters = new Dictionary> { - [Image.ID] = true - } - }, - All = true - }, Cts.Token) - .ConfigureAwait(false); - - foreach (var container in containers) - { - await DockerClient.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters { Force = true }, Cts.Token) + ["reference"] = new Dictionary + { + [Images[daemon].ID] = true + } + }, + All = true + }, Cts.Token) .ConfigureAwait(false); - } - foreach (var image in images) - { - await DockerClient.Images.DeleteImageAsync(image.ID, new ImageDeleteParameters { Force = true }, Cts.Token) - .ConfigureAwait(false); + foreach (var container in containers) + { + await DockerClients[GetClientForDaemon(daemon)].Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters { Force = true }, Cts.Token) + .ConfigureAwait(false); + } + + foreach (var image in images) + { + await DockerClients[GetClientForDaemon(daemon)].Images.DeleteImageAsync(image.ID, new ImageDeleteParameters { Force = true }, Cts.Token) + .ConfigureAwait(false); + } + + _isDisposed.Add(daemon, true); } } @@ -161,8 +304,11 @@ await DockerClient.Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true public void Dispose() { Cts.Dispose(); - DockerClient.Dispose(); - DockerClientConfiguration.Dispose(); + foreach (var client in DockerClients.Values) + { + client?.Dispose(); + } + DockerClients.Clear(); } /// From 301ce6e887d6def124a27f036178abfc44d39159 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Wed, 8 Oct 2025 11:19:36 +0200 Subject: [PATCH 03/50] fix netstandard compile remove http handler from base --- .../DockerClientConfiguration.cs | 18 +- .../BufferedReadStream.cs | 234 ----------- .../ChunkedReadStream.cs | 149 ------- .../ChunkedWriteStream.cs | 90 ---- .../ContentLengthReadStream.cs | 164 -------- .../HttpConnection.cs | 180 -------- .../HttpConnectionResponseContent.cs | 74 ---- .../ManagedHandler.cs | 387 ------------------ .../Microsoft.Net.Http.Client/ProxyMode.cs | 8 - .../Microsoft.Net.Http.Client/RedirectMode.cs | 19 - .../RequestExtensions.cs | 102 ----- .../UnixDomainSocketEndPoint.cs | 84 ---- .../WriteClosableStream.cs | 8 - 13 files changed, 9 insertions(+), 1508 deletions(-) delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/ProxyMode.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/RedirectMode.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/RequestExtensions.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs delete mode 100644 src/Docker.DotNet/Microsoft.Net.Http.Client/WriteClosableStream.cs diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index dda1c4b7..306a1b05 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -53,17 +53,17 @@ public DockerClient CreateClient(Version requestedApiVersion = null, ILogger log { scheme = "Http"; } - + // Try to find a loaded handler factory that matches the scheme and Docker.DotNet var factoryType = AppDomain.CurrentDomain.GetAssemblies() - .Where(a => a.FullName.Contains("Docker.DotNet", StringComparison.OrdinalIgnoreCase)) - .SelectMany(a => a.GetTypes()) - .FirstOrDefault(t => - typeof(IDockerHandlerFactory).IsAssignableFrom(t) && - !t.IsInterface && !t.IsAbstract && - (t.Name.Contains(scheme, StringComparison.OrdinalIgnoreCase) || - t.Namespace?.Contains(scheme, StringComparison.OrdinalIgnoreCase) == true) - ); + .Where(a => a.FullName.IndexOf("Docker.DotNet", StringComparison.OrdinalIgnoreCase) >= 0) + .SelectMany(a => a.GetTypes()) + .FirstOrDefault(t => + typeof(IDockerHandlerFactory).IsAssignableFrom(t) && + !t.IsInterface && !t.IsAbstract && + (t.Name.IndexOf(scheme, StringComparison.OrdinalIgnoreCase) >= 0 || + t.Namespace?.IndexOf(scheme, StringComparison.OrdinalIgnoreCase) >= 0) + ); if (factoryType == null) { diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs deleted file mode 100644 index 0c766b42..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ /dev/null @@ -1,234 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream -{ - private readonly Stream _inner; - private readonly Socket _socket; - private readonly byte[] _buffer; - private readonly ILogger _logger; - private int _bufferRefCount; - private int _bufferOffset; - private int _bufferCount; - - public BufferedReadStream(Stream inner, Socket socket, ILogger logger) - : this(inner, socket, 8192, logger) - { - } - - public BufferedReadStream(Stream inner, Socket socket, int bufferLength, ILogger logger) - { - _inner = inner ?? throw new ArgumentNullException(nameof(inner)); - _socket = socket; - _buffer = ArrayPool.Shared.Rent(bufferLength); - _logger = logger; - _bufferRefCount = 1; - } - - public override bool CanRead - { - get { return _inner.CanRead || _bufferCount > 0; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return _inner.CanWrite; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override bool CanCloseWrite => _socket != null || _inner is WriteClosableStream; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (Interlocked.Exchange(ref _bufferRefCount, 0) == 1) - { - ArrayPool.Shared.Return(_buffer); - } - - _inner.Dispose(); - } - - base.Dispose(disposing); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - _inner.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _inner.FlushAsync(cancellationToken); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _inner.Write(buffer, offset, count); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.WriteAsync(buffer, offset, count, cancellationToken); - } - - public override int Read(byte[] buffer, int offset, int count) - { - int read = ReadBuffer(buffer, offset, count); - if (read > 0) - { - return read; - } - - return _inner.Read(buffer, offset, count); - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - int read = ReadBuffer(buffer, offset, count); - if (read > 0) - { - return Task.FromResult(read); - } - - return _inner.ReadAsync(buffer, offset, count, cancellationToken); - } - - public override void CloseWrite() - { - if (_socket != null) - { - _socket.Shutdown(SocketShutdown.Send); - return; - } - - if (_inner is WriteClosableStream writeClosableStream) - { - writeClosableStream.CloseWrite(); - return; - } - - throw new NotSupportedException("Cannot shutdown write on this transport"); - } - - public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) - { - int read = PeekBuffer(buffer, toPeek, out peeked, out available, out remaining); - if (read > 0) - { - return true; - } - - if (_inner is IPeekableStream peekableStream) - { - return peekableStream.Peek(buffer, toPeek, out peeked, out available, out remaining); - } - - throw new NotSupportedException("_inner stream isn't a peekable stream"); - } - - public async Task ReadLineAsync(CancellationToken cancellationToken) - { - var line = new StringBuilder(_buffer.Length); - - var crIndex = -1; - - var lfIndex = -1; - - bool crlfFound; - - do - { - if (_bufferCount == 0) - { - _bufferOffset = 0; - - _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) - .ConfigureAwait(false); - } - - var c = (char)_buffer[_bufferOffset]; - line.Append(c); - - _bufferOffset++; - _bufferCount--; - - switch (c) - { - case '\r': - crIndex = line.Length; - break; - case '\n': - lfIndex = line.Length; - break; - } - - crlfFound = crIndex + 1 == lfIndex; - } - while (!crlfFound); - - return line.ToString(0, line.Length - 2); - } - - private int ReadBuffer(byte[] buffer, int offset, int count) - { - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, count); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); - _bufferOffset += toCopy; - _bufferCount -= toCopy; - return toCopy; - } - - return 0; - } - - private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) - { - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, (int)toPeek); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); - peeked = (uint) toCopy; - available = (uint)_bufferCount; - remaining = available - peeked; - return toCopy; - } - - peeked = 0; - available = 0; - remaining = 0; - return 0; - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs deleted file mode 100644 index e4991829..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs +++ /dev/null @@ -1,149 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal sealed class ChunkedReadStream : Stream -{ - private readonly BufferedReadStream _inner; - private int _chunkBytesRemaining; - private bool _done; - - public ChunkedReadStream(BufferedReadStream stream) - { - _inner = stream ?? throw new ArgumentNullException(nameof(stream)); - } - - public override bool CanRead - { - get { return _inner.CanRead; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override int ReadTimeout - { - get - { - return _inner.ReadTimeout; - } - set - { - _inner.ReadTimeout = value; - } - } - - public override int WriteTimeout - { - get - { - return _inner.WriteTimeout; - } - set - { - _inner.WriteTimeout = value; - } - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (_done) - { - return 0; - } - - if (_chunkBytesRemaining == 0) - { - var headerLine = await _inner.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) - { - throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); - } - } - - var readBytesCount = 0; - - if (_chunkBytesRemaining > 0) - { - var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); - - readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) - .ConfigureAwait(false); - - if (readBytesCount == 0) - { - throw new EndOfStreamException(); - } - - _chunkBytesRemaining -= readBytesCount; - } - - if (_chunkBytesRemaining == 0) - { - var emptyLine = await _inner.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (!string.IsNullOrEmpty(emptyLine)) - { - throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); - } - - _done = readBytesCount == 0; - } - - return readBytesCount; - } - - public override void Write(byte[] buffer, int offset, int count) - { - _inner.Write(buffer, offset, count); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.WriteAsync(buffer, offset, count, cancellationToken); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - _inner.Flush(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs deleted file mode 100644 index 4a1ca6c0..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs +++ /dev/null @@ -1,90 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal sealed class ChunkedWriteStream : Stream -{ - private static readonly byte[] EndOfContentBytes = Encoding.ASCII.GetBytes("0\r\n\r\n"); - - private readonly Stream _inner; - - public ChunkedWriteStream(Stream stream) - { - _inner = stream ?? throw new ArgumentNullException(nameof(stream)); - } - - public override bool CanRead => false; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length - { - get { throw new NotImplementedException(); } - } - - public override long Position - { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } - } - - public override void Flush() - { - _inner.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _inner.FlushAsync(cancellationToken); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (count == 0) - { - return; - } - - const string crlf = "\r\n"; - - var chunkHeader = count.ToString("X") + crlf; - var headerBytes = Encoding.ASCII.GetBytes(chunkHeader); - - // Write the chunk header - await _inner.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken) - .ConfigureAwait(false); - - // Write the chunk data - await _inner.WriteAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - - // Write the chunk footer (CRLF) - await _inner.WriteAsync(headerBytes, headerBytes.Length - 2, 2, cancellationToken) - .ConfigureAwait(false); - } - - public Task EndContentAsync(CancellationToken cancellationToken) - { - return _inner.WriteAsync(EndOfContentBytes, 0, EndOfContentBytes.Length, cancellationToken); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs deleted file mode 100644 index 2d38d85c..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ContentLengthReadStream.cs +++ /dev/null @@ -1,164 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal class ContentLengthReadStream : Stream -{ - private readonly Stream _inner; - private long _bytesRemaining; - private bool _disposed; - - public ContentLengthReadStream(Stream inner, long contentLength) - { - _inner = inner; - _bytesRemaining = contentLength; - } - - public override bool CanRead - { - get { return !_disposed; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override int ReadTimeout - { - get - { - CheckDisposed(); - return _inner.ReadTimeout; - } - set - { - CheckDisposed(); - _inner.ReadTimeout = value; - } - } - - public override int WriteTimeout - { - get - { - CheckDisposed(); - return _inner.WriteTimeout; - } - set - { - CheckDisposed(); - _inner.WriteTimeout = value; - } - } - - private void UpdateBytesRemaining(int read) - { - _bytesRemaining -= read; - if (_bytesRemaining <= 0) - { - _disposed = true; - } - System.Diagnostics.Debug.Assert(_bytesRemaining >= 0, "Negative bytes remaining? " + _bytesRemaining); - } - - public override int Read(byte[] buffer, int offset, int count) - { - // TODO: Validate buffer - if (_disposed) - { - return 0; - } - - if (_bytesRemaining == 0) - { - return 0; - } - - int toRead = (int)Math.Min(count, _bytesRemaining); - int read = _inner.Read(buffer, offset, toRead); - UpdateBytesRemaining(read); - return read; - } - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - // TODO: Validate args - if (_disposed) - { - return 0; - } - - if (_bytesRemaining == 0) - { - return 0; - } - - cancellationToken.ThrowIfCancellationRequested(); - int toRead = (int)Math.Min(count, _bytesRemaining); - int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken); - UpdateBytesRemaining(read); - return read; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - // TODO: Sync drain with timeout if small number of bytes remaining? This will let us re-use the connection. - _inner.Dispose(); - } - } - - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(typeof(ContentLengthReadStream).FullName); - } - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - throw new NotSupportedException(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs deleted file mode 100644 index 37dc69b2..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs +++ /dev/null @@ -1,180 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal sealed class HttpConnection : IDisposable -{ - private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; - - public HttpConnection(BufferedReadStream transport) - { - Transport = transport; - } - - public BufferedReadStream Transport { get; } - - public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - try - { - // Serialize headers & send - string rawRequest = SerializeRequest(request); - byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest); - await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken); - - if (request.Content != null) - { - if (request.Content.Headers.ContentLength.HasValue) - { - await request.Content.CopyToAsync(Transport); - } - else - { - // The length of the data is unknown. Send it in chunked mode. - using (var chunkedStream = new ChunkedWriteStream(Transport)) - { - await request.Content.CopyToAsync(chunkedStream); - await chunkedStream.EndContentAsync(cancellationToken); - } - } - } - - // Receive headers - List responseLines = await ReadResponseLinesAsync(cancellationToken); - - // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) - return CreateResponseMessage(responseLines); - } - catch (Exception ex) - { - Dispose(); // Any errors at this layer abort the connection. - throw new HttpRequestException("The requested failed, see inner exception for details.", ex); - } - } - - private string SerializeRequest(HttpRequestMessage request) - { - StringBuilder builder = new StringBuilder(); - builder.Append(request.Method); - builder.Append(' '); - builder.Append(request.GetAddressLineProperty()); - builder.Append(" HTTP/"); - builder.Append(request.Version.ToString(2)); - builder.Append("\r\n"); - - builder.Append(request.Headers); - - if (request.Content != null) - { - // Force the content to compute its content length if it has not already. - var contentLength = request.Content.Headers.ContentLength; - if (contentLength.HasValue) - { - request.Content.Headers.ContentLength = contentLength.Value; - } - - builder.Append(request.Content.Headers); - if (!contentLength.HasValue) - { - // Add header for chunked mode. - builder.Append("Transfer-Encoding: chunked\r\n"); - } - } - // Headers end with an empty line - builder.Append("\r\n"); - return builder.ToString(); - } - - private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) - { - var lines = new List(12); - - do - { - var line = await Transport.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (string.IsNullOrEmpty(line)) - { - break; - } - - lines.Add(line); - } - while (true); - - return lines; - } - - private HttpResponseMessage CreateResponseMessage(List responseLines) - { - string responseLine = responseLines.First(); - // HTTP/1.1 200 OK - string[] responseLineParts = responseLine.Split(new[] { ' ' }, 3); - // TODO: Verify HTTP/1.0 or 1.1. - if (responseLineParts.Length < 2) - { - throw new HttpRequestException("Invalid response line: " + responseLine); - } - - if (int.TryParse(responseLineParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode)) - { - // TODO: Validate range - } - else - { - throw new HttpRequestException("Invalid status code: " + responseLineParts[1]); - } - HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)statusCode); - if (responseLineParts.Length >= 3) - { - response.ReasonPhrase = responseLineParts[2]; - } - var content = new HttpConnectionResponseContent(this); - response.Content = content; - - foreach (var rawHeader in responseLines.Skip(1)) - { - int colonOffset = rawHeader.IndexOf(':'); - if (colonOffset <= 0) - { - throw new HttpRequestException("The given header line format is invalid: " + rawHeader); - } - string headerName = rawHeader.Substring(0, colonOffset); - string headerValue = rawHeader.Substring(colonOffset + 2); - if (!response.Headers.TryAddWithoutValidation(headerName, headerValue)) - { - bool success = response.Content.Headers.TryAddWithoutValidation(headerName, headerValue); - System.Diagnostics.Debug.Assert(success, "Failed to add response header: " + rawHeader); - } - } - - // TODO: We'll need to refactor this in the future. - // - // Depending on the request and response (headers), we need to handle the response - // differently. We need to distinguish between four types of responses: - // - // 1. Chunked transfer encoding - // 2. HTTP with a `Content-Length` header - // 3. Hijacked TCP connections (using the connection upgrade headers) - // - `/containers/{id}/attach` - // - `/exec/{id}/start` - // 4. Streams without the connection upgrade headers - // - `/containers/{id}/logs` - - var isConnectionUpgrade = response.Headers.TryGetValues("Upgrade", out var responseHeaderValues) - && responseHeaderValues.Any(header => "tcp".Equals(header)); - - var isStream = content.Headers.TryGetValues("Content-Type", out var contentHeaderValues) - && contentHeaderValues.Any(header => DockerStreamHeaders.Contains(header)); - - var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isStream) || (isStream && !isConnectionUpgrade); - - content.ResolveResponseStream(chunked: isChunkedTransferEncoding); - - return response; - } - - public void Dispose() - { - Transport.Dispose(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs deleted file mode 100644 index f4b7c97f..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs +++ /dev/null @@ -1,74 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -public class HttpConnectionResponseContent : HttpContent -{ - private readonly HttpConnection _connection; - private Stream _responseStream; - - internal HttpConnectionResponseContent(HttpConnection connection) - { - _connection = connection; - } - - internal void ResolveResponseStream(bool chunked) - { - if (_responseStream != null) - { - throw new InvalidOperationException("Called multiple times"); - } - if (chunked) - { - _responseStream = new ChunkedReadStream(_connection.Transport); - } - else if (Headers.ContentLength.HasValue) - { - _responseStream = new ContentLengthReadStream(_connection.Transport, Headers.ContentLength.Value); - } - else - { - _responseStream = _connection.Transport; - } - } - - public WriteClosableStream HijackStream() - { - if (_responseStream != _connection.Transport) - { - throw new InvalidOperationException("cannot hijack chunked or content length stream"); - } - - return _connection.Transport; - } - - protected override Task SerializeToStreamAsync(Stream stream, System.Net.TransportContext context) - { - return _responseStream.CopyToAsync(stream); - } - - protected override Task CreateContentReadStreamAsync() - { - return Task.FromResult(_responseStream); - } - - protected override bool TryComputeLength(out long length) - { - length = 0; - return false; - } - - protected override void Dispose(bool disposing) - { - try - { - if (disposing) - { - _responseStream.Dispose(); - _connection.Dispose(); - } - } - finally - { - base.Dispose(disposing); - } - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs deleted file mode 100644 index 728cf6eb..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ManagedHandler.cs +++ /dev/null @@ -1,387 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -using System; - -public class ManagedHandler : HttpMessageHandler -{ - private readonly ILogger _logger; - - private readonly StreamOpener _streamOpener; - - private readonly SocketOpener _socketOpener; - - private IWebProxy _proxy; - - public delegate Task StreamOpener(string host, int port, CancellationToken cancellationToken); - - public delegate Task SocketOpener(string host, int port, CancellationToken cancellationToken); - - public ManagedHandler(ILogger logger) - { - _logger = logger; - _socketOpener = TcpSocketOpenerAsync; - } - - public ManagedHandler(StreamOpener opener, ILogger logger) - { - _logger = logger; - _streamOpener = opener ?? throw new ArgumentNullException(nameof(opener)); - } - - public ManagedHandler(SocketOpener opener, ILogger logger) - { - _logger = logger; - _socketOpener = opener ?? throw new ArgumentNullException(nameof(opener)); - } - - public IWebProxy Proxy - { - get - { - if (_proxy == null) - { - _proxy = WebRequest.DefaultWebProxy; - } - - return _proxy; - } - set - { - _proxy = value; - } - } - - public bool UseProxy { get; set; } = true; - - public int MaxAutomaticRedirects { get; set; } = 20; - - public RedirectMode RedirectMode { get; set; } = RedirectMode.NoDowngrade; - - public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } - - public X509CertificateCollection ClientCertificates { get; set; } = new X509Certificate2Collection(); - - protected override async Task SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) - { - if (httpRequestMessage == null) - { - throw new ArgumentNullException(nameof(httpRequestMessage)); - } - - HttpResponseMessage httpResponseMessage = null; - - for (var i = 0; i < MaxAutomaticRedirects; i++) - { - httpResponseMessage?.Dispose(); - - httpResponseMessage = await ProcessRequestAsync(httpRequestMessage, cancellationToken) - .ConfigureAwait(false); - - if (!IsRedirectResponse(httpRequestMessage, httpResponseMessage)) - { - return httpResponseMessage; - } - } - - return httpResponseMessage; - } - - private bool IsRedirectResponse(HttpRequestMessage request, HttpResponseMessage response) - { - if (response.StatusCode < HttpStatusCode.MovedPermanently || response.StatusCode >= HttpStatusCode.BadRequest) - { - return false; - } - - if (RedirectMode == RedirectMode.None) - { - return false; - } - - var location = response.Headers.Location; - - if (location == null) - { - return false; - } - - if (!location.IsAbsoluteUri) - { - request.RequestUri = location; - request.Headers.Authorization = null; - request.SetAddressLineProperty(null); - request.SetPathAndQueryProperty(null); - return true; - } - - // Check if redirect from https to http is allowed - if (request.IsHttps() && string.Equals("http", location.Scheme, StringComparison.OrdinalIgnoreCase) - && RedirectMode == RedirectMode.NoDowngrade) - { - return false; - } - - // Reset fields calculated from the URI. - request.RequestUri = location; - request.Headers.Authorization = null; - request.Headers.Host = null; - request.SetConnectionHostProperty(null); - request.SetConnectionPortProperty(null); - request.SetSchemeProperty(null); - request.SetHostProperty(null); - request.SetPortProperty(null); - request.SetAddressLineProperty(null); - request.SetPathAndQueryProperty(null); - return true; - } - - private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - ProcessUrl(request); - ProcessHostHeader(request); - request.Headers.ConnectionClose = !request.Headers.Contains("Connection"); // TODO: Connection reuse is not supported. - - ProxyMode proxyMode = DetermineProxyModeAndAddressLine(request); - Socket socket; - Stream transport; - - try - { - if (_socketOpener != null) - { - socket = await _socketOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); - transport = new NetworkStream(socket, true); - } - else - { - socket = null; - transport = await _streamOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); - } - } - catch (SocketException e) - { - throw new HttpRequestException("Connection failed.", e); - } - - if (proxyMode == ProxyMode.Tunnel) - { - await TunnelThroughProxyAsync(request, transport, cancellationToken); - } - - if (request.IsHttps()) - { - SslStream sslStream = new SslStream(transport, false, ServerCertificateValidationCallback); - await sslStream.AuthenticateAsClientAsync(request.GetHostProperty(), ClientCertificates, SslProtocols.Tls12, false); - transport = sslStream; - } - - var bufferedReadStream = new BufferedReadStream(transport, socket, _logger); - var connection = new HttpConnection(bufferedReadStream); - return await connection.SendAsync(request, cancellationToken); - } - - // Data comes from either the request.RequestUri or from the request.Properties - private static void ProcessUrl(HttpRequestMessage request) - { - string scheme = request.GetSchemeProperty(); - if (string.IsNullOrWhiteSpace(scheme)) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - scheme = request.RequestUri.Scheme; - request.SetSchemeProperty(scheme); - } - - if (!request.IsHttp() && !request.IsHttps()) - { - throw new InvalidOperationException("Only HTTP or HTTPS are supported, not: " + request.RequestUri.Scheme); - } - - string host = request.GetHostProperty(); - if (string.IsNullOrWhiteSpace(host)) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - host = request.RequestUri.DnsSafeHost; - request.SetHostProperty(host); - } - - string connectionHost = request.GetConnectionHostProperty(); - if (string.IsNullOrWhiteSpace(connectionHost)) - { - request.SetConnectionHostProperty(host); - } - - int? port = request.GetPortProperty(); - if (!port.HasValue) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - port = request.RequestUri.Port; - request.SetPortProperty(port); - } - - int? connectionPort = request.GetConnectionPortProperty(); - if (!connectionPort.HasValue) - { - request.SetConnectionPortProperty(port); - } - - string pathAndQuery = request.GetPathAndQueryProperty(); - if (string.IsNullOrWhiteSpace(pathAndQuery)) - { - if (request.RequestUri.IsAbsoluteUri) - { - pathAndQuery = request.RequestUri.PathAndQuery; - } - else - { - pathAndQuery = Uri.EscapeDataString(request.RequestUri.ToString()); - } - request.SetPathAndQueryProperty(pathAndQuery); - } - } - - private static void ProcessHostHeader(HttpRequestMessage request) - { - if (string.IsNullOrWhiteSpace(request.Headers.Host)) - { - string host = request.GetHostProperty(); - int port = request.GetPortProperty().Value; - if (host.Contains(':')) - { - // IPv6 - host = '[' + host + ']'; - } - - request.Headers.Host = host + ":" + port.ToString(CultureInfo.InvariantCulture); - } - } - - private ProxyMode DetermineProxyModeAndAddressLine(HttpRequestMessage request) - { - string scheme = request.GetSchemeProperty(); - string host = request.GetHostProperty(); - int? port = request.GetPortProperty(); - string pathAndQuery = request.GetPathAndQueryProperty(); - string addressLine = request.GetAddressLineProperty(); - - if (string.IsNullOrEmpty(addressLine)) - { - request.SetAddressLineProperty(pathAndQuery); - } - - try - { - if (!UseProxy || Proxy == null || Proxy.IsBypassed(request.RequestUri)) - { - return ProxyMode.None; - } - } - catch (PlatformNotSupportedException) - { - return ProxyMode.None; - } - - var proxyUri = Proxy.GetProxy(request.RequestUri); - if (proxyUri == null) - { - return ProxyMode.None; - } - - if (request.IsHttp()) - { - if (string.IsNullOrEmpty(addressLine)) - { - addressLine = scheme + "://" + host + ":" + port.Value + pathAndQuery; - request.SetAddressLineProperty(addressLine); - } - request.SetConnectionHostProperty(proxyUri.DnsSafeHost); - request.SetConnectionPortProperty(proxyUri.Port); - return ProxyMode.Http; - } - - // Tunneling generates a completely separate request, don't alter the original, just the connection address. - request.SetConnectionHostProperty(proxyUri.DnsSafeHost); - request.SetConnectionPortProperty(proxyUri.Port); - return ProxyMode.Tunnel; - } - - private static async Task TcpSocketOpenerAsync(string host, int port, CancellationToken cancellationToken) - { - var addresses = await Dns.GetHostAddressesAsync(host) - .ConfigureAwait(false); - - if (addresses.Length == 0) - { - throw new Exception($"Unable to resolve any IP addresses for the host '{host}'."); - } - - var exceptions = new List(); - - foreach (var address in addresses) - { - var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - try - { - await socket.ConnectAsync(address, port) - .ConfigureAwait(false); - - return socket; - } - catch (Exception e) - { - socket.Dispose(); - exceptions.Add(e); - } - } - - throw new AggregateException(exceptions); - } - - private async Task TunnelThroughProxyAsync(HttpRequestMessage request, Stream transport, CancellationToken cancellationToken) - { - // Send a Connect request: - // CONNECT server.example.com:80 HTTP / 1.1 - // Host: server.example.com:80 - var connectRequest = new HttpRequestMessage(); - connectRequest.Version = new Version(1, 1); - - connectRequest.Headers.ProxyAuthorization = request.Headers.ProxyAuthorization; - connectRequest.Method = new HttpMethod("CONNECT"); - // TODO: IPv6 hosts - string authority = request.GetHostProperty() + ":" + request.GetPortProperty().Value; - connectRequest.SetAddressLineProperty(authority); - connectRequest.Headers.Host = authority; - - HttpConnection connection = new HttpConnection(new BufferedReadStream(transport, null, _logger)); - HttpResponseMessage connectResponse; - try - { - connectResponse = await connection.SendAsync(connectRequest, cancellationToken); - // TODO:? await connectResponse.Content.LoadIntoBufferAsync(); // Drain any body - // There's no danger of accidentally consuming real response data because the real request hasn't been sent yet. - } - catch (Exception ex) - { - transport.Dispose(); - throw new HttpRequestException("SSL Tunnel failed to initialize", ex); - } - - // Listen for a response. Any 2XX is considered success, anything else is considered a failure. - if ((int)connectResponse.StatusCode < 200 || 300 <= (int)connectResponse.StatusCode) - { - transport.Dispose(); - throw new HttpRequestException("Failed to negotiate the proxy tunnel: " + connectResponse); - } - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/ProxyMode.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/ProxyMode.cs deleted file mode 100644 index 05a823c9..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/ProxyMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -public enum ProxyMode -{ - None, - Http, - Tunnel -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/RedirectMode.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/RedirectMode.cs deleted file mode 100644 index f6c1e2e7..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/RedirectMode.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -public enum RedirectMode -{ - /// - /// Do not follow redirects. - /// - None, - - /// - /// Disallows redirecting from HTTPS to HTTP - /// - NoDowngrade, - - /// - /// Follow all redirects - /// - All, -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/RequestExtensions.cs deleted file mode 100644 index 4c998bc3..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/RequestExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal static class RequestExtensions -{ - public static bool IsHttp(this HttpRequestMessage request) - { - return string.Equals("http", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); - } - - public static bool IsHttps(this HttpRequestMessage request) - { - return string.Equals("https", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); - } - - public static string GetSchemeProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Scheme"); - } - - public static void SetSchemeProperty(this HttpRequestMessage request, string scheme) - { - request.SetProperty("url.Scheme", scheme); - } - - public static string GetHostProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Host"); - } - - public static void SetHostProperty(this HttpRequestMessage request, string host) - { - request.SetProperty("url.Host", host); - } - - public static int? GetPortProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Port"); - } - - public static void SetPortProperty(this HttpRequestMessage request, int? port) - { - request.SetProperty("url.Port", port); - } - - public static string GetConnectionHostProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.ConnectionHost"); - } - - public static void SetConnectionHostProperty(this HttpRequestMessage request, string host) - { - request.SetProperty("url.ConnectionHost", host); - } - - public static int? GetConnectionPortProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.ConnectionPort"); - } - - public static void SetConnectionPortProperty(this HttpRequestMessage request, int? port) - { - request.SetProperty("url.ConnectionPort", port); - } - - public static string GetPathAndQueryProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.PathAndQuery"); - } - - public static void SetPathAndQueryProperty(this HttpRequestMessage request, string pathAndQuery) - { - request.SetProperty("url.PathAndQuery", pathAndQuery); - } - - public static string GetAddressLineProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.AddressLine"); - } - - public static void SetAddressLineProperty(this HttpRequestMessage request, string addressLine) - { - request.SetProperty("url.AddressLine", addressLine); - } - - public static T GetProperty(this HttpRequestMessage request, string key) - { -#if NET6_0_OR_GREATER - return request.Options.TryGetValue(new HttpRequestOptionsKey(key), out var obj) ? obj : default; -#else - return request.Properties.TryGetValue(key, out var obj) ? (T)obj : default; -#endif - } - - public static void SetProperty(this HttpRequestMessage request, string key, T value) - { -#if NET6_0_OR_GREATER - request.Options.Set(new HttpRequestOptionsKey(key), value); -#else - request.Properties[key] = value; -#endif - } -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs deleted file mode 100644 index e24ed924..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal sealed class UnixDomainSocketEndPoint : EndPoint -{ - private const AddressFamily EndPointAddressFamily = AddressFamily.Unix; - - private static readonly Encoding s_pathEncoding = Encoding.UTF8; - - private static readonly int s_nativePathOffset = 2; // = offsetof(struct sockaddr_un, sun_path). It's the same on Linux and OSX - - private static readonly int s_nativePathLength = 91; // sockaddr_un.sun_path at http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/sys_un.h.html, -1 for terminator - - private static readonly int s_nativeAddressSize = s_nativePathOffset + s_nativePathLength; - - private readonly string _path; - - private readonly byte[] _encodedPath; - - public UnixDomainSocketEndPoint(string path) - { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - _path = path; - _encodedPath = s_pathEncoding.GetBytes(_path); - - if (path.Length == 0 || _encodedPath.Length > s_nativePathLength) - { - throw new ArgumentOutOfRangeException(nameof(path), path); - } - } - - internal UnixDomainSocketEndPoint(SocketAddress socketAddress) - { - if (socketAddress == null) - { - throw new ArgumentNullException(nameof(socketAddress)); - } - - if (socketAddress.Family != EndPointAddressFamily || - socketAddress.Size > s_nativeAddressSize) - { - throw new ArgumentOutOfRangeException(nameof(socketAddress)); - } - - if (socketAddress.Size > s_nativePathOffset) - { - _encodedPath = new byte[socketAddress.Size - s_nativePathOffset]; - for (int i = 0; i < _encodedPath.Length; i++) - { - _encodedPath[i] = socketAddress[s_nativePathOffset + i]; - } - - _path = s_pathEncoding.GetString(_encodedPath, 0, _encodedPath.Length); - } - else - { - _encodedPath = Array.Empty(); - _path = string.Empty; - } - } - - public override SocketAddress Serialize() - { - var result = new SocketAddress(AddressFamily.Unix, s_nativeAddressSize); - Debug.Assert(_encodedPath.Length + s_nativePathOffset <= result.Size, "Expected path to fit in address"); - - for (int index = 0; index < _encodedPath.Length; index++) - { - result[s_nativePathOffset + index] = _encodedPath[index]; - } - result[s_nativePathOffset + _encodedPath.Length] = 0; // path must be null-terminated - - return result; - } - - public override EndPoint Create(SocketAddress socketAddress) => new UnixDomainSocketEndPoint(socketAddress); - - public override AddressFamily AddressFamily => EndPointAddressFamily; - - public override string ToString() => _path; -} \ No newline at end of file diff --git a/src/Docker.DotNet/Microsoft.Net.Http.Client/WriteClosableStream.cs b/src/Docker.DotNet/Microsoft.Net.Http.Client/WriteClosableStream.cs deleted file mode 100644 index a6009f42..00000000 --- a/src/Docker.DotNet/Microsoft.Net.Http.Client/WriteClosableStream.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -public abstract class WriteClosableStream : Stream -{ - public abstract bool CanCloseWrite { get; } - - public abstract void CloseWrite(); -} \ No newline at end of file From 80897ecfa818847ed886a6aae299d19ebb8a06a0 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Fri, 10 Oct 2025 10:44:26 +0200 Subject: [PATCH 04/50] fix factory assembly loading --- .../DockerClientConfiguration.cs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index 306a1b05..3875dd73 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -54,20 +54,27 @@ public DockerClient CreateClient(Version requestedApiVersion = null, ILogger log scheme = "Http"; } - // Try to find a loaded handler factory that matches the scheme and Docker.DotNet - var factoryType = AppDomain.CurrentDomain.GetAssemblies() - .Where(a => a.FullName.IndexOf("Docker.DotNet", StringComparison.OrdinalIgnoreCase) >= 0) - .SelectMany(a => a.GetTypes()) - .FirstOrDefault(t => - typeof(IDockerHandlerFactory).IsAssignableFrom(t) && - !t.IsInterface && !t.IsAbstract && - (t.Name.IndexOf(scheme, StringComparison.OrdinalIgnoreCase) >= 0 || - t.Namespace?.IndexOf(scheme, StringComparison.OrdinalIgnoreCase) >= 0) - ); + // Try to find a handler factory assembly in base directory that matches the scheme and Docker.DotNet + var filenameOfFactoryAssembly = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll") + .FirstOrDefault(a => + a.ToLower().Contains("Docker.DotNet".ToLower()) + && a.ToLower().Contains(scheme.ToLower())); + + if (filenameOfFactoryAssembly == null) + { + throw new InvalidOperationException($"No Docker handler factory assembly found for scheme '{scheme}'. Please reference at least one handler package (e.g., NPipe, Unix, NativeHttp, LegacyHttp)."); + } + + var factoryAssembly = Assembly.LoadFile(filenameOfFactoryAssembly); + + var factoryType = factoryAssembly.GetTypes().FirstOrDefault(t => + typeof(IDockerHandlerFactory).IsAssignableFrom(t) && + !t.IsInterface && !t.IsAbstract + ); if (factoryType == null) { - throw new InvalidOperationException($"No Docker handler factory implementation found for scheme '{scheme}'. Please reference at least one handler package (e.g., NPipe, Unix, NativeHttp, LegacyHttp)."); + throw new InvalidOperationException($"No Docker handler factory implementation found for scheme '{scheme}' in assembly '{factoryAssembly.FullName}'."); } var factory = (IDockerHandlerFactory)Activator.CreateInstance(factoryType); From d42ea2b77a934f01880741e58f98fd6836148141 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Fri, 10 Oct 2025 11:55:29 +0200 Subject: [PATCH 05/50] change README for new handler packages --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a965258e..eeb7e971 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ You can add this library to your project using [NuGet][nuget]. Run the following command in the "Package Manager Console": +```console > PM> Install-Package Docker.DotNet.Enhanced +``` **Visual Studio** @@ -29,7 +31,9 @@ Right click to your project in Visual Studio, choose "Manage NuGet Packages" and Run the following command from your favorite shell or terminal: +```console > dotnet add package Docker.DotNet.Enhanced +``` **Development Builds** @@ -37,39 +41,66 @@ Run the following command from your favorite shell or terminal: ## Usage -You can initialize the client like the following: +You can initialize the client as follows: ```csharp using Docker.DotNet; DockerClient client = new DockerClientConfiguration( new Uri("http://ubuntu-docker.cloudapp.net:4243")) - .CreateClient(); + .CreateClient(); ``` -or to connect to your local [Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install/) daemon using named pipes or your local [Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install/) daemon using Unix sockets: +### Connection types and optional sub-packages -```csharp -using Docker.DotNet; -DockerClient client = new DockerClientConfiguration() - .CreateClient(); +Depending on the connection scheme and platform, additional sub-packages may be required: + +- **Docker.DotNet.Enhanced.NPipe**: Support for named pipes on Windows (`npipe://`). +- **Docker.DotNet.Enhanced.Unix**: Support for Unix domain sockets on Linux/macOS (`unix://`). +- **Docker.DotNet.Enhanced.NativeHttp**: Native HTTP handler for specific platforms/scenarios. +- **Docker.DotNet.Enhanced.LegacyHttp**: Legacy HTTP handler for compatibility with older .NET versions. + +These packages are optional and only needed if you want to use the respective protocol or handler. You can install them via NuGet, for example: + +```console +PM> Install-Package Docker.DotNet.Enhanced.NPipe +PM> Install-Package Docker.DotNet.Enhanced.Unix +PM> Install-Package Docker.DotNet.Enhanced.NativeHttp +PM> Install-Package Docker.DotNet.Enhanced.LegacyHttp ``` -For a custom endpoint, you can also pass a named pipe or a Unix socket to the `DockerClientConfiguration` constructor. For example: +**Examples:** + +**Named Pipe (Windows):** ```csharp -// Default Docker Engine on Windows using Docker.DotNet; DockerClient client = new DockerClientConfiguration( new Uri("npipe://./pipe/docker_engine")) - .CreateClient(); + .CreateClient(); +``` + +**Unix Domain Socket (Linux/macOS):** -// Default Docker Engine on Linux +```csharp using Docker.DotNet; DockerClient client = new DockerClientConfiguration( new Uri("unix:///var/run/docker.sock")) - .CreateClient(); + .CreateClient(); +``` + +**Note:** +For HTTP(S) connections or special authentication types (e.g. X509, BasicAuth), see the corresponding sections below. + +To connect to your local [Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install/) instance via named pipe or your local [Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install/) instance via Unix socket: + +```csharp +using Docker.DotNet; +DockerClient client = new DockerClientConfiguration() + .CreateClient(); ``` +For a custom endpoint, you can also explicitly pass a named pipe or Unix socket to the `DockerClientConfiguration` constructor (see examples above). + #### Example: List containers ```csharp @@ -173,7 +204,9 @@ You can cancel streaming using the cancellation token. Or, if you wish to contin If you are [running Docker with TLS (HTTPS)][docker-tls], you can authenticate to the Docker instance using the [**`Docker.DotNet.Enhanced.X509`**][Docker.DotNet.X509] package. You can get this package from NuGet or by running the following command in the "Package Manager Console": +```console PM> Install-Package Docker.DotNet.Enhanced.X509 +``` Once you add `Docker.DotNet.Enhanced.X509` to your project, use the `CertificateCredentials` type: @@ -187,7 +220,9 @@ If you don't want to authenticate you can omit the `credentials` parameter, whic The `CertFile` in the example above should be a PFX file (PKCS12 format), if you have PEM formatted certificates which Docker normally uses you can either convert it programmatically or use `openssl` tool to generate a PFX: +```console openssl pkcs12 -export -inkey key.pem -in cert.pem -out key.pfx +``` (Here, your private key is `key.pem`, public key is `cert.pem` and output file is named `key.pfx`.) This will prompt a password for PFX file and then you can use this PFX file on Windows. If the certificate is self-signed, your application may reject the server certificate, in this case you might want to disable server certificate validation: @@ -200,7 +235,9 @@ credentials.ServerCertificateValidationCallback = (o, c, ch, er) => true; If the Docker instance is secured with "Basic" HTTP authentication, you can use the [**`Docker.DotNet.Enhanced.BasicAuth`**][Docker.DotNet.BasicAuth] package. Get this package from NuGet or by running the following command in the "Package Manager Console": +```console PM> Install-Package Docker.DotNet.Enhanced.BasicAuth +``` Once you added `Docker.DotNet.Enhanced.BasicAuth` to your project, use `BasicAuthCredentials` type: @@ -226,13 +263,13 @@ DockerClient client = config.CreateClient(new Version(1, 49)); Here are typical exceptions thrown from the client library: -* **`DockerApiException`** is thrown when Docker Engine API responds with a non-success result. Subclasses: - * **``DockerContainerNotFoundException``** - * **``DockerImageNotFoundException``** -* **`TaskCanceledException`** is thrown from `System.Net.Http.HttpClient` library by design. It is not a friendly exception, but it indicates your request has timed out. (default request timeout is 100 seconds.) - * Long-running methods (e.g. `WaitContainerAsync`, `StopContainerAsync`) and methods that return Stream (e.g. `CreateImageAsync`, `GetContainerLogsAsync`) have timeout value overridden with infinite timespan by this library. -* **`ArgumentNullException`** is thrown when one of the required parameters are missing/empty. - * Consider reading the [Docker Remote API reference][docker-remote-api] and source code of the corresponding method you are going to use in from this library. This way you can easily find out which parameters are required and their format. +- **`DockerApiException`** is thrown when Docker Engine API responds with a non-success result. Subclasses: + - **`DockerContainerNotFoundException`** + - **`DockerImageNotFoundException`** +- **`TaskCanceledException`** is thrown from `System.Net.Http.HttpClient` library by design. It is not a friendly exception, but it indicates your request has timed out. (default request timeout is 100 seconds.) + - Long-running methods (e.g. `WaitContainerAsync`, `StopContainerAsync`) and methods that return Stream (e.g. `CreateImageAsync`, `GetContainerLogsAsync`) have timeout value overridden with infinite timespan by this library. +- **`ArgumentNullException`** is thrown when one of the required parameters are missing/empty. + - Consider reading the [Docker Remote API reference][docker-remote-api] and source code of the corresponding method you are going to use in from this library. This way you can easily find out which parameters are required and their format. ## License From ff63964c3b7596627a3a7717dc3caff3b64808c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Br=C3=BCggemann?= Date: Fri, 23 Jan 2026 07:34:09 +0100 Subject: [PATCH 06/50] Update docker image version Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6582de3f..a3c2f0e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: services: # Docker without TLS (plain TCP) !DEPRECATED! with next docker release docker-no-tls: - image: docker:28.1-dind + image: docker:29.1.1-dind env: DOCKER_TLS_CERTDIR: "" ports: @@ -21,7 +21,7 @@ jobs: # Docker with TLS (secure TCP) docker-tls: - image: docker:28.1-dind + image: docker:29.1.1-dind env: DOCKER_TLS_CERTDIR: /certs ports: From 88e394c12f693bd50aa5065e29c01181b20215e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Br=C3=BCggemann?= Date: Fri, 23 Jan 2026 07:43:59 +0100 Subject: [PATCH 07/50] Fix spelling Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3c2f0e9..8eb0b7ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,13 +77,13 @@ jobs: --tlscacert=${{ github.workspace }}/certs/client/ca.pem \ --tlscert=${{ github.workspace }}/certs/client/cert.pem \ --tlskey=${{ github.workspace }}/certs/client/key.pem version; then - echo "Docker (TLS) is ready!" + echo "Docker (with TLS) is ready!" exit 0 fi - echo "Waiting for Docker (TLS) to be ready..." + echo "Waiting for Docker (with TLS) to be ready..." sleep 3 done - echo "Docker (TLS) did not become ready in time." + echo "Docker (with TLS) did not become ready in time." exit 1 - name: Test From aca9de5196faa71ff8ac64ec421a60e7cb930349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Br=C3=BCggemann?= Date: Fri, 23 Jan 2026 07:44:30 +0100 Subject: [PATCH 08/50] Update README.md Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eeb7e971..6411dbe7 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ You can cancel streaming using the cancellation token. Or, if you wish to contin If you are [running Docker with TLS (HTTPS)][docker-tls], you can authenticate to the Docker instance using the [**`Docker.DotNet.Enhanced.X509`**][Docker.DotNet.X509] package. You can get this package from NuGet or by running the following command in the "Package Manager Console": ```console - PM> Install-Package Docker.DotNet.Enhanced.X509 +PM> Install-Package Docker.DotNet.Enhanced.X509 ``` Once you add `Docker.DotNet.Enhanced.X509` to your project, use the `CertificateCredentials` type: From ddedc3d4f16c0b5762f16486931e93fbae2cf3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Br=C3=BCggemann?= Date: Fri, 23 Jan 2026 07:45:34 +0100 Subject: [PATCH 09/50] Update README.md Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6411dbe7..66fda125 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ If you don't want to authenticate you can omit the `credentials` parameter, whic The `CertFile` in the example above should be a PFX file (PKCS12 format), if you have PEM formatted certificates which Docker normally uses you can either convert it programmatically or use `openssl` tool to generate a PFX: ```console - openssl pkcs12 -export -inkey key.pem -in cert.pem -out key.pfx +openssl pkcs12 -export -inkey key.pem -in cert.pem -out key.pfx ``` (Here, your private key is `key.pem`, public key is `cert.pem` and output file is named `key.pfx`.) This will prompt a password for PFX file and then you can use this PFX file on Windows. If the certificate is self-signed, your application may reject the server certificate, in this case you might want to disable server certificate validation: From 208590b3c8b082d2d8e15973ab8b0fe23af75ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Br=C3=BCggemann?= Date: Fri, 23 Jan 2026 07:45:46 +0100 Subject: [PATCH 10/50] Update README.md Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 66fda125..9c4b2a00 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ credentials.ServerCertificateValidationCallback = (o, c, ch, er) => true; If the Docker instance is secured with "Basic" HTTP authentication, you can use the [**`Docker.DotNet.Enhanced.BasicAuth`**][Docker.DotNet.BasicAuth] package. Get this package from NuGet or by running the following command in the "Package Manager Console": ```console - PM> Install-Package Docker.DotNet.Enhanced.BasicAuth +PM> Install-Package Docker.DotNet.Enhanced.BasicAuth ``` Once you added `Docker.DotNet.Enhanced.BasicAuth` to your project, use `BasicAuthCredentials` type: From 79f6847611a84d1a6ade10014f00675535547941 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Fri, 23 Jan 2026 08:08:22 +0100 Subject: [PATCH 11/50] fix setup matrix --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6582de3f..b8eefcfe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,8 @@ jobs: strategy: matrix: framework: - - net8.0 - - net9.0 + - 8.0 + - 9.0 steps: - uses: actions/checkout@v4 with: @@ -44,9 +44,9 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.x + dotnet-version: ${{ matrix.framework }} - name: Build - run: dotnet build -c Release --framework ${{ matrix.framework }} + run: dotnet build -c Release --framework net${{ matrix.framework }} working-directory: test - name: Pack client cert, key, ca for C# docker client @@ -87,5 +87,5 @@ jobs: exit 1 - name: Test - run: dotnet test -c Release --framework ${{ matrix.framework }} --no-build --logger console + run: dotnet test -c Release --framework net${{ matrix.framework }} --no-build --logger console working-directory: test From 1c6cebdcb30d8362dfcac4ca386e3a4a2f77c693 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Fri, 23 Jan 2026 09:26:47 +0100 Subject: [PATCH 12/50] deduplicate ms code into shared project --- Docker.DotNet.sln | 140 +++--- .../Docker.DotNet.LegacyHttp.csproj | 2 + .../LegacyHttpHandlerFactory.cs | 3 - .../ChunkedReadStream.cs | 152 ------- .../ChunkedWriteStream.cs | 92 ---- .../ContentLengthReadStream.cs | 166 -------- .../HttpConnection.cs | 185 -------- .../ManagedHandler.cs | 397 ------------------ .../Docker.DotNet.NPipe.csproj | 4 + .../DockerPipeStream.cs | 2 +- src/Docker.DotNet.NPipe/GlobalSuppressions.cs | 8 + .../BufferedReadStream.cs | 240 ----------- .../ChunkedWriteStream.cs | 92 ---- .../ContentLengthReadStream.cs | 166 -------- .../HttpConnection.cs | 185 -------- .../HttpConnectionResponseContent.cs | 76 ---- .../ManagedHandler.cs | 397 ------------------ .../Microsoft.Net.Http.Client/ProxyMode.cs | 8 - .../Microsoft.Net.Http.Client/RedirectMode.cs | 19 - .../RequestExtensions.cs | 102 ----- .../NpipeHandlerFactory.cs | 8 +- .../Docker.DotNet.NativeHttp.csproj | 1 + .../NativeHttpHandlerFactory.cs | 3 - .../Docker.DotNet.Unix.csproj | 6 + .../BufferedReadStream.cs | 240 ----------- .../ChunkedReadStream.cs | 152 ------- .../HttpConnectionResponseContent.cs | 76 ---- .../Microsoft.Net.Http.Client/ProxyMode.cs | 8 - .../Microsoft.Net.Http.Client/RedirectMode.cs | 19 - .../RequestExtensions.cs | 102 ----- .../UnixDomainSocketEndPoint.cs | 8 +- src/Docker.DotNet.Unix/UnixHandlerFactory.cs | 10 +- .../CertificateCredentials.cs | 4 - .../Docker.DotNet.X509.csproj | 1 + .../DockerClientConfiguration.cs | 6 +- src/Docker.DotNet/IDockerHandlerFactory.cs | 3 - .../BufferedReadStream.cs | 6 - .../ChunkedReadStream.cs | 3 - .../ChunkedWriteStream.cs | 2 - .../ContentLengthReadStream.cs | 2 - .../HttpConnection.cs | 5 - .../HttpConnectionResponseContent.cs | 3 - .../ManagedHandler.cs | 12 - .../Microsoft.Net.Http.Client.projitems | 39 ++ .../Microsoft.Net.Http.Client.shproj | 14 + .../Microsoft.Net.Http.Client/ProxyMode.cs | 0 .../Microsoft.Net.Http.Client/RedirectMode.cs | 0 .../RequestExtensions.cs | 0 .../Docker.DotNet.Tests.csproj | 9 +- test/Docker.DotNet.Tests/TestFixture.cs | 9 - 50 files changed, 160 insertions(+), 3027 deletions(-) delete mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedReadStream.cs delete mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedWriteStream.cs delete mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ContentLengthReadStream.cs delete mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnection.cs delete mode 100644 src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ManagedHandler.cs rename src/{Docker.DotNet => Docker.DotNet.NPipe}/DockerPipeStream.cs (98%) create mode 100644 src/Docker.DotNet.NPipe/GlobalSuppressions.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/BufferedReadStream.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedWriteStream.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ContentLengthReadStream.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnection.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ManagedHandler.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ProxyMode.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RedirectMode.cs delete mode 100644 src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RequestExtensions.cs delete mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/BufferedReadStream.cs delete mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedReadStream.cs delete mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs delete mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ProxyMode.cs delete mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RedirectMode.cs delete mode 100644 src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RequestExtensions.cs rename src/Docker.DotNet.Unix/{Microsoft.Net.Http.Client => }/UnixDomainSocketEndPoint.cs (94%) rename src/{Docker.DotNet.LegacyHttp => }/Microsoft.Net.Http.Client/BufferedReadStream.cs (98%) rename src/{Docker.DotNet.NPipe => }/Microsoft.Net.Http.Client/ChunkedReadStream.cs (98%) rename src/{Docker.DotNet.Unix => }/Microsoft.Net.Http.Client/ChunkedWriteStream.cs (99%) rename src/{Docker.DotNet.Unix => }/Microsoft.Net.Http.Client/ContentLengthReadStream.cs (99%) rename src/{Docker.DotNet.Unix => }/Microsoft.Net.Http.Client/HttpConnection.cs (98%) rename src/{Docker.DotNet.LegacyHttp => }/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs (97%) rename src/{Docker.DotNet.Unix => }/Microsoft.Net.Http.Client/ManagedHandler.cs (97%) create mode 100644 src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems create mode 100644 src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj rename src/{Docker.DotNet.LegacyHttp => }/Microsoft.Net.Http.Client/ProxyMode.cs (100%) rename src/{Docker.DotNet.LegacyHttp => }/Microsoft.Net.Http.Client/RedirectMode.cs (100%) rename src/{Docker.DotNet.LegacyHttp => }/Microsoft.Net.Http.Client/RequestExtensions.cs (100%) diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln index 30cefbd1..2c8f25fd 100644 --- a/Docker.DotNet.sln +++ b/Docker.DotNet.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.9 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35201.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{85990620-78A6-4381-8BD6-84E6D0CF0649}" EndProject @@ -15,15 +14,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.X509", "src\D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" EndProject -Project("{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}") = "Docker.DotNet.NPipe", "src\Docker.DotNet.NPipe\Docker.DotNet.NPipe.csproj", "{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.NPipe", "src\Docker.DotNet.NPipe\Docker.DotNet.NPipe.csproj", "{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}" EndProject -Project("{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}") = "Docker.DotNet.Unix", "src\Docker.DotNet.Unix\Docker.DotNet.Unix.csproj", "{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Unix", "src\Docker.DotNet.Unix\Docker.DotNet.Unix.csproj", "{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}" EndProject -Project("{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}") = "Docker.DotNet.LegacyHttp", "src\Docker.DotNet.LegacyHttp\Docker.DotNet.LegacyHttp.csproj", "{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.LegacyHttp", "src\Docker.DotNet.LegacyHttp\Docker.DotNet.LegacyHttp.csproj", "{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}" EndProject -Project("{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}") = "Docker.DotNet.NativeHttp", "src\Docker.DotNet.NativeHttp\Docker.DotNet.NativeHttp.csproj", "{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.NativeHttp", "src\Docker.DotNet.NativeHttp\Docker.DotNet.NativeHttp.csproj", "{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.Net.Http.Client", "src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.shproj", "{DAE2DE68-9B3E-4D5D-8802-EC97B94160ED}" EndProject - Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,66 +34,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.ActiveCfg = Debug|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.Build.0 = Debug|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x86.ActiveCfg = Debug|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x86.Build.0 = Debug|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|Any CPU.Build.0 = Release|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x64.ActiveCfg = Release|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x64.Build.0 = Release|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x86.ActiveCfg = Release|Any CPU - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x86.Build.0 = Release|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x64.ActiveCfg = Debug|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x64.Build.0 = Debug|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x86.ActiveCfg = Debug|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x86.Build.0 = Debug|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|Any CPU.Build.0 = Release|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.ActiveCfg = Release|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.Build.0 = Release|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.ActiveCfg = Release|Any CPU - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.Build.0 = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.Build.0 = Release|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|x64.ActiveCfg = Debug|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|x64.Build.0 = Debug|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|x86.ActiveCfg = Debug|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Debug|x86.Build.0 = Debug|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|Any CPU.Build.0 = Release|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|x64.ActiveCfg = Release|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|x64.Build.0 = Release|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|x86.ActiveCfg = Release|Any CPU - {C3D4E5F6-A7B8-49C0-1D2E-3456789012CD}.Release|x86.Build.0 = Release|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -142,18 +82,76 @@ Global {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.Build.0 = Release|Any CPU {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.ActiveCfg = Release|Any CPU {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x86.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|Any CPU.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x64.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x64.Build.0 = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x86.ActiveCfg = Release|Any CPU + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Release|x86.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x64.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Debug|x86.Build.0 = Debug|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.Build.0 = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.ActiveCfg = Release|Any CPU + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE} = {85990620-78A6-4381-8BD6-84E6D0CF0649} - {E5F6A7B8-C9D0-41E2-3F45-5678901234EF} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {E1F24B25-E027-45E0-A6E1-E08138F1F95D} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC} = {85990620-78A6-4381-8BD6-84E6D0CF0649} - {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} + {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {E5F6A7B8-C9D0-41E2-3F45-5678901234EF} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {DAE2DE68-9B3E-4D5D-8802-EC97B94160ED} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {8F2F229F-C66D-43E4-B804-E5F37DC157CB} + EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.projitems*{a1b2c3d4-e5f6-47a8-9b0c-1234567890ab}*SharedItemsImports = 5 + src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.projitems*{b2c3d4e5-f6a7-48b9-0c1d-2345678901bc}*SharedItemsImports = 5 + src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.projitems*{d4e5f6a7-b8c9-40d1-2e3f-4567890123de}*SharedItemsImports = 5 + src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.projitems*{dae2de68-9b3e-4d5d-8802-ec97b94160ed}*SharedItemsImports = 13 EndGlobalSection EndGlobal diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj index f7a0da9d..aa1dc397 100644 --- a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj +++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj @@ -16,5 +16,7 @@ + + diff --git a/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs index 1faabe68..81ef9b7f 100644 --- a/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs +++ b/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs @@ -1,6 +1,3 @@ -using System; -using Microsoft.Extensions.Logging; - namespace Docker.DotNet.LegacyHttp { public class LegacyHttpHandlerFactory : IDockerHandlerFactory diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedReadStream.cs deleted file mode 100644 index dc2e552d..00000000 --- a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedReadStream.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Globalization; -using System.IO; - -namespace Microsoft.Net.Http.Client; - -internal sealed class ChunkedReadStream : Stream -{ - private readonly BufferedReadStream _inner; - private int _chunkBytesRemaining; - private bool _done; - - public ChunkedReadStream(BufferedReadStream stream) - { - _inner = stream ?? throw new ArgumentNullException(nameof(stream)); - } - - public override bool CanRead - { - get { return _inner.CanRead; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override int ReadTimeout - { - get - { - return _inner.ReadTimeout; - } - set - { - _inner.ReadTimeout = value; - } - } - - public override int WriteTimeout - { - get - { - return _inner.WriteTimeout; - } - set - { - _inner.WriteTimeout = value; - } - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (_done) - { - return 0; - } - - if (_chunkBytesRemaining == 0) - { - var headerLine = await _inner.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) - { - throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); - } - } - - var readBytesCount = 0; - - if (_chunkBytesRemaining > 0) - { - var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); - - readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) - .ConfigureAwait(false); - - if (readBytesCount == 0) - { - throw new EndOfStreamException(); - } - - _chunkBytesRemaining -= readBytesCount; - } - - if (_chunkBytesRemaining == 0) - { - var emptyLine = await _inner.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (!string.IsNullOrEmpty(emptyLine)) - { - throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); - } - - _done = readBytesCount == 0; - } - - return readBytesCount; - } - - public override void Write(byte[] buffer, int offset, int count) - { - _inner.Write(buffer, offset, count); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.WriteAsync(buffer, offset, count, cancellationToken); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - _inner.Flush(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedWriteStream.cs deleted file mode 100644 index b63fe3c6..00000000 --- a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ChunkedWriteStream.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.IO; - -namespace Microsoft.Net.Http.Client; - -internal sealed class ChunkedWriteStream : Stream -{ - private static readonly byte[] EndOfContentBytes = Encoding.ASCII.GetBytes("0\r\n\r\n"); - - private readonly Stream _inner; - - public ChunkedWriteStream(Stream stream) - { - _inner = stream ?? throw new ArgumentNullException(nameof(stream)); - } - - public override bool CanRead => false; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length - { - get { throw new NotImplementedException(); } - } - - public override long Position - { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } - } - - public override void Flush() - { - _inner.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _inner.FlushAsync(cancellationToken); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (count == 0) - { - return; - } - - const string crlf = "\r\n"; - - var chunkHeader = count.ToString("X") + crlf; - var headerBytes = Encoding.ASCII.GetBytes(chunkHeader); - - // Write the chunk header - await _inner.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken) - .ConfigureAwait(false); - - // Write the chunk data - await _inner.WriteAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - - // Write the chunk footer (CRLF) - await _inner.WriteAsync(headerBytes, headerBytes.Length - 2, 2, cancellationToken) - .ConfigureAwait(false); - } - - public Task EndContentAsync(CancellationToken cancellationToken) - { - return _inner.WriteAsync(EndOfContentBytes, 0, EndOfContentBytes.Length, cancellationToken); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ContentLengthReadStream.cs deleted file mode 100644 index c0ba7ef3..00000000 --- a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ContentLengthReadStream.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.IO; - -namespace Microsoft.Net.Http.Client; - -internal class ContentLengthReadStream : Stream -{ - private readonly Stream _inner; - private long _bytesRemaining; - private bool _disposed; - - public ContentLengthReadStream(Stream inner, long contentLength) - { - _inner = inner; - _bytesRemaining = contentLength; - } - - public override bool CanRead - { - get { return !_disposed; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override int ReadTimeout - { - get - { - CheckDisposed(); - return _inner.ReadTimeout; - } - set - { - CheckDisposed(); - _inner.ReadTimeout = value; - } - } - - public override int WriteTimeout - { - get - { - CheckDisposed(); - return _inner.WriteTimeout; - } - set - { - CheckDisposed(); - _inner.WriteTimeout = value; - } - } - - private void UpdateBytesRemaining(int read) - { - _bytesRemaining -= read; - if (_bytesRemaining <= 0) - { - _disposed = true; - } - System.Diagnostics.Debug.Assert(_bytesRemaining >= 0, "Negative bytes remaining? " + _bytesRemaining); - } - - public override int Read(byte[] buffer, int offset, int count) - { - // TODO: Validate buffer - if (_disposed) - { - return 0; - } - - if (_bytesRemaining == 0) - { - return 0; - } - - int toRead = (int)Math.Min(count, _bytesRemaining); - int read = _inner.Read(buffer, offset, toRead); - UpdateBytesRemaining(read); - return read; - } - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - // TODO: Validate args - if (_disposed) - { - return 0; - } - - if (_bytesRemaining == 0) - { - return 0; - } - - cancellationToken.ThrowIfCancellationRequested(); - int toRead = (int)Math.Min(count, _bytesRemaining); - int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken); - UpdateBytesRemaining(read); - return read; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - // TODO: Sync drain with timeout if small number of bytes remaining? This will let us re-use the connection. - _inner.Dispose(); - } - } - - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(typeof(ContentLengthReadStream).FullName); - } - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - throw new NotSupportedException(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnection.cs deleted file mode 100644 index 2f20e3dc..00000000 --- a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnection.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; - -namespace Microsoft.Net.Http.Client; - -internal sealed class HttpConnection : IDisposable -{ - private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; - - public HttpConnection(BufferedReadStream transport) - { - Transport = transport; - } - - public BufferedReadStream Transport { get; } - - public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - try - { - // Serialize headers & send - string rawRequest = SerializeRequest(request); - byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest); - await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken); - - if (request.Content != null) - { - if (request.Content.Headers.ContentLength.HasValue) - { - await request.Content.CopyToAsync(Transport); - } - else - { - // The length of the data is unknown. Send it in chunked mode. - using (var chunkedStream = new ChunkedWriteStream(Transport)) - { - await request.Content.CopyToAsync(chunkedStream); - await chunkedStream.EndContentAsync(cancellationToken); - } - } - } - - // Receive headers - List responseLines = await ReadResponseLinesAsync(cancellationToken); - - // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) - return CreateResponseMessage(responseLines); - } - catch (Exception ex) - { - Dispose(); // Any errors at this layer abort the connection. - throw new HttpRequestException("The requested failed, see inner exception for details.", ex); - } - } - - private string SerializeRequest(HttpRequestMessage request) - { - StringBuilder builder = new StringBuilder(); - builder.Append(request.Method); - builder.Append(' '); - builder.Append(request.GetAddressLineProperty()); - builder.Append(" HTTP/"); - builder.Append(request.Version.ToString(2)); - builder.Append("\r\n"); - - builder.Append(request.Headers); - - if (request.Content != null) - { - // Force the content to compute its content length if it has not already. - var contentLength = request.Content.Headers.ContentLength; - if (contentLength.HasValue) - { - request.Content.Headers.ContentLength = contentLength.Value; - } - - builder.Append(request.Content.Headers); - if (!contentLength.HasValue) - { - // Add header for chunked mode. - builder.Append("Transfer-Encoding: chunked\r\n"); - } - } - // Headers end with an empty line - builder.Append("\r\n"); - return builder.ToString(); - } - - private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) - { - var lines = new List(12); - - do - { - var line = await Transport.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (string.IsNullOrEmpty(line)) - { - break; - } - - lines.Add(line); - } - while (true); - - return lines; - } - - private HttpResponseMessage CreateResponseMessage(List responseLines) - { - string responseLine = responseLines.First(); - // HTTP/1.1 200 OK - string[] responseLineParts = responseLine.Split(new[] { ' ' }, 3); - // TODO: Verify HTTP/1.0 or 1.1. - if (responseLineParts.Length < 2) - { - throw new HttpRequestException("Invalid response line: " + responseLine); - } - - if (int.TryParse(responseLineParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode)) - { - // TODO: Validate range - } - else - { - throw new HttpRequestException("Invalid status code: " + responseLineParts[1]); - } - HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)statusCode); - if (responseLineParts.Length >= 3) - { - response.ReasonPhrase = responseLineParts[2]; - } - var content = new HttpConnectionResponseContent(this); - response.Content = content; - - foreach (var rawHeader in responseLines.Skip(1)) - { - int colonOffset = rawHeader.IndexOf(':'); - if (colonOffset <= 0) - { - throw new HttpRequestException("The given header line format is invalid: " + rawHeader); - } - string headerName = rawHeader.Substring(0, colonOffset); - string headerValue = rawHeader.Substring(colonOffset + 2); - if (!response.Headers.TryAddWithoutValidation(headerName, headerValue)) - { - bool success = response.Content.Headers.TryAddWithoutValidation(headerName, headerValue); - System.Diagnostics.Debug.Assert(success, "Failed to add response header: " + rawHeader); - } - } - - // TODO: We'll need to refactor this in the future. - // - // Depending on the request and response (headers), we need to handle the response - // differently. We need to distinguish between four types of responses: - // - // 1. Chunked transfer encoding - // 2. HTTP with a `Content-Length` header - // 3. Hijacked TCP connections (using the connection upgrade headers) - // - `/containers/{id}/attach` - // - `/exec/{id}/start` - // 4. Streams without the connection upgrade headers - // - `/containers/{id}/logs` - - var isConnectionUpgrade = response.Headers.TryGetValues("Upgrade", out var responseHeaderValues) - && responseHeaderValues.Any(header => "tcp".Equals(header)); - - var isStream = content.Headers.TryGetValues("Content-Type", out var contentHeaderValues) - && contentHeaderValues.Any(header => DockerStreamHeaders.Contains(header)); - - var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isStream) || (isStream && !isConnectionUpgrade); - - content.ResolveResponseStream(chunked: isChunkedTransferEncoding); - - return response; - } - - public void Dispose() - { - Transport.Dispose(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ManagedHandler.cs deleted file mode 100644 index bc17c436..00000000 --- a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ManagedHandler.cs +++ /dev/null @@ -1,397 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Logging; - -public class ManagedHandler : HttpMessageHandler -{ - private readonly ILogger _logger; - - private readonly StreamOpener _streamOpener; - - private readonly SocketOpener _socketOpener; - - private IWebProxy _proxy; - - public delegate Task StreamOpener(string host, int port, CancellationToken cancellationToken); - - public delegate Task SocketOpener(string host, int port, CancellationToken cancellationToken); - - public ManagedHandler(ILogger logger) - { - _logger = logger; - _socketOpener = TcpSocketOpenerAsync; - } - - public ManagedHandler(StreamOpener opener, ILogger logger) - { - _logger = logger; - _streamOpener = opener ?? throw new ArgumentNullException(nameof(opener)); - } - - public ManagedHandler(SocketOpener opener, ILogger logger) - { - _logger = logger; - _socketOpener = opener ?? throw new ArgumentNullException(nameof(opener)); - } - - public IWebProxy Proxy - { - get - { - if (_proxy == null) - { - _proxy = WebRequest.DefaultWebProxy; - } - - return _proxy; - } - set - { - _proxy = value; - } - } - - public bool UseProxy { get; set; } = true; - - public int MaxAutomaticRedirects { get; set; } = 20; - - public RedirectMode RedirectMode { get; set; } = RedirectMode.NoDowngrade; - - public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } - - public X509CertificateCollection ClientCertificates { get; set; } = new X509Certificate2Collection(); - - protected override async Task SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) - { - if (httpRequestMessage == null) - { - throw new ArgumentNullException(nameof(httpRequestMessage)); - } - - HttpResponseMessage httpResponseMessage = null; - - for (var i = 0; i < MaxAutomaticRedirects; i++) - { - httpResponseMessage?.Dispose(); - - httpResponseMessage = await ProcessRequestAsync(httpRequestMessage, cancellationToken) - .ConfigureAwait(false); - - if (!IsRedirectResponse(httpRequestMessage, httpResponseMessage)) - { - return httpResponseMessage; - } - } - - return httpResponseMessage; - } - - private bool IsRedirectResponse(HttpRequestMessage request, HttpResponseMessage response) - { - if (response.StatusCode < HttpStatusCode.MovedPermanently || response.StatusCode >= HttpStatusCode.BadRequest) - { - return false; - } - - if (RedirectMode == RedirectMode.None) - { - return false; - } - - var location = response.Headers.Location; - - if (location == null) - { - return false; - } - - if (!location.IsAbsoluteUri) - { - request.RequestUri = location; - request.Headers.Authorization = null; - request.SetAddressLineProperty(null); - request.SetPathAndQueryProperty(null); - return true; - } - - // Check if redirect from https to http is allowed - if (request.IsHttps() && string.Equals("http", location.Scheme, StringComparison.OrdinalIgnoreCase) - && RedirectMode == RedirectMode.NoDowngrade) - { - return false; - } - - // Reset fields calculated from the URI. - request.RequestUri = location; - request.Headers.Authorization = null; - request.Headers.Host = null; - request.SetConnectionHostProperty(null); - request.SetConnectionPortProperty(null); - request.SetSchemeProperty(null); - request.SetHostProperty(null); - request.SetPortProperty(null); - request.SetAddressLineProperty(null); - request.SetPathAndQueryProperty(null); - return true; - } - - private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - ProcessUrl(request); - ProcessHostHeader(request); - request.Headers.ConnectionClose = !request.Headers.Contains("Connection"); // TODO: Connection reuse is not supported. - - ProxyMode proxyMode = DetermineProxyModeAndAddressLine(request); - Socket socket; - Stream transport; - - try - { - if (_socketOpener != null) - { - socket = await _socketOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); - transport = new NetworkStream(socket, true); - } - else - { - socket = null; - transport = await _streamOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); - } - } - catch (SocketException e) - { - throw new HttpRequestException("Connection failed.", e); - } - - if (proxyMode == ProxyMode.Tunnel) - { - await TunnelThroughProxyAsync(request, transport, cancellationToken); - } - - if (request.IsHttps()) - { - SslStream sslStream = new SslStream(transport, false, ServerCertificateValidationCallback); - await sslStream.AuthenticateAsClientAsync(request.GetHostProperty(), ClientCertificates, SslProtocols.Tls12, false); - transport = sslStream; - } - - var bufferedReadStream = new BufferedReadStream(transport, socket, _logger); - var connection = new HttpConnection(bufferedReadStream); - return await connection.SendAsync(request, cancellationToken); - } - - // Data comes from either the request.RequestUri or from the request.Properties - private static void ProcessUrl(HttpRequestMessage request) - { - string scheme = request.GetSchemeProperty(); - if (string.IsNullOrWhiteSpace(scheme)) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - scheme = request.RequestUri.Scheme; - request.SetSchemeProperty(scheme); - } - - if (!request.IsHttp() && !request.IsHttps()) - { - throw new InvalidOperationException("Only HTTP or HTTPS are supported, not: " + request.RequestUri.Scheme); - } - - string host = request.GetHostProperty(); - if (string.IsNullOrWhiteSpace(host)) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - host = request.RequestUri.DnsSafeHost; - request.SetHostProperty(host); - } - - string connectionHost = request.GetConnectionHostProperty(); - if (string.IsNullOrWhiteSpace(connectionHost)) - { - request.SetConnectionHostProperty(host); - } - - int? port = request.GetPortProperty(); - if (!port.HasValue) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - port = request.RequestUri.Port; - request.SetPortProperty(port); - } - - int? connectionPort = request.GetConnectionPortProperty(); - if (!connectionPort.HasValue) - { - request.SetConnectionPortProperty(port); - } - - string pathAndQuery = request.GetPathAndQueryProperty(); - if (string.IsNullOrWhiteSpace(pathAndQuery)) - { - if (request.RequestUri.IsAbsoluteUri) - { - pathAndQuery = request.RequestUri.PathAndQuery; - } - else - { - pathAndQuery = Uri.EscapeDataString(request.RequestUri.ToString()); - } - request.SetPathAndQueryProperty(pathAndQuery); - } - } - - private static void ProcessHostHeader(HttpRequestMessage request) - { - if (string.IsNullOrWhiteSpace(request.Headers.Host)) - { - string host = request.GetHostProperty(); - int port = request.GetPortProperty().Value; - if (host.Contains(':')) - { - // IPv6 - host = '[' + host + ']'; - } - - request.Headers.Host = host + ":" + port.ToString(CultureInfo.InvariantCulture); - } - } - - private ProxyMode DetermineProxyModeAndAddressLine(HttpRequestMessage request) - { - string scheme = request.GetSchemeProperty(); - string host = request.GetHostProperty(); - int? port = request.GetPortProperty(); - string pathAndQuery = request.GetPathAndQueryProperty(); - string addressLine = request.GetAddressLineProperty(); - - if (string.IsNullOrEmpty(addressLine)) - { - request.SetAddressLineProperty(pathAndQuery); - } - - try - { - if (!UseProxy || Proxy == null || Proxy.IsBypassed(request.RequestUri)) - { - return ProxyMode.None; - } - } - catch (PlatformNotSupportedException) - { - return ProxyMode.None; - } - - var proxyUri = Proxy.GetProxy(request.RequestUri); - if (proxyUri == null) - { - return ProxyMode.None; - } - - if (request.IsHttp()) - { - if (string.IsNullOrEmpty(addressLine)) - { - addressLine = scheme + "://" + host + ":" + port.Value + pathAndQuery; - request.SetAddressLineProperty(addressLine); - } - request.SetConnectionHostProperty(proxyUri.DnsSafeHost); - request.SetConnectionPortProperty(proxyUri.Port); - return ProxyMode.Http; - } - - // Tunneling generates a completely separate request, don't alter the original, just the connection address. - request.SetConnectionHostProperty(proxyUri.DnsSafeHost); - request.SetConnectionPortProperty(proxyUri.Port); - return ProxyMode.Tunnel; - } - - private static async Task TcpSocketOpenerAsync(string host, int port, CancellationToken cancellationToken) - { - var addresses = await Dns.GetHostAddressesAsync(host) - .ConfigureAwait(false); - - if (addresses.Length == 0) - { - throw new Exception($"Unable to resolve any IP addresses for the host '{host}'."); - } - - var exceptions = new List(); - - foreach (var address in addresses) - { - var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - try - { - await socket.ConnectAsync(address, port) - .ConfigureAwait(false); - - return socket; - } - catch (Exception e) - { - socket.Dispose(); - exceptions.Add(e); - } - } - - throw new AggregateException(exceptions); - } - - private async Task TunnelThroughProxyAsync(HttpRequestMessage request, Stream transport, CancellationToken cancellationToken) - { - // Send a Connect request: - // CONNECT server.example.com:80 HTTP / 1.1 - // Host: server.example.com:80 - var connectRequest = new HttpRequestMessage(); - connectRequest.Version = new Version(1, 1); - - connectRequest.Headers.ProxyAuthorization = request.Headers.ProxyAuthorization; - connectRequest.Method = new HttpMethod("CONNECT"); - // TODO: IPv6 hosts - string authority = request.GetHostProperty() + ":" + request.GetPortProperty().Value; - connectRequest.SetAddressLineProperty(authority); - connectRequest.Headers.Host = authority; - - HttpConnection connection = new HttpConnection(new BufferedReadStream(transport, null, _logger)); - HttpResponseMessage connectResponse; - try - { - connectResponse = await connection.SendAsync(connectRequest, cancellationToken); - // TODO:? await connectResponse.Content.LoadIntoBufferAsync(); // Drain any body - // There's no danger of accidentally consuming real response data because the real request hasn't been sent yet. - } - catch (Exception ex) - { - transport.Dispose(); - throw new HttpRequestException("SSL Tunnel failed to initialize", ex); - } - - // Listen for a response. Any 2XX is considered success, anything else is considered a failure. - if ((int)connectResponse.StatusCode < 200 || 300 <= (int)connectResponse.StatusCode) - { - transport.Dispose(); - throw new HttpRequestException("Failed to negotiate the proxy tunnel: " + connectResponse); - } - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj index dbecb65c..7c8b2fee 100644 --- a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -16,5 +16,9 @@ + + + + diff --git a/src/Docker.DotNet/DockerPipeStream.cs b/src/Docker.DotNet.NPipe/DockerPipeStream.cs similarity index 98% rename from src/Docker.DotNet/DockerPipeStream.cs rename to src/Docker.DotNet.NPipe/DockerPipeStream.cs index 71010a18..596ebb07 100644 --- a/src/Docker.DotNet/DockerPipeStream.cs +++ b/src/Docker.DotNet.NPipe/DockerPipeStream.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet; -public sealed class DockerPipeStream : WriteClosableStream, IPeekableStream +internal class DockerPipeStream : WriteClosableStream, IPeekableStream { private readonly EventWaitHandle _event = new EventWaitHandle(false, EventResetMode.AutoReset); private readonly PipeStream _stream; diff --git a/src/Docker.DotNet.NPipe/GlobalSuppressions.cs b/src/Docker.DotNet.NPipe/GlobalSuppressions.cs new file mode 100644 index 00000000..2ef16235 --- /dev/null +++ b/src/Docker.DotNet.NPipe/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Interoperability", "CA1416:Plattformkompatibilität überprüfen", Justification = "", Scope = "member", Target = "~M:Docker.DotNet.DockerPipeStream.CloseWrite")] diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/BufferedReadStream.cs deleted file mode 100644 index b98aa284..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System.Buffers; -using System.IO; -using System.Net.Sockets; -using Docker.DotNet; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Net.Http.Client; - -internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream -{ - private readonly Stream _inner; - private readonly Socket _socket; - private readonly byte[] _buffer; - private readonly ILogger _logger; - private int _bufferRefCount; - private int _bufferOffset; - private int _bufferCount; - - public BufferedReadStream(Stream inner, Socket socket, ILogger logger) - : this(inner, socket, 8192, logger) - { - } - - public BufferedReadStream(Stream inner, Socket socket, int bufferLength, ILogger logger) - { - _inner = inner ?? throw new ArgumentNullException(nameof(inner)); - _socket = socket; - _buffer = ArrayPool.Shared.Rent(bufferLength); - _logger = logger; - _bufferRefCount = 1; - } - - public override bool CanRead - { - get { return _inner.CanRead || _bufferCount > 0; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return _inner.CanWrite; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override bool CanCloseWrite => _socket != null || _inner is WriteClosableStream; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (Interlocked.Exchange(ref _bufferRefCount, 0) == 1) - { - ArrayPool.Shared.Return(_buffer); - } - - _inner.Dispose(); - } - - base.Dispose(disposing); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - _inner.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _inner.FlushAsync(cancellationToken); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _inner.Write(buffer, offset, count); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.WriteAsync(buffer, offset, count, cancellationToken); - } - - public override int Read(byte[] buffer, int offset, int count) - { - int read = ReadBuffer(buffer, offset, count); - if (read > 0) - { - return read; - } - - return _inner.Read(buffer, offset, count); - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - int read = ReadBuffer(buffer, offset, count); - if (read > 0) - { - return Task.FromResult(read); - } - - return _inner.ReadAsync(buffer, offset, count, cancellationToken); - } - - public override void CloseWrite() - { - if (_socket != null) - { - _socket.Shutdown(SocketShutdown.Send); - return; - } - - if (_inner is WriteClosableStream writeClosableStream) - { - writeClosableStream.CloseWrite(); - return; - } - - throw new NotSupportedException("Cannot shutdown write on this transport"); - } - - public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) - { - int read = PeekBuffer(buffer, toPeek, out peeked, out available, out remaining); - if (read > 0) - { - return true; - } - - if (_inner is IPeekableStream peekableStream) - { - return peekableStream.Peek(buffer, toPeek, out peeked, out available, out remaining); - } - - throw new NotSupportedException("_inner stream isn't a peekable stream"); - } - - public async Task ReadLineAsync(CancellationToken cancellationToken) - { - var line = new StringBuilder(_buffer.Length); - - var crIndex = -1; - - var lfIndex = -1; - - bool crlfFound; - - do - { - if (_bufferCount == 0) - { - _bufferOffset = 0; - - _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) - .ConfigureAwait(false); - } - - var c = (char)_buffer[_bufferOffset]; - line.Append(c); - - _bufferOffset++; - _bufferCount--; - - switch (c) - { - case '\r': - crIndex = line.Length; - break; - case '\n': - lfIndex = line.Length; - break; - } - - crlfFound = crIndex + 1 == lfIndex; - } - while (!crlfFound); - - return line.ToString(0, line.Length - 2); - } - - private int ReadBuffer(byte[] buffer, int offset, int count) - { - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, count); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); - _bufferOffset += toCopy; - _bufferCount -= toCopy; - return toCopy; - } - - return 0; - } - - private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) - { - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, (int)toPeek); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); - peeked = (uint) toCopy; - available = (uint)_bufferCount; - remaining = available - peeked; - return toCopy; - } - - peeked = 0; - available = 0; - remaining = 0; - return 0; - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedWriteStream.cs deleted file mode 100644 index b63fe3c6..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedWriteStream.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.IO; - -namespace Microsoft.Net.Http.Client; - -internal sealed class ChunkedWriteStream : Stream -{ - private static readonly byte[] EndOfContentBytes = Encoding.ASCII.GetBytes("0\r\n\r\n"); - - private readonly Stream _inner; - - public ChunkedWriteStream(Stream stream) - { - _inner = stream ?? throw new ArgumentNullException(nameof(stream)); - } - - public override bool CanRead => false; - - public override bool CanSeek => false; - - public override bool CanWrite => true; - - public override long Length - { - get { throw new NotImplementedException(); } - } - - public override long Position - { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } - } - - public override void Flush() - { - _inner.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _inner.FlushAsync(cancellationToken); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (count == 0) - { - return; - } - - const string crlf = "\r\n"; - - var chunkHeader = count.ToString("X") + crlf; - var headerBytes = Encoding.ASCII.GetBytes(chunkHeader); - - // Write the chunk header - await _inner.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken) - .ConfigureAwait(false); - - // Write the chunk data - await _inner.WriteAsync(buffer, offset, count, cancellationToken) - .ConfigureAwait(false); - - // Write the chunk footer (CRLF) - await _inner.WriteAsync(headerBytes, headerBytes.Length - 2, 2, cancellationToken) - .ConfigureAwait(false); - } - - public Task EndContentAsync(CancellationToken cancellationToken) - { - return _inner.WriteAsync(EndOfContentBytes, 0, EndOfContentBytes.Length, cancellationToken); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ContentLengthReadStream.cs deleted file mode 100644 index c0ba7ef3..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ContentLengthReadStream.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System.IO; - -namespace Microsoft.Net.Http.Client; - -internal class ContentLengthReadStream : Stream -{ - private readonly Stream _inner; - private long _bytesRemaining; - private bool _disposed; - - public ContentLengthReadStream(Stream inner, long contentLength) - { - _inner = inner; - _bytesRemaining = contentLength; - } - - public override bool CanRead - { - get { return !_disposed; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override int ReadTimeout - { - get - { - CheckDisposed(); - return _inner.ReadTimeout; - } - set - { - CheckDisposed(); - _inner.ReadTimeout = value; - } - } - - public override int WriteTimeout - { - get - { - CheckDisposed(); - return _inner.WriteTimeout; - } - set - { - CheckDisposed(); - _inner.WriteTimeout = value; - } - } - - private void UpdateBytesRemaining(int read) - { - _bytesRemaining -= read; - if (_bytesRemaining <= 0) - { - _disposed = true; - } - System.Diagnostics.Debug.Assert(_bytesRemaining >= 0, "Negative bytes remaining? " + _bytesRemaining); - } - - public override int Read(byte[] buffer, int offset, int count) - { - // TODO: Validate buffer - if (_disposed) - { - return 0; - } - - if (_bytesRemaining == 0) - { - return 0; - } - - int toRead = (int)Math.Min(count, _bytesRemaining); - int read = _inner.Read(buffer, offset, toRead); - UpdateBytesRemaining(read); - return read; - } - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - // TODO: Validate args - if (_disposed) - { - return 0; - } - - if (_bytesRemaining == 0) - { - return 0; - } - - cancellationToken.ThrowIfCancellationRequested(); - int toRead = (int)Math.Min(count, _bytesRemaining); - int read = await _inner.ReadAsync(buffer, offset, toRead, cancellationToken); - UpdateBytesRemaining(read); - return read; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - // TODO: Sync drain with timeout if small number of bytes remaining? This will let us re-use the connection. - _inner.Dispose(); - } - } - - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(typeof(ContentLengthReadStream).FullName); - } - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - throw new NotSupportedException(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnection.cs deleted file mode 100644 index 2f20e3dc..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnection.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; - -namespace Microsoft.Net.Http.Client; - -internal sealed class HttpConnection : IDisposable -{ - private static readonly ISet DockerStreamHeaders = new HashSet{ "application/vnd.docker.raw-stream", "application/vnd.docker.multiplexed-stream" }; - - public HttpConnection(BufferedReadStream transport) - { - Transport = transport; - } - - public BufferedReadStream Transport { get; } - - public async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - try - { - // Serialize headers & send - string rawRequest = SerializeRequest(request); - byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest); - await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken); - - if (request.Content != null) - { - if (request.Content.Headers.ContentLength.HasValue) - { - await request.Content.CopyToAsync(Transport); - } - else - { - // The length of the data is unknown. Send it in chunked mode. - using (var chunkedStream = new ChunkedWriteStream(Transport)) - { - await request.Content.CopyToAsync(chunkedStream); - await chunkedStream.EndContentAsync(cancellationToken); - } - } - } - - // Receive headers - List responseLines = await ReadResponseLinesAsync(cancellationToken); - - // Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque) - return CreateResponseMessage(responseLines); - } - catch (Exception ex) - { - Dispose(); // Any errors at this layer abort the connection. - throw new HttpRequestException("The requested failed, see inner exception for details.", ex); - } - } - - private string SerializeRequest(HttpRequestMessage request) - { - StringBuilder builder = new StringBuilder(); - builder.Append(request.Method); - builder.Append(' '); - builder.Append(request.GetAddressLineProperty()); - builder.Append(" HTTP/"); - builder.Append(request.Version.ToString(2)); - builder.Append("\r\n"); - - builder.Append(request.Headers); - - if (request.Content != null) - { - // Force the content to compute its content length if it has not already. - var contentLength = request.Content.Headers.ContentLength; - if (contentLength.HasValue) - { - request.Content.Headers.ContentLength = contentLength.Value; - } - - builder.Append(request.Content.Headers); - if (!contentLength.HasValue) - { - // Add header for chunked mode. - builder.Append("Transfer-Encoding: chunked\r\n"); - } - } - // Headers end with an empty line - builder.Append("\r\n"); - return builder.ToString(); - } - - private async Task> ReadResponseLinesAsync(CancellationToken cancellationToken) - { - var lines = new List(12); - - do - { - var line = await Transport.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (string.IsNullOrEmpty(line)) - { - break; - } - - lines.Add(line); - } - while (true); - - return lines; - } - - private HttpResponseMessage CreateResponseMessage(List responseLines) - { - string responseLine = responseLines.First(); - // HTTP/1.1 200 OK - string[] responseLineParts = responseLine.Split(new[] { ' ' }, 3); - // TODO: Verify HTTP/1.0 or 1.1. - if (responseLineParts.Length < 2) - { - throw new HttpRequestException("Invalid response line: " + responseLine); - } - - if (int.TryParse(responseLineParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode)) - { - // TODO: Validate range - } - else - { - throw new HttpRequestException("Invalid status code: " + responseLineParts[1]); - } - HttpResponseMessage response = new HttpResponseMessage((HttpStatusCode)statusCode); - if (responseLineParts.Length >= 3) - { - response.ReasonPhrase = responseLineParts[2]; - } - var content = new HttpConnectionResponseContent(this); - response.Content = content; - - foreach (var rawHeader in responseLines.Skip(1)) - { - int colonOffset = rawHeader.IndexOf(':'); - if (colonOffset <= 0) - { - throw new HttpRequestException("The given header line format is invalid: " + rawHeader); - } - string headerName = rawHeader.Substring(0, colonOffset); - string headerValue = rawHeader.Substring(colonOffset + 2); - if (!response.Headers.TryAddWithoutValidation(headerName, headerValue)) - { - bool success = response.Content.Headers.TryAddWithoutValidation(headerName, headerValue); - System.Diagnostics.Debug.Assert(success, "Failed to add response header: " + rawHeader); - } - } - - // TODO: We'll need to refactor this in the future. - // - // Depending on the request and response (headers), we need to handle the response - // differently. We need to distinguish between four types of responses: - // - // 1. Chunked transfer encoding - // 2. HTTP with a `Content-Length` header - // 3. Hijacked TCP connections (using the connection upgrade headers) - // - `/containers/{id}/attach` - // - `/exec/{id}/start` - // 4. Streams without the connection upgrade headers - // - `/containers/{id}/logs` - - var isConnectionUpgrade = response.Headers.TryGetValues("Upgrade", out var responseHeaderValues) - && responseHeaderValues.Any(header => "tcp".Equals(header)); - - var isStream = content.Headers.TryGetValues("Content-Type", out var contentHeaderValues) - && contentHeaderValues.Any(header => DockerStreamHeaders.Contains(header)); - - var isChunkedTransferEncoding = (response.Headers.TransferEncodingChunked.GetValueOrDefault() && !isStream) || (isStream && !isConnectionUpgrade); - - content.ResolveResponseStream(chunked: isChunkedTransferEncoding); - - return response; - } - - public void Dispose() - { - Transport.Dispose(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs deleted file mode 100644 index 01de9b77..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.IO; - -namespace Microsoft.Net.Http.Client; - -public class HttpConnectionResponseContent : HttpContent -{ - private readonly HttpConnection _connection; - private Stream _responseStream; - - internal HttpConnectionResponseContent(HttpConnection connection) - { - _connection = connection; - } - - internal void ResolveResponseStream(bool chunked) - { - if (_responseStream != null) - { - throw new InvalidOperationException("Called multiple times"); - } - if (chunked) - { - _responseStream = new ChunkedReadStream(_connection.Transport); - } - else if (Headers.ContentLength.HasValue) - { - _responseStream = new ContentLengthReadStream(_connection.Transport, Headers.ContentLength.Value); - } - else - { - _responseStream = _connection.Transport; - } - } - - public Docker.DotNet.WriteClosableStream HijackStream() - { - if (_responseStream != _connection.Transport) - { - throw new InvalidOperationException("cannot hijack chunked or content length stream"); - } - - return _connection.Transport; - } - - protected override Task SerializeToStreamAsync(Stream stream, System.Net.TransportContext context) - { - return _responseStream.CopyToAsync(stream); - } - - protected override Task CreateContentReadStreamAsync() - { - return Task.FromResult(_responseStream); - } - - protected override bool TryComputeLength(out long length) - { - length = 0; - return false; - } - - protected override void Dispose(bool disposing) - { - try - { - if (disposing) - { - _responseStream.Dispose(); - _connection.Dispose(); - } - } - finally - { - base.Dispose(disposing); - } - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ManagedHandler.cs deleted file mode 100644 index bc17c436..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ManagedHandler.cs +++ /dev/null @@ -1,397 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Logging; - -public class ManagedHandler : HttpMessageHandler -{ - private readonly ILogger _logger; - - private readonly StreamOpener _streamOpener; - - private readonly SocketOpener _socketOpener; - - private IWebProxy _proxy; - - public delegate Task StreamOpener(string host, int port, CancellationToken cancellationToken); - - public delegate Task SocketOpener(string host, int port, CancellationToken cancellationToken); - - public ManagedHandler(ILogger logger) - { - _logger = logger; - _socketOpener = TcpSocketOpenerAsync; - } - - public ManagedHandler(StreamOpener opener, ILogger logger) - { - _logger = logger; - _streamOpener = opener ?? throw new ArgumentNullException(nameof(opener)); - } - - public ManagedHandler(SocketOpener opener, ILogger logger) - { - _logger = logger; - _socketOpener = opener ?? throw new ArgumentNullException(nameof(opener)); - } - - public IWebProxy Proxy - { - get - { - if (_proxy == null) - { - _proxy = WebRequest.DefaultWebProxy; - } - - return _proxy; - } - set - { - _proxy = value; - } - } - - public bool UseProxy { get; set; } = true; - - public int MaxAutomaticRedirects { get; set; } = 20; - - public RedirectMode RedirectMode { get; set; } = RedirectMode.NoDowngrade; - - public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } - - public X509CertificateCollection ClientCertificates { get; set; } = new X509Certificate2Collection(); - - protected override async Task SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken) - { - if (httpRequestMessage == null) - { - throw new ArgumentNullException(nameof(httpRequestMessage)); - } - - HttpResponseMessage httpResponseMessage = null; - - for (var i = 0; i < MaxAutomaticRedirects; i++) - { - httpResponseMessage?.Dispose(); - - httpResponseMessage = await ProcessRequestAsync(httpRequestMessage, cancellationToken) - .ConfigureAwait(false); - - if (!IsRedirectResponse(httpRequestMessage, httpResponseMessage)) - { - return httpResponseMessage; - } - } - - return httpResponseMessage; - } - - private bool IsRedirectResponse(HttpRequestMessage request, HttpResponseMessage response) - { - if (response.StatusCode < HttpStatusCode.MovedPermanently || response.StatusCode >= HttpStatusCode.BadRequest) - { - return false; - } - - if (RedirectMode == RedirectMode.None) - { - return false; - } - - var location = response.Headers.Location; - - if (location == null) - { - return false; - } - - if (!location.IsAbsoluteUri) - { - request.RequestUri = location; - request.Headers.Authorization = null; - request.SetAddressLineProperty(null); - request.SetPathAndQueryProperty(null); - return true; - } - - // Check if redirect from https to http is allowed - if (request.IsHttps() && string.Equals("http", location.Scheme, StringComparison.OrdinalIgnoreCase) - && RedirectMode == RedirectMode.NoDowngrade) - { - return false; - } - - // Reset fields calculated from the URI. - request.RequestUri = location; - request.Headers.Authorization = null; - request.Headers.Host = null; - request.SetConnectionHostProperty(null); - request.SetConnectionPortProperty(null); - request.SetSchemeProperty(null); - request.SetHostProperty(null); - request.SetPortProperty(null); - request.SetAddressLineProperty(null); - request.SetPathAndQueryProperty(null); - return true; - } - - private async Task ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - ProcessUrl(request); - ProcessHostHeader(request); - request.Headers.ConnectionClose = !request.Headers.Contains("Connection"); // TODO: Connection reuse is not supported. - - ProxyMode proxyMode = DetermineProxyModeAndAddressLine(request); - Socket socket; - Stream transport; - - try - { - if (_socketOpener != null) - { - socket = await _socketOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); - transport = new NetworkStream(socket, true); - } - else - { - socket = null; - transport = await _streamOpener(request.GetConnectionHostProperty(), request.GetConnectionPortProperty().Value, cancellationToken).ConfigureAwait(false); - } - } - catch (SocketException e) - { - throw new HttpRequestException("Connection failed.", e); - } - - if (proxyMode == ProxyMode.Tunnel) - { - await TunnelThroughProxyAsync(request, transport, cancellationToken); - } - - if (request.IsHttps()) - { - SslStream sslStream = new SslStream(transport, false, ServerCertificateValidationCallback); - await sslStream.AuthenticateAsClientAsync(request.GetHostProperty(), ClientCertificates, SslProtocols.Tls12, false); - transport = sslStream; - } - - var bufferedReadStream = new BufferedReadStream(transport, socket, _logger); - var connection = new HttpConnection(bufferedReadStream); - return await connection.SendAsync(request, cancellationToken); - } - - // Data comes from either the request.RequestUri or from the request.Properties - private static void ProcessUrl(HttpRequestMessage request) - { - string scheme = request.GetSchemeProperty(); - if (string.IsNullOrWhiteSpace(scheme)) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - scheme = request.RequestUri.Scheme; - request.SetSchemeProperty(scheme); - } - - if (!request.IsHttp() && !request.IsHttps()) - { - throw new InvalidOperationException("Only HTTP or HTTPS are supported, not: " + request.RequestUri.Scheme); - } - - string host = request.GetHostProperty(); - if (string.IsNullOrWhiteSpace(host)) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - host = request.RequestUri.DnsSafeHost; - request.SetHostProperty(host); - } - - string connectionHost = request.GetConnectionHostProperty(); - if (string.IsNullOrWhiteSpace(connectionHost)) - { - request.SetConnectionHostProperty(host); - } - - int? port = request.GetPortProperty(); - if (!port.HasValue) - { - if (!request.RequestUri.IsAbsoluteUri) - { - throw new InvalidOperationException("Missing URL Scheme"); - } - port = request.RequestUri.Port; - request.SetPortProperty(port); - } - - int? connectionPort = request.GetConnectionPortProperty(); - if (!connectionPort.HasValue) - { - request.SetConnectionPortProperty(port); - } - - string pathAndQuery = request.GetPathAndQueryProperty(); - if (string.IsNullOrWhiteSpace(pathAndQuery)) - { - if (request.RequestUri.IsAbsoluteUri) - { - pathAndQuery = request.RequestUri.PathAndQuery; - } - else - { - pathAndQuery = Uri.EscapeDataString(request.RequestUri.ToString()); - } - request.SetPathAndQueryProperty(pathAndQuery); - } - } - - private static void ProcessHostHeader(HttpRequestMessage request) - { - if (string.IsNullOrWhiteSpace(request.Headers.Host)) - { - string host = request.GetHostProperty(); - int port = request.GetPortProperty().Value; - if (host.Contains(':')) - { - // IPv6 - host = '[' + host + ']'; - } - - request.Headers.Host = host + ":" + port.ToString(CultureInfo.InvariantCulture); - } - } - - private ProxyMode DetermineProxyModeAndAddressLine(HttpRequestMessage request) - { - string scheme = request.GetSchemeProperty(); - string host = request.GetHostProperty(); - int? port = request.GetPortProperty(); - string pathAndQuery = request.GetPathAndQueryProperty(); - string addressLine = request.GetAddressLineProperty(); - - if (string.IsNullOrEmpty(addressLine)) - { - request.SetAddressLineProperty(pathAndQuery); - } - - try - { - if (!UseProxy || Proxy == null || Proxy.IsBypassed(request.RequestUri)) - { - return ProxyMode.None; - } - } - catch (PlatformNotSupportedException) - { - return ProxyMode.None; - } - - var proxyUri = Proxy.GetProxy(request.RequestUri); - if (proxyUri == null) - { - return ProxyMode.None; - } - - if (request.IsHttp()) - { - if (string.IsNullOrEmpty(addressLine)) - { - addressLine = scheme + "://" + host + ":" + port.Value + pathAndQuery; - request.SetAddressLineProperty(addressLine); - } - request.SetConnectionHostProperty(proxyUri.DnsSafeHost); - request.SetConnectionPortProperty(proxyUri.Port); - return ProxyMode.Http; - } - - // Tunneling generates a completely separate request, don't alter the original, just the connection address. - request.SetConnectionHostProperty(proxyUri.DnsSafeHost); - request.SetConnectionPortProperty(proxyUri.Port); - return ProxyMode.Tunnel; - } - - private static async Task TcpSocketOpenerAsync(string host, int port, CancellationToken cancellationToken) - { - var addresses = await Dns.GetHostAddressesAsync(host) - .ConfigureAwait(false); - - if (addresses.Length == 0) - { - throw new Exception($"Unable to resolve any IP addresses for the host '{host}'."); - } - - var exceptions = new List(); - - foreach (var address in addresses) - { - var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - - try - { - await socket.ConnectAsync(address, port) - .ConfigureAwait(false); - - return socket; - } - catch (Exception e) - { - socket.Dispose(); - exceptions.Add(e); - } - } - - throw new AggregateException(exceptions); - } - - private async Task TunnelThroughProxyAsync(HttpRequestMessage request, Stream transport, CancellationToken cancellationToken) - { - // Send a Connect request: - // CONNECT server.example.com:80 HTTP / 1.1 - // Host: server.example.com:80 - var connectRequest = new HttpRequestMessage(); - connectRequest.Version = new Version(1, 1); - - connectRequest.Headers.ProxyAuthorization = request.Headers.ProxyAuthorization; - connectRequest.Method = new HttpMethod("CONNECT"); - // TODO: IPv6 hosts - string authority = request.GetHostProperty() + ":" + request.GetPortProperty().Value; - connectRequest.SetAddressLineProperty(authority); - connectRequest.Headers.Host = authority; - - HttpConnection connection = new HttpConnection(new BufferedReadStream(transport, null, _logger)); - HttpResponseMessage connectResponse; - try - { - connectResponse = await connection.SendAsync(connectRequest, cancellationToken); - // TODO:? await connectResponse.Content.LoadIntoBufferAsync(); // Drain any body - // There's no danger of accidentally consuming real response data because the real request hasn't been sent yet. - } - catch (Exception ex) - { - transport.Dispose(); - throw new HttpRequestException("SSL Tunnel failed to initialize", ex); - } - - // Listen for a response. Any 2XX is considered success, anything else is considered a failure. - if ((int)connectResponse.StatusCode < 200 || 300 <= (int)connectResponse.StatusCode) - { - transport.Dispose(); - throw new HttpRequestException("Failed to negotiate the proxy tunnel: " + connectResponse); - } - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ProxyMode.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ProxyMode.cs deleted file mode 100644 index 05a823c9..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ProxyMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -public enum ProxyMode -{ - None, - Http, - Tunnel -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RedirectMode.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RedirectMode.cs deleted file mode 100644 index f6c1e2e7..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RedirectMode.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -public enum RedirectMode -{ - /// - /// Do not follow redirects. - /// - None, - - /// - /// Disallows redirecting from HTTPS to HTTP - /// - NoDowngrade, - - /// - /// Follow all redirects - /// - All, -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RequestExtensions.cs deleted file mode 100644 index 4c998bc3..00000000 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/RequestExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal static class RequestExtensions -{ - public static bool IsHttp(this HttpRequestMessage request) - { - return string.Equals("http", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); - } - - public static bool IsHttps(this HttpRequestMessage request) - { - return string.Equals("https", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); - } - - public static string GetSchemeProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Scheme"); - } - - public static void SetSchemeProperty(this HttpRequestMessage request, string scheme) - { - request.SetProperty("url.Scheme", scheme); - } - - public static string GetHostProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Host"); - } - - public static void SetHostProperty(this HttpRequestMessage request, string host) - { - request.SetProperty("url.Host", host); - } - - public static int? GetPortProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Port"); - } - - public static void SetPortProperty(this HttpRequestMessage request, int? port) - { - request.SetProperty("url.Port", port); - } - - public static string GetConnectionHostProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.ConnectionHost"); - } - - public static void SetConnectionHostProperty(this HttpRequestMessage request, string host) - { - request.SetProperty("url.ConnectionHost", host); - } - - public static int? GetConnectionPortProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.ConnectionPort"); - } - - public static void SetConnectionPortProperty(this HttpRequestMessage request, int? port) - { - request.SetProperty("url.ConnectionPort", port); - } - - public static string GetPathAndQueryProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.PathAndQuery"); - } - - public static void SetPathAndQueryProperty(this HttpRequestMessage request, string pathAndQuery) - { - request.SetProperty("url.PathAndQuery", pathAndQuery); - } - - public static string GetAddressLineProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.AddressLine"); - } - - public static void SetAddressLineProperty(this HttpRequestMessage request, string addressLine) - { - request.SetProperty("url.AddressLine", addressLine); - } - - public static T GetProperty(this HttpRequestMessage request, string key) - { -#if NET6_0_OR_GREATER - return request.Options.TryGetValue(new HttpRequestOptionsKey(key), out var obj) ? obj : default; -#else - return request.Properties.TryGetValue(key, out var obj) ? (T)obj : default; -#endif - } - - public static void SetProperty(this HttpRequestMessage request, string key, T value) - { -#if NET6_0_OR_GREATER - request.Options.Set(new HttpRequestOptionsKey(key), value); -#else - request.Properties[key] = value; -#endif - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs b/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs index 98b3537f..e18d7884 100644 --- a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs @@ -1,9 +1,3 @@ -using System; -using System.IO.Pipes; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - namespace Docker.DotNet.NPipe { public class NpipeHandlerFactory : IDockerHandlerFactory @@ -26,7 +20,7 @@ public Tuple CreateHandler(Uri uri, DockerClientConfigu } var pipeName = uri.Segments[2]; uri = new UriBuilder("http", pipeName).Uri; - + return new Tuple( new Microsoft.Net.Http.Client.ManagedHandler(async (host, port, cancellationToken) => { diff --git a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj index 4f837cc0..71d6d098 100644 --- a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj +++ b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj @@ -16,5 +16,6 @@ + diff --git a/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs b/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs index 05921aaa..424e47e3 100644 --- a/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs +++ b/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs @@ -1,6 +1,3 @@ -using System; -using Microsoft.Extensions.Logging; - namespace Docker.DotNet.NativeHttp { public class NativeHttpHandlerFactory : IDockerHandlerFactory diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj index 40f453be..4b972de2 100644 --- a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -9,6 +9,9 @@ + + + @@ -16,5 +19,8 @@ + + + diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/BufferedReadStream.cs deleted file mode 100644 index b98aa284..00000000 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ /dev/null @@ -1,240 +0,0 @@ -using System.Buffers; -using System.IO; -using System.Net.Sockets; -using Docker.DotNet; -using Microsoft.Extensions.Logging; - -namespace Microsoft.Net.Http.Client; - -internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream -{ - private readonly Stream _inner; - private readonly Socket _socket; - private readonly byte[] _buffer; - private readonly ILogger _logger; - private int _bufferRefCount; - private int _bufferOffset; - private int _bufferCount; - - public BufferedReadStream(Stream inner, Socket socket, ILogger logger) - : this(inner, socket, 8192, logger) - { - } - - public BufferedReadStream(Stream inner, Socket socket, int bufferLength, ILogger logger) - { - _inner = inner ?? throw new ArgumentNullException(nameof(inner)); - _socket = socket; - _buffer = ArrayPool.Shared.Rent(bufferLength); - _logger = logger; - _bufferRefCount = 1; - } - - public override bool CanRead - { - get { return _inner.CanRead || _bufferCount > 0; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return _inner.CanWrite; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override bool CanCloseWrite => _socket != null || _inner is WriteClosableStream; - - protected override void Dispose(bool disposing) - { - if (disposing) - { - if (Interlocked.Exchange(ref _bufferRefCount, 0) == 1) - { - ArrayPool.Shared.Return(_buffer); - } - - _inner.Dispose(); - } - - base.Dispose(disposing); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - _inner.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _inner.FlushAsync(cancellationToken); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _inner.Write(buffer, offset, count); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.WriteAsync(buffer, offset, count, cancellationToken); - } - - public override int Read(byte[] buffer, int offset, int count) - { - int read = ReadBuffer(buffer, offset, count); - if (read > 0) - { - return read; - } - - return _inner.Read(buffer, offset, count); - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - int read = ReadBuffer(buffer, offset, count); - if (read > 0) - { - return Task.FromResult(read); - } - - return _inner.ReadAsync(buffer, offset, count, cancellationToken); - } - - public override void CloseWrite() - { - if (_socket != null) - { - _socket.Shutdown(SocketShutdown.Send); - return; - } - - if (_inner is WriteClosableStream writeClosableStream) - { - writeClosableStream.CloseWrite(); - return; - } - - throw new NotSupportedException("Cannot shutdown write on this transport"); - } - - public bool Peek(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) - { - int read = PeekBuffer(buffer, toPeek, out peeked, out available, out remaining); - if (read > 0) - { - return true; - } - - if (_inner is IPeekableStream peekableStream) - { - return peekableStream.Peek(buffer, toPeek, out peeked, out available, out remaining); - } - - throw new NotSupportedException("_inner stream isn't a peekable stream"); - } - - public async Task ReadLineAsync(CancellationToken cancellationToken) - { - var line = new StringBuilder(_buffer.Length); - - var crIndex = -1; - - var lfIndex = -1; - - bool crlfFound; - - do - { - if (_bufferCount == 0) - { - _bufferOffset = 0; - - _bufferCount = await _inner.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken) - .ConfigureAwait(false); - } - - var c = (char)_buffer[_bufferOffset]; - line.Append(c); - - _bufferOffset++; - _bufferCount--; - - switch (c) - { - case '\r': - crIndex = line.Length; - break; - case '\n': - lfIndex = line.Length; - break; - } - - crlfFound = crIndex + 1 == lfIndex; - } - while (!crlfFound); - - return line.ToString(0, line.Length - 2); - } - - private int ReadBuffer(byte[] buffer, int offset, int count) - { - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, count); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); - _bufferOffset += toCopy; - _bufferCount -= toCopy; - return toCopy; - } - - return 0; - } - - private int PeekBuffer(byte[] buffer, uint toPeek, out uint peeked, out uint available, out uint remaining) - { - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, (int)toPeek); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, 0, toCopy); - peeked = (uint) toCopy; - available = (uint)_bufferCount; - remaining = available - peeked; - return toCopy; - } - - peeked = 0; - available = 0; - remaining = 0; - return 0; - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedReadStream.cs deleted file mode 100644 index dc2e552d..00000000 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedReadStream.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System.Globalization; -using System.IO; - -namespace Microsoft.Net.Http.Client; - -internal sealed class ChunkedReadStream : Stream -{ - private readonly BufferedReadStream _inner; - private int _chunkBytesRemaining; - private bool _done; - - public ChunkedReadStream(BufferedReadStream stream) - { - _inner = stream ?? throw new ArgumentNullException(nameof(stream)); - } - - public override bool CanRead - { - get { return _inner.CanRead; } - } - - public override bool CanSeek - { - get { return false; } - } - - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override int ReadTimeout - { - get - { - return _inner.ReadTimeout; - } - set - { - _inner.ReadTimeout = value; - } - } - - public override int WriteTimeout - { - get - { - return _inner.WriteTimeout; - } - set - { - _inner.WriteTimeout = value; - } - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (_done) - { - return 0; - } - - if (_chunkBytesRemaining == 0) - { - var headerLine = await _inner.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (!int.TryParse(headerLine, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _chunkBytesRemaining)) - { - throw new IOException($"Invalid chunk header encountered: '{headerLine}'."); - } - } - - var readBytesCount = 0; - - if (_chunkBytesRemaining > 0) - { - var remainingBytesCount = Math.Min(_chunkBytesRemaining, count); - - readBytesCount = await _inner.ReadAsync(buffer, offset, remainingBytesCount, cancellationToken) - .ConfigureAwait(false); - - if (readBytesCount == 0) - { - throw new EndOfStreamException(); - } - - _chunkBytesRemaining -= readBytesCount; - } - - if (_chunkBytesRemaining == 0) - { - var emptyLine = await _inner.ReadLineAsync(cancellationToken) - .ConfigureAwait(false); - - if (!string.IsNullOrEmpty(emptyLine)) - { - throw new IOException($"Expected an empty line, but received: '{emptyLine}'."); - } - - _done = readBytesCount == 0; - } - - return readBytesCount; - } - - public override void Write(byte[] buffer, int offset, int count) - { - _inner.Write(buffer, offset, count); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.WriteAsync(buffer, offset, count, cancellationToken); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Flush() - { - _inner.Flush(); - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs deleted file mode 100644 index 01de9b77..00000000 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.IO; - -namespace Microsoft.Net.Http.Client; - -public class HttpConnectionResponseContent : HttpContent -{ - private readonly HttpConnection _connection; - private Stream _responseStream; - - internal HttpConnectionResponseContent(HttpConnection connection) - { - _connection = connection; - } - - internal void ResolveResponseStream(bool chunked) - { - if (_responseStream != null) - { - throw new InvalidOperationException("Called multiple times"); - } - if (chunked) - { - _responseStream = new ChunkedReadStream(_connection.Transport); - } - else if (Headers.ContentLength.HasValue) - { - _responseStream = new ContentLengthReadStream(_connection.Transport, Headers.ContentLength.Value); - } - else - { - _responseStream = _connection.Transport; - } - } - - public Docker.DotNet.WriteClosableStream HijackStream() - { - if (_responseStream != _connection.Transport) - { - throw new InvalidOperationException("cannot hijack chunked or content length stream"); - } - - return _connection.Transport; - } - - protected override Task SerializeToStreamAsync(Stream stream, System.Net.TransportContext context) - { - return _responseStream.CopyToAsync(stream); - } - - protected override Task CreateContentReadStreamAsync() - { - return Task.FromResult(_responseStream); - } - - protected override bool TryComputeLength(out long length) - { - length = 0; - return false; - } - - protected override void Dispose(bool disposing) - { - try - { - if (disposing) - { - _responseStream.Dispose(); - _connection.Dispose(); - } - } - finally - { - base.Dispose(disposing); - } - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ProxyMode.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ProxyMode.cs deleted file mode 100644 index 05a823c9..00000000 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ProxyMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -public enum ProxyMode -{ - None, - Http, - Tunnel -} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RedirectMode.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RedirectMode.cs deleted file mode 100644 index f6c1e2e7..00000000 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RedirectMode.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -public enum RedirectMode -{ - /// - /// Do not follow redirects. - /// - None, - - /// - /// Disallows redirecting from HTTPS to HTTP - /// - NoDowngrade, - - /// - /// Follow all redirects - /// - All, -} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RequestExtensions.cs deleted file mode 100644 index 4c998bc3..00000000 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/RequestExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace Microsoft.Net.Http.Client; - -internal static class RequestExtensions -{ - public static bool IsHttp(this HttpRequestMessage request) - { - return string.Equals("http", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); - } - - public static bool IsHttps(this HttpRequestMessage request) - { - return string.Equals("https", request.GetSchemeProperty(), StringComparison.OrdinalIgnoreCase); - } - - public static string GetSchemeProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Scheme"); - } - - public static void SetSchemeProperty(this HttpRequestMessage request, string scheme) - { - request.SetProperty("url.Scheme", scheme); - } - - public static string GetHostProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Host"); - } - - public static void SetHostProperty(this HttpRequestMessage request, string host) - { - request.SetProperty("url.Host", host); - } - - public static int? GetPortProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.Port"); - } - - public static void SetPortProperty(this HttpRequestMessage request, int? port) - { - request.SetProperty("url.Port", port); - } - - public static string GetConnectionHostProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.ConnectionHost"); - } - - public static void SetConnectionHostProperty(this HttpRequestMessage request, string host) - { - request.SetProperty("url.ConnectionHost", host); - } - - public static int? GetConnectionPortProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.ConnectionPort"); - } - - public static void SetConnectionPortProperty(this HttpRequestMessage request, int? port) - { - request.SetProperty("url.ConnectionPort", port); - } - - public static string GetPathAndQueryProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.PathAndQuery"); - } - - public static void SetPathAndQueryProperty(this HttpRequestMessage request, string pathAndQuery) - { - request.SetProperty("url.PathAndQuery", pathAndQuery); - } - - public static string GetAddressLineProperty(this HttpRequestMessage request) - { - return request.GetProperty("url.AddressLine"); - } - - public static void SetAddressLineProperty(this HttpRequestMessage request, string addressLine) - { - request.SetProperty("url.AddressLine", addressLine); - } - - public static T GetProperty(this HttpRequestMessage request, string key) - { -#if NET6_0_OR_GREATER - return request.Options.TryGetValue(new HttpRequestOptionsKey(key), out var obj) ? obj : default; -#else - return request.Properties.TryGetValue(key, out var obj) ? (T)obj : default; -#endif - } - - public static void SetProperty(this HttpRequestMessage request, string key, T value) - { -#if NET6_0_OR_GREATER - request.Options.Set(new HttpRequestOptionsKey(key), value); -#else - request.Properties[key] = value; -#endif - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs b/src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs similarity index 94% rename from src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs rename to src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs index 74bd3fb1..c095a5f4 100644 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/UnixDomainSocketEndPoint.cs +++ b/src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs @@ -1,10 +1,6 @@ -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; +namespace Docker.DotNet.Unix; -namespace Microsoft.Net.Http.Client; - -internal sealed class UnixDomainSocketEndPoint : EndPoint +internal class UnixDomainSocketEndPoint : EndPoint { private const AddressFamily EndPointAddressFamily = AddressFamily.Unix; diff --git a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs b/src/Docker.DotNet.Unix/UnixHandlerFactory.cs index 6f62249a..fb8fbeef 100644 --- a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs +++ b/src/Docker.DotNet.Unix/UnixHandlerFactory.cs @@ -1,9 +1,3 @@ -using System; -using System.Linq; -using System.Net.Sockets; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - namespace Docker.DotNet.Unix { public class UnixHandlerFactory : IDockerHandlerFactory @@ -16,11 +10,11 @@ public Tuple CreateHandler(Uri uri, DockerClientConfigu new Microsoft.Net.Http.Client.ManagedHandler(async (host, port, cancellationToken) => { var sock = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - await sock.ConnectAsync(new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(pipeString)).ConfigureAwait(false); + await sock.ConnectAsync(new UnixDomainSocketEndPoint(pipeString)).ConfigureAwait(false); return sock; }, logger), uri - ); + ); } } } diff --git a/src/Docker.DotNet.X509/CertificateCredentials.cs b/src/Docker.DotNet.X509/CertificateCredentials.cs index a8d692c1..7278dd2c 100644 --- a/src/Docker.DotNet.X509/CertificateCredentials.cs +++ b/src/Docker.DotNet.X509/CertificateCredentials.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Security.Authentication; - namespace Docker.DotNet.X509; public class CertificateCredentials : Credentials diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index c3c9cd52..bdcb0e47 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index 3875dd73..dd91b4fe 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -1,7 +1,5 @@ namespace Docker.DotNet; -using System; - public class DockerClientConfiguration : IDisposable { public DockerClientConfiguration( @@ -45,7 +43,7 @@ public DockerClientConfiguration( public TimeSpan DefaultTimeout { get; } - public DockerClient CreateClient(Version requestedApiVersion = null, ILogger logger = null) + public DockerClient CreateClient(System.Version requestedApiVersion = null, ILogger logger = null) { var scheme = EndpointBaseUri.Scheme; if (scheme.Equals("http", StringComparison.OrdinalIgnoreCase) || @@ -82,7 +80,7 @@ public DockerClient CreateClient(Version requestedApiVersion = null, ILogger log return new DockerClient(this, requestedApiVersion, factory, logger); } - public DockerClient CreateClient(Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) + public DockerClient CreateClient(System.Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) { if (handlerFactory == null) { diff --git a/src/Docker.DotNet/IDockerHandlerFactory.cs b/src/Docker.DotNet/IDockerHandlerFactory.cs index fef37eb1..a4047deb 100644 --- a/src/Docker.DotNet/IDockerHandlerFactory.cs +++ b/src/Docker.DotNet/IDockerHandlerFactory.cs @@ -1,6 +1,3 @@ -using System; -using Microsoft.Extensions.Logging; - public interface IDockerHandlerFactory { Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger); diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/BufferedReadStream.cs b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs similarity index 98% rename from src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/BufferedReadStream.cs rename to src/Microsoft.Net.Http.Client/BufferedReadStream.cs index b98aa284..0c766b42 100644 --- a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/BufferedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/BufferedReadStream.cs @@ -1,9 +1,3 @@ -using System.Buffers; -using System.IO; -using System.Net.Sockets; -using Docker.DotNet; -using Microsoft.Extensions.Logging; - namespace Microsoft.Net.Http.Client; internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream diff --git a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedReadStream.cs b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs similarity index 98% rename from src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedReadStream.cs rename to src/Microsoft.Net.Http.Client/ChunkedReadStream.cs index dc2e552d..e4991829 100644 --- a/src/Docker.DotNet.NPipe/Microsoft.Net.Http.Client/ChunkedReadStream.cs +++ b/src/Microsoft.Net.Http.Client/ChunkedReadStream.cs @@ -1,6 +1,3 @@ -using System.Globalization; -using System.IO; - namespace Microsoft.Net.Http.Client; internal sealed class ChunkedReadStream : Stream diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedWriteStream.cs b/src/Microsoft.Net.Http.Client/ChunkedWriteStream.cs similarity index 99% rename from src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedWriteStream.cs rename to src/Microsoft.Net.Http.Client/ChunkedWriteStream.cs index b63fe3c6..4a1ca6c0 100644 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ChunkedWriteStream.cs +++ b/src/Microsoft.Net.Http.Client/ChunkedWriteStream.cs @@ -1,5 +1,3 @@ -using System.IO; - namespace Microsoft.Net.Http.Client; internal sealed class ChunkedWriteStream : Stream diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ContentLengthReadStream.cs b/src/Microsoft.Net.Http.Client/ContentLengthReadStream.cs similarity index 99% rename from src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ContentLengthReadStream.cs rename to src/Microsoft.Net.Http.Client/ContentLengthReadStream.cs index c0ba7ef3..2d38d85c 100644 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ContentLengthReadStream.cs +++ b/src/Microsoft.Net.Http.Client/ContentLengthReadStream.cs @@ -1,5 +1,3 @@ -using System.IO; - namespace Microsoft.Net.Http.Client; internal class ContentLengthReadStream : Stream diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnection.cs b/src/Microsoft.Net.Http.Client/HttpConnection.cs similarity index 98% rename from src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnection.cs rename to src/Microsoft.Net.Http.Client/HttpConnection.cs index 2f20e3dc..37dc69b2 100644 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/HttpConnection.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnection.cs @@ -1,8 +1,3 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; - namespace Microsoft.Net.Http.Client; internal sealed class HttpConnection : IDisposable diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs similarity index 97% rename from src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs rename to src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs index f6f99a0f..f4b7c97f 100644 --- a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs +++ b/src/Microsoft.Net.Http.Client/HttpConnectionResponseContent.cs @@ -1,6 +1,3 @@ -using System.IO; -using Docker.DotNet; - namespace Microsoft.Net.Http.Client; public class HttpConnectionResponseContent : HttpContent diff --git a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ManagedHandler.cs b/src/Microsoft.Net.Http.Client/ManagedHandler.cs similarity index 97% rename from src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ManagedHandler.cs rename to src/Microsoft.Net.Http.Client/ManagedHandler.cs index bc17c436..a7f55cf5 100644 --- a/src/Docker.DotNet.Unix/Microsoft.Net.Http.Client/ManagedHandler.cs +++ b/src/Microsoft.Net.Http.Client/ManagedHandler.cs @@ -1,17 +1,5 @@ namespace Microsoft.Net.Http.Client; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Authentication; -using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Logging; - public class ManagedHandler : HttpMessageHandler { private readonly ILogger _logger; diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems new file mode 100644 index 00000000..1015069f --- /dev/null +++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems @@ -0,0 +1,39 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + dae2de68-9b3e-4d5d-8802-ec97b94160ed + + + Microsoft.Net.Http.Client + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj new file mode 100644 index 00000000..cfad65fe --- /dev/null +++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj @@ -0,0 +1,14 @@ + + + + dae2de68-9b3e-4d5d-8802-ec97b94160ed + 14.0 + + + + + + + + diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ProxyMode.cs b/src/Microsoft.Net.Http.Client/ProxyMode.cs similarity index 100% rename from src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/ProxyMode.cs rename to src/Microsoft.Net.Http.Client/ProxyMode.cs diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RedirectMode.cs b/src/Microsoft.Net.Http.Client/RedirectMode.cs similarity index 100% rename from src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RedirectMode.cs rename to src/Microsoft.Net.Http.Client/RedirectMode.cs diff --git a/src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RequestExtensions.cs b/src/Microsoft.Net.Http.Client/RequestExtensions.cs similarity index 100% rename from src/Docker.DotNet.LegacyHttp/Microsoft.Net.Http.Client/RequestExtensions.cs rename to src/Microsoft.Net.Http.Client/RequestExtensions.cs diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index f773cd36..8bb7b124 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -17,13 +17,15 @@ - + + + @@ -32,6 +34,11 @@ + + + + + diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index 2a031df3..644f8033 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -1,12 +1,3 @@ -using System.IO; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using Docker.DotNet.LegacyHttp; -using Docker.DotNet.NativeHttp; -using Docker.DotNet.NPipe; -using Docker.DotNet.Unix; -using Docker.DotNet.X509; - namespace Docker.DotNet.Tests; [CollectionDefinition(nameof(TestCollection))] From 7e081281f74fd71cde3e8172ffa69d7097fe1dc8 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Fri, 23 Jan 2026 09:36:36 +0100 Subject: [PATCH 13/50] ignore CA1416 (Plattform windows) --- src/Docker.DotNet.NPipe/DockerPipeStream.cs | 4 ++++ src/Docker.DotNet.NPipe/GlobalSuppressions.cs | 8 -------- 2 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 src/Docker.DotNet.NPipe/GlobalSuppressions.cs diff --git a/src/Docker.DotNet.NPipe/DockerPipeStream.cs b/src/Docker.DotNet.NPipe/DockerPipeStream.cs index 596ebb07..960de51b 100644 --- a/src/Docker.DotNet.NPipe/DockerPipeStream.cs +++ b/src/Docker.DotNet.NPipe/DockerPipeStream.cs @@ -42,13 +42,17 @@ public override void CloseWrite() { // The Docker daemon expects a write of zero bytes to signal the end of writes. Use native // calls to achieve this since CoreCLR ignores a zero-byte write. +#pragma warning disable CA1416 var overlapped = new NativeOverlapped(); +#pragma warning restore CA1416 var handle = _event.GetSafeWaitHandle(); // Set the low bit to tell Windows not to send the result of this IO to the // completion port. +#pragma warning disable CA1416 overlapped.EventHandle = (IntPtr)(handle.DangerousGetHandle().ToInt64() | 1); +#pragma warning restore CA1416 if (WriteFile(_stream.SafePipeHandle, IntPtr.Zero, 0, IntPtr.Zero, ref overlapped) == 0) { const int ERROR_IO_PENDING = 997; diff --git a/src/Docker.DotNet.NPipe/GlobalSuppressions.cs b/src/Docker.DotNet.NPipe/GlobalSuppressions.cs deleted file mode 100644 index 2ef16235..00000000 --- a/src/Docker.DotNet.NPipe/GlobalSuppressions.cs +++ /dev/null @@ -1,8 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Interoperability", "CA1416:Plattformkompatibilität überprüfen", Justification = "", Scope = "member", Target = "~M:Docker.DotNet.DockerPipeStream.CloseWrite")] From e1fd8073c2c61edde05c134f0cfd3387d985fdca Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Fri, 23 Jan 2026 10:44:18 +0100 Subject: [PATCH 14/50] remove usings --- src/Docker.DotNet/DockerClient.cs | 9 +++------ src/Docker.DotNet/HttpUtility.cs | 4 +--- src/Docker.DotNet/WriteClosableStreamWrapper.cs | 3 --- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 1a395b6c..27e5bc30 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -1,8 +1,5 @@ namespace Docker.DotNet; -using System; -using System.Reflection; - public sealed class DockerClient : IDockerClient { internal readonly IEnumerable NoErrorHandlers = Enumerable.Empty(); @@ -13,11 +10,11 @@ public sealed class DockerClient : IDockerClient private readonly Uri _endpointBaseUri; - private readonly Version _requestedApiVersion; + private readonly System.Version _requestedApiVersion; private readonly IDockerHandlerFactory _handlerFactory; - internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion, IDockerHandlerFactory handlerFactory = null, ILogger logger = null) + internal DockerClient(DockerClientConfiguration configuration, System.Version requestedApiVersion, IDockerHandlerFactory handlerFactory = null, ILogger logger = null) { _requestedApiVersion = requestedApiVersion; Configuration = configuration; @@ -390,7 +387,7 @@ private HttpRequestMessage PrepareRequest(HttpMethod method, string path, IQuery } var request = new HttpRequestMessage(method, HttpUtility.BuildUri(_endpointBaseUri, _requestedApiVersion, path, queryString)); - request.Version = new Version(1, 1); + request.Version = new System.Version(1, 1); request.Headers.Add("User-Agent", UserAgent); var customHeaders = headers == null diff --git a/src/Docker.DotNet/HttpUtility.cs b/src/Docker.DotNet/HttpUtility.cs index 8cdfc1a5..e8ca8e43 100644 --- a/src/Docker.DotNet/HttpUtility.cs +++ b/src/Docker.DotNet/HttpUtility.cs @@ -1,10 +1,8 @@ namespace Docker.DotNet; -using System; - internal static class HttpUtility { - public static Uri BuildUri(Uri baseUri, Version requestedApiVersion, string path, IQueryString queryString) + public static Uri BuildUri(Uri baseUri, System.Version requestedApiVersion, string path, IQueryString queryString) { if (baseUri == null) { diff --git a/src/Docker.DotNet/WriteClosableStreamWrapper.cs b/src/Docker.DotNet/WriteClosableStreamWrapper.cs index a2d27289..43f8a2ff 100644 --- a/src/Docker.DotNet/WriteClosableStreamWrapper.cs +++ b/src/Docker.DotNet/WriteClosableStreamWrapper.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; - namespace Docker.DotNet; public class WriteClosableStreamWrapper : WriteClosableStream From cb852a4c60de609aca5c8b18e3d179cc001a5d1d Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:38:10 +0100 Subject: [PATCH 15/50] chore: Order project references and usings --- .../Docker.DotNet.BasicAuth.csproj | 2 +- .../Docker.DotNet.NPipe.csproj | 4 ++-- .../Docker.DotNet.Unix.csproj | 4 ++-- .../Docker.DotNet.X509.csproj | 8 ++++---- src/Docker.DotNet/Docker.DotNet.csproj | 4 ++-- .../Docker.DotNet.Tests.csproj | 18 +++++++++--------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj index 8080b7f7..114c9d45 100644 --- a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj +++ b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj index 7c8b2fee..3ee19fdc 100644 --- a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -9,6 +9,8 @@ + + @@ -16,8 +18,6 @@ - - diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj index 4b972de2..7d2c8693 100644 --- a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -10,16 +10,16 @@ + - + - diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index bdcb0e47..b0033479 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -13,14 +13,14 @@ + - - - + + - \ No newline at end of file + diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index 1fcd26ba..47c4a82f 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -24,8 +24,8 @@ - + @@ -49,4 +49,4 @@ - \ No newline at end of file + diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index 8bb7b124..8a14929f 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -10,31 +10,31 @@ + - + + - - - + - - - + + + - + @@ -45,4 +45,4 @@ - \ No newline at end of file + From 17a89ad3c3b9f2c2504070ce0b05644814ce296c Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:40:23 +0100 Subject: [PATCH 16/50] chore: Add Enhanced to package id to keep packages together --- src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj | 2 +- src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj | 2 +- src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj | 2 +- src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj index aa1dc397..53dd9f20 100644 --- a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj +++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj @@ -1,7 +1,7 @@ Docker.DotNet.LegacyHttp - Docker.DotNet.LegacyHttp + Docker.DotNet.Enhanced.LegacyHttp Docker.DotNet.LegacyHttp is a library that allows you to connect via http(s) with a Docker engine programmatically in your .NET applications. diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj index 3ee19fdc..bbfe93b2 100644 --- a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -1,7 +1,7 @@ Docker.DotNet.NPipe - Docker.DotNet.NPipe + Docker.DotNet.Enhanced.NPipe Docker.DotNet.NPipe is a library that allows you to connect via windows npipe with a Docker engine programmatically in your .NET applications. diff --git a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj index 71d6d098..cfcffcdb 100644 --- a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj +++ b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj @@ -1,7 +1,7 @@ Docker.DotNet.NativeHttp - Docker.DotNet.NativeHttp + Docker.DotNet.Enhanced.NativeHttp Docker.DotNet.NativeHttp is a library that allows you to connect via http(s) with a Docker engine programmatically in your .NET applications. diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj index 7d2c8693..4221200c 100644 --- a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -1,7 +1,7 @@ Docker.DotNet.Unix - Docker.DotNet.Unix + Docker.DotNet.Enhanced.Unix Docker.DotNet.Unix is a library that allows you to connect via unix socket with a Docker engine programmatically in your .NET applications. From c78d837f46cc6f7c89c4dbeeee4b30b7f852705c Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:43:36 +0100 Subject: [PATCH 17/50] chore: Align descriptions --- src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj | 2 +- src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj | 2 +- src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj | 2 +- src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj | 2 +- src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj | 2 +- src/Docker.DotNet.X509/Docker.DotNet.X509.csproj | 2 +- src/Docker.DotNet/Docker.DotNet.csproj | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj index 114c9d45..6148e4b6 100644 --- a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj +++ b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj @@ -2,7 +2,7 @@ Docker.DotNet.BasicAuth Docker.DotNet.Enhanced.BasicAuth - Docker.DotNet.BasicAuth is a library that allows you to use basic authentication with a remote Docker engine programmatically in your .NET applications. + A Docker.DotNet extension that adds Basic Authentication for remote Docker Engine connections. diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj index 53dd9f20..bba81ee4 100644 --- a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj +++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj @@ -2,7 +2,7 @@ Docker.DotNet.LegacyHttp Docker.DotNet.Enhanced.LegacyHttp - Docker.DotNet.LegacyHttp is a library that allows you to connect via http(s) with a Docker engine programmatically in your .NET applications. + A Docker.DotNet transport implementation for HTTP(S) Docker Engine connections. diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj index bbfe93b2..af4bf162 100644 --- a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -2,7 +2,7 @@ Docker.DotNet.NPipe Docker.DotNet.Enhanced.NPipe - Docker.DotNet.NPipe is a library that allows you to connect via windows npipe with a Docker engine programmatically in your .NET applications. + A Docker.DotNet transport implementation for Windows named pipe (npipe) Docker Engine connections. diff --git a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj index cfcffcdb..85a183c1 100644 --- a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj +++ b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj @@ -2,7 +2,7 @@ Docker.DotNet.NativeHttp Docker.DotNet.Enhanced.NativeHttp - Docker.DotNet.NativeHttp is a library that allows you to connect via http(s) with a Docker engine programmatically in your .NET applications. + A Docker.DotNet transport implementation for HTTP(S) Docker Engine connections. diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj index 4221200c..6e20b2c2 100644 --- a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -2,7 +2,7 @@ Docker.DotNet.Unix Docker.DotNet.Enhanced.Unix - Docker.DotNet.Unix is a library that allows you to connect via unix socket with a Docker engine programmatically in your .NET applications. + A Docker.DotNet transport implementation for Unix domain socket Docker Engine connections. diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index b0033479..e49da288 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -2,7 +2,7 @@ Docker.DotNet.X509 Docker.DotNet.Enhanced.X509 - Docker.DotNet.X509 is a library that allows you to use certificate authentication with a remote Docker engine programmatically in your .NET applications. + A Docker.DotNet extension that adds X.509 client certificate authentication for remote Docker Engine connections. diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index 47c4a82f..20101695 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -2,7 +2,7 @@ Docker.DotNet Docker.DotNet.Enhanced - Docker.DotNet is a library that allows you to interact with the Docker Remote API programmatically with fully asynchronous, non-blocking and object-oriented code in your .NET applications. + A .NET client for the Docker Engine API with fully asynchronous, non-blocking, object-oriented APIs. From f1d5da4626cd57cc6c62fe5fc86cf1f20c1d69c8 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:50:43 +0100 Subject: [PATCH 18/50] chore: Align shared project --- README.md | 2 +- src/Directory.Build.props | 1 + .../Microsoft.Net.Http.Client.projitems | 12 ++++++------ .../Microsoft.Net.Http.Client.shproj | 3 +-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9c4b2a00..16b1a77d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ You can add this library to your project using [NuGet][nuget]. Run the following command in the "Package Manager Console": ```console -> PM> Install-Package Docker.DotNet.Enhanced +PM> Install-Package Docker.DotNet.Enhanced ``` **Visual Studio** diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a9c572ad..f008b04b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -37,4 +37,5 @@ runtime; build; native; contentfiles; analyzers + diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems index 1015069f..d489e12d 100644 --- a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems +++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems @@ -9,19 +9,19 @@ Microsoft.Net.Http.Client - + + + - - + - - + @@ -36,4 +36,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj index cfad65fe..b7fd1252 100644 --- a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj +++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.shproj @@ -1,6 +1,5 @@ - + dae2de68-9b3e-4d5d-8802-ec97b94160ed 14.0 From fc4b73b56504fc7cd279d13cb10078bc1055daa4 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:53:44 +0100 Subject: [PATCH 19/50] chore: Remove BOM --- .../Microsoft.Net.Http.Client.projitems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems index d489e12d..5c2c9157 100644 --- a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems +++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) From 91b982fac93b4782692f29189ddbeb53dd33ce84 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 24 Jan 2026 08:32:28 +0100 Subject: [PATCH 20/50] chore: Order csproj --- Docker.DotNet.sln | 144 +++++++++++++++++++++++----------------------- README.md | 10 ++-- 2 files changed, 77 insertions(+), 77 deletions(-) diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln index 2c8f25fd..dd3b8685 100644 --- a/Docker.DotNet.sln +++ b/Docker.DotNet.sln @@ -4,26 +4,26 @@ VisualStudioVersion = 17.10.35201.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{85990620-78A6-4381-8BD6-84E6D0CF0649}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet", "src\Docker.DotNet\Docker.DotNet.csproj", "{C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.BasicAuth", "src\Docker.DotNet.BasicAuth\Docker.DotNet.BasicAuth.csproj", "{E1F24B25-E027-45E0-A6E1-E08138F1F95D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.X509", "src\Docker.DotNet.X509\Docker.DotNet.X509.csproj", "{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.LegacyHttp", "src\Docker.DotNet.LegacyHttp\Docker.DotNet.LegacyHttp.csproj", "{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.NativeHttp", "src\Docker.DotNet.NativeHttp\Docker.DotNet.NativeHttp.csproj", "{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.NPipe", "src\Docker.DotNet.NPipe\Docker.DotNet.NPipe.csproj", "{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Unix", "src\Docker.DotNet.Unix\Docker.DotNet.Unix.csproj", "{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.LegacyHttp", "src\Docker.DotNet.LegacyHttp\Docker.DotNet.LegacyHttp.csproj", "{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.X509", "src\Docker.DotNet.X509\Docker.DotNet.X509.csproj", "{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.NativeHttp", "src\Docker.DotNet.NativeHttp\Docker.DotNet.NativeHttp.csproj", "{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet", "src\Docker.DotNet\Docker.DotNet.csproj", "{C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.Net.Http.Client", "src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.shproj", "{DAE2DE68-9B3E-4D5D-8802-EC97B94160ED}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,18 +34,6 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.ActiveCfg = Debug|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.Build.0 = Debug|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x86.ActiveCfg = Debug|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x86.Build.0 = Debug|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|Any CPU.Build.0 = Release|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.ActiveCfg = Release|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.Build.0 = Release|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.ActiveCfg = Release|Any CPU - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.Build.0 = Release|Any CPU {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Debug|Any CPU.Build.0 = Debug|Any CPU {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -58,54 +46,6 @@ Global {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x64.Build.0 = Release|Any CPU {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x86.ActiveCfg = Release|Any CPU {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x86.Build.0 = Release|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|Any CPU.Build.0 = Debug|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x64.ActiveCfg = Debug|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x64.Build.0 = Debug|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x86.ActiveCfg = Debug|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x86.Build.0 = Debug|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|Any CPU.ActiveCfg = Release|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|Any CPU.Build.0 = Release|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x64.ActiveCfg = Release|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x64.Build.0 = Release|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x86.ActiveCfg = Release|Any CPU - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x86.Build.0 = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.Build.0 = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.ActiveCfg = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.Build.0 = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x86.ActiveCfg = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x86.Build.0 = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|Any CPU.ActiveCfg = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|Any CPU.Build.0 = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.ActiveCfg = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.Build.0 = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.ActiveCfg = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.Build.0 = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.Build.0 = Debug|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.Build.0 = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.Build.0 = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.ActiveCfg = Release|Any CPU - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.ActiveCfg = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.Build.0 = Debug|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.Build.0 = Release|Any CPU {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -130,20 +70,80 @@ Global {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x64.Build.0 = Release|Any CPU {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.ActiveCfg = Release|Any CPU {E5F6A7B8-C9D0-41E2-3F45-5678901234EF}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}.Release|x86.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x64.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Debug|x86.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|Any CPU.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x64.Build.0 = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.ActiveCfg = Release|Any CPU + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}.Release|x86.Build.0 = Release|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x64.ActiveCfg = Debug|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x64.Build.0 = Debug|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x86.ActiveCfg = Debug|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Debug|x86.Build.0 = Debug|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|Any CPU.Build.0 = Release|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x64.ActiveCfg = Release|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x64.Build.0 = Release|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x86.ActiveCfg = Release|Any CPU + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}.Release|x86.Build.0 = Release|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.ActiveCfg = Debug|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x64.Build.0 = Debug|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Debug|x86.Build.0 = Debug|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|Any CPU.Build.0 = Release|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.ActiveCfg = Release|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.Build.0 = Release|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.ActiveCfg = Release|Any CPU + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.Build.0 = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.ActiveCfg = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.Build.0 = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x86.ActiveCfg = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x86.Build.0 = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|Any CPU.Build.0 = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.ActiveCfg = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.Build.0 = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.ActiveCfg = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {E1F24B25-E027-45E0-A6E1-E08138F1F95D} = {85990620-78A6-4381-8BD6-84E6D0CF0649} - {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44} = {85990620-78A6-4381-8BD6-84E6D0CF0649} - {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} - {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB} = {85990620-78A6-4381-8BD6-84E6D0CF0649} - {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {E5F6A7B8-C9D0-41E2-3F45-5678901234EF} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {B2C3D4E5-F6A7-48B9-0C1D-2345678901BC} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {DAE2DE68-9B3E-4D5D-8802-EC97B94160ED} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8F2F229F-C66D-43E4-B804-E5F37DC157CB} diff --git a/README.md b/README.md index 6d2618a3..df7d508f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Right click to your project in Visual Studio, choose "Manage NuGet Packages" and Run the following command from your favorite shell or terminal: ```console -> dotnet add package Docker.DotNet.Enhanced +dotnet add package Docker.DotNet.Enhanced ``` **Development Builds** @@ -264,12 +264,12 @@ DockerClient client = config.CreateClient(new Version(1, 49)); Here are typical exceptions thrown from the client library: - **`DockerApiException`** is thrown when Docker Engine API responds with a non-success result. Subclasses: - - **`DockerContainerNotFoundException`** - - **`DockerImageNotFoundException`** + - **`DockerContainerNotFoundException`** + - **`DockerImageNotFoundException`** - **`TaskCanceledException`** is thrown from `System.Net.Http.HttpClient` library by design. It is not a friendly exception, but it indicates your request has timed out. (default request timeout is 100 seconds.) - - Long-running methods (e.g. `WaitContainerAsync`, `StopContainerAsync`) and methods that return Stream (e.g. `CreateImageAsync`, `GetContainerLogsAsync`) have timeout value overridden with infinite timespan by this library. + - Long-running methods (e.g. `WaitContainerAsync`, `StopContainerAsync`) and methods that return Stream (e.g. `CreateImageAsync`, `GetContainerLogsAsync`) have timeout value overridden with infinite timespan by this library. - **`ArgumentNullException`** is thrown when one of the required parameters are missing/empty. - - Consider reading the [Docker Remote API reference][docker-remote-api] and source code of the corresponding method you are going to use in from this library. This way you can easily find out which parameters are required and their format. + - Consider reading the [Docker Remote API reference][docker-remote-api] and source code of the corresponding method you are going to use in from this library. This way you can easily find out which parameters are required and their format. ## License From eadcfceb520bef42336cf61c40d9e43c6699a604 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sat, 24 Jan 2026 08:33:15 +0100 Subject: [PATCH 21/50] fix: Set sdk and tfm according to the used action --- .github/workflows/ci.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9afb333..7c434aee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,10 +33,13 @@ jobs: strategy: matrix: - framework: - - 8.0 - - 9.0 - - 10.0 + include: + - sdk: 8.x + tfm: net8.0 + - sdk: 9.x + tfm: net9.0 + - sdk: 10.x + tfm: net10.0 steps: - uses: actions/checkout@v4 @@ -47,18 +50,16 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: - dotnet-version: ${{ matrix.framework }} + dotnet-version: ${{ matrix.sdk }} - name: Build - run: dotnet build -c Release --framework net${{ matrix.framework }} + run: dotnet build -c Release --framework ${{ matrix.tfm }} working-directory: test - - name: Pack client cert, key, ca for C# docker client + - name: Pack client cert, key, ca for .NET Docker client run: | mkdir -p ${{ github.workspace }}/certs sudo chmod 777 ${{ github.workspace }}/certs - - # create pfx openssl pkcs12 -export -out ${{ github.workspace }}/certs/client.pfx -inkey ${{ github.workspace }}/certs/client/key.pem -in ${{ github.workspace }}/certs/client/cert.pem -certfile ${{ github.workspace }}/certs/client/ca.pem -passout pass: - name: Wait for Docker (no TLS) to be healthy @@ -91,5 +92,5 @@ jobs: exit 1 - name: Test - run: dotnet test -c Release --framework net${{ matrix.framework }} --no-build --logger console + run: dotnet test -c Release --framework ${{ matrix.tfm }} --no-build --logger console working-directory: test From f21190ef5a75c20e4ea7fbf3a6ecf6cc09a730a4 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 25 Jan 2026 09:43:01 +0100 Subject: [PATCH 22/50] chore: Clean minor parts and remove unnecessary changes --- .github/workflows/ci.yml | 18 ++--- .github/workflows/publish.yml | 6 +- .../Docker.DotNet.LegacyHttp.csproj | 1 + .../LegacyHttpHandlerFactory.cs | 23 +++--- .../Docker.DotNet.NPipe.csproj | 1 + src/Docker.DotNet.NPipe/DockerPipeStream.cs | 21 +++--- .../NpipeHandlerFactory.cs | 71 ++++++++++--------- .../NativeHttpHandlerFactory.cs | 47 ++++++------ .../Docker.DotNet.Unix.csproj | 1 + .../UnixDomainSocketEndPoint.cs | 2 +- src/Docker.DotNet.Unix/UnixHandlerFactory.cs | 36 +++++----- src/Docker.DotNet/DockerClient.cs | 23 +++--- .../DockerClientConfiguration.cs | 9 ++- src/Docker.DotNet/HttpUtility.cs | 4 +- src/Docker.DotNet/IDockerHandlerFactory.cs | 4 +- 15 files changed, 145 insertions(+), 122 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c434aee..6edacff6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 services: # Docker without TLS (plain TCP) !DEPRECATED! with next docker release docker-no-tls: @@ -42,25 +42,26 @@ jobs: tfm: net10.0 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: - path: test fetch-depth: 0 - name: Setup .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ matrix.sdk }} - name: Build run: dotnet build -c Release --framework ${{ matrix.tfm }} - working-directory: test - name: Pack client cert, key, ca for .NET Docker client run: | - mkdir -p ${{ github.workspace }}/certs - sudo chmod 777 ${{ github.workspace }}/certs - openssl pkcs12 -export -out ${{ github.workspace }}/certs/client.pfx -inkey ${{ github.workspace }}/certs/client/key.pem -in ${{ github.workspace }}/certs/client/cert.pem -certfile ${{ github.workspace }}/certs/client/ca.pem -passout pass: + openssl pkcs12 -export \ + -out "${{ github.workspace }}/certs/client.pfx" \ + -inkey "${{ github.workspace }}/certs/client/key.pem" \ + -in "${{ github.workspace }}/certs/client/cert.pem" \ + -certfile "${{ github.workspace }}/certs/client/ca.pem" \ + -passout pass: - name: Wait for Docker (no TLS) to be healthy run: | @@ -93,4 +94,3 @@ jobs: - name: Test run: dotnet test -c Release --framework ${{ matrix.tfm }} --no-build --logger console - working-directory: test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e2464a2e..927743e3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,11 +12,11 @@ jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup .NET Core - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x - name: Install NBGV tool @@ -28,7 +28,7 @@ jobs: - name: Push packages to NuGet.org run: dotnet nuget push ./packages/Docker.DotNet.*.nupkg --skip-duplicate -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json - name: Create Release - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.repos.createRelease({ diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj index bba81ee4..a6b635bb 100644 --- a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj +++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs index 81ef9b7f..76267a1e 100644 --- a/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs +++ b/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs @@ -1,18 +1,11 @@ -namespace Docker.DotNet.LegacyHttp +namespace Docker.DotNet.LegacyHttp; + +public class LegacyHttpHandlerFactory : IDockerHandlerFactory { - public class LegacyHttpHandlerFactory : IDockerHandlerFactory + public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) { - public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) - { - var builder = new UriBuilder(uri) - { - Scheme = configuration.Credentials.IsTlsCredentials() ? "https" : "http" - }; - uri = builder.Uri; - return new Tuple( - new Microsoft.Net.Http.Client.ManagedHandler(logger), - uri - ); - } + var scheme = configuration.Credentials.IsTlsCredentials() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + uri = new UriBuilder(uri) { Scheme = scheme }.Uri; + return new Tuple(new ManagedHandler(logger), uri); } -} +} \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj index af4bf162..11e06695 100644 --- a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Docker.DotNet.NPipe/DockerPipeStream.cs b/src/Docker.DotNet.NPipe/DockerPipeStream.cs index 960de51b..edf1d960 100644 --- a/src/Docker.DotNet.NPipe/DockerPipeStream.cs +++ b/src/Docker.DotNet.NPipe/DockerPipeStream.cs @@ -1,12 +1,17 @@ -namespace Docker.DotNet; +namespace Docker.DotNet.NPipe; -internal class DockerPipeStream : WriteClosableStream, IPeekableStream +internal sealed class DockerPipeStream : WriteClosableStream, IPeekableStream { private readonly EventWaitHandle _event = new EventWaitHandle(false, EventResetMode.AutoReset); private readonly PipeStream _stream; public DockerPipeStream(PipeStream stream) { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new PlatformNotSupportedException("DockerPipeStream is only supported on Windows."); + } + _stream = stream; } @@ -40,25 +45,25 @@ public override long Position public override void CloseWrite() { + const int errorIoPending = 997; + +#pragma warning disable CA1416 // The Docker daemon expects a write of zero bytes to signal the end of writes. Use native // calls to achieve this since CoreCLR ignores a zero-byte write. -#pragma warning disable CA1416 var overlapped = new NativeOverlapped(); -#pragma warning restore CA1416 var handle = _event.GetSafeWaitHandle(); // Set the low bit to tell Windows not to send the result of this IO to the // completion port. -#pragma warning disable CA1416 overlapped.EventHandle = (IntPtr)(handle.DangerousGetHandle().ToInt64() | 1); #pragma warning restore CA1416 + if (WriteFile(_stream.SafePipeHandle, IntPtr.Zero, 0, IntPtr.Zero, ref overlapped) == 0) { - const int ERROR_IO_PENDING = 997; - if (Marshal.GetLastWin32Error() == ERROR_IO_PENDING) + if (Marshal.GetLastWin32Error() == errorIoPending) { - if (GetOverlappedResult(_stream.SafePipeHandle, ref overlapped, out var _, 1) == 0) + if (GetOverlappedResult(_stream.SafePipeHandle, ref overlapped, out _, 1) == 0) { Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); } diff --git a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs b/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs index e18d7884..550962a7 100644 --- a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs @@ -1,37 +1,44 @@ -namespace Docker.DotNet.NPipe +namespace Docker.DotNet.NPipe; + +public class NpipeHandlerFactory : IDockerHandlerFactory { - public class NpipeHandlerFactory : IDockerHandlerFactory + public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) { - public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + if (configuration.Credentials.IsTlsCredentials()) { - if (configuration.Credentials.IsTlsCredentials()) - { - throw new Exception("TLS not supported over npipe"); - } - var segments = uri.Segments; - if (segments.Length != 3 || !segments[1].Equals("pipe/", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException($"{configuration.EndpointBaseUri} is not a valid npipe URI"); - } - var serverName = uri.Host; - if (string.Equals(serverName, "localhost", StringComparison.OrdinalIgnoreCase)) - { - serverName = "."; - } - var pipeName = uri.Segments[2]; - uri = new UriBuilder("http", pipeName).Uri; - - return new Tuple( - new Microsoft.Net.Http.Client.ManagedHandler(async (host, port, cancellationToken) => - { - var timeout = (int)TimeSpan.FromSeconds(10).TotalMilliseconds; - var stream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); - var dockerStream = new DockerPipeStream(stream); - await stream.ConnectAsync(timeout, cancellationToken).ConfigureAwait(false); - return dockerStream; - }, logger), - uri - ); + throw new NotSupportedException("TLS is not supported over npipe."); } + + var segments = uri.Segments; + + if (segments.Length != 3 || !"pipe/".Equals(segments[1], StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException("The endpoint is not a valid npipe URI."); + } + + var pipeName = uri.Segments[2]; + + var serverName = "localhost".Equals(uri.Host, StringComparison.OrdinalIgnoreCase) ? "." : uri.Host; + uri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp, Host = pipeName }.Uri; + + var streamOpener = new ManagedHandler.StreamOpener(async (_, _, cancellationToken) => + { + var clientStream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + + var dockerStream = new DockerPipeStream(clientStream); + +#if NETSTANDARD + var namedPipeConnectTimeout = (int)configuration.NamedPipeConnectTimeout.TotalMilliseconds; +#else + var namedPipeConnectTimeout = configuration.NamedPipeConnectTimeout; +#endif + + await clientStream.ConnectAsync(namedPipeConnectTimeout, cancellationToken) + .ConfigureAwait(false); + + return dockerStream; + }); + + return new Tuple(new ManagedHandler(streamOpener, logger), uri); } -} +} \ No newline at end of file diff --git a/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs b/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs index 424e47e3..ef90d29d 100644 --- a/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs +++ b/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs @@ -1,32 +1,29 @@ -namespace Docker.DotNet.NativeHttp +namespace Docker.DotNet.NativeHttp; + +public class NativeHttpHandlerFactory : IDockerHandlerFactory { - public class NativeHttpHandlerFactory : IDockerHandlerFactory - { - public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) - { + private const int MaxConnectionsPerServer = 10; + + private static readonly TimeSpan PooledConnectionLifetime = TimeSpan.FromMinutes(5); - var builder = new UriBuilder(uri) - { - Scheme = configuration.Credentials.IsTlsCredentials() ? "https" : "http" - }; - uri = builder.Uri; + private static readonly TimeSpan PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2); + + public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + { + var scheme = configuration.Credentials.IsTlsCredentials() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; + uri = new UriBuilder(uri) { Scheme = scheme }.Uri; #if NET6_0_OR_GREATER - return new Tuple( - new SocketsHttpHandler() - { - PooledConnectionLifetime = TimeSpan.FromMinutes(5), - PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), - MaxConnectionsPerServer = 10 - }, - uri - ); + var handler = new SocketsHttpHandler + { + MaxConnectionsPerServer = MaxConnectionsPerServer, + PooledConnectionLifetime = PooledConnectionLifetime, + PooledConnectionIdleTimeout = PooledConnectionIdleTimeout, + }; #else - return new Tuple( - new HttpClientHandler(), - uri - ); + var handler = new HttpClientHandler(); #endif - } + + return new Tuple(handler, uri); } -} +} \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj index 6e20b2c2..9795354f 100644 --- a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs b/src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs index c095a5f4..15bb77d8 100644 --- a/src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs +++ b/src/Docker.DotNet.Unix/UnixDomainSocketEndPoint.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet.Unix; -internal class UnixDomainSocketEndPoint : EndPoint +internal sealed class UnixDomainSocketEndPoint : EndPoint { private const AddressFamily EndPointAddressFamily = AddressFamily.Unix; diff --git a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs b/src/Docker.DotNet.Unix/UnixHandlerFactory.cs index fb8fbeef..12a56438 100644 --- a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs +++ b/src/Docker.DotNet.Unix/UnixHandlerFactory.cs @@ -1,20 +1,24 @@ -namespace Docker.DotNet.Unix +namespace Docker.DotNet.Unix; + +public class UnixHandlerFactory : IDockerHandlerFactory { - public class UnixHandlerFactory : IDockerHandlerFactory + public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) { - public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + var socketPath = uri.LocalPath; + uri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp, Host = uri.Segments.Last() }.Uri; + + var socketOpener = new ManagedHandler.SocketOpener(async (_, _, _) => { - var pipeString = uri.LocalPath; - uri = new UriBuilder("http", uri.Segments.Last()).Uri; - return new Tuple( - new Microsoft.Net.Http.Client.ManagedHandler(async (host, port, cancellationToken) => - { - var sock = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - await sock.ConnectAsync(new UnixDomainSocketEndPoint(pipeString)).ConfigureAwait(false); - return sock; - }, logger), - uri - ); - } + var endpoint = new UnixDomainSocketEndPoint(socketPath); + + var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + + await socket.ConnectAsync(endpoint) + .ConfigureAwait(false); + + return socket; + }); + + return new Tuple(new ManagedHandler(socketOpener, logger), uri); } -} +} \ No newline at end of file diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 27e5bc30..5cdcd96a 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -1,5 +1,7 @@ namespace Docker.DotNet; +using System; + public sealed class DockerClient : IDockerClient { internal readonly IEnumerable NoErrorHandlers = Enumerable.Empty(); @@ -10,12 +12,15 @@ public sealed class DockerClient : IDockerClient private readonly Uri _endpointBaseUri; - private readonly System.Version _requestedApiVersion; - - private readonly IDockerHandlerFactory _handlerFactory; + private readonly Version _requestedApiVersion; - internal DockerClient(DockerClientConfiguration configuration, System.Version requestedApiVersion, IDockerHandlerFactory handlerFactory = null, ILogger logger = null) + internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) { + if (handlerFactory == null) + { + throw new ArgumentNullException(nameof(handlerFactory)); + } + _requestedApiVersion = requestedApiVersion; Configuration = configuration; DefaultTimeout = configuration.DefaultTimeout; @@ -32,13 +37,11 @@ internal DockerClient(DockerClientConfiguration configuration, System.Version re Plugin = new PluginOperations(this); Exec = new ExecOperations(this); - _handlerFactory = handlerFactory ?? throw new InvalidOperationException("No handler factory provided"); - - var handlerAndUri = _handlerFactory.CreateHandler(Configuration.EndpointBaseUri, Configuration, logger); + var (handler, endpoint) = handlerFactory.CreateHandler(Configuration.EndpointBaseUri, Configuration, logger); - _endpointBaseUri = handlerAndUri.Item2; - _client = new HttpClient(Configuration.Credentials.GetHandler(handlerAndUri.Item1), true); + _client = new HttpClient(Configuration.Credentials.GetHandler(handler), true); _client.Timeout = Timeout.InfiniteTimeSpan; + _endpointBaseUri = endpoint; } public DockerClientConfiguration Configuration { get; } @@ -387,7 +390,7 @@ private HttpRequestMessage PrepareRequest(HttpMethod method, string path, IQuery } var request = new HttpRequestMessage(method, HttpUtility.BuildUri(_endpointBaseUri, _requestedApiVersion, path, queryString)); - request.Version = new System.Version(1, 1); + request.Version = new Version(1, 1); request.Headers.Add("User-Agent", UserAgent); var customHeaders = headers == null diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index dd91b4fe..add0aeb6 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -1,12 +1,15 @@ namespace Docker.DotNet; +using System; + public class DockerClientConfiguration : IDisposable { public DockerClientConfiguration( Credentials credentials = null, TimeSpan defaultTimeout = default, + TimeSpan namedPipeConnectTimeout = default, IReadOnlyDictionary defaultHttpRequestHeaders = null) - : this(GetLocalDockerEndpoint(), credentials, defaultTimeout, defaultHttpRequestHeaders) + : this(GetLocalDockerEndpoint(), credentials, defaultTimeout, namedPipeConnectTimeout, defaultHttpRequestHeaders) { } @@ -14,6 +17,7 @@ public DockerClientConfiguration( Uri endpoint, Credentials credentials = null, TimeSpan defaultTimeout = default, + TimeSpan namedPipeConnectTimeout = default, IReadOnlyDictionary defaultHttpRequestHeaders = null) { if (endpoint == null) @@ -29,6 +33,7 @@ public DockerClientConfiguration( EndpointBaseUri = endpoint; Credentials = credentials ?? new AnonymousCredentials(); DefaultTimeout = TimeSpan.Equals(TimeSpan.Zero, defaultTimeout) ? TimeSpan.FromSeconds(100) : defaultTimeout; + NamedPipeConnectTimeout = TimeSpan.Equals(TimeSpan.Zero, namedPipeConnectTimeout) ? TimeSpan.FromMilliseconds(100) : namedPipeConnectTimeout; DefaultHttpRequestHeaders = defaultHttpRequestHeaders ?? new Dictionary(); } @@ -43,6 +48,8 @@ public DockerClientConfiguration( public TimeSpan DefaultTimeout { get; } + public TimeSpan NamedPipeConnectTimeout { get; } + public DockerClient CreateClient(System.Version requestedApiVersion = null, ILogger logger = null) { var scheme = EndpointBaseUri.Scheme; diff --git a/src/Docker.DotNet/HttpUtility.cs b/src/Docker.DotNet/HttpUtility.cs index e8ca8e43..8cdfc1a5 100644 --- a/src/Docker.DotNet/HttpUtility.cs +++ b/src/Docker.DotNet/HttpUtility.cs @@ -1,8 +1,10 @@ namespace Docker.DotNet; +using System; + internal static class HttpUtility { - public static Uri BuildUri(Uri baseUri, System.Version requestedApiVersion, string path, IQueryString queryString) + public static Uri BuildUri(Uri baseUri, Version requestedApiVersion, string path, IQueryString queryString) { if (baseUri == null) { diff --git a/src/Docker.DotNet/IDockerHandlerFactory.cs b/src/Docker.DotNet/IDockerHandlerFactory.cs index a4047deb..8de24e39 100644 --- a/src/Docker.DotNet/IDockerHandlerFactory.cs +++ b/src/Docker.DotNet/IDockerHandlerFactory.cs @@ -1,4 +1,6 @@ +namespace Docker.DotNet; + public interface IDockerHandlerFactory { Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger); -} +} \ No newline at end of file From 053fd53ac465a63c3b9e7516ca2f2deb373f899c Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 25 Jan 2026 09:54:22 +0100 Subject: [PATCH 23/50] fix: Don't mount certs inside $GITHUB_WORKSPACE --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6edacff6..7dba4c1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: options: >- --privileged volumes: - - ${{ github.workspace }}/certs:/certs + - /tmp/certs:/certs strategy: matrix: @@ -57,10 +57,10 @@ jobs: - name: Pack client cert, key, ca for .NET Docker client run: | openssl pkcs12 -export \ - -out "${{ github.workspace }}/certs/client.pfx" \ - -inkey "${{ github.workspace }}/certs/client/key.pem" \ - -in "${{ github.workspace }}/certs/client/cert.pem" \ - -certfile "${{ github.workspace }}/certs/client/ca.pem" \ + -out "/tmp/certs/client.pfx" \ + -inkey "/tmp/certs/client/key.pem" \ + -in "/tmp/certs/client/cert.pem" \ + -certfile "/tmp/certs/client/ca.pem" \ -passout pass: - name: Wait for Docker (no TLS) to be healthy @@ -80,9 +80,9 @@ jobs: run: | for i in {1..10}; do if docker --host=tcp://localhost:2376 --tlsverify \ - --tlscacert=${{ github.workspace }}/certs/client/ca.pem \ - --tlscert=${{ github.workspace }}/certs/client/cert.pem \ - --tlskey=${{ github.workspace }}/certs/client/key.pem version; then + --tlscacert=/tmp/certs/client/ca.pem \ + --tlscert=/tmp/certs/client/cert.pem \ + --tlskey=/tmp/certs/client/key.pem version; then echo "Docker (with TLS) is ready!" exit 0 fi From d2208ca50697a1d0d078cd39f4f9f6ad5ba49225 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 25 Jan 2026 09:59:19 +0100 Subject: [PATCH 24/50] chore: Remove unnecessary namespace --- src/Docker.DotNet/DockerClientConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index add0aeb6..1bf19731 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -50,7 +50,7 @@ public DockerClientConfiguration( public TimeSpan NamedPipeConnectTimeout { get; } - public DockerClient CreateClient(System.Version requestedApiVersion = null, ILogger logger = null) + public DockerClient CreateClient(Version requestedApiVersion = null, ILogger logger = null) { var scheme = EndpointBaseUri.Scheme; if (scheme.Equals("http", StringComparison.OrdinalIgnoreCase) || @@ -87,7 +87,7 @@ public DockerClient CreateClient(System.Version requestedApiVersion = null, ILog return new DockerClient(this, requestedApiVersion, factory, logger); } - public DockerClient CreateClient(System.Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) + public DockerClient CreateClient(Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) { if (handlerFactory == null) { From c90601389fcd43e49e8d144410c04330abc29182 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:15:48 +0100 Subject: [PATCH 25/50] fix: Set user permissions --- .github/workflows/ci.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dba4c1b..46e50ad4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: options: >- --privileged volumes: - - /tmp/certs:/certs + - ${{ env.HOME }}/certs:/certs strategy: matrix: @@ -56,11 +56,12 @@ jobs: - name: Pack client cert, key, ca for .NET Docker client run: | + sudo chown -R $USER:$USER $HOME/certs openssl pkcs12 -export \ - -out "/tmp/certs/client.pfx" \ - -inkey "/tmp/certs/client/key.pem" \ - -in "/tmp/certs/client/cert.pem" \ - -certfile "/tmp/certs/client/ca.pem" \ + -out "$HOME/certs/client.pfx" \ + -inkey "$HOME/certs/client/key.pem" \ + -in "$HOME/certs/client/cert.pem" \ + -certfile "$HOME/certs/client/ca.pem" \ -passout pass: - name: Wait for Docker (no TLS) to be healthy @@ -80,9 +81,9 @@ jobs: run: | for i in {1..10}; do if docker --host=tcp://localhost:2376 --tlsverify \ - --tlscacert=/tmp/certs/client/ca.pem \ - --tlscert=/tmp/certs/client/cert.pem \ - --tlskey=/tmp/certs/client/key.pem version; then + --tlscacert="$HOME/certs/client/ca.pem" \ + --tlscert="$HOME/certs/client/cert.pem" \ + --tlskey="$HOME/certs/client/key.pem" version; then echo "Docker (with TLS) is ready!" exit 0 fi From ff2ddc251c890eb4ff0b2c49449cc2daf64803d8 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:22:20 +0100 Subject: [PATCH 26/50] fix: Set runner home --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46e50ad4..bd68be13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: options: >- --privileged volumes: - - ${{ env.HOME }}/certs:/certs + - /home/runner/certs:/certs strategy: matrix: From 2f3e317f4ebb42e8a640fa1975892488057bade1 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:38:05 +0100 Subject: [PATCH 27/50] fix: Override Npipe URI correct --- src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs | 2 +- src/Docker.DotNet.Unix/UnixHandlerFactory.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs b/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs index 550962a7..98c4e2d4 100644 --- a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs @@ -19,7 +19,7 @@ public Tuple CreateHandler(Uri uri, DockerClientConfigu var pipeName = uri.Segments[2]; var serverName = "localhost".Equals(uri.Host, StringComparison.OrdinalIgnoreCase) ? "." : uri.Host; - uri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp, Host = pipeName }.Uri; + uri = new UriBuilder(Uri.UriSchemeHttp, pipeName).Uri; var streamOpener = new ManagedHandler.StreamOpener(async (_, _, cancellationToken) => { diff --git a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs b/src/Docker.DotNet.Unix/UnixHandlerFactory.cs index 12a56438..ce2627f2 100644 --- a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs +++ b/src/Docker.DotNet.Unix/UnixHandlerFactory.cs @@ -4,8 +4,9 @@ public class UnixHandlerFactory : IDockerHandlerFactory { public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) { + var socketName = uri.Segments.Last(); var socketPath = uri.LocalPath; - uri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp, Host = uri.Segments.Last() }.Uri; + uri = new UriBuilder(Uri.UriSchemeHttp, socketName).Uri; var socketOpener = new ManagedHandler.SocketOpener(async (_, _, _) => { From c6aa1f455686e983cf813d557c3bed04250046ff Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Sun, 25 Jan 2026 11:47:56 +0100 Subject: [PATCH 28/50] fix: Set correct certs path --- test/Docker.DotNet.Tests/TestFixture.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index 644f8033..4aa22768 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -33,14 +33,13 @@ public TestFixture(IMessageSink messageSink) try { - var tempDir = Environment.GetEnvironmentVariable("GITHUB_WORKSPACE"); #if NET9_0_OR_GREATER - var credentials = new CertificateCredentials(X509CertificateLoader.LoadPkcs12FromFile(Path.Combine(tempDir, "certs", "client.pfx"), "")) + var credentials = new CertificateCredentials(X509CertificateLoader.LoadPkcs12FromFile(Path.Combine("/home/runner/certs/client.pfx"), "")) { ServerCertificateValidationCallback = ValidateServerCertificate }; #else - var credentials = new CertificateCredentials(new X509Certificate2(Path.Combine(tempDir, "certs", "client.pfx"), "")) + var credentials = new CertificateCredentials(new X509Certificate2("/home/runner/certs/client.pfx", "")) { ServerCertificateValidationCallback = ValidateServerCertificate }; @@ -93,7 +92,7 @@ internal static bool ValidateServerCertificate( /// /// Gets or sets the Docker image. /// - public Dictionary Images { get; private set; } + public Dictionary Images { get; } /// public async Task InitializeAsync() @@ -241,7 +240,7 @@ public async Task DisposeAsync() if (_isDisposed.TryGetValue(daemon, out var disposed) && disposed) continue; - if (_hasInitializedSwarm.TryGetValue(daemon, out var swarm) && swarm) + if (true || _hasInitializedSwarm.TryGetValue(daemon, out var swarm) && swarm) { await DockerClients[GetClientForDaemon(daemon)].Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true }, Cts.Token) .ConfigureAwait(false); From 3e7b93b0d4c8abe7b0ece82af91cde3778656a24 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Mon, 26 Jan 2026 20:06:55 +0100 Subject: [PATCH 29/50] remove reflection and dynamic loading --- Docker.DotNet.sln | 15 +++++++ StrongNamePublicKeys.cs | 8 ++-- .../Docker.DotNet.BasicAuth.csproj | 3 +- .../Docker.DotNet.LegacyHttp.csproj | 4 +- ...ttpHandlerFactory.cs => HandlerFactory.cs} | 4 +- .../HijackStreamHelper.cs | 14 ++++++ .../Docker.DotNet.NPipe.csproj | 3 +- ...ipeHandlerFactory.cs => HandlerFactory.cs} | 6 +-- src/Docker.DotNet.NPipe/HijackStreamHelper.cs | 14 ++++++ .../Docker.DotNet.NativeHttp.csproj | 5 ++- ...ttpHandlerFactory.cs => HandlerFactory.cs} | 4 +- .../HijackStreamHelper.cs | 12 ++++++ .../WriteClosableStreamWrapper.cs | 2 +- .../Docker.DotNet.Unix.csproj | 3 +- ...nixHandlerFactory.cs => HandlerFactory.cs} | 4 +- src/Docker.DotNet.Unix/HijackStreamHelper.cs | 14 ++++++ .../Docker.DotNet.X509.csproj | 3 +- src/Docker.DotNet/Docker.DotNet.csproj | 8 ++++ src/Docker.DotNet/DockerClient.cs | 28 ++++-------- .../DockerClientConfiguration.cs | 43 +++++-------------- src/Docker.DotNet/HijackStreamHelper.cs | 7 --- src/Docker.DotNet/IDockerHandlerFactory.cs | 6 --- .../Credentials.cs | 2 +- .../Docker.Dotnet.HandlerFactory.csproj | 34 +++++++++++++++ .../IDockerClientConfiguration.cs | 17 ++++++++ .../IDockerHandlerFactory.cs | 6 +++ .../IPeekableStream.cs | 2 +- .../WriteClosableStream.cs | 2 +- .../Microsoft.Net.Http.Client.projitems | 1 + 29 files changed, 186 insertions(+), 88 deletions(-) rename src/Docker.DotNet.LegacyHttp/{LegacyHttpHandlerFactory.cs => HandlerFactory.cs} (75%) create mode 100644 src/Docker.DotNet.LegacyHttp/HijackStreamHelper.cs rename src/Docker.DotNet.NPipe/{NpipeHandlerFactory.cs => HandlerFactory.cs} (88%) create mode 100644 src/Docker.DotNet.NPipe/HijackStreamHelper.cs rename src/Docker.DotNet.NativeHttp/{NativeHttpHandlerFactory.cs => HandlerFactory.cs} (88%) create mode 100644 src/Docker.DotNet.NativeHttp/HijackStreamHelper.cs rename src/{Docker.DotNet => Docker.DotNet.NativeHttp}/WriteClosableStreamWrapper.cs (97%) rename src/Docker.DotNet.Unix/{UnixHandlerFactory.cs => HandlerFactory.cs} (86%) create mode 100644 src/Docker.DotNet.Unix/HijackStreamHelper.cs delete mode 100644 src/Docker.DotNet/HijackStreamHelper.cs delete mode 100644 src/Docker.DotNet/IDockerHandlerFactory.cs rename src/{Docker.DotNet => Docker.Dotnet.HandlerFactory}/Credentials.cs (84%) create mode 100644 src/Docker.Dotnet.HandlerFactory/Docker.Dotnet.HandlerFactory.csproj create mode 100644 src/Docker.Dotnet.HandlerFactory/IDockerClientConfiguration.cs create mode 100644 src/Docker.Dotnet.HandlerFactory/IDockerHandlerFactory.cs rename src/{Docker.DotNet => Docker.Dotnet.HandlerFactory}/IPeekableStream.cs (95%) rename src/{Docker.DotNet => Docker.Dotnet.HandlerFactory}/WriteClosableStream.cs (78%) diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln index dd3b8685..730800c4 100644 --- a/Docker.DotNet.sln +++ b/Docker.DotNet.sln @@ -24,6 +24,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.Net.Http.Client", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.HandlerFactory", "src\Docker.DotNet.HandlerFactory\Docker.DotNet.HandlerFactory.csproj", "{22C42314-615F-4B11-B111-58F1D6D54F4D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -130,6 +132,18 @@ Global {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.Build.0 = Release|Any CPU {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.ActiveCfg = Release|Any CPU {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.Build.0 = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.ActiveCfg = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.Build.0 = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x86.ActiveCfg = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x86.Build.0 = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|Any CPU.Build.0 = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.ActiveCfg = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.Build.0 = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.ActiveCfg = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -144,6 +158,7 @@ Global {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {DAE2DE68-9B3E-4D5D-8802-EC97B94160ED} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} + {22C42314-615F-4B11-B111-58F1D6D54F4D} = {85990620-78A6-4381-8BD6-84E6D0CF0649} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8F2F229F-C66D-43E4-B804-E5F37DC157CB} diff --git a/StrongNamePublicKeys.cs b/StrongNamePublicKeys.cs index 6841e783..58566321 100644 --- a/StrongNamePublicKeys.cs +++ b/StrongNamePublicKeys.cs @@ -3,8 +3,8 @@ /// internal static class StrongNamePublicKeys { - /// - /// The public key used for assemblies in this repo (Key.snk). - /// - public const string DockerDotNetPublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010013a6d952388480a1ce272e8c8ac11d710668c8723e696a421190445a1e6198288112f5e04eb99a626f8bb1454cdf30ebfb0a09cb7fc7b299cb03aa6fea1ae9a58f05f9fb92a85ce82ad4490bb2f0074822d8b0a786684f26a6eb1765f9026dae4857925b4e077d04b6311bec7dacf8e8a031dcc9f7e0384bca914256abee25d9"; + /// + /// The public key used for assemblies in this repo (Key.snk). + /// + public const string DockerDotNetPublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010013a6d952388480a1ce272e8c8ac11d710668c8723e696a421190445a1e6198288112f5e04eb99a626f8bb1454cdf30ebfb0a09cb7fc7b299cb03aa6fea1ae9a58f05f9fb92a85ce82ad4490bb2f0074822d8b0a786684f26a6eb1765f9026dae4857925b4e077d04b6311bec7dacf8e8a031dcc9f7e0384bca914256abee25d9"; } \ No newline at end of file diff --git a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj index 6148e4b6..3b2f0dd3 100644 --- a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj +++ b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj @@ -5,7 +5,7 @@ A Docker.DotNet extension that adds Basic Authentication for remote Docker Engine connections. - + @@ -16,5 +16,6 @@ + diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj index a6b635bb..4b9a6f28 100644 --- a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj +++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj @@ -5,7 +5,7 @@ A Docker.DotNet transport implementation for HTTP(S) Docker Engine connections. - + @@ -17,7 +17,9 @@ + + diff --git a/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/HandlerFactory.cs similarity index 75% rename from src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs rename to src/Docker.DotNet.LegacyHttp/HandlerFactory.cs index 76267a1e..d3c95e24 100644 --- a/src/Docker.DotNet.LegacyHttp/LegacyHttpHandlerFactory.cs +++ b/src/Docker.DotNet.LegacyHttp/HandlerFactory.cs @@ -1,8 +1,8 @@ namespace Docker.DotNet.LegacyHttp; -public class LegacyHttpHandlerFactory : IDockerHandlerFactory +public class HandlerFactory : IDockerHandlerFactory { - public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { var scheme = configuration.Credentials.IsTlsCredentials() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; uri = new UriBuilder(uri) { Scheme = scheme }.Uri; diff --git a/src/Docker.DotNet.LegacyHttp/HijackStreamHelper.cs b/src/Docker.DotNet.LegacyHttp/HijackStreamHelper.cs new file mode 100644 index 00000000..f2081d26 --- /dev/null +++ b/src/Docker.DotNet.LegacyHttp/HijackStreamHelper.cs @@ -0,0 +1,14 @@ +namespace Docker.DotNet.LegacyHttp; + +public class HijackStreamHelper +{ + static public WriteClosableStream HijackStream(HttpContent content) + { + if (content is not HttpConnectionResponseContent contentHijackAble) + { + throw new NotSupportedException("message handler does not support hijacked streams"); + } + + return contentHijackAble.HijackStream(); + } +} diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj index 11e06695..78541112 100644 --- a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -5,7 +5,7 @@ A Docker.DotNet transport implementation for Windows named pipe (npipe) Docker Engine connections. - + @@ -20,6 +20,7 @@ + diff --git a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs b/src/Docker.DotNet.NPipe/HandlerFactory.cs similarity index 88% rename from src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs rename to src/Docker.DotNet.NPipe/HandlerFactory.cs index 98c4e2d4..88b10de9 100644 --- a/src/Docker.DotNet.NPipe/NpipeHandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/HandlerFactory.cs @@ -1,8 +1,8 @@ namespace Docker.DotNet.NPipe; -public class NpipeHandlerFactory : IDockerHandlerFactory +public class HandlerFactory : IDockerHandlerFactory { - public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { if (configuration.Credentials.IsTlsCredentials()) { @@ -23,7 +23,7 @@ public Tuple CreateHandler(Uri uri, DockerClientConfigu var streamOpener = new ManagedHandler.StreamOpener(async (_, _, cancellationToken) => { - var clientStream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + var clientStream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous); var dockerStream = new DockerPipeStream(clientStream); diff --git a/src/Docker.DotNet.NPipe/HijackStreamHelper.cs b/src/Docker.DotNet.NPipe/HijackStreamHelper.cs new file mode 100644 index 00000000..016b5ba6 --- /dev/null +++ b/src/Docker.DotNet.NPipe/HijackStreamHelper.cs @@ -0,0 +1,14 @@ +namespace Docker.DotNet.NPipe; + +public class HijackStreamHelper +{ + static public WriteClosableStream HijackStream(HttpContent content) + { + if (content is not HttpConnectionResponseContent contentHijackAble) + { + throw new NotSupportedException("message handler does not support hijacked streams"); + } + + return contentHijackAble.HijackStream(); + } +} diff --git a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj index 85a183c1..afad2399 100644 --- a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj +++ b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj @@ -5,10 +5,12 @@ A Docker.DotNet transport implementation for HTTP(S) Docker Engine connections. - + + + @@ -17,5 +19,6 @@ + diff --git a/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs b/src/Docker.DotNet.NativeHttp/HandlerFactory.cs similarity index 88% rename from src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs rename to src/Docker.DotNet.NativeHttp/HandlerFactory.cs index ef90d29d..84b853ed 100644 --- a/src/Docker.DotNet.NativeHttp/NativeHttpHandlerFactory.cs +++ b/src/Docker.DotNet.NativeHttp/HandlerFactory.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet.NativeHttp; -public class NativeHttpHandlerFactory : IDockerHandlerFactory +public class HandlerFactory : IDockerHandlerFactory { private const int MaxConnectionsPerServer = 10; @@ -8,7 +8,7 @@ public class NativeHttpHandlerFactory : IDockerHandlerFactory private static readonly TimeSpan PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2); - public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { var scheme = configuration.Credentials.IsTlsCredentials() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; uri = new UriBuilder(uri) { Scheme = scheme }.Uri; diff --git a/src/Docker.DotNet.NativeHttp/HijackStreamHelper.cs b/src/Docker.DotNet.NativeHttp/HijackStreamHelper.cs new file mode 100644 index 00000000..a7f11598 --- /dev/null +++ b/src/Docker.DotNet.NativeHttp/HijackStreamHelper.cs @@ -0,0 +1,12 @@ +namespace Docker.DotNet.NativeHttp; + +public class HijackStreamHelper +{ + static public WriteClosableStream HijackStream(HttpContent content) + { + return new WriteClosableStreamWrapper(content.ReadAsStreamAsync() + .ConfigureAwait(false) + .GetAwaiter() + .GetResult()); + } +} diff --git a/src/Docker.DotNet/WriteClosableStreamWrapper.cs b/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs similarity index 97% rename from src/Docker.DotNet/WriteClosableStreamWrapper.cs rename to src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs index 43f8a2ff..20fcd912 100644 --- a/src/Docker.DotNet/WriteClosableStreamWrapper.cs +++ b/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet; +namespace Docker.DotNet.NativeHttp; public class WriteClosableStreamWrapper : WriteClosableStream { diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj index 9795354f..17e6a95d 100644 --- a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -5,7 +5,7 @@ A Docker.DotNet transport implementation for Unix domain socket Docker Engine connections. - + @@ -22,6 +22,7 @@ + diff --git a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs b/src/Docker.DotNet.Unix/HandlerFactory.cs similarity index 86% rename from src/Docker.DotNet.Unix/UnixHandlerFactory.cs rename to src/Docker.DotNet.Unix/HandlerFactory.cs index ce2627f2..9f0a7a2d 100644 --- a/src/Docker.DotNet.Unix/UnixHandlerFactory.cs +++ b/src/Docker.DotNet.Unix/HandlerFactory.cs @@ -1,8 +1,8 @@ namespace Docker.DotNet.Unix; -public class UnixHandlerFactory : IDockerHandlerFactory +public class HandlerFactory : IDockerHandlerFactory { - public Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger) + public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { var socketName = uri.Segments.Last(); var socketPath = uri.LocalPath; diff --git a/src/Docker.DotNet.Unix/HijackStreamHelper.cs b/src/Docker.DotNet.Unix/HijackStreamHelper.cs new file mode 100644 index 00000000..9d818b67 --- /dev/null +++ b/src/Docker.DotNet.Unix/HijackStreamHelper.cs @@ -0,0 +1,14 @@ +namespace Docker.DotNet.Unix; + +public class HijackStreamHelper +{ + static public WriteClosableStream HijackStream(HttpContent content) + { + if (content is not HttpConnectionResponseContent contentHijackAble) + { + throw new NotSupportedException("message handler does not support hijacked streams"); + } + + return contentHijackAble.HijackStream(); + } +} diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index e48b9a4e..16ecc8a0 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -8,7 +8,7 @@ - + @@ -22,5 +22,6 @@ + diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index f051eaed..a52ea77b 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -46,7 +46,15 @@ + + + + + + + + diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 5cdcd96a..24497018 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -328,26 +328,16 @@ internal async Task MakeRequestForHijackedStreamAsync( await HandleIfErrorResponseAsync(response.StatusCode, response, errorHandlers) .ConfigureAwait(false); - // Use reflection to support different handler without direct reference - var hijackMethod = HijackStreamHelper.GetHijackMethodFromType(response.Content.GetType()); - - if (hijackMethod == null) - { - // Native http handler - var stream = await response.Content.ReadAsStreamAsync() - .ConfigureAwait(false); - return new WriteClosableStreamWrapper(stream); - } - else + return _endpointBaseUri.Scheme.ToLower() switch { - var hijackedStream = hijackMethod.Invoke(response.Content, null) as WriteClosableStream; - if (hijackedStream == null) - { - throw new NotSupportedException("HijackStream did not return a WriteClosableStream"); - } - - return hijackedStream; - } + "npipe" => NPipe.HijackStreamHelper.HijackStream(response.Content), + "unix" => Unix.HijackStreamHelper.HijackStream(response.Content), + "http" or "https" => + Environment.GetEnvironmentVariable("DOCKER_DOTNET_USE_NATIVE_HTTP") == "1" ? + NativeHttp.HijackStreamHelper.HijackStream(response.Content) : + LegacyHttp.HijackStreamHelper.HijackStream(response.Content), + _ => throw new NotSupportedException($"The URI scheme '{_endpointBaseUri.Scheme}' is not supports stream hijacking."), + }; } private async Task PrivateMakeRequestAsync( diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index 1bf19731..3259014f 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -2,7 +2,7 @@ namespace Docker.DotNet; using System; -public class DockerClientConfiguration : IDisposable +public class DockerClientConfiguration : IDockerClientConfiguration, IDisposable { public DockerClientConfiguration( Credentials credentials = null, @@ -52,39 +52,16 @@ public DockerClientConfiguration( public DockerClient CreateClient(Version requestedApiVersion = null, ILogger logger = null) { - var scheme = EndpointBaseUri.Scheme; - if (scheme.Equals("http", StringComparison.OrdinalIgnoreCase) || - scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + return EndpointBaseUri.Scheme.ToLower() switch { - scheme = "Http"; - } - - // Try to find a handler factory assembly in base directory that matches the scheme and Docker.DotNet - var filenameOfFactoryAssembly = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll") - .FirstOrDefault(a => - a.ToLower().Contains("Docker.DotNet".ToLower()) - && a.ToLower().Contains(scheme.ToLower())); - - if (filenameOfFactoryAssembly == null) - { - throw new InvalidOperationException($"No Docker handler factory assembly found for scheme '{scheme}'. Please reference at least one handler package (e.g., NPipe, Unix, NativeHttp, LegacyHttp)."); - } - - var factoryAssembly = Assembly.LoadFile(filenameOfFactoryAssembly); - - var factoryType = factoryAssembly.GetTypes().FirstOrDefault(t => - typeof(IDockerHandlerFactory).IsAssignableFrom(t) && - !t.IsInterface && !t.IsAbstract - ); - - if (factoryType == null) - { - throw new InvalidOperationException($"No Docker handler factory implementation found for scheme '{scheme}' in assembly '{factoryAssembly.FullName}'."); - } - - var factory = (IDockerHandlerFactory)Activator.CreateInstance(factoryType); - - return new DockerClient(this, requestedApiVersion, factory, logger); + "npipe" => CreateClient(requestedApiVersion, new NPipe.HandlerFactory(), logger), + "unix" => CreateClient(requestedApiVersion, new Unix.HandlerFactory(), logger), + "http" or "https" => + Environment.GetEnvironmentVariable("DOCKER_DOTNET_USE_NATIVE_HTTP") == "1" ? + CreateClient(requestedApiVersion, new NativeHttp.HandlerFactory(), logger) : + CreateClient(requestedApiVersion, new LegacyHttp.HandlerFactory(), logger), + _ => throw new NotSupportedException($"The URI scheme '{EndpointBaseUri.Scheme}' is not supported."), + }; } public DockerClient CreateClient(Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) diff --git a/src/Docker.DotNet/HijackStreamHelper.cs b/src/Docker.DotNet/HijackStreamHelper.cs deleted file mode 100644 index 25f4b2d8..00000000 --- a/src/Docker.DotNet/HijackStreamHelper.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Docker.DotNet; - -public class HijackStreamHelper -{ - public static MethodInfo GetHijackMethodFromType(Type type) => - type.GetMethod("HijackStream", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); -} \ No newline at end of file diff --git a/src/Docker.DotNet/IDockerHandlerFactory.cs b/src/Docker.DotNet/IDockerHandlerFactory.cs deleted file mode 100644 index 8de24e39..00000000 --- a/src/Docker.DotNet/IDockerHandlerFactory.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Docker.DotNet; - -public interface IDockerHandlerFactory -{ - Tuple CreateHandler(Uri uri, DockerClientConfiguration configuration, ILogger logger); -} \ No newline at end of file diff --git a/src/Docker.DotNet/Credentials.cs b/src/Docker.Dotnet.HandlerFactory/Credentials.cs similarity index 84% rename from src/Docker.DotNet/Credentials.cs rename to src/Docker.Dotnet.HandlerFactory/Credentials.cs index a20428a6..03f45663 100644 --- a/src/Docker.DotNet/Credentials.cs +++ b/src/Docker.Dotnet.HandlerFactory/Credentials.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet; +namespace Docker.DotNet.HandlerFactory; public abstract class Credentials : IDisposable { diff --git a/src/Docker.Dotnet.HandlerFactory/Docker.Dotnet.HandlerFactory.csproj b/src/Docker.Dotnet.HandlerFactory/Docker.Dotnet.HandlerFactory.csproj new file mode 100644 index 00000000..75b68af9 --- /dev/null +++ b/src/Docker.Dotnet.HandlerFactory/Docker.Dotnet.HandlerFactory.csproj @@ -0,0 +1,34 @@ + + + Docker.DotNet.HandlerFactory + Docker.DotNet.Enhanced.HandlerFactory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Docker.Dotnet.HandlerFactory/IDockerClientConfiguration.cs b/src/Docker.Dotnet.HandlerFactory/IDockerClientConfiguration.cs new file mode 100644 index 00000000..ab34b09c --- /dev/null +++ b/src/Docker.Dotnet.HandlerFactory/IDockerClientConfiguration.cs @@ -0,0 +1,17 @@ +namespace Docker.DotNet.HandlerFactory; + +public interface IDockerClientConfiguration +{ + /// + /// Gets the collection of default HTTP request headers. + /// + public IReadOnlyDictionary DefaultHttpRequestHeaders { get; } + + public Uri EndpointBaseUri { get; } + + public Credentials Credentials { get; } + + public TimeSpan DefaultTimeout { get; } + + public TimeSpan NamedPipeConnectTimeout { get; } +} \ No newline at end of file diff --git a/src/Docker.Dotnet.HandlerFactory/IDockerHandlerFactory.cs b/src/Docker.Dotnet.HandlerFactory/IDockerHandlerFactory.cs new file mode 100644 index 00000000..cd38f99a --- /dev/null +++ b/src/Docker.Dotnet.HandlerFactory/IDockerHandlerFactory.cs @@ -0,0 +1,6 @@ +namespace Docker.DotNet.HandlerFactory; + +public interface IDockerHandlerFactory +{ + Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger); +} diff --git a/src/Docker.DotNet/IPeekableStream.cs b/src/Docker.Dotnet.HandlerFactory/IPeekableStream.cs similarity index 95% rename from src/Docker.DotNet/IPeekableStream.cs rename to src/Docker.Dotnet.HandlerFactory/IPeekableStream.cs index 89a7c099..9de0d8f3 100644 --- a/src/Docker.DotNet/IPeekableStream.cs +++ b/src/Docker.Dotnet.HandlerFactory/IPeekableStream.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet; +namespace Docker.DotNet.HandlerFactory; public interface IPeekableStream { diff --git a/src/Docker.DotNet/WriteClosableStream.cs b/src/Docker.Dotnet.HandlerFactory/WriteClosableStream.cs similarity index 78% rename from src/Docker.DotNet/WriteClosableStream.cs rename to src/Docker.Dotnet.HandlerFactory/WriteClosableStream.cs index 52135e61..16f4f5b9 100644 --- a/src/Docker.DotNet/WriteClosableStream.cs +++ b/src/Docker.Dotnet.HandlerFactory/WriteClosableStream.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet; +namespace Docker.DotNet.HandlerFactory; public abstract class WriteClosableStream : Stream { diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems index 5c2c9157..c94268c7 100644 --- a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems +++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems @@ -23,6 +23,7 @@ + From 5ec38a5822a06274cdb908bcf5c2307e04527c0f Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:41:36 +0100 Subject: [PATCH 30/50] chore: Rename HandlerFactory to Handler.Abstractions --- Docker.DotNet.sln | 4 +-- .../Docker.DotNet.BasicAuth.csproj | 2 +- .../Credentials.cs | 0 .../Docker.DotNet.Handler.Abstractions.csproj | 17 ++++++++++ .../IDockerClientConfiguration.cs | 0 .../IDockerHandlerFactory.cs | 2 +- .../IPeekableStream.cs | 0 .../WriteClosableStream.cs | 0 .../Docker.DotNet.LegacyHttp.csproj | 5 ++- .../Docker.DotNet.NPipe.csproj | 4 +-- .../Docker.DotNet.NativeHttp.csproj | 4 +-- .../Docker.DotNet.Unix.csproj | 4 +-- .../Docker.DotNet.X509.csproj | 2 +- src/Docker.DotNet/Docker.DotNet.csproj | 16 ++++----- .../Docker.Dotnet.HandlerFactory.csproj | 34 ------------------- 15 files changed, 38 insertions(+), 56 deletions(-) rename src/{Docker.Dotnet.HandlerFactory => Docker.DotNet.Handler.Abstractions}/Credentials.cs (100%) create mode 100644 src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj rename src/{Docker.Dotnet.HandlerFactory => Docker.DotNet.Handler.Abstractions}/IDockerClientConfiguration.cs (100%) rename src/{Docker.Dotnet.HandlerFactory => Docker.DotNet.Handler.Abstractions}/IDockerHandlerFactory.cs (99%) rename src/{Docker.Dotnet.HandlerFactory => Docker.DotNet.Handler.Abstractions}/IPeekableStream.cs (100%) rename src/{Docker.Dotnet.HandlerFactory => Docker.DotNet.Handler.Abstractions}/WriteClosableStream.cs (100%) delete mode 100644 src/Docker.Dotnet.HandlerFactory/Docker.Dotnet.HandlerFactory.csproj diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln index 730800c4..bde945cf 100644 --- a/Docker.DotNet.sln +++ b/Docker.DotNet.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.35201.131 MinimumVisualStudioVersion = 10.0.40219.1 @@ -24,7 +24,7 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.Net.Http.Client", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.HandlerFactory", "src\Docker.DotNet.HandlerFactory\Docker.DotNet.HandlerFactory.csproj", "{22C42314-615F-4B11-B111-58F1D6D54F4D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Handler.Abstractions", "src\Docker.DotNet.Handler.Abstractions\Docker.DotNet.Handler.Abstractions.csproj", "{22C42314-615F-4B11-B111-58F1D6D54F4D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj index 3b2f0dd3..a65ca5c2 100644 --- a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj +++ b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj @@ -5,7 +5,7 @@ A Docker.DotNet extension that adds Basic Authentication for remote Docker Engine connections. - + diff --git a/src/Docker.Dotnet.HandlerFactory/Credentials.cs b/src/Docker.DotNet.Handler.Abstractions/Credentials.cs similarity index 100% rename from src/Docker.Dotnet.HandlerFactory/Credentials.cs rename to src/Docker.DotNet.Handler.Abstractions/Credentials.cs diff --git a/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj new file mode 100644 index 00000000..015e90c2 --- /dev/null +++ b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj @@ -0,0 +1,17 @@ + + + Docker.DotNet.Handler.Abstractions + Docker.DotNet.Enhanced.Handler.Abstractions + TODO + + + + + + + + + + + + diff --git a/src/Docker.Dotnet.HandlerFactory/IDockerClientConfiguration.cs b/src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs similarity index 100% rename from src/Docker.Dotnet.HandlerFactory/IDockerClientConfiguration.cs rename to src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs diff --git a/src/Docker.Dotnet.HandlerFactory/IDockerHandlerFactory.cs b/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs similarity index 99% rename from src/Docker.Dotnet.HandlerFactory/IDockerHandlerFactory.cs rename to src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs index cd38f99a..38d8053a 100644 --- a/src/Docker.Dotnet.HandlerFactory/IDockerHandlerFactory.cs +++ b/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs @@ -3,4 +3,4 @@ namespace Docker.DotNet.HandlerFactory; public interface IDockerHandlerFactory { Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger); -} +} \ No newline at end of file diff --git a/src/Docker.Dotnet.HandlerFactory/IPeekableStream.cs b/src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs similarity index 100% rename from src/Docker.Dotnet.HandlerFactory/IPeekableStream.cs rename to src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs diff --git a/src/Docker.Dotnet.HandlerFactory/WriteClosableStream.cs b/src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs similarity index 100% rename from src/Docker.Dotnet.HandlerFactory/WriteClosableStream.cs rename to src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj index 4b9a6f28..358fc43e 100644 --- a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj +++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj @@ -5,7 +5,7 @@ A Docker.DotNet transport implementation for HTTP(S) Docker Engine connections. - + @@ -16,10 +16,9 @@ + - - diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj index 78541112..7d9a1f37 100644 --- a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -5,7 +5,7 @@ A Docker.DotNet transport implementation for Windows named pipe (npipe) Docker Engine connections. - + @@ -18,9 +18,9 @@ + - diff --git a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj index afad2399..8886b32d 100644 --- a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj +++ b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj @@ -5,7 +5,7 @@ A Docker.DotNet transport implementation for HTTP(S) Docker Engine connections. - + @@ -18,7 +18,7 @@ - + diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj index 17e6a95d..d75fa18c 100644 --- a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -5,7 +5,7 @@ A Docker.DotNet transport implementation for Unix domain socket Docker Engine connections. - + @@ -20,9 +20,9 @@ + - diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index 16ecc8a0..ced19db1 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index a52ea77b..830d3e5d 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -15,6 +15,13 @@ + + + + + + + @@ -45,16 +52,9 @@ - + - - - - - - - diff --git a/src/Docker.Dotnet.HandlerFactory/Docker.Dotnet.HandlerFactory.csproj b/src/Docker.Dotnet.HandlerFactory/Docker.Dotnet.HandlerFactory.csproj deleted file mode 100644 index 75b68af9..00000000 --- a/src/Docker.Dotnet.HandlerFactory/Docker.Dotnet.HandlerFactory.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - Docker.DotNet.HandlerFactory - Docker.DotNet.Enhanced.HandlerFactory - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 740e890fd798220a0bf6b0ec5188ae7fa40daf4f Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:41:52 +0100 Subject: [PATCH 31/50] feat: Add CPM --- Directory.Packages.props | 18 ++++++++++++++++++ src/Directory.Build.props | 7 ------- .../Docker.DotNet.Handler.Abstractions.csproj | 2 +- .../Docker.DotNet.X509.csproj | 2 +- src/Docker.DotNet/Docker.DotNet.csproj | 10 +++++----- .../Docker.DotNet.Tests.csproj | 6 +++--- 6 files changed, 28 insertions(+), 17 deletions(-) create mode 100644 Directory.Packages.props diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..c8be7ea0 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,18 @@ + + + true + + + + + + + + + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 76b2c926..00b70293 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -31,11 +31,4 @@ - - - all - runtime; build; native; contentfiles; analyzers - - - diff --git a/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj index 015e90c2..63590bd4 100644 --- a/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj +++ b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj @@ -5,7 +5,7 @@ TODO - + diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index ced19db1..4519293c 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -5,7 +5,7 @@ A Docker.DotNet extension that adds X.509 client certificate authentication for remote Docker Engine connections. - + diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index 830d3e5d..91613fc3 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -5,15 +5,15 @@ A .NET client for the Docker Engine API with fully asynchronous, non-blocking, object-oriented APIs. - + - + - - - + + + diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index ae2866b4..3f6a12fa 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -5,9 +5,9 @@ false - - - + + + From a3664288ca3a8982b6e85ad61fa0382142aa5e13 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:41:57 +0100 Subject: [PATCH 32/50] chore: Align namespace --- src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj | 2 +- src/Docker.DotNet.Handler.Abstractions/Credentials.cs | 2 +- .../IDockerClientConfiguration.cs | 2 +- src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs | 2 +- src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs | 2 +- src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs | 2 +- src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj | 2 +- src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj | 2 +- src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj | 2 +- src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj | 2 +- src/Docker.DotNet.X509/Docker.DotNet.X509.csproj | 2 +- src/Docker.DotNet/Docker.DotNet.csproj | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj index a65ca5c2..4bfe41cf 100644 --- a/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj +++ b/src/Docker.DotNet.BasicAuth/Docker.DotNet.BasicAuth.csproj @@ -16,6 +16,6 @@ - + diff --git a/src/Docker.DotNet.Handler.Abstractions/Credentials.cs b/src/Docker.DotNet.Handler.Abstractions/Credentials.cs index 03f45663..5d80ee26 100644 --- a/src/Docker.DotNet.Handler.Abstractions/Credentials.cs +++ b/src/Docker.DotNet.Handler.Abstractions/Credentials.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet.HandlerFactory; +namespace Docker.DotNet.Handler.Abstractions; public abstract class Credentials : IDisposable { diff --git a/src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs b/src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs index ab34b09c..d984100f 100644 --- a/src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs +++ b/src/Docker.DotNet.Handler.Abstractions/IDockerClientConfiguration.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet.HandlerFactory; +namespace Docker.DotNet.Handler.Abstractions; public interface IDockerClientConfiguration { diff --git a/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs b/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs index 38d8053a..537b3d76 100644 --- a/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs +++ b/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet.HandlerFactory; +namespace Docker.DotNet.Handler.Abstractions; public interface IDockerHandlerFactory { diff --git a/src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs b/src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs index 9de0d8f3..09d62e1d 100644 --- a/src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs +++ b/src/Docker.DotNet.Handler.Abstractions/IPeekableStream.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet.HandlerFactory; +namespace Docker.DotNet.Handler.Abstractions; public interface IPeekableStream { diff --git a/src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs b/src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs index 16f4f5b9..84aa4209 100644 --- a/src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs +++ b/src/Docker.DotNet.Handler.Abstractions/WriteClosableStream.cs @@ -1,4 +1,4 @@ -namespace Docker.DotNet.HandlerFactory; +namespace Docker.DotNet.Handler.Abstractions; public abstract class WriteClosableStream : Stream { diff --git a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj index 358fc43e..4fd99d00 100644 --- a/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj +++ b/src/Docker.DotNet.LegacyHttp/Docker.DotNet.LegacyHttp.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj index 7d9a1f37..f1cf22f2 100644 --- a/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj +++ b/src/Docker.DotNet.NPipe/Docker.DotNet.NPipe.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj index 8886b32d..84632d93 100644 --- a/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj +++ b/src/Docker.DotNet.NativeHttp/Docker.DotNet.NativeHttp.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj index d75fa18c..328e5446 100644 --- a/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj +++ b/src/Docker.DotNet.Unix/Docker.DotNet.Unix.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index 4519293c..8a3233f3 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -22,6 +22,6 @@ - + diff --git a/src/Docker.DotNet/Docker.DotNet.csproj b/src/Docker.DotNet/Docker.DotNet.csproj index 91613fc3..38f2905b 100644 --- a/src/Docker.DotNet/Docker.DotNet.csproj +++ b/src/Docker.DotNet/Docker.DotNet.csproj @@ -52,7 +52,7 @@ - + From 9cf86e2b306641ec350baa4d3f4ea62dc799695c Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:42:00 +0100 Subject: [PATCH 33/50] chore: Remove unnecessary changes --- .../{HandlerFactory.cs => DockerHandlerFactory.cs} | 2 +- .../{HandlerFactory.cs => DockerHandlerFactory.cs} | 4 ++-- .../{HandlerFactory.cs => DockerHandlerFactory.cs} | 2 +- .../{HandlerFactory.cs => DockerHandlerFactory.cs} | 2 +- src/Docker.DotNet/DockerClientConfiguration.cs | 8 ++++---- .../Microsoft.Net.Http.Client.projitems | 1 - 6 files changed, 9 insertions(+), 10 deletions(-) rename src/Docker.DotNet.LegacyHttp/{HandlerFactory.cs => DockerHandlerFactory.cs} (88%) rename src/Docker.DotNet.NPipe/{HandlerFactory.cs => DockerHandlerFactory.cs} (92%) rename src/Docker.DotNet.NativeHttp/{HandlerFactory.cs => DockerHandlerFactory.cs} (94%) rename src/Docker.DotNet.Unix/{HandlerFactory.cs => DockerHandlerFactory.cs} (93%) diff --git a/src/Docker.DotNet.LegacyHttp/HandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs similarity index 88% rename from src/Docker.DotNet.LegacyHttp/HandlerFactory.cs rename to src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs index d3c95e24..7394234b 100644 --- a/src/Docker.DotNet.LegacyHttp/HandlerFactory.cs +++ b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet.LegacyHttp; -public class HandlerFactory : IDockerHandlerFactory +public class DockerHandlerFactory : IDockerHandlerFactory { public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { diff --git a/src/Docker.DotNet.NPipe/HandlerFactory.cs b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs similarity index 92% rename from src/Docker.DotNet.NPipe/HandlerFactory.cs rename to src/Docker.DotNet.NPipe/DockerHandlerFactory.cs index 88b10de9..3611639c 100644 --- a/src/Docker.DotNet.NPipe/HandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet.NPipe; -public class HandlerFactory : IDockerHandlerFactory +public class DockerHandlerFactory : IDockerHandlerFactory { public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { @@ -23,7 +23,7 @@ public Tuple CreateHandler(Uri uri, IDockerClientConfig var streamOpener = new ManagedHandler.StreamOpener(async (_, _, cancellationToken) => { - var clientStream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, System.IO.Pipes.PipeOptions.Asynchronous); + var clientStream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); var dockerStream = new DockerPipeStream(clientStream); diff --git a/src/Docker.DotNet.NativeHttp/HandlerFactory.cs b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs similarity index 94% rename from src/Docker.DotNet.NativeHttp/HandlerFactory.cs rename to src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs index 84b853ed..2a251460 100644 --- a/src/Docker.DotNet.NativeHttp/HandlerFactory.cs +++ b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet.NativeHttp; -public class HandlerFactory : IDockerHandlerFactory +public class DockerHandlerFactory : IDockerHandlerFactory { private const int MaxConnectionsPerServer = 10; diff --git a/src/Docker.DotNet.Unix/HandlerFactory.cs b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs similarity index 93% rename from src/Docker.DotNet.Unix/HandlerFactory.cs rename to src/Docker.DotNet.Unix/DockerHandlerFactory.cs index 9f0a7a2d..a8567da2 100644 --- a/src/Docker.DotNet.Unix/HandlerFactory.cs +++ b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet.Unix; -public class HandlerFactory : IDockerHandlerFactory +public class DockerHandlerFactory : IDockerHandlerFactory { public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index 3259014f..e870ac1d 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -54,12 +54,12 @@ public DockerClient CreateClient(Version requestedApiVersion = null, ILogger log { return EndpointBaseUri.Scheme.ToLower() switch { - "npipe" => CreateClient(requestedApiVersion, new NPipe.HandlerFactory(), logger), - "unix" => CreateClient(requestedApiVersion, new Unix.HandlerFactory(), logger), + "npipe" => CreateClient(requestedApiVersion, new NPipe.DockerHandlerFactory(), logger), + "unix" => CreateClient(requestedApiVersion, new Unix.DockerHandlerFactory(), logger), "http" or "https" => Environment.GetEnvironmentVariable("DOCKER_DOTNET_USE_NATIVE_HTTP") == "1" ? - CreateClient(requestedApiVersion, new NativeHttp.HandlerFactory(), logger) : - CreateClient(requestedApiVersion, new LegacyHttp.HandlerFactory(), logger), + CreateClient(requestedApiVersion, new NativeHttp.DockerHandlerFactory(), logger) : + CreateClient(requestedApiVersion, new LegacyHttp.DockerHandlerFactory(), logger), _ => throw new NotSupportedException($"The URI scheme '{EndpointBaseUri.Scheme}' is not supported."), }; } diff --git a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems index c94268c7..5c2c9157 100644 --- a/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems +++ b/src/Microsoft.Net.Http.Client/Microsoft.Net.Http.Client.projitems @@ -23,7 +23,6 @@ - From f255cb4c4f095e07922911b164de3466eb2ec942 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:42:03 +0100 Subject: [PATCH 34/50] chore: Turn handler to singleton --- src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs | 8 +++++++- src/Docker.DotNet.NPipe/DockerHandlerFactory.cs | 8 +++++++- src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs | 8 +++++++- src/Docker.DotNet.Unix/DockerHandlerFactory.cs | 8 +++++++- src/Docker.DotNet/DockerClientConfiguration.cs | 8 ++++---- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs index 7394234b..9e22d210 100644 --- a/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs @@ -1,7 +1,13 @@ namespace Docker.DotNet.LegacyHttp; -public class DockerHandlerFactory : IDockerHandlerFactory +public sealed class DockerHandlerFactory : IDockerHandlerFactory { + private DockerHandlerFactory() + { + } + + public static IDockerHandlerFactory Instance { get; } = new DockerHandlerFactory(); + public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { var scheme = configuration.Credentials.IsTlsCredentials() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; diff --git a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs index 3611639c..83f52c31 100644 --- a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs @@ -1,7 +1,13 @@ namespace Docker.DotNet.NPipe; -public class DockerHandlerFactory : IDockerHandlerFactory +public sealed class DockerHandlerFactory : IDockerHandlerFactory { + private DockerHandlerFactory() + { + } + + public static IDockerHandlerFactory Instance { get; } = new DockerHandlerFactory(); + public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { if (configuration.Credentials.IsTlsCredentials()) diff --git a/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs index 2a251460..8e96b070 100644 --- a/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet.NativeHttp; -public class DockerHandlerFactory : IDockerHandlerFactory +public sealed class DockerHandlerFactory : IDockerHandlerFactory { private const int MaxConnectionsPerServer = 10; @@ -8,6 +8,12 @@ public class DockerHandlerFactory : IDockerHandlerFactory private static readonly TimeSpan PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2); + private DockerHandlerFactory() + { + } + + public static IDockerHandlerFactory Instance { get; } = new DockerHandlerFactory(); + public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { var scheme = configuration.Credentials.IsTlsCredentials() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; diff --git a/src/Docker.DotNet.Unix/DockerHandlerFactory.cs b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs index a8567da2..f53b24ce 100644 --- a/src/Docker.DotNet.Unix/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs @@ -1,7 +1,13 @@ namespace Docker.DotNet.Unix; -public class DockerHandlerFactory : IDockerHandlerFactory +public sealed class DockerHandlerFactory : IDockerHandlerFactory { + private DockerHandlerFactory() + { + } + + public static IDockerHandlerFactory Instance { get; } = new DockerHandlerFactory(); + public Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger) { var socketName = uri.Segments.Last(); diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index e870ac1d..dbd818d0 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -54,12 +54,12 @@ public DockerClient CreateClient(Version requestedApiVersion = null, ILogger log { return EndpointBaseUri.Scheme.ToLower() switch { - "npipe" => CreateClient(requestedApiVersion, new NPipe.DockerHandlerFactory(), logger), - "unix" => CreateClient(requestedApiVersion, new Unix.DockerHandlerFactory(), logger), + "npipe" => CreateClient(requestedApiVersion, NPipe.DockerHandlerFactory.Instance, logger), + "unix" => CreateClient(requestedApiVersion, Unix.DockerHandlerFactory.Instance, logger), "http" or "https" => Environment.GetEnvironmentVariable("DOCKER_DOTNET_USE_NATIVE_HTTP") == "1" ? - CreateClient(requestedApiVersion, new NativeHttp.DockerHandlerFactory(), logger) : - CreateClient(requestedApiVersion, new LegacyHttp.DockerHandlerFactory(), logger), + CreateClient(requestedApiVersion, NativeHttp.DockerHandlerFactory.Instance, logger) : + CreateClient(requestedApiVersion, LegacyHttp.DockerHandlerFactory.Instance, logger), _ => throw new NotSupportedException($"The URI scheme '{EndpointBaseUri.Scheme}' is not supported."), }; } From f1268db4a305f728e40df5f3136b1f9e8768bd7b Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:42:06 +0100 Subject: [PATCH 35/50] chore: Add IStreamHijacker --- .../Docker.DotNet.Handler.Abstractions.csproj | 1 + .../IDockerHandlerFactory.cs | 2 +- .../IStreamHijacker.cs | 6 ++ .../DockerHandlerFactory.cs | 10 ++++ .../HijackStreamHelper.cs | 14 ----- .../DockerHandlerFactory.cs | 10 ++++ src/Docker.DotNet.NPipe/HijackStreamHelper.cs | 14 ----- .../DockerHandlerFactory.cs | 8 +++ .../HijackStreamHelper.cs | 12 ---- .../WriteClosableStreamWrapper.cs | 59 ++++++++++--------- .../DockerHandlerFactory.cs | 10 ++++ src/Docker.DotNet.Unix/HijackStreamHelper.cs | 14 ----- src/Docker.DotNet/DockerClient.cs | 22 +++---- .../DockerClientConfiguration.cs | 2 +- 14 files changed, 88 insertions(+), 96 deletions(-) create mode 100644 src/Docker.DotNet.Handler.Abstractions/IStreamHijacker.cs delete mode 100644 src/Docker.DotNet.LegacyHttp/HijackStreamHelper.cs delete mode 100644 src/Docker.DotNet.NPipe/HijackStreamHelper.cs delete mode 100644 src/Docker.DotNet.NativeHttp/HijackStreamHelper.cs delete mode 100644 src/Docker.DotNet.Unix/HijackStreamHelper.cs diff --git a/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj index 63590bd4..023d3266 100644 --- a/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj +++ b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs b/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs index 537b3d76..6bbc6929 100644 --- a/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs +++ b/src/Docker.DotNet.Handler.Abstractions/IDockerHandlerFactory.cs @@ -1,6 +1,6 @@ namespace Docker.DotNet.Handler.Abstractions; -public interface IDockerHandlerFactory +public interface IDockerHandlerFactory : IStreamHijacker { Tuple CreateHandler(Uri uri, IDockerClientConfiguration configuration, ILogger logger); } \ No newline at end of file diff --git a/src/Docker.DotNet.Handler.Abstractions/IStreamHijacker.cs b/src/Docker.DotNet.Handler.Abstractions/IStreamHijacker.cs new file mode 100644 index 00000000..8d733b55 --- /dev/null +++ b/src/Docker.DotNet.Handler.Abstractions/IStreamHijacker.cs @@ -0,0 +1,6 @@ +namespace Docker.DotNet.Handler.Abstractions; + +public interface IStreamHijacker +{ + Task HijackStreamAsync(HttpContent content); +} \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs index 9e22d210..bb2da941 100644 --- a/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs @@ -14,4 +14,14 @@ public Tuple CreateHandler(Uri uri, IDockerClientConfig uri = new UriBuilder(uri) { Scheme = scheme }.Uri; return new Tuple(new ManagedHandler(logger), uri); } + + public Task HijackStreamAsync(HttpContent content) + { + if (content is not HttpConnectionResponseContent hijackable) + { + throw new NotSupportedException("Not supported content type for stream hijacking."); + } + + return Task.FromResult(hijackable.HijackStream()); + } } \ No newline at end of file diff --git a/src/Docker.DotNet.LegacyHttp/HijackStreamHelper.cs b/src/Docker.DotNet.LegacyHttp/HijackStreamHelper.cs deleted file mode 100644 index f2081d26..00000000 --- a/src/Docker.DotNet.LegacyHttp/HijackStreamHelper.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Docker.DotNet.LegacyHttp; - -public class HijackStreamHelper -{ - static public WriteClosableStream HijackStream(HttpContent content) - { - if (content is not HttpConnectionResponseContent contentHijackAble) - { - throw new NotSupportedException("message handler does not support hijacked streams"); - } - - return contentHijackAble.HijackStream(); - } -} diff --git a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs index 83f52c31..5d485593 100644 --- a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs @@ -47,4 +47,14 @@ await clientStream.ConnectAsync(namedPipeConnectTimeout, cancellationToken) return new Tuple(new ManagedHandler(streamOpener, logger), uri); } + + public Task HijackStreamAsync(HttpContent content) + { + if (content is not HttpConnectionResponseContent hijackable) + { + throw new NotSupportedException("Not supported content type for stream hijacking."); + } + + return Task.FromResult(hijackable.HijackStream()); + } } \ No newline at end of file diff --git a/src/Docker.DotNet.NPipe/HijackStreamHelper.cs b/src/Docker.DotNet.NPipe/HijackStreamHelper.cs deleted file mode 100644 index 016b5ba6..00000000 --- a/src/Docker.DotNet.NPipe/HijackStreamHelper.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Docker.DotNet.NPipe; - -public class HijackStreamHelper -{ - static public WriteClosableStream HijackStream(HttpContent content) - { - if (content is not HttpConnectionResponseContent contentHijackAble) - { - throw new NotSupportedException("message handler does not support hijacked streams"); - } - - return contentHijackAble.HijackStream(); - } -} diff --git a/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs index 8e96b070..762c1260 100644 --- a/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.NativeHttp/DockerHandlerFactory.cs @@ -32,4 +32,12 @@ public Tuple CreateHandler(Uri uri, IDockerClientConfig return new Tuple(handler, uri); } + + public async Task HijackStreamAsync(HttpContent content) + { + var stream = await content.ReadAsStreamAsync() + .ConfigureAwait(false); + + return new WriteClosableStreamWrapper(stream); + } } \ No newline at end of file diff --git a/src/Docker.DotNet.NativeHttp/HijackStreamHelper.cs b/src/Docker.DotNet.NativeHttp/HijackStreamHelper.cs deleted file mode 100644 index a7f11598..00000000 --- a/src/Docker.DotNet.NativeHttp/HijackStreamHelper.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Docker.DotNet.NativeHttp; - -public class HijackStreamHelper -{ - static public WriteClosableStream HijackStream(HttpContent content) - { - return new WriteClosableStreamWrapper(content.ReadAsStreamAsync() - .ConfigureAwait(false) - .GetAwaiter() - .GetResult()); - } -} diff --git a/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs b/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs index 20fcd912..0ac93df8 100644 --- a/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs +++ b/src/Docker.DotNet.NativeHttp/WriteClosableStreamWrapper.cs @@ -1,51 +1,56 @@ namespace Docker.DotNet.NativeHttp; -public class WriteClosableStreamWrapper : WriteClosableStream +internal sealed class WriteClosableStreamWrapper(Stream stream) : WriteClosableStream { - private readonly Stream _baseStream; + private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - public WriteClosableStreamWrapper(Stream baseStream) - { - _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); - } + public override bool CanRead + => _stream.CanRead; - public override void CloseWrite() - { - _baseStream.Close(); // Replace with half-close logic if available - } + public override bool CanWrite + => _stream.CanWrite; - public override bool CanRead => _baseStream.CanRead; - public override bool CanSeek => _baseStream.CanSeek; - public override bool CanWrite => _baseStream.CanWrite; - public override bool CanCloseWrite => true; - public override long Length => _baseStream.Length; + public override bool CanSeek + => _stream.CanSeek; + + public override bool CanCloseWrite + => true; + + public override long Length + => _stream.Length; public override long Position { - get => _baseStream.Position; - set => _baseStream.Position = value; + get => _stream.Position; + set => _stream.Position = value; } - public override void Flush() => _baseStream.Flush(); + public override void Flush() + => _stream.Flush(); - public override int Read(byte[] buffer, int offset, int count) => - _baseStream.Read(buffer, offset, count); + public override int Read(byte[] buffer, int offset, int count) + => _stream.Read(buffer, offset, count); - public override long Seek(long offset, SeekOrigin origin) => - _baseStream.Seek(offset, origin); + public override void Write(byte[] buffer, int offset, int count) + => _stream.Write(buffer, offset, count); - public override void SetLength(long value) => - _baseStream.SetLength(value); + public override long Seek(long offset, SeekOrigin origin) + => _stream.Seek(offset, origin); - public override void Write(byte[] buffer, int offset, int count) => - _baseStream.Write(buffer, offset, count); + // Replace with half-close logic if available. + public override void CloseWrite() + => _stream.Close(); + + public override void SetLength(long value) + => _stream.SetLength(value); protected override void Dispose(bool disposing) { if (disposing) { - _baseStream.Dispose(); + _stream.Dispose(); } + base.Dispose(disposing); } } \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/DockerHandlerFactory.cs b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs index f53b24ce..ef06cb18 100644 --- a/src/Docker.DotNet.Unix/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs @@ -28,4 +28,14 @@ await socket.ConnectAsync(endpoint) return new Tuple(new ManagedHandler(socketOpener, logger), uri); } + + public Task HijackStreamAsync(HttpContent content) + { + if (content is not HttpConnectionResponseContent hijackable) + { + throw new NotSupportedException("Not supported content type for stream hijacking."); + } + + return Task.FromResult(hijackable.HijackStream()); + } } \ No newline at end of file diff --git a/src/Docker.DotNet.Unix/HijackStreamHelper.cs b/src/Docker.DotNet.Unix/HijackStreamHelper.cs deleted file mode 100644 index 9d818b67..00000000 --- a/src/Docker.DotNet.Unix/HijackStreamHelper.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Docker.DotNet.Unix; - -public class HijackStreamHelper -{ - static public WriteClosableStream HijackStream(HttpContent content) - { - if (content is not HttpConnectionResponseContent contentHijackAble) - { - throw new NotSupportedException("message handler does not support hijacked streams"); - } - - return contentHijackAble.HijackStream(); - } -} diff --git a/src/Docker.DotNet/DockerClient.cs b/src/Docker.DotNet/DockerClient.cs index 24497018..2d59b558 100644 --- a/src/Docker.DotNet/DockerClient.cs +++ b/src/Docker.DotNet/DockerClient.cs @@ -14,6 +14,8 @@ public sealed class DockerClient : IDockerClient private readonly Version _requestedApiVersion; + private readonly IDockerHandlerFactory _handlerFactory; + internal DockerClient(DockerClientConfiguration configuration, Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) { if (handlerFactory == null) @@ -22,6 +24,8 @@ internal DockerClient(DockerClientConfiguration configuration, Version requested } _requestedApiVersion = requestedApiVersion; + _handlerFactory = handlerFactory; + Configuration = configuration; DefaultTimeout = configuration.DefaultTimeout; @@ -37,7 +41,7 @@ internal DockerClient(DockerClientConfiguration configuration, Version requested Plugin = new PluginOperations(this); Exec = new ExecOperations(this); - var (handler, endpoint) = handlerFactory.CreateHandler(Configuration.EndpointBaseUri, Configuration, logger); + var (handler, endpoint) = _handlerFactory.CreateHandler(Configuration.EndpointBaseUri, Configuration, logger); _client = new HttpClient(Configuration.Credentials.GetHandler(handler), true); _client.Timeout = Timeout.InfiniteTimeSpan; @@ -328,16 +332,8 @@ internal async Task MakeRequestForHijackedStreamAsync( await HandleIfErrorResponseAsync(response.StatusCode, response, errorHandlers) .ConfigureAwait(false); - return _endpointBaseUri.Scheme.ToLower() switch - { - "npipe" => NPipe.HijackStreamHelper.HijackStream(response.Content), - "unix" => Unix.HijackStreamHelper.HijackStream(response.Content), - "http" or "https" => - Environment.GetEnvironmentVariable("DOCKER_DOTNET_USE_NATIVE_HTTP") == "1" ? - NativeHttp.HijackStreamHelper.HijackStream(response.Content) : - LegacyHttp.HijackStreamHelper.HijackStream(response.Content), - _ => throw new NotSupportedException($"The URI scheme '{_endpointBaseUri.Scheme}' is not supports stream hijacking."), - }; + return await _handlerFactory.HijackStreamAsync(response.Content) + .ConfigureAwait(false); } private async Task PrivateMakeRequestAsync( @@ -410,7 +406,7 @@ private async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpRes if (isErrorResponse) { // If it is not an error response, we do not read the response body because the caller may wish to consume it. - // If it is an error response, we do because there is nothing else going to be done with it anyway and + // If it is an error response, we do because there is nothing else going to be done with it anyway, and // we want to report the response body in the error message as it contains potentially useful info. responseBody = await response.Content.ReadAsStringAsync() .ConfigureAwait(false); @@ -441,7 +437,7 @@ private async Task HandleIfErrorResponseAsync(HttpStatusCode statusCode, HttpRes if (isErrorResponse) { // If it is not an error response, we do not read the response body because the caller may wish to consume it. - // If it is an error response, we do because there is nothing else going to be done with it anyway and + // If it is an error response, we do because there is nothing else going to be done with it anyway, and // we want to report the response body in the error message as it contains potentially useful info. responseBody = await response.Content.ReadAsStringAsync() .ConfigureAwait(false); diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index dbd818d0..12cdd200 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -56,7 +56,7 @@ public DockerClient CreateClient(Version requestedApiVersion = null, ILogger log { "npipe" => CreateClient(requestedApiVersion, NPipe.DockerHandlerFactory.Instance, logger), "unix" => CreateClient(requestedApiVersion, Unix.DockerHandlerFactory.Instance, logger), - "http" or "https" => + "tcp" or "http" or "https" => Environment.GetEnvironmentVariable("DOCKER_DOTNET_USE_NATIVE_HTTP") == "1" ? CreateClient(requestedApiVersion, NativeHttp.DockerHandlerFactory.Instance, logger) : CreateClient(requestedApiVersion, LegacyHttp.DockerHandlerFactory.Instance, logger), From be2d695c39963388503851c55d9b6dd46226be87 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:42:10 +0100 Subject: [PATCH 36/50] chore: Read env var once --- .../DockerHandlerFactory.cs | 2 +- .../DockerHandlerFactory.cs | 4 ++-- .../DockerHandlerFactory.cs | 2 +- .../DockerClientConfiguration.cs | 19 +++++++++++-------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs index bb2da941..24f8ccab 100644 --- a/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.LegacyHttp/DockerHandlerFactory.cs @@ -19,7 +19,7 @@ public Task HijackStreamAsync(HttpContent content) { if (content is not HttpConnectionResponseContent hijackable) { - throw new NotSupportedException("Not supported content type for stream hijacking."); + throw new NotSupportedException("The content type is not supported for stream hijacking."); } return Task.FromResult(hijackable.HijackStream()); diff --git a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs index 5d485593..03e83709 100644 --- a/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.NPipe/DockerHandlerFactory.cs @@ -19,7 +19,7 @@ public Tuple CreateHandler(Uri uri, IDockerClientConfig if (segments.Length != 3 || !"pipe/".Equals(segments[1], StringComparison.OrdinalIgnoreCase)) { - throw new InvalidOperationException("The endpoint is not a valid npipe URI."); + throw new InvalidOperationException("The endpoint is not a npipe URI."); } var pipeName = uri.Segments[2]; @@ -52,7 +52,7 @@ public Task HijackStreamAsync(HttpContent content) { if (content is not HttpConnectionResponseContent hijackable) { - throw new NotSupportedException("Not supported content type for stream hijacking."); + throw new NotSupportedException("The content type is not supported for stream hijacking."); } return Task.FromResult(hijackable.HijackStream()); diff --git a/src/Docker.DotNet.Unix/DockerHandlerFactory.cs b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs index ef06cb18..a0e4be0c 100644 --- a/src/Docker.DotNet.Unix/DockerHandlerFactory.cs +++ b/src/Docker.DotNet.Unix/DockerHandlerFactory.cs @@ -33,7 +33,7 @@ public Task HijackStreamAsync(HttpContent content) { if (content is not HttpConnectionResponseContent hijackable) { - throw new NotSupportedException("Not supported content type for stream hijacking."); + throw new NotSupportedException("The content type is not supported for stream hijacking."); } return Task.FromResult(hijackable.HijackStream()); diff --git a/src/Docker.DotNet/DockerClientConfiguration.cs b/src/Docker.DotNet/DockerClientConfiguration.cs index 12cdd200..ccf40f59 100644 --- a/src/Docker.DotNet/DockerClientConfiguration.cs +++ b/src/Docker.DotNet/DockerClientConfiguration.cs @@ -4,6 +4,8 @@ namespace Docker.DotNet; public class DockerClientConfiguration : IDockerClientConfiguration, IDisposable { + private static readonly bool NativeHttpEnabled = Environment.GetEnvironmentVariable("DOCKER_DOTNET_NATIVE_HTTP_ENABLED") == "1"; + public DockerClientConfiguration( Credentials credentials = null, TimeSpan defaultTimeout = default, @@ -52,16 +54,17 @@ public DockerClientConfiguration( public DockerClient CreateClient(Version requestedApiVersion = null, ILogger logger = null) { - return EndpointBaseUri.Scheme.ToLower() switch + var handlerFactory = EndpointBaseUri.Scheme.ToLowerInvariant() switch { - "npipe" => CreateClient(requestedApiVersion, NPipe.DockerHandlerFactory.Instance, logger), - "unix" => CreateClient(requestedApiVersion, Unix.DockerHandlerFactory.Instance, logger), - "tcp" or "http" or "https" => - Environment.GetEnvironmentVariable("DOCKER_DOTNET_USE_NATIVE_HTTP") == "1" ? - CreateClient(requestedApiVersion, NativeHttp.DockerHandlerFactory.Instance, logger) : - CreateClient(requestedApiVersion, LegacyHttp.DockerHandlerFactory.Instance, logger), - _ => throw new NotSupportedException($"The URI scheme '{EndpointBaseUri.Scheme}' is not supported."), + "npipe" => NPipe.DockerHandlerFactory.Instance, + "unix" => Unix.DockerHandlerFactory.Instance, + "tcp" or "http" or "https" => NativeHttpEnabled + ? NativeHttp.DockerHandlerFactory.Instance + : LegacyHttp.DockerHandlerFactory.Instance, + _ => throw new NotSupportedException($"The URI scheme '{EndpointBaseUri.Scheme}' is not supported.") }; + + return CreateClient(requestedApiVersion, handlerFactory, logger); } public DockerClient CreateClient(Version requestedApiVersion, IDockerHandlerFactory handlerFactory, ILogger logger = null) From 22d6432357d43aafe7b281a9c7edea69e3137660 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:27:52 +0100 Subject: [PATCH 37/50] chore: Add description --- .../Docker.DotNet.Handler.Abstractions.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj index 023d3266..44805ec3 100644 --- a/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj +++ b/src/Docker.DotNet.Handler.Abstractions/Docker.DotNet.Handler.Abstractions.csproj @@ -2,7 +2,7 @@ Docker.DotNet.Handler.Abstractions Docker.DotNet.Enhanced.Handler.Abstractions - TODO + An abstraction layer for Docker.DotNet that defines the classes and interfaces for implementing Docker Engine handlers. From ad32e4eeeb3d73494271b8a7c4f296e0f3a7705b Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:50:26 +0100 Subject: [PATCH 38/50] chore: Order sln projects --- Docker.DotNet.sln | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln index bde945cf..1e5c2807 100644 --- a/Docker.DotNet.sln +++ b/Docker.DotNet.sln @@ -4,7 +4,7 @@ VisualStudioVersion = 17.10.35201.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{85990620-78A6-4381-8BD6-84E6D0CF0649}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.BasicAuth", "src\Docker.DotNet.BasicAuth\Docker.DotNet.BasicAuth.csproj", "{E1F24B25-E027-45E0-A6E1-E08138F1F95D}" EndProject @@ -22,10 +22,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet", "src\Docker EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.Net.Http.Client", "src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.shproj", "{DAE2DE68-9B3E-4D5D-8802-EC97B94160ED}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Handler.Abstractions", "src\Docker.DotNet.Handler.Abstractions\Docker.DotNet.Handler.Abstractions.csproj", "{22C42314-615F-4B11-B111-58F1D6D54F4D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -120,18 +120,6 @@ Global {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.Build.0 = Release|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.ActiveCfg = Release|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.Build.0 = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.Build.0 = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.ActiveCfg = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.Build.0 = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x86.ActiveCfg = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x86.Build.0 = Debug|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|Any CPU.ActiveCfg = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|Any CPU.Build.0 = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.ActiveCfg = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.Build.0 = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.ActiveCfg = Release|Any CPU - {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.Build.0 = Release|Any CPU {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -144,6 +132,18 @@ Global {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.Build.0 = Release|Any CPU {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.ActiveCfg = Release|Any CPU {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.Build.0 = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.ActiveCfg = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.Build.0 = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x86.ActiveCfg = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x86.Build.0 = Debug|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|Any CPU.Build.0 = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.ActiveCfg = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x64.Build.0 = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.ActiveCfg = Release|Any CPU + {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -157,8 +157,8 @@ Global {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {DAE2DE68-9B3E-4D5D-8802-EC97B94160ED} = {85990620-78A6-4381-8BD6-84E6D0CF0649} - {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} {22C42314-615F-4B11-B111-58F1D6D54F4D} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8F2F229F-C66D-43E4-B804-E5F37DC157CB} From 260e3e04c238755ef20051f089c9fd80d7f4f5f3 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:04:23 +0100 Subject: [PATCH 39/50] fix: Revert test changes --- test/Docker.DotNet.Tests/CommonCommands.cs | 4 +- .../Docker.DotNet.Tests.csproj | 4 - .../IConfigOperationsTests.cs | 20 +- .../IContainerOperationsTests.cs | 500 +++++------------- .../IImageOperationsTests.cs | 32 +- .../ISwarmOperationsTests.cs | 93 ++-- .../ISystemOperations.Tests.cs | 94 ++-- .../IVolumeOperationsTests.cs | 26 +- test/Docker.DotNet.Tests/TestFixture.cs | 288 +++------- 9 files changed, 321 insertions(+), 740 deletions(-) diff --git a/test/Docker.DotNet.Tests/CommonCommands.cs b/test/Docker.DotNet.Tests/CommonCommands.cs index 19db1d0d..b6219392 100644 --- a/test/Docker.DotNet.Tests/CommonCommands.cs +++ b/test/Docker.DotNet.Tests/CommonCommands.cs @@ -4,7 +4,5 @@ public static class CommonCommands { public static readonly string[] SleepInfinity = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; sleep infinity"]; - public static readonly string[] EchoToStdoutAndStderr = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; RND=$RANDOM; while true; do echo \"stdout message $RND\"; echo \"stderr message $RND\" >&2; sleep 1; done"]; - - public static readonly string[] EchoToStdoutAndStderrFast = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; RND=$RANDOM; while true; do echo \"stdout message $RND\"; echo \"stderr message $RND\" >&2; done"]; + public static readonly string[] EchoToStdoutAndStderr = ["/bin/sh", "-c", "trap \"exit 0\" TERM INT; while true; do echo \"stdout message\"; echo \"stderr message\" >&2; sleep 1; done"]; } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index 3f6a12fa..1b06c222 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -12,10 +12,6 @@ - - - - diff --git a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs index db018547..533d5d25 100644 --- a/test/Docker.DotNet.Tests/IConfigOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IConfigOperationsTests.cs @@ -12,14 +12,10 @@ public IConfigOperationsTests(TestFixture testFixture, ITestOutputHelper testOut _testOutputHelper = testOutputHelper; } - public static IEnumerable GetDockerClientTypes() => - TestFixture.GetDockerClientTypes(); - - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task SwarmConfig_CanCreateAndRead(TestClientsEnum clientType) + [Fact] + public async Task SwarmConfig_CanCreateAndRead() { - var currentConfigs = await _testFixture.DockerClients[clientType].Configs.ListConfigsAsync(); + var currentConfigs = await _testFixture.DockerClient.Configs.ListConfigsAsync(); _testOutputHelper.WriteLine($"Current Configs: {currentConfigs.Count}"); @@ -35,15 +31,15 @@ public async Task SwarmConfig_CanCreateAndRead(TestClientsEnum clientType) Config = testConfigSpec }; - var createdConfig = await _testFixture.DockerClients[clientType].Configs.CreateConfigAsync(configParameters); + var createdConfig = await _testFixture.DockerClient.Configs.CreateConfigAsync(configParameters); Assert.NotNull(createdConfig.ID); _testOutputHelper.WriteLine($"Config created: {createdConfig.ID}"); - var configs = await _testFixture.DockerClients[clientType].Configs.ListConfigsAsync(); + var configs = await _testFixture.DockerClient.Configs.ListConfigsAsync(); Assert.Contains(configs, c => c.ID == createdConfig.ID); _testOutputHelper.WriteLine($"Current Configs: {configs.Count}"); - var configResponse = await _testFixture.DockerClients[clientType].Configs.InspectConfigAsync(createdConfig.ID); + var configResponse = await _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID); Assert.NotNull(configResponse); @@ -55,8 +51,8 @@ public async Task SwarmConfig_CanCreateAndRead(TestClientsEnum clientType) _testOutputHelper.WriteLine("Config created is the same."); - await _testFixture.DockerClients[clientType].Configs.RemoveConfigAsync(createdConfig.ID); + await _testFixture.DockerClient.Configs.RemoveConfigAsync(createdConfig.ID); - await Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].Configs.InspectConfigAsync(createdConfig.ID)); + await Assert.ThrowsAsync(() => _testFixture.DockerClient.Configs.InspectConfigAsync(createdConfig.ID)); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs index 3535064e..cc2ae303 100644 --- a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs @@ -1,7 +1,3 @@ -using System.Collections.Concurrent; -using System.Net.NetworkInformation; - - namespace Docker.DotNet.Tests; [Collection(nameof(TestCollection))] @@ -16,17 +12,13 @@ public IContainerOperationsTests(TestFixture testFixture, ITestOutputHelper test _testOutputHelper = testOutputHelper; } - public static IEnumerable GetDockerClientTypes() => - TestFixture.GetDockerClientTypes(); - - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task CreateContainerAsync_CreatesContainer(TestClientsEnum clientType) + [Fact] + public async Task CreateContainerAsync_CreatesContainer() { - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr }, _testFixture.Cts.Token @@ -36,23 +28,22 @@ public async Task CreateContainerAsync_CreatesContainer(TestClientsEnum clientTy Assert.NotEmpty(createContainerResponse.ID); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerLogs_Tty_False_Follow_True_TaskIsCompleted(TestClientsEnum clientType) + [Fact] + public async Task GetContainerLogs_Tty_False_Follow_True_TaskIsCompleted() { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -60,7 +51,7 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -72,7 +63,7 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( new Progress(m => _testOutputHelper.WriteLine(m)), containerLogsCts.Token); - await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( + await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), _testFixture.Cts.Token @@ -82,23 +73,22 @@ await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( Assert.True(containerLogsTask.IsCompletedSuccessfully); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerLogs_Tty_False_Follow_False_ReadsLogs(TestClientsEnum clientType) + [Fact] + public async Task GetContainerLogs_Tty_False_Follow_False_ReadsLogs() { var logList = new List(); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -106,7 +96,7 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( await Task.Delay(TimeSpan.FromSeconds(5)); - await _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( + await _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -119,7 +109,7 @@ await _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( + await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), _testFixture.Cts.Token @@ -130,168 +120,22 @@ await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( Assert.NotEmpty(logList); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs(TestClientsEnum clientType) - { - if (clientType == TestClientsEnum.ManagedHttps) - { - // Skip this test for ManagedHttps client type because something is blocking - // [xUnit.net 00:00:42.97] Docker.DotNet.Tests.IContainerOperationsTests.GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs(clientType: ManagedHttps) [FAIL] - // Failed Docker.DotNet.Tests.IContainerOperationsTests.GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs(clientType: ManagedHttps) [13 s] - // Error Message: - // Average line count 1.0 is less than expected 20 - // Stack Trace: - // at Docker.DotNet.Tests.IContainerOperationsTests.GetContainerLogs_Parallel_Tty_False_Follow_False_ReadsLogs(TestClientsEnum clientType) in /home/runner/work/TestContainers.Docker.DotNet/TestContainers.Docker.DotNet/test/test/Docker.DotNet.Tests/IContainerOperationsTests.cs:line 258 - // --- End of stack trace from previous location --- - // Standard Output Messages: - // ClientType ManagedHttps: avg. Line count: 1.0, cpu ticks: 55,100,000, mem usage: 19,343,368, sockets: -2 - // ClientType ManagedHttps: FirstLine: - return; - } - - using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - - var parallelContainerCount = 3; - var parallelThreadCount = 100; - var runtimeInSeconds = 9; - - var containerIds = new string[parallelContainerCount]; - - long memoryUsageBefore = GC.GetTotalAllocatedBytes(true); - - long socketsBefore = IPGlobalProperties.GetIPGlobalProperties() - .GetTcpIPv4Statistics() - .CurrentConnections; - - Process process = Process.GetCurrentProcess(); - TimeSpan cpuTimeBefore = process.TotalProcessorTime; - - ParallelOptions parallelOptions = new ParallelOptions - { - MaxDegreeOfParallelism = parallelContainerCount, - CancellationToken = _testFixture.Cts.Token - }; - - await Parallel.ForEachAsync(Enumerable.Range(0, parallelContainerCount), parallelOptions, async (parallel, ct) => - { - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( - new CreateContainerParameters - { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, - Entrypoint = CommonCommands.EchoToStdoutAndStderr, - Tty = false - }, - _testFixture.Cts.Token - ); - - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( - createContainerResponse.ID, - new ContainerStartParameters(), - _testFixture.Cts.Token - ); - containerIds[parallel] = createContainerResponse.ID; - }); - - await Task.Delay(TimeSpan.FromSeconds(runtimeInSeconds)); - - await Parallel.ForEachAsync(Enumerable.Range(0, parallelContainerCount), parallelOptions, async (parallel, ct) => - { - await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( - containerIds[parallel], - new ContainerStopParameters(), - _testFixture.Cts.Token - ); - }); - - containerLogsCts.CancelAfter(TimeSpan.FromSeconds(1)); - - var logLists = new ConcurrentDictionary(); - var threads = new List(); - - for (int parallel = 0; parallel < parallelContainerCount * parallelThreadCount; parallel++) - { - int index = parallel; - string containerId = containerIds[parallel % parallelContainerCount]; - CancellationToken ct = containerLogsCts.Token; - - var thread = new Thread(() => - { - var logList = new StringBuilder(2000); - try - { - var task = _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( - containerId, - new ContainerLogsParameters - { - ShowStderr = true, - ShowStdout = true, - Timestamps = true, - Follow = false - }, - new Progress(m => logList.AppendLine(m)), - ct - ); - - task.GetAwaiter().GetResult(); - } - catch (OperationCanceledException) - { - } - - Thread.Sleep(100); - - logLists.TryAdd(index, logList.ToString()); - logList.Clear(); - }); - - threads.Add(thread); - thread.Start(); - } - - foreach (var thread in threads) - { - thread.Join(); - } - - TimeSpan cpuTimeAfter = process.TotalProcessorTime; - - long socketsAfter = IPGlobalProperties.GetIPGlobalProperties() - .GetTcpIPv4Statistics() - .CurrentConnections; - - if (clientType == TestClientsEnum.ManagedPipe) - socketsAfter = socketsBefore = 0; - - long memoryUsageAfter = GC.GetTotalAllocatedBytes(true); - - var averageLineCount = logLists.Values.Average(logs => logs.Split('\n').Count()); - - _testOutputHelper.WriteLine($"ClientType {clientType}: avg. Line count: {averageLineCount:N1}, cpu ticks: {cpuTimeAfter.Ticks - cpuTimeBefore.Ticks:N0}, mem usage: {memoryUsageAfter - memoryUsageBefore:N0}, sockets: {socketsAfter - socketsBefore:N0}"); - _testOutputHelper.WriteLine($"ClientType {clientType}: FirstLine: {logLists.Values.FirstOrDefault()}"); - - // one container should produce 2 lines per second (stdout + stderr) plus 1 for last empty line of split - Assert.True(averageLineCount > (runtimeInSeconds + 1) * 2, $"Average line count {averageLineCount:N1} is less than expected {(runtimeInSeconds + 1) * 2}"); - GC.Collect(); - } - - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerLogs_Tty_True_Follow_False_ReadsLogs(TestClientsEnum clientType) + [Fact] + public async Task GetContainerLogs_Tty_True_Follow_False_ReadsLogs() { var logList = new List(); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -299,7 +143,7 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( await Task.Delay(TimeSpan.FromSeconds(5)); - await _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( + await _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -312,7 +156,7 @@ await _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( + await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), _testFixture.Cts.Token @@ -323,23 +167,22 @@ await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( Assert.NotEmpty(logList); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerLogs_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled(TestClientsEnum clientType) + [Fact] + public async Task GetContainerLogs_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled() { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -347,7 +190,7 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - await Assert.ThrowsAnyAsync(() => _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( + await Assert.ThrowsAsync(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -361,87 +204,22 @@ await Assert.ThrowsAnyAsync(() => _testFixture.Docke )); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerLogs_SpeedTest_Tty_False_Follow_True_Requires_Task_To_Be_Cancelled(TestClientsEnum clientType) - { - using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - - var runtimeInSeconds = 15; - - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( - new CreateContainerParameters - { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, - Entrypoint = CommonCommands.EchoToStdoutAndStderrFast, - Tty = false - }, - _testFixture.Cts.Token - ); - - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( - createContainerResponse.ID, - new ContainerStartParameters(), - _testFixture.Cts.Token - ); - - containerLogsCts.CancelAfter(TimeSpan.FromSeconds(runtimeInSeconds)); - - long memoryUsageBefore = GC.GetTotalAllocatedBytes(true); - - var counter = 0; - try - { - await _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( - createContainerResponse.ID, - new ContainerLogsParameters - { - ShowStderr = true, - ShowStdout = true, - Timestamps = true, - Follow = true - }, - new Progress(m => counter++), - containerLogsCts.Token); - } - catch (OperationCanceledException) - { - - } - - - long memoryUsageAfter = GC.GetTotalAllocatedBytes(true); - - await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( - createContainerResponse.ID, - new ContainerStopParameters(), - _testFixture.Cts.Token - ); - - _testOutputHelper.WriteLine($"ClientType {clientType}: Line count: {counter}, mem usage: {memoryUsageAfter - memoryUsageBefore:N0}"); - - Assert.True(counter > runtimeInSeconds * 25000, $"Line count {counter} is less than expected {runtimeInSeconds * 25000}"); - - GC.Collect(); - } - - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerLogs_Tty_True_Follow_True_Requires_Task_To_Be_Cancelled(TestClientsEnum clientType) + [Fact] + public async Task GetContainerLogs_Tty_True_Follow_True_Requires_Task_To_Be_Cancelled() { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -449,7 +227,7 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -462,27 +240,26 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( containerLogsCts.Token ); - await Assert.ThrowsAnyAsync(() => containerLogsTask); + await Assert.ThrowsAsync(() => containerLogsTask); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerLogs_Tty_True_Follow_True_ReadsLogs_TaskIsCancelled(TestClientsEnum clientType) + [Fact] + public async Task GetContainerLogs_Tty_True_Follow_True_ReadsLogs_TaskIsCancelled() { using var containerLogsCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var logList = new List(); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -490,7 +267,7 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - var containerLogsTask = _testFixture.DockerClients[clientType].Containers.GetContainerLogsAsync( + var containerLogsTask = _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -505,37 +282,35 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( await Task.Delay(TimeSpan.FromSeconds(5)); - await _testFixture.DockerClients[clientType].Containers.StopContainerAsync( + await _testFixture.DockerClient.Containers.StopContainerAsync( createContainerResponse.ID, new ContainerStopParameters(), _testFixture.Cts.Token ); - await Assert.ThrowsAnyAsync(() => containerLogsTask); - + await Assert.ThrowsAsync(() => containerLogsTask); _testOutputHelper.WriteLine($"Line count: {logList.Count}"); Assert.NotEmpty(logList); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerStatsAsync_Tty_False_Stream_False_ReadsStats(TestClientsEnum clientType) + [Fact] + public async Task GetContainerStatsAsync_Tty_False_Stream_False_ReadsStats() { using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var containerStatsList = new List(); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + _ = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -543,7 +318,7 @@ public async Task GetContainerStatsAsync_Tty_False_Stream_False_ReadsStats(TestC tcs.CancelAfter(TimeSpan.FromSeconds(10)); - await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( + await _testFixture.DockerClient.Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { @@ -557,12 +332,11 @@ await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( Assert.NotEmpty(containerStatsList); Assert.Single(containerStatsList); - _testOutputHelper.WriteLine($"ContainerStats count: {containerStatsList.Count}"); + _testOutputHelper.WriteLine($"ConntainerStats count: {containerStatsList.Count}"); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerStatsAsync_Tty_False_StreamStats(TestClientsEnum clientType) + [Fact] + public async Task GetContainerStatsAsync_Tty_False_StreamStats() { using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); using (tcs.Token.Register(() => throw new TimeoutException("GetContainerStatsAsync_Tty_False_StreamStats"))) @@ -571,17 +345,17 @@ public async Task GetContainerStatsAsync_Tty_False_StreamStats(TestClientsEnum c _testOutputHelper.WriteLine($"Running test '{method!.Module}' -> '{method!.Name}'"); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = false }, _testFixture.Cts.Token ); - _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + _ = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -593,7 +367,7 @@ public async Task GetContainerStatsAsync_Tty_False_StreamStats(TestClientsEnum c linkedCts.CancelAfter(TimeSpan.FromSeconds(5)); try { - await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( + await _testFixture.DockerClient.Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { @@ -613,24 +387,23 @@ await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( } } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerStatsAsync_Tty_True_Stream_False_ReadsStats(TestClientsEnum clientType) + [Fact] + public async Task GetContainerStatsAsync_Tty_True_Stream_False_ReadsStats() { using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var containerStatsList = new List(); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + _ = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -638,7 +411,7 @@ public async Task GetContainerStatsAsync_Tty_True_Stream_False_ReadsStats(TestCl tcs.CancelAfter(TimeSpan.FromSeconds(10)); - await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( + await _testFixture.DockerClient.Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { @@ -652,12 +425,11 @@ await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( Assert.NotEmpty(containerStatsList); Assert.Single(containerStatsList); - _testOutputHelper.WriteLine($"ContainerStats count: {containerStatsList.Count}"); + _testOutputHelper.WriteLine($"ConntainerStats count: {containerStatsList.Count}"); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetContainerStatsAsync_Tty_True_StreamStats(TestClientsEnum clientType) + [Fact] + public async Task GetContainerStatsAsync_Tty_True_StreamStats() { using var tcs = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); @@ -665,17 +437,17 @@ public async Task GetContainerStatsAsync_Tty_True_StreamStats(TestClientsEnum cl { _testOutputHelper.WriteLine("Running test GetContainerStatsAsync_Tty_True_StreamStats"); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, Tty = true }, _testFixture.Cts.Token ); - _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + _ = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -688,7 +460,7 @@ public async Task GetContainerStatsAsync_Tty_True_StreamStats(TestClientsEnum cl try { - await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( + await _testFixture.DockerClient.Containers.GetContainerStatsAsync( createContainerResponse.ID, new ContainerStatsParameters { @@ -709,34 +481,33 @@ await _testFixture.DockerClients[clientType].Containers.GetContainerStatsAsync( } } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task KillContainerAsync_ContainerRunning_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task KillContainerAsync_ContainerRunning_Succeeds() { - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr }, _testFixture.Cts.Token); - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token ); - var inspectRunningContainerResponse = await _testFixture.DockerClients[clientType].Containers.InspectContainerAsync( + var inspectRunningContainerResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync( createContainerResponse.ID, _testFixture.Cts.Token); - await _testFixture.DockerClients[clientType].Containers.KillContainerAsync( + await _testFixture.DockerClient.Containers.KillContainerAsync( createContainerResponse.ID, new ContainerKillParameters(), _testFixture.Cts.Token); - var inspectKilledContainerResponse = await _testFixture.DockerClients[clientType].Containers.InspectContainerAsync( + var inspectKilledContainerResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync( createContainerResponse.ID, _testFixture.Cts.Token); @@ -748,26 +519,25 @@ await _testFixture.DockerClients[clientType].Containers.KillContainerAsync( _testOutputHelper.WriteLine(JsonSerializer.Instance.Serialize(inspectKilledContainerResponse)); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task ListContainersAsync_ContainerExists_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task ListContainersAsync_ContainerExists_Succeeds() { - await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, _testFixture.Cts.Token); - IList containerList = await _testFixture.DockerClients[clientType].Containers.ListContainersAsync( + IList containerList = await _testFixture.DockerClient.Containers.ListContainersAsync( new ContainersListParameters { Filters = new Dictionary> { ["ancestor"] = new Dictionary { - [_testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID] = true + [_testFixture.Image.ID] = true } }, All = true @@ -779,26 +549,25 @@ await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( Assert.NotEmpty(containerList); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task ListProcessesAsync_RunningContainer_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task ListProcessesAsync_RunningContainer_Succeeds() { - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr }, _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token ); - var containerProcessesResponse = await _testFixture.DockerClients[clientType].Containers.ListProcessesAsync( + var containerProcessesResponse = await _testFixture.DockerClient.Containers.ListProcessesAsync( createContainerResponse.ID, new ContainerListProcessesParameters(), _testFixture.Cts.Token @@ -815,25 +584,24 @@ await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( Assert.NotEmpty(containerProcessesResponse.Processes); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task RemoveContainerAsync_ContainerExists_Succeedes(TestClientsEnum clientType) + [Fact] + public async Task RemoveContainerAsync_ContainerExists_Succeedes() { - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, _testFixture.Cts.Token ); - ContainerInspectResponse inspectCreatedContainer = await _testFixture.DockerClients[clientType].Containers.InspectContainerAsync( + ContainerInspectResponse inspectCreatedContainer = await _testFixture.DockerClient.Containers.InspectContainerAsync( createContainerResponse.ID, _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Containers.RemoveContainerAsync( + await _testFixture.DockerClient.Containers.RemoveContainerAsync( createContainerResponse.ID, new ContainerRemoveParameters { @@ -842,7 +610,7 @@ await _testFixture.DockerClients[clientType].Containers.RemoveContainerAsync( _testFixture.Cts.Token ); - Task inspectRemovedContainerTask = _testFixture.DockerClients[clientType].Containers.InspectContainerAsync( + Task inspectRemovedContainerTask = _testFixture.DockerClient.Containers.InspectContainerAsync( createContainerResponse.ID, _testFixture.Cts.Token ); @@ -851,20 +619,19 @@ await _testFixture.DockerClients[clientType].Containers.RemoveContainerAsync( await Assert.ThrowsAsync(() => inspectRemovedContainerTask); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task StartContainerAsync_ContainerExists_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task StartContainerAsync_ContainerExists_Succeeds() { - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr, }, _testFixture.Cts.Token ); - var startContainerResult = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + var startContainerResult = await _testFixture.DockerClient.Containers.StartContainerAsync( createContainerResponse.ID, new ContainerStartParameters(), _testFixture.Cts.Token @@ -873,11 +640,10 @@ public async Task StartContainerAsync_ContainerExists_Succeeds(TestClientsEnum c Assert.True(startContainerResult); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task StartContainerAsync_ContainerNotExists_ThrowsException(TestClientsEnum clientType) + [Fact] + public async Task StartContainerAsync_ContainerNotExists_ThrowsException() { - Task startContainerTask = _testFixture.DockerClients[clientType].Containers.StartContainerAsync( + Task startContainerTask = _testFixture.DockerClient.Containers.StartContainerAsync( Guid.NewGuid().ToString(), new ContainerStartParameters(), _testFixture.Cts.Token @@ -886,9 +652,8 @@ public async Task StartContainerAsync_ContainerNotExists_ThrowsException(TestCli await Assert.ThrowsAsync(() => startContainerTask); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledException(TestClientsEnum clientType) + [Fact] + public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledException() { using var waitContainerCts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); @@ -896,10 +661,10 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio var delay = TimeSpan.FromSeconds(5); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync( + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync( new CreateContainerParameters { - Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, + Image = _testFixture.Image.ID, Entrypoint = CommonCommands.EchoToStdoutAndStderr }, waitContainerCts.Token @@ -907,7 +672,7 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio _testOutputHelper.WriteLine($"CreateContainerResponse: '{JsonSerializer.Instance.Serialize(createContainerResponse)}'"); - _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), waitContainerCts.Token); + _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters(), waitContainerCts.Token); _testOutputHelper.WriteLine("Starting timeout to cancel WaitContainer operation."); @@ -915,7 +680,7 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio stopWatch.Start(); // Will wait forever here if cancellation fails. - var waitContainerTask = _testFixture.DockerClients[clientType].Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token); + var waitContainerTask = _testFixture.DockerClient.Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token); _ = await Assert.ThrowsAsync(() => waitContainerTask); @@ -931,27 +696,25 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio Assert.True(waitContainerTask.IsCanceled); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task CreateImageAsync_NonExistingImage_ThrowsDockerImageNotFoundException(TestClientsEnum clientType) + [Fact] + public async Task CreateImageAsync_NonExistingImage_ThrowsDockerImageNotFoundException() { var createContainerParameters = new CreateContainerParameters(); createContainerParameters.Image = Guid.NewGuid().ToString("D"); - Func op = () => _testFixture.DockerClients[clientType].Containers.CreateContainerAsync(createContainerParameters); + Func op = () => _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); await Assert.ThrowsAsync(op); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToPid1Stdin_CompletesPid1Process(TestClientsEnum clientType) + [Fact] + public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToPid1Stdin_CompletesPid1Process() { // Given var linefeedByte = new byte[] { 10 }; var createContainerParameters = new CreateContainerParameters(); - createContainerParameters.Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID; + createContainerParameters.Image = _testFixture.Image.ID; createContainerParameters.Entrypoint = new[] { "/bin/sh", "-c" }; createContainerParameters.Cmd = new[] { "read line; echo Done" }; createContainerParameters.OpenStdin = true; @@ -964,32 +727,31 @@ public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToPid1Stdin_Comple containerAttachParameters.Stream = true; // When - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync(createContainerParameters); - _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); + _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); - using var stream = await _testFixture.DockerClients[clientType].Containers.AttachContainerAsync(createContainerResponse.ID, containerAttachParameters); + using var stream = await _testFixture.DockerClient.Containers.AttachContainerAsync(createContainerResponse.ID, containerAttachParameters); await stream.WriteAsync(linefeedByte, 0, linefeedByte.Length, _testFixture.Cts.Token); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var (stdout, _) = await stream.ReadOutputToEndAsync(cts.Token); - var containerInspectResponse = await _testFixture.DockerClients[clientType].Containers.InspectContainerAsync(createContainerResponse.ID, _testFixture.Cts.Token); + var containerInspectResponse = await _testFixture.DockerClient.Containers.InspectContainerAsync(createContainerResponse.ID, _testFixture.Cts.Token); // Then Assert.Equal(0, containerInspectResponse.State.ExitCode); Assert.Equal("Done\n", stdout); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToExecStdin_CompletesExecProcess(TestClientsEnum clientType) + [Fact] + public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToExecStdin_CompletesExecProcess() { // Given var linefeedByte = new byte[] { 10 }; var createContainerParameters = new CreateContainerParameters(); - createContainerParameters.Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID; + createContainerParameters.Image = _testFixture.Image.ID; createContainerParameters.Entrypoint = CommonCommands.SleepInfinity; var containerExecCreateParameters = new ContainerExecCreateParameters(); @@ -1000,19 +762,19 @@ public async Task WriteAsync_OnMultiplexedStream_ForwardsInputToExecStdin_Comple var containerExecStartParameters = new ContainerExecStartParameters(); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync(createContainerParameters); - _ = await _testFixture.DockerClients[clientType].Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(createContainerParameters); + _ = await _testFixture.DockerClient.Containers.StartContainerAsync(createContainerResponse.ID, new ContainerStartParameters()); // When - var containerExecCreateResponse = await _testFixture.DockerClients[clientType].Exec.CreateContainerExecAsync(createContainerResponse.ID, containerExecCreateParameters); - using var stream = await _testFixture.DockerClients[clientType].Exec.StartContainerExecAsync(containerExecCreateResponse.ID, containerExecStartParameters); + var containerExecCreateResponse = await _testFixture.DockerClient.Exec.CreateContainerExecAsync(createContainerResponse.ID, containerExecCreateParameters); + using var stream = await _testFixture.DockerClient.Exec.StartContainerExecAsync(containerExecCreateResponse.ID, containerExecStartParameters); await stream.WriteAsync(linefeedByte, 0, linefeedByte.Length, _testFixture.Cts.Token); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var (stdout, _) = await stream.ReadOutputToEndAsync(cts.Token); - var containerExecInspectResponse = await _testFixture.DockerClients[clientType].Exec.InspectContainerExecAsync(containerExecCreateResponse.ID, _testFixture.Cts.Token); + var containerExecInspectResponse = await _testFixture.DockerClient.Exec.InspectContainerExecAsync(containerExecCreateResponse.ID, _testFixture.Cts.Token); // Then Assert.Equal(0, containerExecInspectResponse.ExitCode); diff --git a/test/Docker.DotNet.Tests/IImageOperationsTests.cs b/test/Docker.DotNet.Tests/IImageOperationsTests.cs index 7b1061a8..50d522f7 100644 --- a/test/Docker.DotNet.Tests/IImageOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IImageOperationsTests.cs @@ -12,19 +12,15 @@ public IImageOperationsTests(TestFixture testFixture, ITestOutputHelper testOutp _testOutputHelper = testOutputHelper; } - public static IEnumerable GetDockerClientTypes() => - TestFixture.GetDockerClientTypes(); - - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task CreateImageAsync_TaskCancelled_ThrowsTaskCanceledException(TestClientsEnum clientType) + [Fact] + public async Task CreateImageAsync_TaskCancelled_ThrowsTaskCanceledException() { using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var newTag = Guid.NewGuid().ToString(); var newRepositoryName = Guid.NewGuid().ToString(); - await _testFixture.DockerClients[clientType].Images.TagImageAsync( + await _testFixture.DockerClient.Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -34,7 +30,7 @@ await _testFixture.DockerClients[clientType].Images.TagImageAsync( cts.Token ); - var createImageTask = _testFixture.DockerClients[clientType].Images.CreateImageAsync( + var createImageTask = _testFixture.DockerClient.Images.CreateImageAsync( new ImagesCreateParameters { FromImage = $"{newRepositoryName}:{newTag}" @@ -51,11 +47,10 @@ await _testFixture.DockerClients[clientType].Images.TagImageAsync( Assert.True(createImageTask.IsCanceled); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public Task CreateImageAsync_ErrorResponse_ThrowsDockerApiException(TestClientsEnum clientType) + [Fact] + public Task CreateImageAsync_ErrorResponse_ThrowsDockerApiException() { - return Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].Images.CreateImageAsync( + return Assert.ThrowsAsync(() => _testFixture.DockerClient.Images.CreateImageAsync( new ImagesCreateParameters { FromImage = "1.2.3.Apparently&this$is+not-a_valid%repository//name", @@ -63,13 +58,12 @@ public Task CreateImageAsync_ErrorResponse_ThrowsDockerApiException(TestClientsE }, null, null)); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task DeleteImageAsync_RemovesImage(TestClientsEnum clientType) + [Fact] + public async Task DeleteImageAsync_RemovesImage() { var newImageTag = Guid.NewGuid().ToString(); - await _testFixture.DockerClients[clientType].Images.TagImageAsync( + await _testFixture.DockerClient.Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -79,18 +73,18 @@ await _testFixture.DockerClients[clientType].Images.TagImageAsync( _testFixture.Cts.Token ); - var inspectExistingImageResponse = await _testFixture.DockerClients[clientType].Images.InspectImageAsync( + var inspectExistingImageResponse = await _testFixture.DockerClient.Images.InspectImageAsync( $"{_testFixture.Repository}:{newImageTag}", _testFixture.Cts.Token ); - await _testFixture.DockerClients[clientType].Images.DeleteImageAsync( + await _testFixture.DockerClient.Images.DeleteImageAsync( $"{_testFixture.Repository}:{newImageTag}", new ImageDeleteParameters(), _testFixture.Cts.Token ); - Task inspectDeletedImageTask = _testFixture.DockerClients[clientType].Images.InspectImageAsync( + Task inspectDeletedImageTask = _testFixture.DockerClient.Images.InspectImageAsync( $"{_testFixture.Repository}:{newImageTag}", _testFixture.Cts.Token ); diff --git a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs index 58c5f44c..e620446c 100644 --- a/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs +++ b/test/Docker.DotNet.Tests/ISwarmOperationsTests.cs @@ -12,43 +12,39 @@ public ISwarmOperationsTests(TestFixture testFixture, ITestOutputHelper testOutp _testOutputHelper = testOutputHelper; } - public static IEnumerable GetDockerClientTypes() => - TestFixture.GetDockerClientTypes(); - - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetFilteredServicesByName_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task GetFilteredServicesByName_Succeeds() { var serviceName = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}"; - var firstServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = serviceName, - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var secondServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var thirdServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var services = await _testFixture.DockerClients[clientType].Swarm.ListServicesAsync(new ServiceListParameters + var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(new ServiceListParameters { Filters = new Dictionary> { @@ -61,43 +57,42 @@ public async Task GetFilteredServicesByName_Succeeds(TestClientsEnum clientType) Assert.Single(services); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetFilteredServicesById_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task GetFilteredServicesById_Succeeds() { - var firstServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var secondServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var thirdServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var services = await _testFixture.DockerClients[clientType].Swarm.ListServicesAsync(new ServiceListParameters + var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(new ServiceListParameters { Filters = new Dictionary> { @@ -110,71 +105,69 @@ public async Task GetFilteredServicesById_Succeeds(TestClientsEnum clientType) Assert.Single(services); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetServices_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task GetServices_Succeeds() { - var initialServiceCount = (await _testFixture.DockerClients[clientType].Swarm.ListServicesAsync(cancellationToken: CancellationToken.None)).Count(); + var initialServiceCount = (await _testFixture.DockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None)).Count(); - var firstServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var firstServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service1-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var secondServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var secondServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service2-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var thirdServiceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var thirdServiceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = $"service3-{Guid.NewGuid().ToString().Substring(1, 10)}", - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID } } } })).ID; - var services = await _testFixture.DockerClients[clientType].Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); + var services = await _testFixture.DockerClient.Swarm.ListServicesAsync(cancellationToken: CancellationToken.None); Assert.True(services.Count() > initialServiceCount); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(firstServiceId); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(secondServiceId); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(thirdServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(firstServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(secondServiceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(thirdServiceId); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetServiceLogs_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task GetServiceLogs_Succeeds() { var cts = new CancellationTokenSource(); var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token, cts.Token); var serviceName = $"service-withLogs-{Guid.NewGuid().ToString().Substring(1, 10)}"; - var serviceId = (await _testFixture.DockerClients[clientType].Swarm.CreateServiceAsync(new ServiceCreateParameters + var serviceId = (await _testFixture.DockerClient.Swarm.CreateServiceAsync(new ServiceCreateParameters { Service = new ServiceSpec { Name = serviceName, - TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Images[TestFixture.GetDaemonForClient(clientType)].ID, Command = CommonCommands.EchoToStdoutAndStderr } } + TaskTemplate = new TaskSpec { ContainerSpec = new ContainerSpec { Image = _testFixture.Image.ID, Command = CommonCommands.EchoToStdoutAndStderr } } } })).ID; - using var stream = await _testFixture.DockerClients[clientType].Swarm.GetServiceLogsAsync(serviceName, false, new ServiceLogsParameters + using var stream = await _testFixture.DockerClient.Swarm.GetServiceLogsAsync(serviceName, false, new ServiceLogsParameters { Follow = true, ShowStdout = true, @@ -253,6 +246,6 @@ public async Task GetServiceLogs_Succeeds(TestClientsEnum clientType) Assert.NotNull(logLines); Assert.NotEmpty(logLines); - await _testFixture.DockerClients[clientType].Swarm.RemoveServiceAsync(serviceId); + await _testFixture.DockerClient.Swarm.RemoveServiceAsync(serviceId); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs index 65aea9b4..3609a79c 100644 --- a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs +++ b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs @@ -12,39 +12,29 @@ public ISystemOperationsTests(TestFixture testFixture, ITestOutputHelper testOut _testOutputHelper = testOutputHelper; } - public static IEnumerable GetDockerClientTypes() => - TestFixture.GetDockerClientTypes(); - [Fact] public void Docker_IsRunning() { - var processNames = Process.GetProcesses().Select(Process => Process.ProcessName); - var dockerProcess = processNames.FirstOrDefault( - name => name.Equals("docker", StringComparison.InvariantCultureIgnoreCase) - || name.Equals("com.docker.service", StringComparison.InvariantCultureIgnoreCase) - || name.Equals("dockerd", StringComparison.InvariantCultureIgnoreCase)); + var dockerProcess = Process.GetProcesses().FirstOrDefault(process => process.ProcessName.Equals("docker", StringComparison.InvariantCultureIgnoreCase) || process.ProcessName.Equals("dockerd", StringComparison.InvariantCultureIgnoreCase)); Assert.NotNull(dockerProcess); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetSystemInfoAsync_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task GetSystemInfoAsync_Succeeds() { - var info = await _testFixture.DockerClients[clientType].System.GetSystemInfoAsync(); + var info = await _testFixture.DockerClient.System.GetSystemInfoAsync(); Assert.NotNull(info.Architecture); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task GetVersionAsync_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task GetVersionAsync_Succeeds() { - var version = await _testFixture.DockerClients[clientType].System.GetVersionAsync(); + var version = await _testFixture.DockerClient.System.GetVersionAsync(); Assert.NotNull(version.APIVersion); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled(TestClientsEnum clientType) + [Fact] + public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled() { var progress = new Progress(); @@ -52,27 +42,24 @@ public async Task MonitorEventsAsync_EmptyContainersList_CanBeCancelled(TestClie await cts.CancelAsync(); await Task.Delay(1); - await Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token)); + await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), progress, cts.Token)); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task MonitorEventsAsync_NullParameters_Throws(TestClientsEnum clientType) + [Fact] + public async Task MonitorEventsAsync_NullParameters_Throws() { - await Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].System.MonitorEventsAsync(null, null)); + await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(null, null)); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task MonitorEventsAsync_NullProgress_Throws(TestClientsEnum clientType) + [Fact] + public async Task MonitorEventsAsync_NullProgress_Throws() { - await Assert.ThrowsAsync(() => _testFixture.DockerClients[clientType].System.MonitorEventsAsync(new ContainerEventsParameters(), null)); + await Assert.ThrowsAsync(() => _testFixture.DockerClient.System.MonitorEventsAsync(new ContainerEventsParameters(), null)); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task MonitorEventsAsync_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task MonitorEventsAsync_Succeeds() { var newTag = $"MonitorTests-{Guid.NewGuid().ToString().Substring(1, 10)}"; @@ -87,14 +74,14 @@ public async Task MonitorEventsAsync_Succeeds(TestClientsEnum clientType) using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); - var task = _testFixture.DockerClients[clientType].System.MonitorEventsAsync( + var task = _testFixture.DockerClient.System.MonitorEventsAsync( new ContainerEventsParameters(), progressMessage, cts.Token); - await _testFixture.DockerClients[clientType].Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }, _testFixture.Cts.Token); + await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }, _testFixture.Cts.Token); - await _testFixture.DockerClients[clientType].Images.DeleteImageAsync( + await _testFixture.DockerClient.Images.DeleteImageAsync( name: $"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters { @@ -112,9 +99,8 @@ await _testFixture.DockerClients[clientType].Images.DeleteImageAsync( Assert.True(wasProgressCalled); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption(TestClientsEnum clientType) + [Fact] + public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption() { var stopwatch = new Stopwatch(); @@ -127,7 +113,7 @@ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption(TestClientsE string newImageTag = Guid.NewGuid().ToString(); - var monitorTask = _testFixture.DockerClients[clientType].System.MonitorEventsAsync( + var monitorTask = _testFixture.DockerClient.System.MonitorEventsAsync( new ContainerEventsParameters(), new Progress(value => _testOutputHelper.WriteLine($"DockerSystemEvent: {JsonSerializer.Instance.Serialize(value)}")), cts.Token); @@ -136,7 +122,7 @@ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption(TestClientsE await Task.Delay(100, CancellationToken.None); // (3) Invoke another request that will attempt to grab the same buffer - var listImagesTask1 = _testFixture.DockerClients[clientType].Images.TagImageAsync( + var listImagesTask1 = _testFixture.DockerClient.Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -155,7 +141,7 @@ public async Task MonitorEventsAsync_IsCancelled_NoStreamCorruption(TestClientsE await listImagesTask1; - await _testFixture.DockerClients[clientType].Images.TagImageAsync( + await _testFixture.DockerClient.Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -172,14 +158,13 @@ await _testFixture.DockerClients[clientType].Images.TagImageAsync( } } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task MonitorEventsFiltered_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task MonitorEventsFiltered_Succeeds() { string newTag = $"MonitorTests-{Guid.NewGuid().ToString().Substring(1, 10)}"; string newImageRepositoryName = Guid.NewGuid().ToString(); - await _testFixture.DockerClients[clientType].Images.TagImageAsync( + await _testFixture.DockerClient.Images.TagImageAsync( $"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { @@ -189,7 +174,7 @@ await _testFixture.DockerClients[clientType].Images.TagImageAsync( _testFixture.Cts.Token ); - ImageInspectResponse image = await _testFixture.DockerClients[clientType].Images.InspectImageAsync( + ImageInspectResponse image = await _testFixture.DockerClient.Images.InspectImageAsync( $"{newImageRepositoryName}:{newTag}", _testFixture.Cts.Token ); @@ -238,15 +223,13 @@ await _testFixture.DockerClients[clientType].Images.TagImageAsync( }); using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); - var task = Task.Run(() => _testFixture.DockerClients[clientType].System.MonitorEventsAsync(eventsParams, progress, cts.Token)); + var task = Task.Run(() => _testFixture.DockerClient.System.MonitorEventsAsync(eventsParams, progress, cts.Token)); - await Task.Delay(TimeSpan.FromSeconds(1)); - - await _testFixture.DockerClients[clientType].Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }); - await _testFixture.DockerClients[clientType].Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters()); + await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }); + await _testFixture.DockerClient.Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters()); - var createContainerResponse = await _testFixture.DockerClients[clientType].Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_testFixture.Repository}:{_testFixture.Tag}", Entrypoint = CommonCommands.SleepInfinity }); - await _testFixture.DockerClients[clientType].Containers.RemoveContainerAsync(createContainerResponse.ID, new ContainerRemoveParameters(), cts.Token); + var createContainerResponse = await _testFixture.DockerClient.Containers.CreateContainerAsync(new CreateContainerParameters { Image = $"{_testFixture.Repository}:{_testFixture.Tag}", Entrypoint = CommonCommands.SleepInfinity }); + await _testFixture.DockerClient.Containers.RemoveContainerAsync(createContainerResponse.ID, new ContainerRemoveParameters(), cts.Token); await Task.Delay(TimeSpan.FromSeconds(1)); await cts.CancelAsync(); @@ -257,10 +240,9 @@ await _testFixture.DockerClients[clientType].Images.TagImageAsync( Assert.True(task.IsCanceled); } - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task PingAsync_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task PingAsync_Succeeds() { - await _testFixture.DockerClients[clientType].System.PingAsync(); + await _testFixture.DockerClient.System.PingAsync(); } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs b/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs index 9960f7a7..e77ad7d1 100644 --- a/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IVolumeOperationsTests.cs @@ -12,34 +12,30 @@ public IVolumeOperationsTests(TestFixture testFixture, ITestOutputHelper testOut _testOutputHelper = testOutputHelper; } - public static IEnumerable GetDockerClientTypes() => - TestFixture.GetDockerClientTypes(); - - [Theory] - [MemberData(nameof(GetDockerClientTypes))] - public async Task ListAsync_VolumeExists_Succeeds(TestClientsEnum clientType) + [Fact] + public async Task ListAsync_VolumeExists_Succeeds() { const string volumeName = "docker-dotnet-test-volume"; - await _testFixture.DockerClients[clientType].Volumes.CreateAsync(new VolumesCreateParameters - { - Name = volumeName, - }, + await _testFixture.DockerClient.Volumes.CreateAsync(new VolumesCreateParameters + { + Name = volumeName, + }, _testFixture.Cts.Token); try { - var response = await _testFixture.DockerClients[clientType].Volumes.ListAsync(new VolumesListParameters - { - Filters = new Dictionary>(), - }, + var response = await _testFixture.DockerClient.Volumes.ListAsync(new VolumesListParameters + { + Filters = new Dictionary>(), + }, _testFixture.Cts.Token); Assert.Contains(volumeName, response.Volumes.Select(volume => volume.Name)); } finally { - await _testFixture.DockerClients[clientType].Volumes.RemoveAsync(volumeName, force: true, _testFixture.Cts.Token); + await _testFixture.DockerClient.Volumes.RemoveAsync(volumeName, force: true, _testFixture.Cts.Token); } } } \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index 4aa22768..b32ecd6e 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -11,9 +11,7 @@ public sealed class TestFixture : Progress, IAsyncLifetime, IDispos private readonly IMessageSink _messageSink; - private Dictionary _isInitialized = new(); - private Dictionary _isDisposed = new(); - private Dictionary _hasInitializedSwarm = new(); + private bool _hasInitializedSwarm; /// /// Initializes a new instance of the class. @@ -23,50 +21,12 @@ public sealed class TestFixture : Progress, IAsyncLifetime, IDispos public TestFixture(IMessageSink messageSink) { _messageSink = messageSink; - - DockerClients = new Dictionary - { - { TestClientsEnum.ManagedPipe, new DockerClientConfiguration().CreateClient(null, OperatingSystem.IsWindows() ? new NpipeHandlerFactory() : new UnixHandlerFactory(), logger: this) }, - { TestClientsEnum.ManagedHttp, new DockerClientConfiguration(endpoint: new Uri("http://localhost:2375")).CreateClient(null, new LegacyHttpHandlerFactory(), this) }, - { TestClientsEnum.NativeHttp, new DockerClientConfiguration(endpoint: new Uri("http://localhost:2375")).CreateClient(null, new NativeHttpHandlerFactory(), logger: this) }, - }; - - try - { -#if NET9_0_OR_GREATER - var credentials = new CertificateCredentials(X509CertificateLoader.LoadPkcs12FromFile(Path.Combine("/home/runner/certs/client.pfx"), "")) - { - ServerCertificateValidationCallback = ValidateServerCertificate - }; -#else - var credentials = new CertificateCredentials(new X509Certificate2("/home/runner/certs/client.pfx", "")) - { - ServerCertificateValidationCallback = ValidateServerCertificate - }; -#endif - DockerClients.Add(TestClientsEnum.ManagedHttps, new DockerClientConfiguration(endpoint: new Uri("http://localhost:2376"), credentials).CreateClient(null, new LegacyHttpHandlerFactory(), this)); - DockerClients.Add(TestClientsEnum.NativeHttps, new DockerClientConfiguration(endpoint: new Uri("http://localhost:2376"), credentials).CreateClient(null, new NativeHttpHandlerFactory(), logger: this)); - } - catch (Exception ex) - { - this.LogWarning(ex, "Couldn't init tls clients because of certificate errors."); - } - - - Images = new Dictionary(); - Cts = new CancellationTokenSource(TimeSpan.FromMinutes(10)); + DockerClientConfiguration = new DockerClientConfiguration(); + DockerClient = DockerClientConfiguration.CreateClient(logger: this); + Cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); Cts.Token.Register(() => throw new TimeoutException("Docker.DotNet tests timed out.")); } - internal static bool ValidateServerCertificate( - object sender, - X509Certificate cert, - X509Chain chain, - SslPolicyErrors sslPolicyErrors) - { - return true; - } - /// /// Gets the Docker image repository. /// @@ -80,9 +40,14 @@ internal static bool ValidateServerCertificate( = Guid.NewGuid().ToString("N"); /// - /// Gets the Docker clients. + /// Gets the Docker client configuration. /// - public Dictionary DockerClients { get; } + public DockerClientConfiguration DockerClientConfiguration { get; } + + /// + /// Gets the Docker client. + /// + public DockerClient DockerClient { get; } /// /// Gets the cancellation token source. @@ -92,7 +57,7 @@ internal static bool ValidateServerCertificate( /// /// Gets or sets the Docker image. /// - public Dictionary Images { get; } + public ImagesListResponse Image { get; private set; } /// public async Task InitializeAsync() @@ -101,192 +66,94 @@ public async Task InitializeAsync() const string tag = "3.20"; - foreach (TestDaemonsEnum daemon in Enum.GetValues(typeof(TestDaemonsEnum))) - { - if (_isInitialized.TryGetValue(daemon, out var value) && value) - continue; + // Create image + await DockerClient.Images.CreateImageAsync(new ImagesCreateParameters { FromImage = repository, Tag = tag }, null, this, Cts.Token) + .ConfigureAwait(false); - // Create image - await DockerClients[GetClientForDaemon(daemon)].Images.CreateImageAsync(new ImagesCreateParameters { FromImage = repository, Tag = tag }, null, this, Cts.Token) - .ConfigureAwait(false); - - // Get images - var images = await DockerClients[GetClientForDaemon(daemon)].Images.ListImagesAsync( - new ImagesListParameters + // Get images + var images = await DockerClient.Images.ListImagesAsync( + new ImagesListParameters + { + Filters = new Dictionary> { - Filters = new Dictionary> + ["reference"] = new Dictionary { - ["reference"] = new Dictionary - { - [repository + ":" + tag] = true - } + [repository + ":" + tag] = true } - }, Cts.Token) - .ConfigureAwait(false); - - // Set image - Images.Add(daemon, images.Single()); - - // Tag image - await DockerClients[GetClientForDaemon(daemon)].Images.TagImageAsync(Images[daemon].ID, new ImageTagParameters { RepositoryName = Repository, Tag = Tag }, Cts.Token) - .ConfigureAwait(false); - - // Init a new swarm, if not part of an existing one - try - { - _ = await DockerClients[GetClientForDaemon(daemon)].Swarm.InitSwarmAsync(new SwarmInitParameters { AdvertiseAddr = "10.10.10.10", ListenAddr = "127.0.0.1" }, Cts.Token) - .ConfigureAwait(false); - - _hasInitializedSwarm.Add(daemon, true); - } - catch - { - this.LogInformation("Couldn't init a new swarm, the node should take part of an existing one."); + } + }, Cts.Token) + .ConfigureAwait(false); - _hasInitializedSwarm.Add(daemon, false); - } + // Set image + Image = images.Single(); - _isInitialized.Add(daemon, false); - } - } + // Tag image + await DockerClient.Images.TagImageAsync(Image.ID, new ImageTagParameters { RepositoryName = Repository, Tag = Tag }, Cts.Token) + .ConfigureAwait(false); - public static TestDaemonsEnum GetDaemonForClient(TestClientsEnum client) - { - if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true") - { - return client switch - { - TestClientsEnum.ManagedPipe => TestDaemonsEnum.Local, - TestClientsEnum.ManagedHttp => TestDaemonsEnum.DindHttp, - TestClientsEnum.NativeHttp => TestDaemonsEnum.DindHttp, - TestClientsEnum.ManagedHttps => TestDaemonsEnum.DindHttps, - TestClientsEnum.NativeHttps => TestDaemonsEnum.DindHttps, - _ => throw new ArgumentOutOfRangeException(nameof(client), client, null) - }; - } - else + // Init a new swarm, if not part of an existing one + try { - return client switch - { - TestClientsEnum.ManagedPipe => TestDaemonsEnum.Local, - TestClientsEnum.ManagedHttp => TestDaemonsEnum.Local, - TestClientsEnum.NativeHttp => TestDaemonsEnum.Local, - _ => throw new ArgumentOutOfRangeException(nameof(client), client, null) - }; - } - } + _ = await DockerClient.Swarm.InitSwarmAsync(new SwarmInitParameters { AdvertiseAddr = "10.10.10.10", ListenAddr = "127.0.0.1" }, Cts.Token) + .ConfigureAwait(false); - public static TestClientsEnum GetClientForDaemon(TestDaemonsEnum daemon) - { - if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true") - { - return daemon switch - { - TestDaemonsEnum.Local => TestClientsEnum.ManagedPipe, - TestDaemonsEnum.DindHttp => TestClientsEnum.ManagedHttp, - TestDaemonsEnum.DindHttps => TestClientsEnum.ManagedHttps, - _ => throw new ArgumentOutOfRangeException(nameof(daemon), daemon, null) - - }; - } - else - { - return daemon switch - { - TestDaemonsEnum.Local => TestClientsEnum.ManagedPipe, - TestDaemonsEnum.DindHttp => TestClientsEnum.ManagedPipe, - TestDaemonsEnum.DindHttps => TestClientsEnum.ManagedPipe, - _ => throw new ArgumentOutOfRangeException(nameof(daemon), daemon, null) - }; + _hasInitializedSwarm = true; } - } - - public static IEnumerable GetDockerClientTypes() - { - var allClients = Enum.GetValues(typeof(TestClientsEnum)) - .Cast(); - - if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") != "true") + catch { - return allClients - .Where(t => t == TestClientsEnum.ManagedPipe || - t == TestClientsEnum.ManagedHttp || - t == TestClientsEnum.NativeHttp) - .Select(t => new object[] { t }); - } - - return allClients.Select(t => new object[] { t }); - } + this.LogInformation("Couldn't init a new swarm, the node should take part of an existing one."); - public static IEnumerable GetDockerDaemonTypes() - { - var allDaemons = Enum.GetValues(typeof(TestDaemonsEnum)) - .Cast(); - - if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") != "true") - { - return allDaemons - .Where(t => t == TestDaemonsEnum.Local); + _hasInitializedSwarm = false; } - - return allDaemons; } /// public async Task DisposeAsync() { - foreach (TestDaemonsEnum daemon in GetDockerDaemonTypes()) + if (_hasInitializedSwarm) { - if (_isDisposed.TryGetValue(daemon, out var disposed) && disposed) - continue; - - if (true || _hasInitializedSwarm.TryGetValue(daemon, out var swarm) && swarm) - { - await DockerClients[GetClientForDaemon(daemon)].Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true }, Cts.Token) - .ConfigureAwait(false); - } + await DockerClient.Swarm.LeaveSwarmAsync(new SwarmLeaveParameters { Force = true }, Cts.Token) + .ConfigureAwait(false); + } - var containers = await DockerClients[GetClientForDaemon(daemon)].Containers.ListContainersAsync( - new ContainersListParameters + var containers = await DockerClient.Containers.ListContainersAsync( + new ContainersListParameters + { + Filters = new Dictionary> { - Filters = new Dictionary> + ["ancestor"] = new Dictionary { - ["ancestor"] = new Dictionary - { - [Images[daemon].ID] = true - } - }, - All = true - }, Cts.Token) - .ConfigureAwait(false); - - var images = await DockerClients[GetClientForDaemon(daemon)].Images.ListImagesAsync( - new ImagesListParameters + [Image.ID] = true + } + }, + All = true + }, Cts.Token) + .ConfigureAwait(false); + + var images = await DockerClient.Images.ListImagesAsync( + new ImagesListParameters + { + Filters = new Dictionary> { - Filters = new Dictionary> + ["reference"] = new Dictionary { - ["reference"] = new Dictionary - { - [Images[daemon].ID] = true - } - }, - All = true - }, Cts.Token) - .ConfigureAwait(false); - - foreach (var container in containers) - { - await DockerClients[GetClientForDaemon(daemon)].Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters { Force = true }, Cts.Token) - .ConfigureAwait(false); - } + [Image.ID] = true + } + }, + All = true + }, Cts.Token) + .ConfigureAwait(false); - foreach (var image in images) - { - await DockerClients[GetClientForDaemon(daemon)].Images.DeleteImageAsync(image.ID, new ImageDeleteParameters { Force = true }, Cts.Token) - .ConfigureAwait(false); - } + foreach (var container in containers) + { + await DockerClient.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters { Force = true }, Cts.Token) + .ConfigureAwait(false); + } - _isDisposed.Add(daemon, true); + foreach (var image in images) + { + await DockerClient.Images.DeleteImageAsync(image.ID, new ImageDeleteParameters { Force = true }, Cts.Token) + .ConfigureAwait(false); } } @@ -294,11 +161,8 @@ await DockerClients[GetClientForDaemon(daemon)].Swarm.LeaveSwarmAsync(new SwarmL public void Dispose() { Cts.Dispose(); - foreach (var client in DockerClients.Values) - { - client?.Dispose(); - } - DockerClients.Clear(); + DockerClient.Dispose(); + DockerClientConfiguration.Dispose(); } /// From cc1c61aa56eb16cececa28896286af525896efee Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:23:10 +0100 Subject: [PATCH 40/50] feat: Pass configurations to test --- .github/workflows/ci.yml | 81 ++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd68be13..3112a9c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-24.04 services: # Docker without TLS (plain TCP) !DEPRECATED! with next docker release - docker-no-tls: + docker-without-tls: image: docker:29.1.1-dind env: DOCKER_TLS_CERTDIR: "" @@ -20,7 +20,7 @@ jobs: --privileged # Docker with TLS (secure TCP) - docker-tls: + docker-with-tls: image: docker:29.1.1-dind env: DOCKER_TLS_CERTDIR: /certs @@ -32,14 +32,46 @@ jobs: - /home/runner/certs:/certs strategy: + fail-fast: false matrix: - include: + dotnet: - sdk: 8.x tfm: net8.0 - sdk: 9.x tfm: net9.0 - sdk: 10.x tfm: net10.0 + docker: + - name: unix + docker_host: unix:///var/run/docker.sock + tls_verify: "" + cert_path: "" + native_http: 0 + needs_dind: false + - name: tcp-2375 + docker_host: tcp://localhost:2375 + tls_verify: "" + cert_path: "" + native_http: 0 + needs_dind: true + - name: tcp-2376-tls + docker_host: tcp://localhost:2376 + tls_verify: 1 + cert_path: /home/runner/certs/client + native_http: 0 + needs_dind: true + - name: tcp-2375-native + docker_host: tcp://localhost:2375 + tls_verify: "" + cert_path: "" + native_http: 1 + needs_dind: true + - name: tcp-2376-tls-native + docker_host: tcp://localhost:2376 + tls_verify: 1 + cert_path: /home/runner/certs/client + native_http: 1 + needs_dind: true steps: - uses: actions/checkout@v6 @@ -49,12 +81,16 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v5 with: - dotnet-version: ${{ matrix.sdk }} + dotnet-version: ${{ matrix.dotnet.sdk }} - name: Build - run: dotnet build -c Release --framework ${{ matrix.tfm }} + run: >- + dotnet build + --configuration Release + --framework ${{ matrix.dotnet.tfm }} - - name: Pack client cert, key, ca for .NET Docker client + - name: Create client PKCS#12 bundle + if: ${{ matrix.docker.tls_verify == 1 }} run: | sudo chown -R $USER:$USER $HOME/certs openssl pkcs12 -export \ @@ -64,34 +100,47 @@ jobs: -certfile "$HOME/certs/client/ca.pem" \ -passout pass: - - name: Wait for Docker (no TLS) to be healthy + - name: Wait for Docker to be healthy (2375) + if: ${{ matrix.docker.needs_dind && matrix.docker.docker_host == 'tcp://localhost:2375' }} run: | for i in {1..10}; do if docker --host=tcp://localhost:2375 version; then - echo "Docker (no TLS) is ready!" + echo "Docker is ready on port 2375" exit 0 fi - echo "Waiting for Docker (no TLS) to be ready..." + echo "Waiting for Docker on port 2375..." sleep 3 done - echo "Docker (no TLS) did not become ready in time." + echo "Docker on port 2375 did not become ready in time." exit 1 - - name: Wait for Docker (with TLS) to be healthy + - name: Wait for Docker to be healthy (2376) + if: ${{ matrix.docker.needs_dind && matrix.docker.docker_host == 'tcp://localhost:2376' }} run: | for i in {1..10}; do if docker --host=tcp://localhost:2376 --tlsverify \ --tlscacert="$HOME/certs/client/ca.pem" \ --tlscert="$HOME/certs/client/cert.pem" \ --tlskey="$HOME/certs/client/key.pem" version; then - echo "Docker (with TLS) is ready!" + echo "Docker is ready on port 2376" exit 0 fi - echo "Waiting for Docker (with TLS) to be ready..." + echo "Waiting for Docker on port 2376..." sleep 3 done - echo "Docker (with TLS) did not become ready in time." + echo "Docker on port 2376 did not become ready in time." exit 1 - - name: Test - run: dotnet test -c Release --framework ${{ matrix.tfm }} --no-build --logger console + - name: Test (${{ matrix.docker.name }}) + run: >- + dotnet test + --configuration Release + --framework ${{ matrix.dotnet.tfm }} + --no-restore + --no-build + --logger console + env: + DOCKER_HOST: ${{ matrix.docker.docker_host }} + DOCKER_TLS_VERIFY: ${{ matrix.docker.tls_verify }} + DOCKER_CERT_PATH: ${{ matrix.docker.cert_path }} + DOCKER_DOTNET_NATIVE_HTTP_ENABLED: ${{ matrix.docker.native_http }} From 55e68be43eb50081eedd5b1449c4f8ef4c3e832b Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:06:48 +0100 Subject: [PATCH 41/50] feat: Support Docker env var --- .github/workflows/ci.yml | 2 +- src/Docker.DotNet.X509/RSAUtil.cs | 26 ------ src/Docker.DotNet.X509/RsaUtil.cs | 35 ++++++++ .../Docker.DotNet.Tests.csproj | 1 + test/Docker.DotNet.Tests/TestFixture.cs | 81 ++++++++++++++++++- 5 files changed, 117 insertions(+), 28 deletions(-) delete mode 100644 src/Docker.DotNet.X509/RSAUtil.cs create mode 100644 src/Docker.DotNet.X509/RsaUtil.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3112a9c2..0559e652 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,7 @@ jobs: run: | sudo chown -R $USER:$USER $HOME/certs openssl pkcs12 -export \ - -out "$HOME/certs/client.pfx" \ + -out "$HOME/certs/client/client.pfx" \ -inkey "$HOME/certs/client/key.pem" \ -in "$HOME/certs/client/cert.pem" \ -certfile "$HOME/certs/client/ca.pem" \ diff --git a/src/Docker.DotNet.X509/RSAUtil.cs b/src/Docker.DotNet.X509/RSAUtil.cs deleted file mode 100644 index 9f9ceda9..00000000 --- a/src/Docker.DotNet.X509/RSAUtil.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Docker.DotNet.X509; - -public static class RSAUtil -{ - public static X509Certificate2 GetCertFromPFX(string pfxFilePath, string password) - { -#if NET9_0_OR_GREATER - return X509CertificateLoader.LoadPkcs12FromFile(pfxFilePath, password); -#else - return new X509Certificate2(pfxFilePath, password); -#endif - } - - public static X509Certificate2 GetCertFromPEM(string certFilePath, string keyFilePath) - { -#if NETSTANDARD - return Polyfills.X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); -#elif NET9_0_OR_GREATER - var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); - return OperatingSystem.IsWindows() ? X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx), null) : certificate; -#elif NET6_0_OR_GREATER - var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); - return OperatingSystem.IsWindows() ? new X509Certificate2(certificate.Export(X509ContentType.Pfx)) : certificate; -#endif - } -} \ No newline at end of file diff --git a/src/Docker.DotNet.X509/RsaUtil.cs b/src/Docker.DotNet.X509/RsaUtil.cs new file mode 100644 index 00000000..79299016 --- /dev/null +++ b/src/Docker.DotNet.X509/RsaUtil.cs @@ -0,0 +1,35 @@ +namespace Docker.DotNet.X509; + +public static class RsaUtil +{ + public static X509Certificate2 GetCertFromPfx(string pfxFilePath, string password) + { +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12FromFile(pfxFilePath, password); +#else + return new X509Certificate2(pfxFilePath, password); +#endif + } + + public static X509Certificate2 GetCertFromPem(string certFilePath, string keyFilePath) + { +#if NETSTANDARD + return Polyfills.X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); +#else + return OperatingSystem.IsWindows() ? CreateWindowsCert(certFilePath, keyFilePath) : X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); +#endif + } + +#if !NETSTANDARD + private static X509Certificate2 CreateWindowsCert(string certFilePath, string keyFilePath) + { + using var cert = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); + +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), null); +#else + return new X509Certificate2(cert.Export(X509ContentType.Pfx)); +#endif + } +#endif +} \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj index 1b06c222..0c0c14ed 100644 --- a/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj +++ b/test/Docker.DotNet.Tests/Docker.DotNet.Tests.csproj @@ -29,6 +29,7 @@ + diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index b32ecd6e..4f742089 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -21,7 +21,7 @@ public sealed class TestFixture : Progress, IAsyncLifetime, IDispos public TestFixture(IMessageSink messageSink) { _messageSink = messageSink; - DockerClientConfiguration = new DockerClientConfiguration(); + DockerClientConfiguration = CreateDockerClientConfigurationFromEnvironment(); DockerClient = DockerClientConfiguration.CreateClient(logger: this); Cts = new CancellationTokenSource(TimeSpan.FromMinutes(5)); Cts.Token.Register(() => throw new TimeoutException("Docker.DotNet tests timed out.")); @@ -194,6 +194,85 @@ protected override void OnReport(JSONMessage value) this.LogInformation("Progress: '{Progress}'.", message); } + private static DockerClientConfiguration CreateDockerClientConfigurationFromEnvironment() + { + var dockerHost = Environment.GetEnvironmentVariable("DOCKER_HOST"); + + // Fall back to OS-specific default (npipe on Windows, unix socket on Linux/macOS). + if (string.IsNullOrWhiteSpace(dockerHost)) + { + return new DockerClientConfiguration(); + } + + var endpoint = new Uri(dockerHost); + var credentials = CreateCredentialsFromEnvironment(); + + return new DockerClientConfiguration(endpoint, credentials); + } + + private static Credentials CreateCredentialsFromEnvironment() + { + var tlsVerify = Environment.GetEnvironmentVariable("DOCKER_TLS_VERIFY"); + if (!string.Equals(tlsVerify, "1", StringComparison.Ordinal)) + { + return new AnonymousCredentials(); + } + + var certPath = Environment.GetEnvironmentVariable("DOCKER_CERT_PATH"); + if (string.IsNullOrWhiteSpace(certPath)) + { + throw new InvalidOperationException("DOCKER_TLS_VERIFY=1 requires DOCKER_CERT_PATH to be set."); + } + + var caPemPath = Path.Combine(certPath, "ca.pem"); + var clientCertPemPath = Path.Combine(certPath, "cert.pem"); + var clientKeyPemPath = Path.Combine(certPath, "key.pem"); + var clientPfxPath = Path.Combine(certPath, "client.pfx"); + + X509Certificate2 clientCertificate; + + if (File.Exists(clientCertPemPath) && File.Exists(clientKeyPemPath)) + { + clientCertificate = RsaUtil.GetCertFromPem(clientCertPemPath, clientKeyPemPath); + } + else if (File.Exists(clientPfxPath)) + { + clientCertificate = RsaUtil.GetCertFromPfx(clientPfxPath, string.Empty); + } + else + { + throw new FileNotFoundException($"Could not locate Docker TLS client credentials. Looked for '{clientCertPemPath}', '{clientKeyPemPath}', and '{clientPfxPath}'."); + } + + var credentials = new CertificateCredentials(clientCertificate); + + if (File.Exists(caPemPath)) + { + var caCertificate = X509Certificate2.CreateFromPemFile(caPemPath); + + credentials.ServerCertificateValidationCallback = (_, certificate, _, _) => + { + if (certificate is null) + { + return false; + } + + if (certificate is not X509Certificate2 serverCertificate2) + { + return false; + } + + using var chain = new X509Chain(); + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(caCertificate); + return chain.Build(serverCertificate2); + }; + } + + return credentials; + } + private sealed class Disposable : IDisposable { public void Dispose() From 899f3845971734f62954cff2d4732433504e08d3 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:11:45 +0100 Subject: [PATCH 42/50] chore: Remove unused enums --- test/Docker.DotNet.Tests/TestClientsEnum.cs | 11 ----------- test/Docker.DotNet.Tests/TestDaemonsEnum.cs | 9 --------- 2 files changed, 20 deletions(-) delete mode 100644 test/Docker.DotNet.Tests/TestClientsEnum.cs delete mode 100644 test/Docker.DotNet.Tests/TestDaemonsEnum.cs diff --git a/test/Docker.DotNet.Tests/TestClientsEnum.cs b/test/Docker.DotNet.Tests/TestClientsEnum.cs deleted file mode 100644 index b4af7ba5..00000000 --- a/test/Docker.DotNet.Tests/TestClientsEnum.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Docker.DotNet.Tests -{ - public enum TestClientsEnum - { - ManagedPipe = 1, - ManagedHttp = 2, - NativeHttp = 3, - ManagedHttps = 4, - NativeHttps = 5, - } -} \ No newline at end of file diff --git a/test/Docker.DotNet.Tests/TestDaemonsEnum.cs b/test/Docker.DotNet.Tests/TestDaemonsEnum.cs deleted file mode 100644 index 242a8c57..00000000 --- a/test/Docker.DotNet.Tests/TestDaemonsEnum.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Docker.DotNet.Tests -{ - public enum TestDaemonsEnum - { - Local = 1, - DindHttp = 2, - DindHttps = 3 - } -} \ No newline at end of file From 7b6256b976114be059cbaea381941788fe1fd92c Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:15:18 +0100 Subject: [PATCH 43/50] chore: Order projects in sln file --- Docker.DotNet.sln | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Docker.DotNet.sln b/Docker.DotNet.sln index 1e5c2807..fb833bc3 100644 --- a/Docker.DotNet.sln +++ b/Docker.DotNet.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.35201.131 MinimumVisualStudioVersion = 10.0.40219.1 @@ -6,25 +6,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{85990620-78A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.BasicAuth", "src\Docker.DotNet.BasicAuth\Docker.DotNet.BasicAuth.csproj", "{E1F24B25-E027-45E0-A6E1-E08138F1F95D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.BasicAuth", "src\Docker.DotNet.BasicAuth\Docker.DotNet.BasicAuth.csproj", "{E1F24B25-E027-45E0-A6E1-E08138F1F95D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.LegacyHttp", "src\Docker.DotNet.LegacyHttp\Docker.DotNet.LegacyHttp.csproj", "{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Handler.Abstractions", "src\Docker.DotNet.Handler.Abstractions\Docker.DotNet.Handler.Abstractions.csproj", "{22C42314-615F-4B11-B111-58F1D6D54F4D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.NativeHttp", "src\Docker.DotNet.NativeHttp\Docker.DotNet.NativeHttp.csproj", "{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.LegacyHttp", "src\Docker.DotNet.LegacyHttp\Docker.DotNet.LegacyHttp.csproj", "{D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.NPipe", "src\Docker.DotNet.NPipe\Docker.DotNet.NPipe.csproj", "{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.NativeHttp", "src\Docker.DotNet.NativeHttp\Docker.DotNet.NativeHttp.csproj", "{E5F6A7B8-C9D0-41E2-3F45-5678901234EF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Unix", "src\Docker.DotNet.Unix\Docker.DotNet.Unix.csproj", "{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.NPipe", "src\Docker.DotNet.NPipe\Docker.DotNet.NPipe.csproj", "{A1B2C3D4-E5F6-47A8-9B0C-1234567890AB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.X509", "src\Docker.DotNet.X509\Docker.DotNet.X509.csproj", "{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Unix", "src\Docker.DotNet.Unix\Docker.DotNet.Unix.csproj", "{B2C3D4E5-F6A7-48B9-0C1D-2345678901BC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet", "src\Docker.DotNet\Docker.DotNet.csproj", "{C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.X509", "src\Docker.DotNet.X509\Docker.DotNet.X509.csproj", "{89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.Net.Http.Client", "src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.shproj", "{DAE2DE68-9B3E-4D5D-8802-EC97B94160ED}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet", "src\Docker.DotNet\Docker.DotNet.csproj", "{C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Handler.Abstractions", "src\Docker.DotNet.Handler.Abstractions\Docker.DotNet.Handler.Abstractions.csproj", "{22C42314-615F-4B11-B111-58F1D6D54F4D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Net.Http.Client", "src\Microsoft.Net.Http.Client\Microsoft.Net.Http.Client.shproj", "{DAE2DE68-9B3E-4D5D-8802-EC97B94160ED}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Docker.DotNet.Tests", "test\Docker.DotNet.Tests\Docker.DotNet.Tests.csproj", "{248C5D51-2B33-4A06-A0EA-AA709F752E52}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -48,6 +48,18 @@ Global {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x64.Build.0 = Release|Any CPU {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x86.ActiveCfg = Release|Any CPU {E1F24B25-E027-45E0-A6E1-E08138F1F95D}.Release|x86.Build.0 = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.ActiveCfg = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.Build.0 = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x86.ActiveCfg = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x86.Build.0 = Debug|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|Any CPU.Build.0 = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.ActiveCfg = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.Build.0 = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.ActiveCfg = Release|Any CPU + {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.Build.0 = Release|Any CPU {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -120,18 +132,6 @@ Global {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x64.Build.0 = Release|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.ActiveCfg = Release|Any CPU {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A}.Release|x86.Build.0 = Release|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.ActiveCfg = Debug|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x64.Build.0 = Debug|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x86.ActiveCfg = Debug|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Debug|x86.Build.0 = Debug|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|Any CPU.Build.0 = Release|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.ActiveCfg = Release|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x64.Build.0 = Release|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.ActiveCfg = Release|Any CPU - {22C42314-615F-4B11-B111-58F1D6D54F4D}.Release|x86.Build.0 = Release|Any CPU {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|Any CPU.Build.0 = Debug|Any CPU {248C5D51-2B33-4A06-A0EA-AA709F752E52}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -150,6 +150,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {E1F24B25-E027-45E0-A6E1-E08138F1F95D} = {85990620-78A6-4381-8BD6-84E6D0CF0649} + {22C42314-615F-4B11-B111-58F1D6D54F4D} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {D4E5F6A7-B8C9-40D1-2E3F-4567890123DE} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {E5F6A7B8-C9D0-41E2-3F45-5678901234EF} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {A1B2C3D4-E5F6-47A8-9B0C-1234567890AB} = {85990620-78A6-4381-8BD6-84E6D0CF0649} @@ -157,7 +158,6 @@ Global {89BD76AD-78C9-4E4A-96A2-E5DA6D4AFA44} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {C2EA98A7-FC7A-4EA6-A316-562A832D3D9A} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {DAE2DE68-9B3E-4D5D-8802-EC97B94160ED} = {85990620-78A6-4381-8BD6-84E6D0CF0649} - {22C42314-615F-4B11-B111-58F1D6D54F4D} = {85990620-78A6-4381-8BD6-84E6D0CF0649} {248C5D51-2B33-4A06-A0EA-AA709F752E52} = {AA4B8CC2-1431-4FC7-9DF3-533EC6C86D3A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution From 6cb63f39b8f3246217180e9408f804d9208b6328 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:52:08 +0100 Subject: [PATCH 44/50] feat: Load the CA properly --- test/Docker.DotNet.Tests/TestFixture.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index 4f742089..d3cc49d5 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -248,7 +248,11 @@ private static Credentials CreateCredentialsFromEnvironment() if (File.Exists(caPemPath)) { - var caCertificate = X509Certificate2.CreateFromPemFile(caPemPath); +#if NET9_0_OR_GREATER + var caCertificate = X509CertificateLoader.LoadCertificateFromFile(caPemPath); +#else + var caCertificate = new X509Certificate2(caPemPath); +#endif credentials.ServerCertificateValidationCallback = (_, certificate, _, _) => { From 4ea4b87a01f3ae4a9f8d511711a5d7d0a296e9f2 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:51:06 +0100 Subject: [PATCH 45/50] feat: Add DockerTlsCertificates --- .../DockerTlsCertificates.cs | 157 ++++++++++++++++++ src/Docker.DotNet.X509/RSAUtil.cs | 27 +++ src/Docker.DotNet.X509/RsaUtil.cs | 35 ---- src/Docker.DotNet.X509/X509Certificate2.cs | 12 ++ test/Docker.DotNet.Tests/TestFixture.cs | 53 ++---- 5 files changed, 209 insertions(+), 75 deletions(-) create mode 100644 src/Docker.DotNet.X509/DockerTlsCertificates.cs create mode 100644 src/Docker.DotNet.X509/RSAUtil.cs delete mode 100644 src/Docker.DotNet.X509/RsaUtil.cs diff --git a/src/Docker.DotNet.X509/DockerTlsCertificates.cs b/src/Docker.DotNet.X509/DockerTlsCertificates.cs new file mode 100644 index 00000000..2706a9ea --- /dev/null +++ b/src/Docker.DotNet.X509/DockerTlsCertificates.cs @@ -0,0 +1,157 @@ +namespace Docker.DotNet.X509; + +public sealed class DockerTlsCertificates +{ + private const string DefaultCaPemFileName = "ca.pem"; + + private const string DefaultCertPemFileName = "cert.pem"; + + private const string DefaultKeyPemFileName = "key.pem"; + + public DockerTlsCertificates( + X509Certificate2 certificate, + X509Certificate2 certificateAuthorityCertificate = null) + { + Certificate = certificate ?? throw new ArgumentNullException(nameof(certificate)); + CertificateAuthorityCertificate = certificateAuthorityCertificate; + } + + private X509Certificate2 Certificate { get; } + + private X509Certificate2 CertificateAuthorityCertificate { get; } + + public static DockerTlsCertificates LoadFromDirectory( + string directoryPath, + bool loadCertificateAuthority = true, + string caPemFileName = DefaultCaPemFileName, + string certPemFileName = DefaultCertPemFileName, + string keyPemFileName = DefaultKeyPemFileName) + { + if (!Directory.Exists(directoryPath)) + { + throw new DirectoryNotFoundException(directoryPath); + } + + X509Certificate2 caCertificate = null; + + var certPemPath = Path.Combine(directoryPath, certPemFileName); + var keyPemPath = Path.Combine(directoryPath, keyPemFileName); + var caPemPath = Path.Combine(directoryPath, caPemFileName); + + var certificate = LoadCertificateFromPemFiles(certPemPath, keyPemPath); + + if (loadCertificateAuthority && File.Exists(caPemPath)) + { + caCertificate = LoadCertificateAuthorityFromPemFile(caPemPath); + } + + return new DockerTlsCertificates(certificate, caCertificate); + } + + public static X509Certificate2 LoadCertificateFromPemFiles(string certPemPath, string keyPemPath) + { + EnsureFileExists(certPemPath); + EnsureFileExists(keyPemPath); + +#if NET9_0_OR_GREATER + return X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); +#elif NETSTANDARD + return Polyfills.X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); +#else + return X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); +#endif + } + + public static X509Certificate2 LoadCertificateFromPfxFile(string pfxPath, string password) + { + EnsureFileExists(pfxPath); + + password ??= string.Empty; + +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12FromFile(pfxPath, password); +#elif NETSTANDARD + return new X509Certificate2(File.ReadAllBytes(pfxPath), password); +#else + return new X509Certificate2(pfxPath, password, X509KeyStorageFlags.EphemeralKeySet); +#endif + } + + public static X509Certificate2 LoadCertificateAuthorityFromPemFile(string caPemPath) + { + EnsureFileExists(caPemPath); + +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadCertificateFromFile(caPemPath); +#elif NETSTANDARD + return Polyfills.X509Certificate2.CreateFromPemFile(caPemPath); +#else + return X509Certificate2.CreateFromPemFile(caPemPath); +#endif + } + + public CertificateCredentials CreateCredentials() + { + var credentials = new CertificateCredentials(Certificate); + + if (CertificateAuthorityCertificate is not null) + { + credentials.ServerCertificateValidationCallback = CreateCertificateAuthorityValidationCallback(CertificateAuthorityCertificate); + } + + return credentials; + } + + public static RemoteCertificateValidationCallback CreateCertificateAuthorityValidationCallback(X509Certificate2 certificateAuthorityCertificate) + { + if (certificateAuthorityCertificate is null) + { + throw new ArgumentNullException(nameof(certificateAuthorityCertificate)); + } + + return (_, certificate, _, _) => + { + if (certificate is null) + { + return false; + } + + var serverCertificate2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate); + + using var chain = new X509Chain(); + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + +#if NET5_0_OR_GREATER + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(certificateAuthorityCertificate); + return chain.Build(serverCertificate2); +#else + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; + chain.ChainPolicy.ExtraStore.Add(certificateAuthorityCertificate); + + if (!chain.Build(serverCertificate2)) + { + return false; + } + + foreach (var chainElement in chain.ChainElements) + { + if (string.Equals(chainElement.Certificate.Thumbprint, certificateAuthorityCertificate.Thumbprint, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; +#endif + }; + } + + private static void EnsureFileExists(string path) + { + if (!File.Exists(path)) + { + throw new FileNotFoundException(path); + } + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.X509/RSAUtil.cs b/src/Docker.DotNet.X509/RSAUtil.cs new file mode 100644 index 00000000..66d8064f --- /dev/null +++ b/src/Docker.DotNet.X509/RSAUtil.cs @@ -0,0 +1,27 @@ +namespace Docker.DotNet.X509; + +[Obsolete("RSAUtil is obsolete. Use DockerTlsCertificates instead.")] +public static class RSAUtil +{ + public static X509Certificate2 GetCertFromPFX(string pfxFilePath, string password) + { +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12FromFile(pfxFilePath, password); +#else + return new X509Certificate2(pfxFilePath, password); +#endif + } + + public static X509Certificate2 GetCertFromPEM(string certFilePath, string keyFilePath) + { +#if NETSTANDARD + return Polyfills.X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); +#elif NET9_0_OR_GREATER + var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); + return OperatingSystem.IsWindows() ? X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pfx), null) : certificate; +#elif NET6_0_OR_GREATER + var certificate = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); + return OperatingSystem.IsWindows() ? new X509Certificate2(certificate.Export(X509ContentType.Pfx)) : certificate; +#endif + } +} \ No newline at end of file diff --git a/src/Docker.DotNet.X509/RsaUtil.cs b/src/Docker.DotNet.X509/RsaUtil.cs deleted file mode 100644 index 79299016..00000000 --- a/src/Docker.DotNet.X509/RsaUtil.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Docker.DotNet.X509; - -public static class RsaUtil -{ - public static X509Certificate2 GetCertFromPfx(string pfxFilePath, string password) - { -#if NET9_0_OR_GREATER - return X509CertificateLoader.LoadPkcs12FromFile(pfxFilePath, password); -#else - return new X509Certificate2(pfxFilePath, password); -#endif - } - - public static X509Certificate2 GetCertFromPem(string certFilePath, string keyFilePath) - { -#if NETSTANDARD - return Polyfills.X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); -#else - return OperatingSystem.IsWindows() ? CreateWindowsCert(certFilePath, keyFilePath) : X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); -#endif - } - -#if !NETSTANDARD - private static X509Certificate2 CreateWindowsCert(string certFilePath, string keyFilePath) - { - using var cert = X509Certificate2.CreateFromPemFile(certFilePath, keyFilePath); - -#if NET9_0_OR_GREATER - return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx), null); -#else - return new X509Certificate2(cert.Export(X509ContentType.Pfx)); -#endif - } -#endif -} \ No newline at end of file diff --git a/src/Docker.DotNet.X509/X509Certificate2.cs b/src/Docker.DotNet.X509/X509Certificate2.cs index e1a438ca..26e30b4b 100644 --- a/src/Docker.DotNet.X509/X509Certificate2.cs +++ b/src/Docker.DotNet.X509/X509Certificate2.cs @@ -12,6 +12,18 @@ public static class X509Certificate2 { private static readonly X509CertificateParser CertificateParser = new X509CertificateParser(); + public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath) + { + if (!File.Exists(certPemFilePath)) + { + throw new FileNotFoundException(certPemFilePath); + } + + var certificate = CertificateParser.ReadCertificate(File.ReadAllBytes(certPemFilePath)); + + return new System.Security.Cryptography.X509Certificates.X509Certificate2(certificate.GetEncoded()); + } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateFromPemFile(string certPemFilePath, string keyPemFilePath) { if (!File.Exists(certPemFilePath)) diff --git a/test/Docker.DotNet.Tests/TestFixture.cs b/test/Docker.DotNet.Tests/TestFixture.cs index d3cc49d5..62ac4f62 100644 --- a/test/Docker.DotNet.Tests/TestFixture.cs +++ b/test/Docker.DotNet.Tests/TestFixture.cs @@ -225,56 +225,29 @@ private static Credentials CreateCredentialsFromEnvironment() } var caPemPath = Path.Combine(certPath, "ca.pem"); - var clientCertPemPath = Path.Combine(certPath, "cert.pem"); - var clientKeyPemPath = Path.Combine(certPath, "key.pem"); - var clientPfxPath = Path.Combine(certPath, "client.pfx"); + var certPemPath = Path.Combine(certPath, "cert.pem"); + var keyPemPath = Path.Combine(certPath, "key.pem"); + var pfxPath = Path.Combine(certPath, "client.pfx"); - X509Certificate2 clientCertificate; + DockerTlsCertificates tlsCertificates; - if (File.Exists(clientCertPemPath) && File.Exists(clientKeyPemPath)) + if (File.Exists(certPemPath) && File.Exists(keyPemPath)) { - clientCertificate = RsaUtil.GetCertFromPem(clientCertPemPath, clientKeyPemPath); + tlsCertificates = DockerTlsCertificates.LoadFromDirectory(certPath, loadCertificateAuthority: true); } - else if (File.Exists(clientPfxPath)) + else if (File.Exists(pfxPath)) { - clientCertificate = RsaUtil.GetCertFromPfx(clientPfxPath, string.Empty); + var clientCertificate = DockerTlsCertificates.LoadCertificateFromPfxFile(pfxPath, string.Empty); + var caCertificate = File.Exists(caPemPath) ? DockerTlsCertificates.LoadCertificateAuthorityFromPemFile(caPemPath) : null; + + tlsCertificates = new DockerTlsCertificates(clientCertificate, caCertificate); } else { - throw new FileNotFoundException($"Could not locate Docker TLS client credentials. Looked for '{clientCertPemPath}', '{clientKeyPemPath}', and '{clientPfxPath}'."); - } - - var credentials = new CertificateCredentials(clientCertificate); - - if (File.Exists(caPemPath)) - { -#if NET9_0_OR_GREATER - var caCertificate = X509CertificateLoader.LoadCertificateFromFile(caPemPath); -#else - var caCertificate = new X509Certificate2(caPemPath); -#endif - - credentials.ServerCertificateValidationCallback = (_, certificate, _, _) => - { - if (certificate is null) - { - return false; - } - - if (certificate is not X509Certificate2 serverCertificate2) - { - return false; - } - - using var chain = new X509Chain(); - chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; - chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; - chain.ChainPolicy.CustomTrustStore.Add(caCertificate); - return chain.Build(serverCertificate2); - }; + throw new FileNotFoundException($"Could not locate Docker TLS client credentials. Looked for '{certPemPath}', '{keyPemPath}', and '{pfxPath}'."); } - return credentials; + return tlsCertificates.CreateCredentials(); } private sealed class Disposable : IDisposable From d3fb09dbdba57bad884fa9e2a40d0217c4724e2a Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:03:03 +0100 Subject: [PATCH 46/50] fix: Import/export on Windows --- .../DockerTlsCertificates.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Docker.DotNet.X509/DockerTlsCertificates.cs b/src/Docker.DotNet.X509/DockerTlsCertificates.cs index 2706a9ea..66665f35 100644 --- a/src/Docker.DotNet.X509/DockerTlsCertificates.cs +++ b/src/Docker.DotNet.X509/DockerTlsCertificates.cs @@ -54,7 +54,27 @@ public static X509Certificate2 LoadCertificateFromPemFiles(string certPemPath, s EnsureFileExists(keyPemPath); #if NET9_0_OR_GREATER - return X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); + var certificate = X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); + + if (OperatingSystem.IsWindows()) + { + var pfxBytes = certificate.Export(X509ContentType.Pfx); + certificate.Dispose(); + return X509CertificateLoader.LoadPkcs12(pfxBytes, password: null); + } + + return certificate; +#elif NET6_0_OR_GREATER + var certificate = X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); + + if (OperatingSystem.IsWindows()) + { + var pfxBytes = certificate.Export(X509ContentType.Pfx); + certificate.Dispose(); + return new X509Certificate2(pfxBytes); + } + + return certificate; #elif NETSTANDARD return Polyfills.X509Certificate2.CreateFromPemFile(certPemPath, keyPemPath); #else From 70decf4f5aa1817839290ed0c8bc50812cbb9b94 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:13:17 +0100 Subject: [PATCH 47/50] fix: CryptographicException --- src/Docker.DotNet.X509/DockerTlsCertificates.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Docker.DotNet.X509/DockerTlsCertificates.cs b/src/Docker.DotNet.X509/DockerTlsCertificates.cs index 66665f35..afd82368 100644 --- a/src/Docker.DotNet.X509/DockerTlsCertificates.cs +++ b/src/Docker.DotNet.X509/DockerTlsCertificates.cs @@ -106,7 +106,7 @@ public static X509Certificate2 LoadCertificateAuthorityFromPemFile(string caPemP #elif NETSTANDARD return Polyfills.X509Certificate2.CreateFromPemFile(caPemPath); #else - return X509Certificate2.CreateFromPemFile(caPemPath); + return new X509Certificate2(caPemPath); #endif } From 3c056b5c145b2971f6aaa1a036f731d631d6aa8d Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:47:22 +0100 Subject: [PATCH 48/50] fix: Assert base exception --- test/Docker.DotNet.Tests/IContainerOperationsTests.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs index cc2ae303..ff3ebab9 100644 --- a/test/Docker.DotNet.Tests/IContainerOperationsTests.cs +++ b/test/Docker.DotNet.Tests/IContainerOperationsTests.cs @@ -190,7 +190,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.CancelAfter(TimeSpan.FromSeconds(5)); - await Assert.ThrowsAsync(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync( + await Assert.ThrowsAnyAsync(() => _testFixture.DockerClient.Containers.GetContainerLogsAsync( createContainerResponse.ID, new ContainerLogsParameters { @@ -240,7 +240,7 @@ await _testFixture.DockerClient.Containers.StartContainerAsync( containerLogsCts.Token ); - await Assert.ThrowsAsync(() => containerLogsTask); + await Assert.ThrowsAnyAsync(() => containerLogsTask); } [Fact] @@ -288,7 +288,7 @@ await _testFixture.DockerClient.Containers.StopContainerAsync( _testFixture.Cts.Token ); - await Assert.ThrowsAsync(() => containerLogsTask); + await Assert.ThrowsAnyAsync(() => containerLogsTask); _testOutputHelper.WriteLine($"Line count: {logList.Count}"); Assert.NotEmpty(logList); @@ -682,7 +682,7 @@ public async Task WaitContainerAsync_TokenIsCancelled_OperationCancelledExceptio // Will wait forever here if cancellation fails. var waitContainerTask = _testFixture.DockerClient.Containers.WaitContainerAsync(createContainerResponse.ID, waitContainerCts.Token); - _ = await Assert.ThrowsAsync(() => waitContainerTask); + _ = await Assert.ThrowsAnyAsync(() => waitContainerTask); stopWatch.Stop(); From 0bc0a300c9aa5c73d59ef9349e4abc0cf28156d9 Mon Sep 17 00:00:00 2001 From: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:07:49 +0100 Subject: [PATCH 49/50] fix: Wait for monitoring --- test/Docker.DotNet.Tests/ISystemOperations.Tests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs index 3609a79c..b94faa24 100644 --- a/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs +++ b/test/Docker.DotNet.Tests/ISystemOperations.Tests.cs @@ -225,6 +225,10 @@ await _testFixture.DockerClient.Images.TagImageAsync( using var cts = CancellationTokenSource.CreateLinkedTokenSource(_testFixture.Cts.Token); var task = Task.Run(() => _testFixture.DockerClient.System.MonitorEventsAsync(eventsParams, progress, cts.Token)); + // Wait briefly to ensure the monitoring task is fully established before triggering Docker events. + // Ideally, the API would return (or signal) once monitoring is active. + await Task.Delay(TimeSpan.FromSeconds(1)); + await _testFixture.DockerClient.Images.TagImageAsync($"{_testFixture.Repository}:{_testFixture.Tag}", new ImageTagParameters { RepositoryName = _testFixture.Repository, Tag = newTag }); await _testFixture.DockerClient.Images.DeleteImageAsync($"{_testFixture.Repository}:{newTag}", new ImageDeleteParameters()); From 4526a463801246576a8e099f8cdfd8dcdf9f93f3 Mon Sep 17 00:00:00 2001 From: Thomas Brueggemann Date: Sun, 1 Feb 2026 12:49:04 +0100 Subject: [PATCH 50/50] remove proxy setting for native handler remove reflection from CertificateCredentials revert readme (remove dynamic loading) --- README.md | 20 ------------ .../CertificateCredentials.cs | 32 ++++++++----------- .../Docker.DotNet.X509.csproj | 4 +++ 3 files changed, 17 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index df7d508f..5e2775a6 100644 --- a/README.md +++ b/README.md @@ -50,26 +50,6 @@ DockerClient client = new DockerClientConfiguration( .CreateClient(); ``` -### Connection types and optional sub-packages - -Depending on the connection scheme and platform, additional sub-packages may be required: - -- **Docker.DotNet.Enhanced.NPipe**: Support for named pipes on Windows (`npipe://`). -- **Docker.DotNet.Enhanced.Unix**: Support for Unix domain sockets on Linux/macOS (`unix://`). -- **Docker.DotNet.Enhanced.NativeHttp**: Native HTTP handler for specific platforms/scenarios. -- **Docker.DotNet.Enhanced.LegacyHttp**: Legacy HTTP handler for compatibility with older .NET versions. - -These packages are optional and only needed if you want to use the respective protocol or handler. You can install them via NuGet, for example: - -```console -PM> Install-Package Docker.DotNet.Enhanced.NPipe -PM> Install-Package Docker.DotNet.Enhanced.Unix -PM> Install-Package Docker.DotNet.Enhanced.NativeHttp -PM> Install-Package Docker.DotNet.Enhanced.LegacyHttp -``` - -**Examples:** - **Named Pipe (Windows):** ```csharp diff --git a/src/Docker.DotNet.X509/CertificateCredentials.cs b/src/Docker.DotNet.X509/CertificateCredentials.cs index 7278dd2c..d6289f47 100644 --- a/src/Docker.DotNet.X509/CertificateCredentials.cs +++ b/src/Docker.DotNet.X509/CertificateCredentials.cs @@ -27,11 +27,10 @@ public override HttpMessageHandler GetHandler(HttpMessageHandler handler) #if NET6_0_OR_GREATER if (handler is SocketsHttpHandler nativeHandler) { - nativeHandler.UseProxy = true; nativeHandler.AllowAutoRedirect = true; nativeHandler.MaxAutomaticRedirections = 20; - nativeHandler.Proxy = WebRequest.DefaultWebProxy; - nativeHandler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions + + nativeHandler.SslOptions = new SslClientAuthenticationOptions { ClientCertificates = new X509CertificateCollection { _certificate }, CertificateRevocationCheckMode = X509RevocationMode.NoCheck, @@ -48,10 +47,9 @@ public override HttpMessageHandler GetHandler(HttpMessageHandler handler) nativeHandler.ClientCertificates.Add(_certificate); } - nativeHandler.UseProxy = true; nativeHandler.AllowAutoRedirect = true; nativeHandler.MaxAutomaticRedirections = 20; - nativeHandler.Proxy = WebRequest.DefaultWebProxy; + nativeHandler.ClientCertificateOptions = ClientCertificateOption.Manual; nativeHandler.CheckCertificateRevocationList = false; nativeHandler.SslProtocols = SslProtocols.Tls12; @@ -59,23 +57,19 @@ public override HttpMessageHandler GetHandler(HttpMessageHandler handler) return nativeHandler; } #endif - else + else if (handler is ManagedHandler managedHandler) { - // Use reflection to support different handler without direct reference - var handlerType = handler.GetType(); - var clientCertificatesProp = handlerType.GetProperty("ClientCertificates"); - var serverCertValidationProp = handlerType.GetProperty("ServerCertificateValidationCallback"); - - if (clientCertificatesProp != null && serverCertValidationProp != null) + if (!managedHandler.ClientCertificates.Contains(_certificate)) { - var clientCertificates = clientCertificatesProp.GetValue(handler) as System.Collections.IList; - if (clientCertificates != null && !clientCertificates.Contains(_certificate)) - { - clientCertificates.Add(_certificate); - } - - serverCertValidationProp.SetValue(handler, ServerCertificateValidationCallback); + managedHandler.ClientCertificates.Add(_certificate); } + + managedHandler.ServerCertificateValidationCallback = ServerCertificateValidationCallback; + + return handler; + } + else + { return handler; } } diff --git a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj index 8a3233f3..18f30387 100644 --- a/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj +++ b/src/Docker.DotNet.X509/Docker.DotNet.X509.csproj @@ -22,6 +22,10 @@ + + + +