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("}"); }