Skip to content

Commit

Permalink
feat(#370): Added support docker TLS endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
vlaskal committed Oct 9, 2022
1 parent d57eda3 commit 9dd775f
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 5 deletions.
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

0 comments on commit 9dd775f

Please sign in to comment.