From 9dd775fa385285420b743b7b4048231ab4fe8c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vlastimil=20Kalu=C5=BEa?= Date: Mon, 18 Jul 2022 12:48:45 +0200 Subject: [PATCH] feat(#370): Added support docker TLS endpoint --- .../TlsEndpointAuthenticationProvider.cs | 86 +++++++++++++++++++ .../Credentials/TlsCredentials.cs | 31 +++++++ ...ckerEndpointAuthenticationConfiguration.cs | 9 +- ...ckerEndpointAuthenticationConfiguration.cs | 6 ++ .../Configurations/TestcontainersSettings.cs | 8 +- ...ockerEndpointAuthenticationProviderTest.cs | 12 ++- 6 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 src/Testcontainers/Builders/TlsEndpointAuthenticationProvider.cs create mode 100644 src/Testcontainers/Configurations/Credentials/TlsCredentials.cs diff --git a/src/Testcontainers/Builders/TlsEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/TlsEndpointAuthenticationProvider.cs new file mode 100644 index 000000000..e8f5c9a80 --- /dev/null +++ b/src/Testcontainers/Builders/TlsEndpointAuthenticationProvider.cs @@ -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; + + /// + 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 caCertificate; + private readonly Uri dockerEngine; + private readonly bool dockerTlsEnabled; + + /// + /// Initializes a new instance of the class. + /// + public TlsEndpointAuthenticationProvider() + : this(PropertiesFileConfiguration.Instance, EnvironmentConfiguration.Instance) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// A list of custom configurations. + 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(() => new X509Certificate2(dockerCaCertFile)); + } + + /// + public override bool IsApplicable() + { + return this.dockerTlsEnabled; + } + + /// + 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().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; + } + } + } +} diff --git a/src/Testcontainers/Configurations/Credentials/TlsCredentials.cs b/src/Testcontainers/Configurations/Credentials/TlsCredentials.cs new file mode 100644 index 000000000..cb8d90d7c --- /dev/null +++ b/src/Testcontainers/Configurations/Credentials/TlsCredentials.cs @@ -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; + } + } +} diff --git a/src/Testcontainers/Configurations/DockerEndpointAuthenticationConfiguration.cs b/src/Testcontainers/Configurations/DockerEndpointAuthenticationConfiguration.cs index 10c63b35b..67767e6bb 100644 --- a/src/Testcontainers/Configurations/DockerEndpointAuthenticationConfiguration.cs +++ b/src/Testcontainers/Configurations/DockerEndpointAuthenticationConfiguration.cs @@ -12,11 +12,16 @@ /// Initializes a new instance of the struct. /// /// The Docker API endpoint. - public DockerEndpointAuthenticationConfiguration(Uri endpoint) + /// The Docker API authentication credentials. + public DockerEndpointAuthenticationConfiguration(Uri endpoint, Docker.DotNet.Credentials credentials = null) { + this.Credentials = credentials; this.Endpoint = endpoint; } + /// + public Docker.DotNet.Credentials Credentials { get; } + /// public Uri Endpoint { get; } @@ -24,7 +29,7 @@ public DockerEndpointAuthenticationConfiguration(Uri endpoint) public DockerClientConfiguration GetDockerClientConfiguration(Guid sessionId = default) { var defaultHttpRequestHeaders = new ReadOnlyDictionary(new Dictionary { { "x-tc-sid", sessionId.ToString("D") } }); - return new DockerClientConfiguration(this.Endpoint, defaultHttpRequestHeaders: defaultHttpRequestHeaders); + return new DockerClientConfiguration(this.Endpoint, this.Credentials, defaultHttpRequestHeaders: defaultHttpRequestHeaders); } } } diff --git a/src/Testcontainers/Configurations/IDockerEndpointAuthenticationConfiguration.cs b/src/Testcontainers/Configurations/IDockerEndpointAuthenticationConfiguration.cs index 3a8a48508..9f0b63f13 100644 --- a/src/Testcontainers/Configurations/IDockerEndpointAuthenticationConfiguration.cs +++ b/src/Testcontainers/Configurations/IDockerEndpointAuthenticationConfiguration.cs @@ -15,6 +15,12 @@ public interface IDockerEndpointAuthenticationConfiguration [NotNull] Uri Endpoint { get; } + /// + /// Gets the Docker API authentication credentials. + /// + [CanBeNull] + Docker.DotNet.Credentials Credentials { get; } + /// /// Gets the Docker client configuration. /// diff --git a/src/Testcontainers/Configurations/TestcontainersSettings.cs b/src/Testcontainers/Configurations/TestcontainersSettings.cs index 2a959bd7f..1f963fff5 100644 --- a/src/Testcontainers/Configurations/TestcontainersSettings.cs +++ b/src/Testcontainers/Configurations/TestcontainersSettings.cs @@ -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()) diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs index eeec3102d..ca0a3649a 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs @@ -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))] @@ -35,7 +36,12 @@ public AuthProviderTestData() { var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var defaultConfiguration = new PropertiesFileConfiguration(Array.Empty()); - 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()), 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()), false }); @@ -49,7 +55,9 @@ private sealed class AuthConfigTestData : List { 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") });