diff --git a/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt b/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt index c7bf709..6c7aaa6 100644 --- a/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt +++ b/src/Akka.Hosting.API.Tests/verify/CoreApiSpec.ApproveRemoting.verified.txt @@ -6,13 +6,31 @@ public static Akka.Hosting.AkkaConfigurationBuilder WithRemoting(this Akka.Hosting.AkkaConfigurationBuilder builder, System.Action configure) { } public static Akka.Hosting.AkkaConfigurationBuilder WithRemoting(this Akka.Hosting.AkkaConfigurationBuilder builder, string? hostname = null, int? port = default, string? publicHostname = null, int? publicPort = default) { } } - public sealed class RemoteOptions + public class RemoteOptions { public RemoteOptions() { } + public bool? EnableSsl { get; set; } public string? HostName { get; set; } public int? Port { get; set; } public string? PublicHostName { get; set; } public int? PublicPort { get; set; } - public override string ToString() { } + public Akka.Remote.Hosting.SslOptions Ssl { get; set; } + } + public sealed class SslCertificateOptions + { + public SslCertificateOptions() { } + public string? Password { get; set; } + public string? Path { get; set; } + public string? StoreLocation { get; set; } + public string? StoreName { get; set; } + public string? Thumbprint { get; set; } + public bool? UseThumbprintOverFile { get; set; } + } + public sealed class SslOptions + { + public SslOptions() { } + public Akka.Remote.Hosting.SslCertificateOptions CertificateOptions { get; set; } + public bool? SuppressValidation { get; set; } + public System.Security.Cryptography.X509Certificates.X509Certificate2? X509Certificate { get; set; } } } \ No newline at end of file diff --git a/src/Akka.Remote.Hosting.Tests/Akka.Remote.Hosting.Tests.csproj b/src/Akka.Remote.Hosting.Tests/Akka.Remote.Hosting.Tests.csproj index d371266..6d01c4a 100644 --- a/src/Akka.Remote.Hosting.Tests/Akka.Remote.Hosting.Tests.csproj +++ b/src/Akka.Remote.Hosting.Tests/Akka.Remote.Hosting.Tests.csproj @@ -16,4 +16,10 @@ + + + + Always + + diff --git a/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs b/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs index 5ddf868..5f6a162 100644 --- a/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs +++ b/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs @@ -1,9 +1,13 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; using Akka.Hosting; +using Akka.Remote.Transport.DotNetty; using FluentAssertions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -41,6 +45,37 @@ public async Task EmptyWithRemotingConfigTest() tcpConfig.GetInt("port").Should().Be(2552); tcpConfig.GetString("public-hostname").Should().BeEmpty(); tcpConfig.GetInt("public-port").Should().Be(0); + tcpConfig.GetBoolean("enable-ssl").Should().BeFalse(); + } + + [Fact(DisplayName = "Empty WithRemoting should return default remoting settings")] + public async Task WithRemotingWithEmptyOptionsConfigTest() + { + // arrange + using var host = new HostBuilder().ConfigureServices(services => + { + services.AddAkka("RemoteSys", (builder, provider) => + { + builder.WithRemoting(new RemoteOptions()); + }); + }).Build(); + + // act + await host.StartAsync(); + var actorSystem = (ExtendedActorSystem)host.Services.GetRequiredService(); + var config = actorSystem.Settings.Config; + var adapters = config.GetStringList("akka.remote.enabled-transports"); + var tcpConfig = config.GetConfig("akka.remote.dot-netty.tcp"); + + // assert + adapters.Count.Should().Be(1); + adapters[0].Should().Be("akka.remote.dot-netty.tcp"); + + tcpConfig.GetString("hostname").Should().BeEmpty(); + tcpConfig.GetInt("port").Should().Be(2552); + tcpConfig.GetString("public-hostname").Should().BeEmpty(); + tcpConfig.GetInt("public-port").Should().Be(0); + tcpConfig.GetBoolean("enable-ssl").Should().BeFalse(); } [Fact(DisplayName = "WithRemoting should override remote settings")] @@ -136,6 +171,145 @@ public async Task WithRemotingConfigOverrideTest() tcpConfig.GetInt("public-port").Should().Be(12345); } + [Fact(DisplayName = "RemoteOptions should override remote settings that are overriden")] + public void WithRemotingOptionsOverrideTest() + { + // arrange + var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test"); + builder.WithRemoting(new RemoteOptions + { + HostName = "a", + PublicHostName = "b", + Port = 123, + PublicPort = 456, + EnableSsl = true, + Ssl = new SslOptions + { + SuppressValidation = true, + CertificateOptions = new SslCertificateOptions + { + Path = "c", + Password = "d", + UseThumbprintOverFile = true, + Thumbprint = "e", + StoreName = "f", + StoreLocation = "g", + } + } + }); + + // act + var config = builder.Configuration.Value; + var tcpConfig = config.GetConfig("akka.remote.dot-netty.tcp"); + + // assert + tcpConfig.GetString("hostname").Should().Be("a"); + tcpConfig.GetInt("port").Should().Be(123); + tcpConfig.GetString("public-hostname").Should().Be("b"); + tcpConfig.GetInt("public-port").Should().Be(456); + + var sslConfig = tcpConfig.GetConfig("ssl"); + sslConfig.GetBoolean("suppress-validation").Should().BeTrue(); + + var certConfig = sslConfig.GetConfig("certificate"); + certConfig.GetString("path").Should().Be("c"); + certConfig.GetString("password").Should().Be("d"); + certConfig.GetBoolean("use-thumbprint-over-file").Should().BeTrue(); + certConfig.GetString("thumbprint").Should().Be("e"); + certConfig.GetString("store-name").Should().Be("f"); + certConfig.GetString("store-location").Should().Be("g"); + } + + [Fact(DisplayName = "RemoteOptions using configurator should override remote settings that are overriden")] + public void WithRemotingOptionsConfiguratorOverrideTest() + { + // arrange + var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test"); + builder.WithRemoting(opt => + { + opt.HostName = "a"; + opt.PublicHostName = "b"; + opt.Port = 123; + opt.PublicPort = 456; + opt.EnableSsl = true; + opt.Ssl.SuppressValidation = true; + opt.Ssl.CertificateOptions.Path = "c"; + opt.Ssl.CertificateOptions.Password = "d"; + opt.Ssl.CertificateOptions.UseThumbprintOverFile = true; + opt.Ssl.CertificateOptions.Thumbprint = "e"; + opt.Ssl.CertificateOptions.StoreName = "f"; + opt.Ssl.CertificateOptions.StoreLocation = "g"; + }); + + // act + var config = builder.Configuration.Value; + var tcpConfig = config.GetConfig("akka.remote.dot-netty.tcp"); + + // assert + tcpConfig.GetString("hostname").Should().Be("a"); + tcpConfig.GetInt("port").Should().Be(123); + tcpConfig.GetString("public-hostname").Should().Be("b"); + tcpConfig.GetInt("public-port").Should().Be(456); + + var sslConfig = tcpConfig.GetConfig("ssl"); + sslConfig.GetBoolean("suppress-validation").Should().BeTrue(); + + var certConfig = sslConfig.GetConfig("certificate"); + certConfig.GetString("path").Should().Be("c"); + certConfig.GetString("password").Should().Be("d"); + certConfig.GetBoolean("use-thumbprint-over-file").Should().BeTrue(); + certConfig.GetString("thumbprint").Should().Be("e"); + certConfig.GetString("store-name").Should().Be("f"); + certConfig.GetString("store-location").Should().Be("g"); + } + + [Fact(DisplayName = "RemoteOptions with explicit certificate and ssl enabled should use provided certificate")] + public void WithRemotingOptionsSslEnabledCertificateTest() + { + // arrange + var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password"); + var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test"); + builder.WithRemoting(new RemoteOptions + { + EnableSsl = true, + Ssl = new SslOptions + { + SuppressValidation = true, + X509Certificate = certificate + } + }); + + // act + var setup = (DotNettySslSetup) builder.Setups.First(s => s is DotNettySslSetup); + + // assert + setup.SuppressValidation.Should().BeTrue(); + setup.Certificate.Should().Be(certificate); + } + + [Fact(DisplayName = "RemoteOptions with explicit certificate and ssl disabled should ignore provided certificate")] + public void WithRemotingOptionsSslDisabledCertificateTest() + { + // arrange + var certificate = new X509Certificate2("./Resources/akka-validcert.pfx", "password"); + var builder = new AkkaConfigurationBuilder(new ServiceCollection(), "test"); + builder.WithRemoting(new RemoteOptions + { + EnableSsl = false, + Ssl = new SslOptions + { + SuppressValidation = true, + X509Certificate = certificate + } + }); + + // act + var setup = builder.Setups.FirstOrDefault(s => s is DotNettySslSetup); + + // assert + setup.Should().BeNull(); + } + [Fact] public async Task AkkaRemoteShouldUsePublicHostnameCorrectly() { diff --git a/src/Akka.Remote.Hosting.Tests/Resources/akka-validcert.pfx b/src/Akka.Remote.Hosting.Tests/Resources/akka-validcert.pfx new file mode 100644 index 0000000..0d2bff3 Binary files /dev/null and b/src/Akka.Remote.Hosting.Tests/Resources/akka-validcert.pfx differ diff --git a/src/Akka.Remote.Hosting/Akka.Remote.Hosting.csproj b/src/Akka.Remote.Hosting/Akka.Remote.Hosting.csproj index 6a4e92e..9de83ac 100644 --- a/src/Akka.Remote.Hosting/Akka.Remote.Hosting.csproj +++ b/src/Akka.Remote.Hosting/Akka.Remote.Hosting.csproj @@ -3,7 +3,7 @@ $(LibraryFramework) README.md Akka.Remote Microsoft.Extensions.Hosting support. - 9.0 + Latest enable diff --git a/src/Akka.Remote.Hosting/AkkaRemoteHostingExtensions.cs b/src/Akka.Remote.Hosting/AkkaRemoteHostingExtensions.cs index 3630e27..457d74f 100644 --- a/src/Akka.Remote.Hosting/AkkaRemoteHostingExtensions.cs +++ b/src/Akka.Remote.Hosting/AkkaRemoteHostingExtensions.cs @@ -60,11 +60,7 @@ public static AkkaConfigurationBuilder WithRemoting( this AkkaConfigurationBuilder builder, RemoteOptions options) { - var config = options.ToString(); - - // prepend the remoting configuration to the front - if(!string.IsNullOrEmpty(config)) - builder.AddHocon(config, HoconAddMode.Prepend); + options.Build(builder); if (builder.ActorRefProvider.HasValue) { diff --git a/src/Akka.Remote.Hosting/RemoteOptions.cs b/src/Akka.Remote.Hosting/RemoteOptions.cs index 6b477cf..08f199c 100644 --- a/src/Akka.Remote.Hosting/RemoteOptions.cs +++ b/src/Akka.Remote.Hosting/RemoteOptions.cs @@ -4,12 +4,18 @@ // // ----------------------------------------------------------------------- +using System; +using System.Collections.Generic; using System.Net; +using System.Security.Cryptography.X509Certificates; using System.Text; +using Akka.Configuration; +using Akka.Hosting; +using Akka.Remote.Transport.DotNetty; namespace Akka.Remote.Hosting { - public sealed class RemoteOptions + public class RemoteOptions { /// /// The hostname or ip to bind akka remoting to, is used if empty @@ -39,25 +45,124 @@ public sealed class RemoteOptions /// public int? PublicPort { get; set; } - public override string ToString() + public bool? EnableSsl { get; set; } + + public SslOptions Ssl { get; set; } = new (); + + internal void Build(AkkaConfigurationBuilder builder) { var sb = new StringBuilder(); + Build(sb); + if (sb.Length > 0) + builder.AddHocon(sb.ToString(), HoconAddMode.Prepend); + + if (EnableSsl is false || Ssl.X509Certificate == null) + return; + + var suppressValidation = Ssl.SuppressValidation ?? false; + builder.AddSetup(new DotNettySslSetup(Ssl.X509Certificate, suppressValidation)); + } + + private void Build(StringBuilder builder) + { + var sb = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(HostName)) - sb.AppendFormat("hostname = {0}\n", HostName); - if (Port != null) - sb.AppendFormat("port = {0}\n", Port); - if(!string.IsNullOrWhiteSpace(PublicHostName)) - sb.AppendFormat("public-hostname = {0}\n", PublicHostName); - if(PublicPort != null) - sb.AppendFormat("public-port = {0}\n", PublicPort); + sb.AppendLine($"hostname = {HostName.ToHocon()}"); + + if (Port is not null) + sb.AppendLine($"port = {Port}"); + + if (!string.IsNullOrWhiteSpace(PublicHostName)) + sb.AppendLine($"public-hostname = {PublicHostName.ToHocon()}"); + + if (PublicPort is not null) + sb.AppendLine($"public-port = {PublicPort}"); + + if (EnableSsl is not null) + { + sb.AppendLine($"enable-ssl = {EnableSsl.ToHocon()}"); + if (EnableSsl.Value) + { + if(Ssl is null) + throw new ConfigurationException("Ssl property need to be populated when EnableSsl is set to true."); + + Ssl.Build(sb); + } + } + + if(sb.Length == 0) + return; + + sb.Insert(0, "akka.remote.dot-netty.tcp {\n"); + sb.Append("}"); + builder.Append(sb); + } + + } + + public sealed class SslOptions + { + public bool? SuppressValidation { get; set; } + public X509Certificate2? X509Certificate { get; set; } + public SslCertificateOptions CertificateOptions { get; set; } = new (); + + internal void Build(StringBuilder builder) + { + var sb = new StringBuilder(); + + if (SuppressValidation is not null) + sb.AppendLine($"suppress-validation = {SuppressValidation.ToHocon()}"); + + CertificateOptions.Build(sb); + + if(sb.Length == 0) + return; + + sb.Insert(0, "ssl {"); + sb.AppendLine("}"); + builder.Append(sb); + } + } + public sealed class SslCertificateOptions + { + public string? Path { get; set; } + public string? Password { get; set; } + public bool? UseThumbprintOverFile { get; set; } + public string? Thumbprint { get; set; } + public string? StoreName { get; set; } + public string? StoreLocation { get; set; } + + internal void Build(StringBuilder builder) + { + var sb = new StringBuilder(); + + if (!string.IsNullOrEmpty(Path)) + sb.AppendLine($"path = {Path.ToHocon()}"); + + if (!string.IsNullOrEmpty(Password)) + sb.AppendLine($"password = {Password.ToHocon()}"); + + if (UseThumbprintOverFile is not null) + sb.AppendLine($"use-thumbprint-over-file = {UseThumbprintOverFile.ToHocon()}"); + + if (!string.IsNullOrEmpty(Thumbprint)) + sb.AppendLine($"thumbprint = {Thumbprint.ToHocon()}"); + + if (!string.IsNullOrEmpty(StoreName)) + sb.AppendLine($"store-name = {StoreName.ToHocon()}"); + + if (!string.IsNullOrEmpty(StoreLocation)) + sb.AppendLine($"store-location = {StoreLocation.ToHocon()}"); + if (sb.Length == 0) - return string.Empty; + return; - sb.Insert(0, "akka.remote.dot-netty.tcp {\n"); - sb.Append("}"); - return sb.ToString(); + sb.Insert(0, "certificate {\n"); + sb.AppendLine("}"); + builder.Append(sb); } } } \ No newline at end of file