From 3d64b68e056802b2f2e877e572dee8f7d698a305 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Fri, 4 Aug 2023 01:13:26 +0700 Subject: [PATCH] Add SSL support for Remote.Hosting (#345) * Add SSL support for Remote.Hosting * Add missing cert file * Update API approval list * Remove multi-transport support --- .../CoreApiSpec.ApproveRemoting.verified.txt | 22 ++- .../Akka.Remote.Hosting.Tests.csproj | 6 + .../RemoteConfigurationSpecs.cs | 176 +++++++++++++++++- .../Resources/akka-validcert.pfx | Bin 0 -> 2509 bytes .../Akka.Remote.Hosting.csproj | 2 +- .../AkkaRemoteHostingExtensions.cs | 6 +- src/Akka.Remote.Hosting/RemoteOptions.cs | 131 +++++++++++-- 7 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 src/Akka.Remote.Hosting.Tests/Resources/akka-validcert.pfx 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 c7bf7097..6c7aaa69 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 d3712660..6d01c4aa 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 5ddf8689..5f6a1625 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 0000000000000000000000000000000000000000..0d2bff3786bf2a940bf3caab791b36cd016df4e3 GIT binary patch literal 2509 zcmaJ@3piBk8eVfT49N&hE+Y}exU69&ROC7mqYR14Wr!3q#x0b~xRgRn%7}6gg9wA{ zD5Qoiqmq!=u7&MVkz4Al_Bp4s&-0wK*R#I0zW4pU_y5-V|MmRe0-5LzI1B+Y(eWs> zTEhN>B~h3lESHH6K{C+{h&}@{Q9}O}5?Dbp1r|Z3z$eINP$K_QZ4idRbD7BRAQQO? z%Ath+O+$lkL<-enU6K!Xiu2%b!7d~d*-MEQ8jbvP{K#Zt@@rN;rcU#%Q^?_t_obVL z1^us+&G_J~lC6GstuHGTG6YG5JiRR0aq2(%!<0&h4dq~GpvdoVN71k*W5|_j3D=s7!nP-;< z!UIyOECv_-&0W50UGKu^y>KQc>!t0#abT8gS(=(ZJ+-rB(z;rns%gkIN}^^djg_x# z4sP1y+Y;W__j%hL^KXMY6ZwGh*|weL$-5PLa0=~0C8-UnU2QA`$BESS0?@(a6sVar z=;*oiIkayk6H1zIa!^+A5f^;P>x6+^j(0Nd9^OtjhSFyCwepzxlP0Is@blVZh(#?n zrTl68jJ%ZBJqI85^@A)!0`1ZNh;@xrc+pjJKclZsf+trbs92x|w zRGeZnZuAt}S55oR-M@Y_{36LIcmoyRS?%JYVpz;zMp#R5*)&$LtHsMy1V!_M2Yt>Z z2-Cw6khHe^BRpBAVnr#^XQsTMs4|G3$uE+zZD&F?c-GU@=a&(5e$ey2lMhdEFI|1oU^!(rY|MJN z;Juv}b+i862kV{dOv2e)UH!v4f}z{vG?T*${ZttRd+9Hoskl`Ql%@yXQJUE7x@AaK zLkCmLkHYl)?j^;*7m#v`zjrTXdnum#F~xDX<~@FVb49*DzOjC#(#C)nkG?h=wssoz zJYAUvk5Mu^`tCg0e42!)*-b^PqkKmzD$SIS+j9CsS%LQ((tIOLxR#9_S{B<~>HgM} z&AHwk3zf_AGB&)?4j|2P$=KUKdi3Nu-F%0zVXl6%so-pd+q{4HT6^s^6|Jw&{jIln zVY+3tSgoPF0a>Z@i0Dq8IzjZTVegqHzq{{0^Tr6okfwA$YeS{3w6)$EZY;C;vw3R6 zD^8B7xXMLQ`YQ>})Gj*ZfdEm_%`U+$ksW(EX2$JlgR^H3dTE|qq*;)aKoQ^hv7=IJ zNB{6+c5Vx3G4a5b3Y$|*HM2N8R`ujJ+B%8ZrG)FfH6iK}*cIQVV3IMUK#lW>_W1T{ zp!?KO`iGuaS6!dam)<{c9XMc)sgk{ImaX{LEB45$q`6{uvQ?GpAB3=PIquHlVy@!6`p5*3 zF>;8&P{keO_K7G6qly)*y>>hjidGr)7I`jw^0ktrg=5Rw$piKkMzYUFpN-@dGME?B zbJYB23G3k!6MFE>2HVJVqWac2tl@q${O%t&luFBnIe{|EFZa34zPW9WzN+?He4F7w zRdb5?VbSQwFWgJ@vM{fdJ?6%8eT>fL3zqLHq%LD}TIX22+{fj@7kt0OKQ~UNw$N)< zEmV4A7lm_LEsswae;?W5O1>~4G(m`Y2TJ_fF40m16cPyo004AGq46jf62{aOhY=3R z+W-{c6yO0c0DmA5GA|$qPyp

rdtka1uhMkRJxnAOqEQ0)jR-hS006fTHiMEV z#9<^tLe$vDho)&`Zm$sJN%I5cv7&@s1Z^GY1fk<7R#fM&@Nbk%m?9|p>pfsF0RX{- zi2=|P3B#E%7*N}LU1lsmQmRf~Ii_hEXBbnS=Xy6$*k5EZbqBR^>UtHb&~IpQ!lt-B zJ={rppJK0wU^OxA!j~;3Ljj36j8S&d5lhrUmz(~aCpZ7_29noYC#Su&Kx?;1#!X9l zQHpy|T-4-T3ZIAXU2|7BEtJ^t^uBGFtaC7x+_AoGK8x+H+I;$Eh^{LT!R0NP#%;wB zX-AJGv43yPTOJqr%BovPp%|#a1qP%y;Iexa=oqV_gkjVdxK%#->5Q_TFkHH9V{ z2B_4f>0s}dgjbU9NP7*iCr>vHkumxR*HME@TlRlu@mE7EO7a#^bc7-p5On}+=;GsN#Mvc~j07XeK9| z)?X(o*WA%ps=V5nvQGYxI=Gm1(uau7YA$NY1e1*!S>?!MXXShJ;!EAo_1|br`Q+aj9Q9}ahJNxo`Xz2YRg4F-s zC-#6_LBW51{xV2O9?ILGfAQcB|1!fDNWI8NnTt(HNJ&)JHiv#9W3-!BxGP?_*M@k+ Kq5kKO)jt5Y$(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 3630e272..457d74fe 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 6b477cfc..08f199cc 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