Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 65 additions & 13 deletions src/Docker.DotNet/DockerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal DockerClient(DockerClientConfiguration configuration, Version requested
Plugin = new PluginOperations(this);
Exec = new ExecOperations(this);

ManagedHandler handler;
HttpMessageHandler handler;
var uri = Configuration.EndpointBaseUri;
switch (uri.Scheme.ToLowerInvariant())
{
Expand All @@ -60,7 +60,7 @@ internal DockerClient(DockerClientConfiguration configuration, Version requested
var pipeName = uri.Segments[2];

uri = new UriBuilder("http", pipeName).Uri;
handler = new ManagedHandler(async (host, port, cancellationToken) =>
var pipeHandler = new ManagedHandler(async (host, port, cancellationToken) =>
{
var timeout = (int)Configuration.NamedPipeConnectTimeout.TotalMilliseconds;
var stream = new NamedPipeClientStream(serverName, pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
Expand All @@ -71,6 +71,9 @@ await stream.ConnectAsync(timeout, cancellationToken)

return dockerStream;
}, logger);
// Named pipes are local connections - disable proxy resolution
pipeHandler.UseProxy = false;
handler = pipeHandler;
break;

case "tcp":
Expand All @@ -80,25 +83,74 @@ await stream.ConnectAsync(timeout, cancellationToken)
Scheme = configuration.Credentials.IsTlsCredentials() ? "https" : "http"
};
uri = builder.Uri;
handler = new ManagedHandler(logger);
handler = new ManagedHandler(logger, Configuration.SocketConnectionConfiguration);
break;

case "https":
handler = new ManagedHandler(logger);
handler = new ManagedHandler(logger, Configuration.SocketConnectionConfiguration);
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);
var socketTimeout = Configuration.SocketConnectTimeout;
var socketConfig = Configuration.SocketConnectionConfiguration;
uri = new UriBuilder("http", uri.Segments.Last()).Uri;

// Use ManagedHandler for Unix socket connections.
// ManagedHandler is required for hijacked stream operations (attach/exec/logs)
// as it provides HttpConnectionResponseContent needed for connection hijacking.
// SocketsHttpHandler cannot support hijacking because it encapsulates the transport stream.
var unixHandler = new ManagedHandler(async (_, _, cancellationToken) =>
{
var endpoint = new Microsoft.Net.Http.Client.UnixDomainSocketEndPoint(pipeString);
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);

try
{
// Apply socket configuration for better proxy compatibility
if (socketConfig.KeepAlive)
{
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
}

using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(socketTimeout);

#if NET5_0_OR_GREATER
// Use modern ConnectAsync with cancellation support
await socket.ConnectAsync(endpoint, timeoutCts.Token)
.ConfigureAwait(false);
#else
var connectTask = socket.ConnectAsync(endpoint);
var timeoutTask = Task.Delay(Timeout.InfiniteTimeSpan, timeoutCts.Token);

var completedTask = await Task.WhenAny(connectTask, timeoutTask)
.ConfigureAwait(false);

if (completedTask == timeoutTask)
{
cancellationToken.ThrowIfCancellationRequested();
throw new TimeoutException($"Connection to Unix socket '{pipeString}' timed out after {socketTimeout.TotalSeconds}s.");
}

await connectTask.ConfigureAwait(false);
#endif
return socket;
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
socket.Dispose();
throw new TimeoutException($"Connection to Unix socket '{pipeString}' timed out after {socketTimeout.TotalSeconds}s.");
}
catch
{
socket.Dispose();
throw;
}
}, logger, socketConfig);
// Unix sockets are local connections - disable proxy resolution
unixHandler.UseProxy = false;
handler = unixHandler;
break;

default:
Expand Down
41 changes: 35 additions & 6 deletions src/Docker.DotNet/DockerClientConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ public DockerClientConfiguration(
Credentials credentials = null,
TimeSpan defaultTimeout = default,
TimeSpan namedPipeConnectTimeout = default,
IReadOnlyDictionary<string, string> defaultHttpRequestHeaders = null)
: this(GetLocalDockerEndpoint(), credentials, defaultTimeout, namedPipeConnectTimeout, defaultHttpRequestHeaders)
TimeSpan socketConnectTimeout = default,
IReadOnlyDictionary<string, string> defaultHttpRequestHeaders = null,
SocketConnectionConfiguration socketConfiguration = null)
: this(GetLocalDockerEndpoint(), credentials, defaultTimeout, namedPipeConnectTimeout, socketConnectTimeout, defaultHttpRequestHeaders, socketConfiguration)
{
}

Expand All @@ -18,7 +20,9 @@ public DockerClientConfiguration(
Credentials credentials = null,
TimeSpan defaultTimeout = default,
TimeSpan namedPipeConnectTimeout = default,
IReadOnlyDictionary<string, string> defaultHttpRequestHeaders = null)
TimeSpan socketConnectTimeout = default,
IReadOnlyDictionary<string, string> defaultHttpRequestHeaders = null,
SocketConnectionConfiguration socketConfiguration = null)
{
if (endpoint == null)
{
Expand All @@ -34,22 +38,47 @@ public DockerClientConfiguration(
Credentials = credentials ?? new AnonymousCredentials();
DefaultTimeout = TimeSpan.Equals(TimeSpan.Zero, defaultTimeout) ? TimeSpan.FromSeconds(100) : defaultTimeout;
NamedPipeConnectTimeout = TimeSpan.Equals(TimeSpan.Zero, namedPipeConnectTimeout) ? TimeSpan.FromMilliseconds(100) : namedPipeConnectTimeout;
SocketConnectTimeout = TimeSpan.Equals(TimeSpan.Zero, socketConnectTimeout) ? TimeSpan.FromSeconds(30) : socketConnectTimeout;
DefaultHttpRequestHeaders = defaultHttpRequestHeaders ?? new Dictionary<string, string>();
SocketConnectionConfiguration = socketConfiguration ?? SocketConnectionConfiguration.Default;
}

/// <summary>
/// Gets the collection of default HTTP request headers.
/// Gets the Docker endpoint base URI.
/// </summary>
public IReadOnlyDictionary<string, string> DefaultHttpRequestHeaders { get; }

public Uri EndpointBaseUri { get; }

/// <summary>
/// Gets the credentials used for authentication.
/// </summary>
public Credentials Credentials { get; }

/// <summary>
/// Gets the default timeout for API requests.
/// </summary>
public TimeSpan DefaultTimeout { get; }

/// <summary>
/// Gets the timeout for named pipe connections (Windows).
/// </summary>
public TimeSpan NamedPipeConnectTimeout { get; }

/// <summary>
/// Gets the timeout for Unix domain socket connections.
/// </summary>
public TimeSpan SocketConnectTimeout { get; }

/// <summary>
/// Gets the socket configuration options for connection handling.
/// These settings help improve proxy compatibility and connection reliability.
/// </summary>
public SocketConnectionConfiguration SocketConnectionConfiguration { get; }

/// <summary>
/// Gets the collection of default HTTP request headers.
/// </summary>
public IReadOnlyDictionary<string, string> DefaultHttpRequestHeaders { get; }

public DockerClient CreateClient(Version requestedApiVersion = null, ILogger logger = null)
{
return new DockerClient(this, requestedApiVersion, logger);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ internal sealed class BufferedReadStream : WriteClosableStream, IPeekableStream
private int _bufferRefCount;
private int _bufferOffset;
private int _bufferCount;
private bool _disposed;


public BufferedReadStream(Stream inner, Socket socket, ILogger logger)
: this(inner, socket, 8192, logger)
Expand Down Expand Up @@ -59,8 +61,14 @@ public override long Position

protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}

if (disposing)
{
_disposed = true;
if (Interlocked.Exchange(ref _bufferRefCount, 0) == 1)
{
ArrayPool<byte>.Shared.Return(_buffer);
Expand Down
11 changes: 11 additions & 0 deletions src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedReadStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal sealed class ChunkedReadStream : Stream
private readonly BufferedReadStream _inner;
private int _chunkBytesRemaining;
private bool _done;
private bool _disposed;

public ChunkedReadStream(BufferedReadStream stream)
{
Expand Down Expand Up @@ -146,4 +147,14 @@ public override void Flush()
{
_inner.Flush();
}

protected override void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_disposed = true;
// Note: We don't dispose _inner here as it's owned by HttpConnection
}
base.Dispose(disposing);
}
}
11 changes: 11 additions & 0 deletions src/Docker.DotNet/Microsoft.Net.Http.Client/ChunkedWriteStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal sealed class ChunkedWriteStream : Stream
private static readonly byte[] EndOfContentBytes = Encoding.ASCII.GetBytes("0\r\n\r\n");

private readonly Stream _inner;
private bool _disposed;

public ChunkedWriteStream(Stream stream)
{
Expand Down Expand Up @@ -87,4 +88,14 @@ public Task EndContentAsync(CancellationToken cancellationToken)
{
return _inner.WriteAsync(EndOfContentBytes, 0, EndOfContentBytes.Length, cancellationToken);
}

protected override void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_disposed = true;
// Note: We don't dispose _inner here as it's owned by the caller
}
base.Dispose(disposing);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count,

protected override void Dispose(bool disposing)
{
if (disposing)
if (disposing && !_disposed)
{
_disposed = true;
// TODO: Sync drain with timeout if small number of bytes remaining? This will let us re-use the connection.
_inner.Dispose();
}
Expand Down
18 changes: 13 additions & 5 deletions src/Docker.DotNet/Microsoft.Net.Http.Client/HttpConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,35 @@ public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, Can
// Serialize headers & send
string rawRequest = SerializeRequest(request);
byte[] requestBytes = Encoding.ASCII.GetBytes(rawRequest);
await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken);
await Transport.WriteAsync(requestBytes, 0, requestBytes.Length, cancellationToken).ConfigureAwait(false);

if (request.Content != null)
{
if (request.Content.Headers.ContentLength.HasValue)
{
await request.Content.CopyToAsync(Transport);
#if NET5_0_OR_GREATER
await request.Content.CopyToAsync(Transport, cancellationToken).ConfigureAwait(false);
#else
await request.Content.CopyToAsync(Transport).ConfigureAwait(false);
#endif
}
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);
#if NET5_0_OR_GREATER
await request.Content.CopyToAsync(chunkedStream, cancellationToken).ConfigureAwait(false);
#else
await request.Content.CopyToAsync(chunkedStream).ConfigureAwait(false);
#endif
await chunkedStream.EndContentAsync(cancellationToken).ConfigureAwait(false);
}
}
}

// Receive headers
List<string> responseLines = await ReadResponseLinesAsync(cancellationToken);
List<string> responseLines = await ReadResponseLinesAsync(cancellationToken).ConfigureAwait(false);

// Receive body and determine the response type (Content-Length, Transfer-Encoding, Opaque)
return CreateResponseMessage(responseLines);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
_responseStream.Dispose();
_responseStream?.Dispose();
_connection.Dispose();
}
}
Expand All @@ -71,4 +71,4 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}
}
}
}
Loading