Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#370): Added support docker TLS endpoint #597

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
86 changes: 86 additions & 0 deletions src/Testcontainers/Builders/TlsEndpointAuthenticationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace DotNet.Testcontainers.Builders
{
using System;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Configurations.Credentials;

/// <inheritdoc cref="IDockerRegistryAuthenticationProvider" />
internal sealed class TlsEndpointAuthenticationProvider : DockerEndpointAuthenticationProvider
{
private const string DefaultUserDockerFolderName = ".docker";
private const string DefaultCaCertFileName = "ca.pem";
private static readonly Uri DefaultTlsDockerEndpoint = new Uri("tcp://localhost:2376");
private static readonly string DefaultCertPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), DefaultUserDockerFolderName);

private readonly Lazy<X509Certificate2> caCertificate;
private readonly Uri dockerEngine;
private readonly bool dockerTlsEnabled;

/// <summary>
/// Initializes a new instance of the <see cref="TlsEndpointAuthenticationProvider" /> class.
/// </summary>
public TlsEndpointAuthenticationProvider()
: this(PropertiesFileConfiguration.Instance, EnvironmentConfiguration.Instance)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TlsEndpointAuthenticationProvider" /> class.
/// </summary>
/// <param name="customConfigurations">A list of custom configurations.</param>
public TlsEndpointAuthenticationProvider(params ICustomConfiguration[] customConfigurations)
{
var dockerCertPath = customConfigurations
.Select(customConfiguration => customConfiguration.GetDockerCertPath())
.FirstOrDefault(value => value != null) ?? DefaultCertPath;
var dockerCaCertFile = Path.Combine(dockerCertPath, DefaultCaCertFileName);

this.dockerEngine = customConfigurations
.Select(customConfiguration => customConfiguration.GetDockerHost())
.FirstOrDefault(value => value != null) ?? DefaultTlsDockerEndpoint;
this.dockerTlsEnabled = customConfigurations
.Select(customConfiguration => customConfiguration.GetDockerTls())
.Aggregate(false, (x, y) => x || y);
this.caCertificate = new Lazy<X509Certificate2>(() => new X509Certificate2(dockerCaCertFile));
}

/// <inheritdoc />
public override bool IsApplicable()
{
return this.dockerTlsEnabled;
}

/// <inheritdoc />
public override IDockerEndpointAuthenticationConfiguration GetAuthConfig()
{
return new DockerEndpointAuthenticationConfiguration(this.dockerEngine, new TlsCredentials(this.ServerCertificateValidationCallback));
}

private bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
switch (sslPolicyErrors)
{
case SslPolicyErrors.None:
return true;
case SslPolicyErrors.RemoteCertificateNotAvailable:
case SslPolicyErrors.RemoteCertificateNameMismatch:
return false;
case SslPolicyErrors.RemoteCertificateChainErrors:
default:
var validationChain = new X509Chain();
validationChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
validationChain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
validationChain.ChainPolicy.ExtraStore.Add(this.caCertificate.Value);
validationChain.ChainPolicy.ExtraStore.AddRange(chain.ChainElements.OfType<X509ChainElement>().Select(element => element.Certificate).ToArray());
var isVerified = validationChain.Build(certificate as X509Certificate2 ?? new X509Certificate2(certificate));
var isSignedByExpectedRoot = validationChain.ChainElements[validationChain.ChainElements.Count - 1].Certificate.RawData.SequenceEqual(this.caCertificate.Value.RawData);
var isSuccess = isVerified && isSignedByExpectedRoot;
return isSuccess;
}
}
}
}
31 changes: 31 additions & 0 deletions src/Testcontainers/Configurations/Credentials/TlsCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace DotNet.Testcontainers.Configurations.Credentials
{
using System.Net;
using System.Net.Http;
using System.Net.Security;
using Docker.DotNet;
using JetBrains.Annotations;
using Microsoft.Net.Http.Client;

public class TlsCredentials : Credentials
{
public TlsCredentials([CanBeNull] RemoteCertificateValidationCallback serverCertificateValidationCallback)
{
this.ServerCertificateValidationCallback = serverCertificateValidationCallback;
}

public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; }

public override bool IsTlsCredentials()
{
return true;
}

public override HttpMessageHandler GetHandler(HttpMessageHandler innerHandler)
{
var handler = (ManagedHandler)innerHandler;
handler.ServerCertificateValidationCallback = this.ServerCertificateValidationCallback ?? ServicePointManager.ServerCertificateValidationCallback;
return handler;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@
/// Initializes a new instance of the <see cref="DockerEndpointAuthenticationConfiguration" /> struct.
/// </summary>
/// <param name="endpoint">The Docker API endpoint.</param>
public DockerEndpointAuthenticationConfiguration(Uri endpoint)
/// <param name="credentials">The Docker API authentication credentials.</param>
public DockerEndpointAuthenticationConfiguration(Uri endpoint, Docker.DotNet.Credentials credentials = null)
{
this.Credentials = credentials;
this.Endpoint = endpoint;
}

/// <inheritdoc />
public Docker.DotNet.Credentials Credentials { get; }

/// <inheritdoc />
public Uri Endpoint { get; }

/// <inheritdoc />
public DockerClientConfiguration GetDockerClientConfiguration(Guid sessionId = default)
{
var defaultHttpRequestHeaders = new ReadOnlyDictionary<string, string>(new Dictionary<string, string> { { "x-tc-sid", sessionId.ToString("D") } });
return new DockerClientConfiguration(this.Endpoint, defaultHttpRequestHeaders: defaultHttpRequestHeaders);
return new DockerClientConfiguration(this.Endpoint, this.Credentials, defaultHttpRequestHeaders: defaultHttpRequestHeaders);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public interface IDockerEndpointAuthenticationConfiguration
[NotNull]
Uri Endpoint { get; }

/// <summary>
/// Gets the Docker API authentication credentials.
/// </summary>
[CanBeNull]
Docker.DotNet.Credentials Credentials { get; }

/// <summary>
/// Gets the Docker client configuration.
/// </summary>
Expand Down
8 changes: 7 additions & 1 deletion src/Testcontainers/Configurations/TestcontainersSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ public static class TestcontainersSettings
private static readonly IDockerImage RyukContainerImage = new DockerImage("testcontainers/ryuk:0.3.4");

private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig =
new IDockerEndpointAuthenticationProvider[] { new EnvironmentEndpointAuthenticationProvider(), new NpipeEndpointAuthenticationProvider(), new UnixEndpointAuthenticationProvider() }
new IDockerEndpointAuthenticationProvider[]
{
new TlsEndpointAuthenticationProvider(),
new EnvironmentEndpointAuthenticationProvider(),
new NpipeEndpointAuthenticationProvider(),
new UnixEndpointAuthenticationProvider(),
}
.AsParallel()
.Where(authProvider => authProvider.IsApplicable())
.Where(authProvider => authProvider.IsAvailable())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace DotNet.Testcontainers.Tests.Unit
public sealed class DockerEndpointAuthenticationProviderTest
{
private const string DockerHost = "tcp://127.0.0.1:2375";
private const string DockerTlsHost = "tcp://127.0.0.1:2376";

[Theory]
[ClassData(typeof(AuthProviderTestData))]
Expand All @@ -35,7 +36,12 @@ public AuthProviderTestData()
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var defaultConfiguration = new PropertiesFileConfiguration(Array.Empty<string>());
var dockerHostConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=" + DockerHost });
var dockerHostConfiguration = new PropertiesFileConfiguration(new[] { $"docker.host={DockerHost}" });
var dockerTlsConfiguration = new PropertiesFileConfiguration(new[] { "docker.tls=true" });
this.Add(new object[] { new TlsEndpointAuthenticationProvider(defaultConfiguration), false });
this.Add(new object[] { new TlsEndpointAuthenticationProvider(dockerTlsConfiguration), true });
this.Add(new object[] { new TlsEndpointAuthenticationProvider(Array.Empty<ICustomConfiguration>()), false });
this.Add(new object[] { new TlsEndpointAuthenticationProvider(defaultConfiguration, dockerTlsConfiguration), true });
this.Add(new object[] { new EnvironmentEndpointAuthenticationProvider(defaultConfiguration), false });
this.Add(new object[] { new EnvironmentEndpointAuthenticationProvider(dockerHostConfiguration), true });
this.Add(new object[] { new EnvironmentEndpointAuthenticationProvider(Array.Empty<ICustomConfiguration>()), false });
Expand All @@ -49,7 +55,9 @@ private sealed class AuthConfigTestData : List<object[]>
{
public AuthConfigTestData()
{
var dockerHostConfiguration = new PropertiesFileConfiguration(new[] { "docker.host=" + DockerHost });
var dockerHostConfiguration = new PropertiesFileConfiguration(new[] { $"docker.host={DockerHost}" });
var dockerTlsHostConfiguration = new PropertiesFileConfiguration(new[] { $"docker.host={DockerTlsHost}" });
this.Add(new object[] { new TlsEndpointAuthenticationProvider(dockerTlsHostConfiguration).GetAuthConfig(), new Uri(DockerTlsHost) });
this.Add(new object[] { new EnvironmentEndpointAuthenticationProvider(dockerHostConfiguration).GetAuthConfig(), new Uri(DockerHost) });
this.Add(new object[] { new NpipeEndpointAuthenticationProvider().GetAuthConfig(), new Uri("npipe://./pipe/docker_engine") });
this.Add(new object[] { new UnixEndpointAuthenticationProvider().GetAuthConfig(), new Uri("unix:/var/run/docker.sock") });
Expand Down