diff --git a/src/NATS.Client.Core/Internal/WebSocketConnection.cs b/src/NATS.Client.Core/Internal/WebSocketConnection.cs
index 74085e21e..60c4e4649 100644
--- a/src/NATS.Client.Core/Internal/WebSocketConnection.cs
+++ b/src/NATS.Client.Core/Internal/WebSocketConnection.cs
@@ -1,3 +1,4 @@
+using System.Net.Security;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Runtime.CompilerServices;
@@ -39,13 +40,13 @@ public Task ConnectAsync(Uri uri, CancellationToken cancellationToken)
///
/// Connect with Timeout. When failed, Dispose this connection.
///
- public async ValueTask ConnectAsync(Uri uri, NatsOpts opts)
+ public async ValueTask ConnectAsync(NatsUri uri, NatsOpts opts)
{
using var cts = new CancellationTokenSource(opts.ConnectTimeout);
try
{
- await InvokeCallbackForClientWebSocketOptionsAsync(opts, uri, _socket.Options, cts.Token).ConfigureAwait(false);
- await _socket.ConnectAsync(uri, cts.Token).ConfigureAwait(false);
+ await opts.WebSocketOpts.ApplyClientWebSocketOptionsAsync(_socket.Options, uri, opts.TlsOpts, cts.Token).ConfigureAwait(false);
+ await _socket.ConnectAsync(uri.Uri, cts.Token).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -131,13 +132,4 @@ public void SignalDisconnected(Exception exception)
{
_waitForClosedSource.TrySetResult(exception);
}
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private async Task InvokeCallbackForClientWebSocketOptionsAsync(NatsOpts opts, Uri uri, ClientWebSocketOptions options, CancellationToken token)
- {
- if (opts.ConfigureWebSocketOpts != null)
- {
- await opts.ConfigureWebSocketOpts(uri, options, token).ConfigureAwait(false);
- }
- }
}
diff --git a/src/NATS.Client.Core/NatsConnection.cs b/src/NATS.Client.Core/NatsConnection.cs
index f08315b8f..a8413a5b5 100644
--- a/src/NATS.Client.Core/NatsConnection.cs
+++ b/src/NATS.Client.Core/NatsConnection.cs
@@ -318,7 +318,7 @@ private async ValueTask InitialConnectAsync()
if (uri.IsWebSocket)
{
var conn = new WebSocketConnection();
- await conn.ConnectAsync(uri.Uri, Opts).ConfigureAwait(false);
+ await conn.ConnectAsync(uri, Opts).ConfigureAwait(false);
_socket = conn;
}
else
@@ -606,7 +606,7 @@ private async void ReconnectLoop()
{
_logger.LogDebug(NatsLogEvents.Connection, "Trying to reconnect using WebSocket {Url} [{ReconnectCount}]", url, reconnectCount);
var conn = new WebSocketConnection();
- await conn.ConnectAsync(url.Uri, Opts).ConfigureAwait(false);
+ await conn.ConnectAsync(url, Opts).ConfigureAwait(false);
_socket = conn;
}
else
diff --git a/src/NATS.Client.Core/NatsOpts.cs b/src/NATS.Client.Core/NatsOpts.cs
index 49b1ef1d9..2c2245b2a 100644
--- a/src/NATS.Client.Core/NatsOpts.cs
+++ b/src/NATS.Client.Core/NatsOpts.cs
@@ -28,6 +28,8 @@ public sealed record NatsOpts
public NatsTlsOpts TlsOpts { get; init; } = NatsTlsOpts.Default;
+ public NatsWebSocketOpts WebSocketOpts { get; init; } = NatsWebSocketOpts.Default;
+
public INatsSerializerRegistry SerializerRegistry { get; init; } = NatsDefaultSerializerRegistry.Default;
public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
@@ -115,28 +117,6 @@ public sealed record NatsOpts
///
public BoundedChannelFullMode SubPendingChannelFullMode { get; init; } = BoundedChannelFullMode.DropNewest;
- ///
- /// An optional async callback handler for manipulation of ClientWebSocketOptions used for WebSocket connections.
- ///
- ///
- /// This can be used to set authorization header and other HTTP header values.
- /// Note: Setting HTTP header values is not supported by Blazor WebAssembly as the underlying browser implementation does not support adding headers to a WebSocket.
- /// The callback's execution time contributes to the connection establishment subject to the .
- /// Implementors should use the passed CancellationToken for async operations called by this handler.
- ///
- ///
- /// await using var nats = new NatsConnection(new NatsOpts
- /// {
- /// Url = "ws://localhost:8080",
- /// ConfigureWebSocketOpts = (serverUri, clientWsOpts, ct) =>
- /// {
- /// clientWsOpts.SetRequestHeader("authorization", $"Bearer MY_TOKEN");
- /// return ValueTask.CompletedTask;
- /// },
- /// });
- ///
- public Func? ConfigureWebSocketOpts { get; init; } = null;
-
internal NatsUri[] GetSeedUris()
{
var urls = Url.Split(',');
diff --git a/src/NATS.Client.Core/NatsWebSocketOpts.cs b/src/NATS.Client.Core/NatsWebSocketOpts.cs
new file mode 100644
index 000000000..3ed2a1db9
--- /dev/null
+++ b/src/NATS.Client.Core/NatsWebSocketOpts.cs
@@ -0,0 +1,79 @@
+using System.Net.WebSockets;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Primitives;
+using NATS.Client.Core.Internal;
+
+namespace NATS.Client.Core;
+
+///
+/// Options for ClientWebSocketOptions
+///
+public sealed record NatsWebSocketOpts
+{
+ public static readonly NatsWebSocketOpts Default = new();
+
+ ///
+ /// An optional dictionary of HTTP request headers to be sent with the WebSocket request.
+ ///
+ ///
+ /// Not supported when running in the Browser, such as when using Blazor WebAssembly,
+ /// as the underlying Browser implementation does not support adding headers to a WebSocket.
+ ///
+ public IDictionary? RequestHeaders { get; init; }
+
+ ///
+ /// An optional async callback handler for manipulation of ClientWebSocketOptions used for WebSocket connections.
+ /// Implementors should use the passed CancellationToken for async operations called by this handler.
+ ///
+ public Func? ConfigureClientWebSocketOptions { get; init; } = null;
+
+ internal async ValueTask ApplyClientWebSocketOptionsAsync(
+ ClientWebSocketOptions clientWebSocketOptions,
+ NatsUri uri,
+ NatsTlsOpts tlsOpts,
+ CancellationToken cancellationToken)
+ {
+ if (RequestHeaders != null)
+ {
+ foreach (var entry in RequestHeaders)
+ {
+ // SetRequestHeader overwrites if called multiple times;
+ // RFC7230 Section 3.2.2 allows for combining them with a comma
+ // https://www.rfc-editor.org/rfc/rfc7230#section-3.2.2
+ clientWebSocketOptions.SetRequestHeader(entry.Key, string.Join(",", entry.Value));
+ }
+ }
+
+ if (tlsOpts.HasTlsCerts)
+ {
+ var authenticateAsClientOptions = await tlsOpts.AuthenticateAsClientOptionsAsync(uri).ConfigureAwait(false);
+ var collection = new X509CertificateCollection();
+
+ // must match LoadClientCertFromX509 method in SslClientAuthenticationOptions.cs
+#if NET8_0_OR_GREATER
+ if (authenticateAsClientOptions.ClientCertificateContext != null)
+ {
+ collection.Add(authenticateAsClientOptions.ClientCertificateContext.TargetCertificate);
+ }
+#else
+ if (authenticateAsClientOptions.ClientCertificates != null)
+ {
+ collection.AddRange(authenticateAsClientOptions.ClientCertificates);
+ }
+#endif
+ if (collection.Count > 0)
+ {
+ clientWebSocketOptions.ClientCertificates = collection;
+ }
+
+#if !NETSTANDARD2_0
+ clientWebSocketOptions.RemoteCertificateValidationCallback = authenticateAsClientOptions.RemoteCertificateValidationCallback;
+#endif
+ }
+
+ if (ConfigureClientWebSocketOptions != null)
+ {
+ await ConfigureClientWebSocketOptions(uri.Uri, clientWebSocketOptions, cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/tests/NATS.Client.Core.Tests/NatsConnectionTest.Transports.cs b/tests/NATS.Client.Core.Tests/NatsConnectionTest.Transports.cs
index 41100bd55..abf2b7b0d 100644
--- a/tests/NATS.Client.Core.Tests/NatsConnectionTest.Transports.cs
+++ b/tests/NATS.Client.Core.Tests/NatsConnectionTest.Transports.cs
@@ -23,3 +23,11 @@ public NatsConnectionTestWs(ITestOutputHelper output)
{
}
}
+
+public class NatsConnectionTestWss : NatsConnectionTest
+{
+ public NatsConnectionTestWss(ITestOutputHelper output)
+ : base(output, TransportType.WebSocketSecure)
+ {
+ }
+}
diff --git a/tests/NATS.Client.Core.Tests/TlsClientTest.cs b/tests/NATS.Client.Core.Tests/TlsClientTest.cs
index 8a230a7c8..1547c006e 100644
--- a/tests/NATS.Client.Core.Tests/TlsClientTest.cs
+++ b/tests/NATS.Client.Core.Tests/TlsClientTest.cs
@@ -11,13 +11,15 @@ public class TlsClientTest
public TlsClientTest(ITestOutputHelper output) => _output = output;
- [Fact]
- public async Task Client_connect_using_certificate()
+ [Theory]
+ [InlineData(TransportType.Tls)]
+ [InlineData(TransportType.WebSocketSecure)]
+ public async Task Client_connect_using_certificate(TransportType transportType)
{
await using var server = NatsServer.Start(
new NullOutputHelper(),
new NatsServerOptsBuilder()
- .UseTransport(TransportType.Tls, tlsVerify: true)
+ .UseTransport(transportType, tlsVerify: true)
.Build());
var clientOpts = server.ClientOpts(NatsOpts.Default with { Name = "tls-test-client" });
@@ -56,13 +58,15 @@ public async Task Client_connect_using_certificate_and_revocation_check()
Assert.Contains("remote certificate was rejected", exception.InnerException!.InnerException!.Message);
}
- [Fact]
- public async Task Client_cannot_connect_without_certificate()
+ [Theory]
+ [InlineData(TransportType.Tls)]
+ [InlineData(TransportType.WebSocketSecure)]
+ public async Task Client_cannot_connect_without_certificate(TransportType transportType)
{
await using var server = NatsServer.Start(
new NullOutputHelper(),
new NatsServerOptsBuilder()
- .UseTransport(TransportType.Tls, tlsVerify: true)
+ .UseTransport(transportType, tlsVerify: true)
.Build());
var clientOpts = server.ClientOpts(NatsOpts.Default);
diff --git a/tests/NATS.Client.Core.Tests/WebSocketOptionsTest.cs b/tests/NATS.Client.Core.Tests/WebSocketOptionsTest.cs
index fce13df15..bd34eb41d 100644
--- a/tests/NATS.Client.Core.Tests/WebSocketOptionsTest.cs
+++ b/tests/NATS.Client.Core.Tests/WebSocketOptionsTest.cs
@@ -1,325 +1,84 @@
using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
using NATS.Client.TestUtilities;
namespace NATS.Client.Core.Tests;
public class WebSocketOptionsTest
{
- private readonly List _logs = new();
-
- // Modeled after similar test in SendBufferTest.cs which also uses the MockServer.
[Fact]
- public async Task MockWebsocketServer_PubSubWithCancelAndReconnect_ShouldCallbackTwice()
+ public async Task Exception_in_callback_throws_nats_exception()
{
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-
- List pubs = new();
- await using var server = new MockServer(
- handler: (client, cmd) =>
- {
- if (cmd.Name == "PUB")
- {
- lock (pubs)
- pubs.Add($"PUB {cmd.Subject}");
- }
-
- if (cmd is { Name: "PUB", Subject: "close" })
- {
- client.Close();
- }
-
- return Task.CompletedTask;
- },
- Log,
- info: $"{{\"max_payload\":{1024 * 4}}}",
- cancellationToken: cts.Token);
-
- await using var wsServer = new WebSocketMockServer(
- server.Url,
- connectHandler: (httpContext) =>
- {
- return true;
- },
- Log,
- cts.Token);
-
- Log("__________________________________");
-
- var testLogger = new InMemoryTestLoggerFactory(LogLevel.Warning, m =>
- {
- Log($"[NC] {m.Message}");
- });
-
- var tokenCount = 0;
- await using var nats = new NatsConnection(new NatsOpts
+ await using var server = NatsServer.Start(
+ new NullOutputHelper(),
+ new NatsServerOptsBuilder()
+ .UseTransport(TransportType.WebSocket)
+ .Build());
+
+ var clientOpts = server.ClientOpts(NatsOpts.Default with { Name = "ws-test-client" });
+ clientOpts = clientOpts with
{
- Url = wsServer.WebSocketUrl,
- LoggerFactory = testLogger,
- ConfigureWebSocketOpts = (serverUri, clientWsOpts, ct) =>
+ WebSocketOpts = new NatsWebSocketOpts
{
- tokenCount++;
- Log($"[C] ConfigureWebSocketOpts {serverUri}, accessToken TOKEN_{tokenCount}");
- clientWsOpts.SetRequestHeader("authorization", $"Bearer TOKEN_{tokenCount}");
- return ValueTask.CompletedTask;
+ ConfigureClientWebSocketOptions = (_, _, _)
+ => throw new Exception("Error in callback"),
},
- });
-
- Log($"[C] connect {server.Url}");
- await nats.ConnectAsync();
-
- Log($"[C] ping");
- var rtt = await nats.PingAsync(cts.Token);
- Log($"[C] ping rtt={rtt}");
-
- Log($"[C] publishing x1...");
- await nats.PublishAsync("x1", "x", cancellationToken: cts.Token);
-
- // we will close the connection in mock server when we receive subject "close"
- Log($"[C] publishing close (4KB)...");
- var pubTask = nats.PublishAsync("close", new byte[1024 * 4], cancellationToken: cts.Token).AsTask();
-
- await pubTask.WaitAsync(cts.Token);
-
- for (var i = 1; i <= 10; i++)
- {
- try
- {
- await nats.PingAsync(cts.Token);
- break;
- }
- catch (OperationCanceledException)
- {
- if (i == 10)
- throw;
- await Task.Delay(10 * i, cts.Token);
- }
- }
-
- Log($"[C] publishing x2...");
- await nats.PublishAsync("x2", "x", cancellationToken: cts.Token);
-
- Log($"[C] flush...");
- await nats.PingAsync(cts.Token);
-
- // Look for logs like the following:
- // [WS] Received WebSocketRequest with authorization header: Bearer TOKEN_2
- var tokens = GetLogs().Where(l => l.Contains("Bearer")).ToList();
- Assert.Equal(2, tokens.Count);
- var token = tokens.Where(t => t.Contains("TOKEN_1"));
- Assert.Single(token);
- token = tokens.Where(t => t.Contains("TOKEN_2"));
- Assert.Single(token);
-
- lock (pubs)
- {
- Assert.Equal(3, pubs.Count);
- Assert.Equal("PUB x1", pubs[0]);
- Assert.Equal("PUB close", pubs[1]);
- Assert.Equal("PUB x2", pubs[2]);
- }
- }
-
- [Fact]
- public async Task WebSocketRespondsWithHttpError_ShouldThrowNatsException()
- {
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-
- await using var server = new MockServer(
- handler: (client, cmd) =>
- {
- return Task.CompletedTask;
- },
- Log,
- info: $"{{\"max_payload\":{1024 * 4}}}",
- cancellationToken: cts.Token);
-
- await using var wsServer = new WebSocketMockServer(
- server.Url,
- connectHandler: (httpContext) =>
- {
- httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
- return false; // reject connection
- },
- Log,
- cts.Token);
-
- Log("__________________________________");
-
- var testLogger = new InMemoryTestLoggerFactory(LogLevel.Warning, m =>
- {
- Log($"[NC] {m.Message}");
- });
-
- await using var nats = new NatsConnection(new NatsOpts
- {
- Url = wsServer.WebSocketUrl,
- LoggerFactory = testLogger,
- });
-
- Log($"[C] connect {server.Url}");
+ };
+ await using var nats = new NatsConnection(clientOpts);
- // expect: NATS.Client.Core.NatsException : can not connect uris: ws://127.0.0.1:5004
- var exception = await Assert.ThrowsAsync(async () => await nats.ConnectAsync());
+ await Assert.ThrowsAsync(async () => await nats.ConnectAsync());
}
[Fact]
- public async Task HttpErrorDuringReconnect_ShouldContinueToReconnect()
+ public async Task Request_headers_are_correct()
{
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-
- await using var server = new MockServer(
- handler: (client, cmd) =>
- {
- if (cmd is { Name: "PUB", Subject: "close" })
- {
- client.Close();
- }
-
- return Task.CompletedTask;
- },
- Log,
- info: $"{{\"max_payload\":{1024 * 4}}}",
- cancellationToken: cts.Token);
-
- var tokenCount = 0;
+ var server = new TcpListener(IPAddress.Parse("127.0.0.1"), 0);
+ server.Start();
- await using var wsServer = new WebSocketMockServer(
- server.Url,
- connectHandler: (httpContext) =>
- {
- var token = httpContext.Request.Headers.Authorization;
- if (token.Contains("Bearer TOKEN_1") || token.Contains("Bearer TOKEN_4"))
- {
- return true;
- }
- else
- {
- httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
- return false; // reject connection
- }
- },
- Log,
- cts.Token);
-
- Log("__________________________________");
+ var port = ((IPEndPoint)server.LocalEndpoint).Port;
- var testLogger = new InMemoryTestLoggerFactory(LogLevel.Warning, m =>
+ var headers = new List();
+ var serverTask = Task.Run(async () =>
{
- Log($"[NC] {m.Message}");
- });
+ using var client = await server.AcceptTcpClientAsync();
+ var stream = client.GetStream();
+ using var sr = new StreamReader(stream);
- await using var nats = new NatsConnection(new NatsOpts
- {
- Url = wsServer.WebSocketUrl,
- LoggerFactory = testLogger,
- ConfigureWebSocketOpts = (serverUri, clientWsOpts, ct) =>
+ while (true)
{
- tokenCount++;
- Log($"[C] ConfigureWebSocketOpts {serverUri}, accessToken TOKEN_{tokenCount}");
- clientWsOpts.SetRequestHeader("authorization", $"Bearer TOKEN_{tokenCount}");
- return ValueTask.CompletedTask;
- },
- });
-
- Log($"[C] connect {server.Url}");
-
- // close connection and trigger reconnect
- Log($"[C] publishing close ...");
- await nats.PublishAsync("close", "x", cancellationToken: cts.Token);
+ var line = await sr.ReadLineAsync();
+ if (string.IsNullOrWhiteSpace(line))
+ return;
- for (var i = 1; i <= 10; i++)
- {
- try
- {
- await nats.PingAsync(cts.Token);
- break;
- }
- catch (OperationCanceledException)
- {
- if (i == 10)
- throw;
- await Task.Delay(100 * i, cts.Token);
+ headers.Add(line);
}
- }
-
- Log($"[C] publishing reconnected");
- await nats.PublishAsync("reconnected", "rc", cancellationToken: cts.Token);
- await Task.Delay(100); // short delay to allow log to be collected for reconnect
-
- // Expect to see in logs:
- // 1st callback and TOKEN_1
- // Initial Connect
- // 2nd callback with rejected TOKEN_2
- // NC reconnect
- // 3rd callback with rejected TOKEN_3
- // NC reconnect
- // 4th callback with good TOKEN_4
- // Successful Publish after reconnect
-
- // 4 tokens
- var logs = GetLogs();
- var tokens = logs.Where(l => l.Contains("Bearer")).ToList();
- Assert.Equal(4, tokens.Count);
- Assert.Single(tokens.Where(t => t.Contains("TOKEN_1")));
- Assert.Single(tokens.Where(t => t.Contains("TOKEN_2")));
- Assert.Single(tokens.Where(t => t.Contains("TOKEN_3")));
- Assert.Single(tokens.Where(t => t.Contains("TOKEN_4")));
-
- // 2 errors in NATS.Client triggering the reconnect
- var failures = logs.Where(l => l.Contains("[NC] Failed to connect NATS"));
- Assert.Equal(2, failures.Count());
-
- // 2 connects in MockServer
- var connects = logs.Where(l => l.Contains("RCV CONNECT"));
- Assert.Equal(2, failures.Count());
-
- // 1 reconnect in MockServer
- var reconnectPublish = logs.Where(l => l.Contains("RCV PUB reconnected"));
- Assert.Single(reconnectPublish);
- }
-
- [Fact]
- public async Task ExceptionThrownInCallback_ShouldThrowNatsException()
- {
- // void Log(string m) => TmpFileLogger.Log(m);
- List logs = new();
- void Log(string m)
- {
- lock (logs)
- logs.Add(m);
- }
-
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-
- var testLogger = new InMemoryTestLoggerFactory(LogLevel.Warning, m =>
- {
- Log($"[NC] {m.Message}");
});
await using var nats = new NatsConnection(new NatsOpts
{
- Url = "ws://localhost:1234",
- LoggerFactory = testLogger,
- ConfigureWebSocketOpts = (serverUri, clientWsOpts, ct) =>
+ Url = $"ws://127.0.0.1:{port}",
+ WebSocketOpts = new NatsWebSocketOpts
{
- throw new Exception("Error in callback");
+ RequestHeaders = new Dictionary { { "Header1", "Header1" }, { "Header2", new StringValues(["Header2.1", "Header2.2"]) }, },
+ ConfigureClientWebSocketOptions = (_, clientWsOpts, _) =>
+ {
+ clientWsOpts.SetRequestHeader("Header3", "Header3");
+ return ValueTask.CompletedTask;
+ },
},
});
- // expect: NATS.Client.Core.NatsException : can not connect uris: ws://localhost:1234
- var exception = await Assert.ThrowsAsync(async () => await nats.ConnectAsync());
- }
+ // not connecting to an actual nats server so this throws
+ await Assert.ThrowsAsync(async () => await nats.ConnectAsync());
- private void Log(string m)
- {
- lock (_logs)
- _logs.Add(m);
- }
+ await serverTask;
- private List GetLogs()
- {
- lock (_logs)
- return _logs.ToList();
+ Assert.Contains("Header1: Header1", headers);
+ Assert.Contains("Header2: Header2.1,Header2.2", headers);
+ Assert.Contains("Header3: Header3", headers);
}
}
diff --git a/tests/NATS.Client.TestUtilities/NatsServer.cs b/tests/NATS.Client.TestUtilities/NatsServer.cs
index 595d5b363..83cb90001 100644
--- a/tests/NATS.Client.TestUtilities/NatsServer.cs
+++ b/tests/NATS.Client.TestUtilities/NatsServer.cs
@@ -81,6 +81,7 @@ private NatsServer(ITestOutputHelper outputHelper, NatsServerOpts opts)
TransportType.Tcp => $"nats://127.0.0.1:{Opts.ServerPort}",
TransportType.Tls => $"tls://127.0.0.1:{Opts.ServerPort}",
TransportType.WebSocket => $"ws://127.0.0.1:{Opts.WebSocketPort}",
+ TransportType.WebSocketSecure => $"wss://127.0.0.1:{Opts.WebSocketPort}",
_ => throw new ArgumentOutOfRangeException(),
};
@@ -88,7 +89,7 @@ public int ConnectionPort
{
get
{
- if (_transportType == TransportType.WebSocket && ServerVersions.V2_9_19 <= Version)
+ if (_transportType is TransportType.WebSocket or TransportType.WebSocketSecure && ServerVersions.V2_9_19 <= Version)
{
return Opts.WebSocketPort!.Value;
}
diff --git a/tests/NATS.Client.TestUtilities/NatsServerOpts.cs b/tests/NATS.Client.TestUtilities/NatsServerOpts.cs
index ea1f4336a..43c69ba26 100644
--- a/tests/NATS.Client.TestUtilities/NatsServerOpts.cs
+++ b/tests/NATS.Client.TestUtilities/NatsServerOpts.cs
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Net.NetworkInformation;
using System.Text;
+using System.Text.RegularExpressions;
namespace NATS.Client.Core.Tests;
@@ -9,6 +10,7 @@ public enum TransportType
Tcp,
Tls,
WebSocket,
+ WebSocketSecure,
}
public sealed class NatsServerOptsBuilder
@@ -77,7 +79,7 @@ public NatsServerOptsBuilder UseTransport(TransportType transportType, bool tlsF
throw new Exception("tlsFirst is only valid for TLS transport");
}
- if (transportType == TransportType.Tls)
+ if (transportType is TransportType.Tls or TransportType.WebSocketSecure)
{
_enableTls = true;
_tlsServerCertFile = "resources/certs/server-cert.pem";
@@ -93,7 +95,8 @@ public NatsServerOptsBuilder UseTransport(TransportType transportType, bool tlsF
_tlsFirst = tlsFirst;
_tlsVerify = tlsVerify;
}
- else if (transportType == TransportType.WebSocket)
+
+ if (transportType is TransportType.WebSocket or TransportType.WebSocketSecure)
{
_enableWebSocket = true;
}
@@ -214,27 +217,11 @@ public string ConfigFileContents
if (Trace)
{
- sb.AppendLine($"trace: true");
- sb.AppendLine($"debug: true");
- }
-
- if (EnableWebSocket)
- {
- sb.AppendLine("websocket {");
- sb.AppendLine($" port: {WebSocketPort}");
- sb.AppendLine(" no_tls: true");
- sb.AppendLine("}");
- }
-
- if (EnableClustering)
- {
- sb.AppendLine("cluster {");
- sb.AppendLine(" name: nats");
- sb.AppendLine($" listen: {ServerHost}:{ClusteringPort}");
- sb.AppendLine($" routes: [{_routes}]");
- sb.AppendLine("}");
+ sb.AppendLine("trace: true");
+ sb.AppendLine("debug: true");
}
+ string? tls = null;
if (EnableTls)
{
if (TlsServerCertFile == default || TlsServerKeyFile == default)
@@ -242,24 +229,44 @@ public string ConfigFileContents
throw new Exception("TLS is enabled but cert or key missing");
}
- sb.AppendLine("tls {");
- sb.AppendLine($" cert_file: {TlsServerCertFile}");
- sb.AppendLine($" key_file: {TlsServerKeyFile}");
+ var tlsSb = new StringBuilder();
+ tlsSb.AppendLine("tls {");
+ tlsSb.AppendLine($" cert_file: {TlsServerCertFile}");
+ tlsSb.AppendLine($" key_file: {TlsServerKeyFile}");
if (TlsCaFile != default)
{
- sb.AppendLine($" ca_file: {TlsCaFile}");
+ tlsSb.AppendLine($" ca_file: {TlsCaFile}");
}
if (TlsFirst)
{
- sb.AppendLine($" handshake_first: true");
+ tlsSb.AppendLine(" handshake_first: true");
}
if (TlsVerify)
{
- sb.AppendLine($" verify_and_map: true");
+ tlsSb.AppendLine($" verify_and_map: true");
}
+ tlsSb.Append("}");
+ tls = tlsSb.ToString();
+ sb.AppendLine(tls);
+ }
+
+ if (EnableWebSocket)
+ {
+ sb.AppendLine("websocket {");
+ sb.AppendLine($" listen: {ServerHost}:{WebSocketPort}");
+ sb.AppendLine(tls != null ? Regex.Replace(tls, "^", " ", RegexOptions.Multiline) : " no_tls: true");
+ sb.AppendLine("}");
+ }
+
+ if (EnableClustering)
+ {
+ sb.AppendLine("cluster {");
+ sb.AppendLine(" name: nats");
+ sb.AppendLine($" listen: {ServerHost}:{ClusteringPort}");
+ sb.AppendLine($" routes: [{_routes}]");
sb.AppendLine("}");
}