diff --git a/src/NATS.Client.Core/NatsConnection.cs b/src/NATS.Client.Core/NatsConnection.cs index 85862e7b7..834446c0c 100644 --- a/src/NATS.Client.Core/NatsConnection.cs +++ b/src/NATS.Client.Core/NatsConnection.cs @@ -76,7 +76,7 @@ public NatsConnection() public NatsConnection(NatsOpts opts) { _logger = opts.LoggerFactory.CreateLogger(); - Opts = opts; + Opts = ReadUserInfoFromConnectionString(opts); ConnectionState = NatsConnectionState.Closed; _waitForOpenConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _disposedCancellationTokenSource = new CancellationTokenSource(); @@ -289,6 +289,62 @@ internal ValueTask UnsubscribeAsync(int sid) return default; } + + private static NatsOpts ReadUserInfoFromConnectionString(NatsOpts opts) + { + var first = true; + + var natsUris = opts.GetSeedUris(); + var maskedUris = new List(natsUris.Length); + + foreach (var natsUri in natsUris) + { + var uriBuilder = new UriBuilder(natsUri.Uri); + + if (uriBuilder.UserName is { Length: > 0 } username) + { + if (first) + { + first = false; + + if (uriBuilder.Password is { Length: > 0 } password) + { + opts = opts with + { + AuthOpts = opts.AuthOpts with + { + Username = uriBuilder.UserName, + Password = uriBuilder.Password, + }, + }; + + uriBuilder.Password = "***"; // to redact the password from logs + } + else + { + opts = opts with + { + AuthOpts = opts.AuthOpts with + { + Token = uriBuilder.UserName, + }, + }; + + uriBuilder.UserName = "***"; // to redact the token from logs + } + } + + } + + maskedUris.Add(uriBuilder.ToString().TrimEnd('/')); + } + + var combinedUri = string.Join(",", maskedUris); + opts = opts with { Url = combinedUri }; + + return opts; + } + private async ValueTask InitialConnectAsync() { Debug.Assert(ConnectionState == NatsConnectionState.Connecting, "Connection state"); diff --git a/tests/NATS.Client.Core.Tests/ClusterTests.cs b/tests/NATS.Client.Core.Tests/ClusterTests.cs index 0ef8be7fc..05b3f2cec 100644 --- a/tests/NATS.Client.Core.Tests/ClusterTests.cs +++ b/tests/NATS.Client.Core.Tests/ClusterTests.cs @@ -2,23 +2,43 @@ namespace NATS.Client.Core.Tests; public class ClusterTests(ITestOutputHelper output) { - [Fact] - public async Task Seed_urls_on_retry() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Seed_urls_on_retry(bool userAuthInUrl) { await using var cluster1 = new NatsCluster( new NullOutputHelper(), TransportType.Tcp, - (i, b) => b.WithServerName($"c1n{i}")); + (i, b) => + { + b.WithServerName($"c1n{i}"); + if (userAuthInUrl) + { + b.AddServerConfig("resources/configs/auth/password.conf"); + b.WithClientUrlAuthentication("a", "b"); + } + }, + userAuthInUrl); await using var cluster2 = new NatsCluster( new NullOutputHelper(), TransportType.Tcp, - (i, b) => b.WithServerName($"c2n{i}")); + (i, b) => + { + b.WithServerName($"c2n{i}"); + if (userAuthInUrl) + { + b.AddServerConfig("resources/configs/auth/password.conf"); + b.WithClientUrlAuthentication("a", "b"); + } + }, + userAuthInUrl); // Use the first node from each cluster as the seed // so that we can confirm seeds are used on retry - var url1 = cluster1.Server1.ClientUrl; - var url2 = cluster2.Server1.ClientUrl; + var url1 = userAuthInUrl ? cluster1.Server1.ClientUrlWithAuth : cluster1.Server1.ClientUrl; + var url2 = userAuthInUrl ? cluster2.Server1.ClientUrlWithAuth : cluster2.Server1.ClientUrl; await using var nats = new NatsConnection(new NatsOpts { diff --git a/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs b/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs index 16aad262b..7e2baa12e 100644 --- a/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs +++ b/tests/NATS.Client.Core.Tests/NatsConnectionTest.Auth.cs @@ -12,6 +12,15 @@ public static IEnumerable GetAuthConfigs() NatsOpts.Default with { AuthOpts = NatsAuthOpts.Default with { Token = "s3cr3t", }, }), }; + yield return new object[] + { + new Auth( + "TOKEN_IN_CONNECTIONSTRING", + "resources/configs/auth/token.conf", + NatsOpts.Default, + urlAuth: "s3cr3t"), + }; + yield return new object[] { new Auth( @@ -23,6 +32,15 @@ NatsOpts.Default with }), }; + yield return new object[] + { + new Auth( + "USER-PASSWORD_IN_CONNECTIONSTRING", + "resources/configs/auth/password.conf", + NatsOpts.Default, + urlAuth: "a:b"), + }; + yield return new object[] { new Auth( @@ -84,15 +102,22 @@ public async Task UserCredentialAuthTest(Auth auth) var name = auth.Name; var serverConfig = auth.ServerConfig; var clientOpts = auth.ClientOpts; + var useAuthInUrl = !string.IsNullOrEmpty(auth.UrlAuth); _output.WriteLine($"AUTH TEST {name}"); - var serverOpts = new NatsServerOptsBuilder() + var serverOptsBuilder = new NatsServerOptsBuilder() .UseTransport(_transportType) - .AddServerConfig(serverConfig) - .Build(); + .AddServerConfig(serverConfig); + + if (useAuthInUrl) + { + serverOptsBuilder.WithClientUrlAuthentication(auth.UrlAuth); + } - await using var server = NatsServer.Start(_output, serverOpts, clientOpts); + var serverOpts = serverOptsBuilder.Build(); + + await using var server = NatsServer.Start(_output, serverOpts, clientOpts, useAuthInUrl); var subject = Guid.NewGuid().ToString("N"); @@ -104,8 +129,8 @@ public async Task UserCredentialAuthTest(Auth auth) Assert.Contains("Authorization Violation", natsException.GetBaseException().Message); } - await using var subConnection = server.CreateClientConnection(clientOpts); - await using var pubConnection = server.CreateClientConnection(clientOpts); + await using var subConnection = server.CreateClientConnection(clientOpts, useAuthInUrl: useAuthInUrl); + await using var pubConnection = server.CreateClientConnection(clientOpts, useAuthInUrl: useAuthInUrl); var signalComplete1 = new WaitSignal(); var signalComplete2 = new WaitSignal(); @@ -141,7 +166,7 @@ await Retry.Until( await disconnectSignal2; _output.WriteLine("START NEW SERVER"); - await using var newServer = NatsServer.Start(_output, serverOpts, clientOpts); + await using var newServer = NatsServer.Start(_output, serverOpts, clientOpts, useAuthInUrl); await subConnection.ConnectAsync(); // wait open again await pubConnection.ConnectAsync(); // wait open again @@ -162,11 +187,12 @@ await Retry.Until( public class Auth { - public Auth(string name, string serverConfig, NatsOpts clientOpts) + public Auth(string name, string serverConfig, NatsOpts clientOpts, string urlAuth = null) { Name = name; ServerConfig = serverConfig; ClientOpts = clientOpts; + UrlAuth = urlAuth; } public string Name { get; } @@ -175,6 +201,8 @@ public Auth(string name, string serverConfig, NatsOpts clientOpts) public NatsOpts ClientOpts { get; } + public string UrlAuth { get; } + public override string ToString() => Name; } } diff --git a/tests/NATS.Client.TestUtilities/NatsServer.cs b/tests/NATS.Client.TestUtilities/NatsServer.cs index 83cb90001..682a3fa79 100644 --- a/tests/NATS.Client.TestUtilities/NatsServer.cs +++ b/tests/NATS.Client.TestUtilities/NatsServer.cs @@ -85,6 +85,22 @@ private NatsServer(ITestOutputHelper outputHelper, NatsServerOpts opts) _ => throw new ArgumentOutOfRangeException(), }; + public string ClientUrlWithAuth + { + get + { + if (!string.IsNullOrEmpty(Opts.ClientUrlUserName)) + { + var uriBuilder = new UriBuilder(ClientUrl); + uriBuilder.UserName = Opts.ClientUrlUserName; + uriBuilder.Password = Opts.ClientUrlPassword; + return uriBuilder.ToString().TrimEnd('/'); + } + + return ClientUrl; + } + } + public int ConnectionPort { get @@ -134,7 +150,7 @@ public static NatsServer StartWithTrace(ITestOutputHelper outputHelper) public static NatsServer Start(ITestOutputHelper outputHelper, TransportType transportType) => Start(outputHelper, new NatsServerOptsBuilder().UseTransport(transportType).Build()); - public static NatsServer Start(ITestOutputHelper outputHelper, NatsServerOpts opts, NatsOpts? clientOpts = default) + public static NatsServer Start(ITestOutputHelper outputHelper, NatsServerOpts opts, NatsOpts? clientOpts = default, bool useAuthInUrl = false) { NatsServer? server = null; NatsConnection? nats = null; @@ -144,7 +160,7 @@ public static NatsServer Start(ITestOutputHelper outputHelper, NatsServerOpts op { server = new NatsServer(outputHelper, opts); server.StartServerProcess(); - nats = server.CreateClientConnection(clientOpts ?? NatsOpts.Default, reTryCount: 3); + nats = server.CreateClientConnection(clientOpts ?? GetDefaultClientOpts(server), reTryCount: 3, useAuthInUrl: useAuthInUrl); #pragma warning disable CA2012 return server; } @@ -162,6 +178,11 @@ public static NatsServer Start(ITestOutputHelper outputHelper, NatsServerOpts op throw new Exception("Can't start nats-server and connect to it"); } + private static NatsOpts GetDefaultClientOpts(NatsServer server) + { + return NatsOpts.Default/* with { Url = server.ClientUrl }*/; + } + public void StartServerProcess() { _cancellationTokenSource = new CancellationTokenSource(); @@ -335,13 +356,13 @@ public async ValueTask DisposeAsync() return (client, proxy); } - public NatsConnection CreateClientConnection(NatsOpts? options = default, int reTryCount = 10, bool ignoreAuthorizationException = false, bool testLogger = true) + public NatsConnection CreateClientConnection(NatsOpts? options = default, int reTryCount = 10, bool ignoreAuthorizationException = false, bool testLogger = true, bool useAuthInUrl = false) { for (var i = 0; i < reTryCount; i++) { try { - var nats = new NatsConnection(ClientOpts(options ?? NatsOpts.Default, testLogger: testLogger)); + var nats = new NatsConnection(ClientOpts(options ?? GetDefaultClientOpts(this), testLogger: testLogger, useAuthInUrl: useAuthInUrl)); try { @@ -369,14 +390,14 @@ public NatsConnection CreateClientConnection(NatsOpts? options = default, int re throw new Exception("Can't create a connection to nats-server"); } - public NatsConnectionPool CreatePooledClientConnection() => CreatePooledClientConnection(NatsOpts.Default); + public NatsConnectionPool CreatePooledClientConnection() => CreatePooledClientConnection(GetDefaultClientOpts(this)); public NatsConnectionPool CreatePooledClientConnection(NatsOpts opts) { return new NatsConnectionPool(4, ClientOpts(opts)); } - public NatsOpts ClientOpts(NatsOpts opts, bool testLogger = true) + public NatsOpts ClientOpts(NatsOpts opts, bool testLogger = true, bool useAuthInUrl = false) { var natsTlsOpts = Opts.EnableTls ? opts.TlsOpts with @@ -392,7 +413,7 @@ public NatsOpts ClientOpts(NatsOpts opts, bool testLogger = true) { LoggerFactory = testLogger ? _loggerFactory : opts.LoggerFactory, TlsOpts = natsTlsOpts, - Url = ClientUrl, + Url = useAuthInUrl ? ClientUrlWithAuth : ClientUrl, }; } @@ -431,6 +452,15 @@ private static (string configFileName, string config, string cmd) GetCmd(NatsSer var cmd = $"{NatsServerPath} -c {configFileName}"; + //var cmdBuilder = new StringBuilder($"{NatsServerPath} -c {configFileName}"); + + //if (!string.IsNullOrEmpty(opts.UserName) && !string.IsNullOrEmpty(opts.Password)) + //{ + // cmdBuilder.Append($" --user {opts.UserName} --pass {opts.Password}"); + //} + + //var cmd = cmdBuilder.ToString(); + return (configFileName, config, cmd); } @@ -464,7 +494,7 @@ public class NatsCluster : IAsyncDisposable { private readonly ITestOutputHelper _outputHelper; - public NatsCluster(ITestOutputHelper outputHelper, TransportType transportType, Action? configure = default) + public NatsCluster(ITestOutputHelper outputHelper, TransportType transportType, Action? configure = default, bool useAuthInUrl = false) { _outputHelper = outputHelper; @@ -516,13 +546,13 @@ public NatsCluster(ITestOutputHelper outputHelper, TransportType transportType, } _outputHelper.WriteLine($"Starting server 1..."); - Server1 = NatsServer.Start(outputHelper, opts1); + Server1 = NatsServer.Start(outputHelper, opts1, useAuthInUrl: useAuthInUrl); _outputHelper.WriteLine($"Starting server 2..."); - Server2 = NatsServer.Start(outputHelper, opts2); + Server2 = NatsServer.Start(outputHelper, opts2, useAuthInUrl: useAuthInUrl); _outputHelper.WriteLine($"Starting server 3..."); - Server3 = NatsServer.Start(outputHelper, opts3); + Server3 = NatsServer.Start(outputHelper, opts3, useAuthInUrl: useAuthInUrl); } public NatsServer Server1 { get; } diff --git a/tests/NATS.Client.TestUtilities/NatsServerOpts.cs b/tests/NATS.Client.TestUtilities/NatsServerOpts.cs index 43c69ba26..2f2b6fc3a 100644 --- a/tests/NATS.Client.TestUtilities/NatsServerOpts.cs +++ b/tests/NATS.Client.TestUtilities/NatsServerOpts.cs @@ -31,6 +31,8 @@ public sealed class NatsServerOptsBuilder private bool _serverDisposeReturnsPorts; private bool _enableClustering; private bool _trace; + private string? _clientUrlUserName; + private string? _clientUrlPassword; public NatsServerOpts Build() => new() { @@ -40,6 +42,8 @@ public sealed class NatsServerOptsBuilder TlsVerify = _tlsVerify, EnableJetStream = _enableJetStream, ServerName = _serverName, + ClientUrlUserName = _clientUrlUserName, + ClientUrlPassword = _clientUrlPassword, TlsServerCertFile = _tlsServerCertFile, TlsServerKeyFile = _tlsServerKeyFile, TlsClientCertFile = _tlsClientCertFile, @@ -110,6 +114,21 @@ public NatsServerOptsBuilder WithServerName(string serverName) return this; } + public NatsServerOptsBuilder WithClientUrlAuthentication(string userName, string password) + { + _clientUrlUserName = userName; + _clientUrlPassword = password; + return this; + } + + public NatsServerOptsBuilder WithClientUrlAuthentication(string authInfo) + { + var infoParts = authInfo.Split(':'); + _clientUrlUserName = infoParts.FirstOrDefault(); + _clientUrlPassword = infoParts.ElementAtOrDefault(1); + return this; + } + public NatsServerOptsBuilder UseJetStream() { _enableJetStream = true; @@ -176,6 +195,10 @@ public NatsServerOpts() public bool ServerDisposeReturnsPorts { get; init; } = true; + public string? ClientUrlUserName { get; set; } + + public string? ClientUrlPassword { get; set; } + public string? TlsClientCertFile { get; init; } public string? TlsClientKeyFile { get; init; }