feat: add WebSocket-specific TLS configuration (Gap 15.1)
This commit is contained in:
78
src/NATS.Server/WebSocket/WebSocketTlsConfig.cs
Normal file
78
src/NATS.Server/WebSocket/WebSocketTlsConfig.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace NATS.Server.WebSocket;
|
||||
|
||||
/// <summary>
|
||||
/// Separate TLS configuration for the WebSocket listener,
|
||||
/// allowing different certificates than the main NATS listener.
|
||||
/// Go reference: server/websocket.go — wsTLSConfig.
|
||||
/// </summary>
|
||||
public sealed class WebSocketTlsConfig
|
||||
{
|
||||
public string? CertFile { get; set; }
|
||||
public string? KeyFile { get; set; }
|
||||
public string? CaFile { get; set; }
|
||||
public bool RequireClientCert { get; set; }
|
||||
public bool InsecureSkipVerify { get; set; }
|
||||
|
||||
/// <summary>Returns true when a certificate file has been specified.</summary>
|
||||
public bool IsConfigured => !string.IsNullOrWhiteSpace(CertFile);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the TLS configuration.
|
||||
/// An empty configuration (no cert, no key) is valid and means no TLS.
|
||||
/// When either cert or key is specified, both must be provided.
|
||||
/// </summary>
|
||||
public WebSocketTlsValidationResult Validate()
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
bool hasCert = !string.IsNullOrWhiteSpace(CertFile);
|
||||
bool hasKey = !string.IsNullOrWhiteSpace(KeyFile);
|
||||
|
||||
if (hasKey && !hasCert)
|
||||
errors.Add("CertFile must be specified when KeyFile is set.");
|
||||
|
||||
if (hasCert && !hasKey)
|
||||
errors.Add("KeyFile must be specified when CertFile is set.");
|
||||
|
||||
return new WebSocketTlsValidationResult(errors.Count == 0, errors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when this configuration differs from <paramref name="other"/>.
|
||||
/// Used for hot-reload diff detection.
|
||||
/// </summary>
|
||||
public bool HasChangedFrom(WebSocketTlsConfig? other)
|
||||
{
|
||||
if (other is null)
|
||||
return true;
|
||||
|
||||
return CertFile != other.CertFile
|
||||
|| KeyFile != other.KeyFile
|
||||
|| CaFile != other.CaFile
|
||||
|| RequireClientCert != other.RequireClientCert
|
||||
|| InsecureSkipVerify != other.InsecureSkipVerify;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="WebSocketTlsConfig"/> from the WebSocket-specific TLS fields
|
||||
/// in <paramref name="options"/>, or returns <c>null</c> if no WebSocket TLS is configured.
|
||||
/// </summary>
|
||||
public static WebSocketTlsConfig? FromOptions(NatsOptions options)
|
||||
{
|
||||
var ws = options.WebSocket;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ws.TlsCert) && string.IsNullOrWhiteSpace(ws.TlsKey))
|
||||
return null;
|
||||
|
||||
return new WebSocketTlsConfig
|
||||
{
|
||||
CertFile = ws.TlsCert,
|
||||
KeyFile = ws.TlsKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Result of <see cref="WebSocketTlsConfig.Validate"/>.</summary>
|
||||
public sealed record WebSocketTlsValidationResult(
|
||||
bool IsValid,
|
||||
IReadOnlyList<string> Errors);
|
||||
119
tests/NATS.Server.Tests/WebSocket/WebSocketTlsTests.cs
Normal file
119
tests/NATS.Server.Tests/WebSocket/WebSocketTlsTests.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
// Go reference: server/websocket.go — wsTLSConfig and related TLS handling.
|
||||
|
||||
using NATS.Server.WebSocket;
|
||||
using Shouldly;
|
||||
|
||||
namespace NATS.Server.Tests.WebSocket;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="WebSocketTlsConfig"/> — WebSocket-specific TLS configuration
|
||||
/// with separate cert/key from the main NATS listener (Gap 15.1).
|
||||
/// </summary>
|
||||
public class WebSocketTlsTests
|
||||
{
|
||||
// ── IsConfigured ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsConfigured_WithCert_ReturnsTrue()
|
||||
{
|
||||
var cfg = new WebSocketTlsConfig { CertFile = "server.pem", KeyFile = "server-key.pem" };
|
||||
cfg.IsConfigured.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsConfigured_WithoutCert_ReturnsFalse()
|
||||
{
|
||||
var cfg = new WebSocketTlsConfig();
|
||||
cfg.IsConfigured.ShouldBeFalse();
|
||||
}
|
||||
|
||||
// ── Validate ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Validate_ValidConfig_NoErrors()
|
||||
{
|
||||
var cfg = new WebSocketTlsConfig { CertFile = "server.pem", KeyFile = "server-key.pem" };
|
||||
var result = cfg.Validate();
|
||||
result.IsValid.ShouldBeTrue();
|
||||
result.Errors.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KeyWithoutCert_HasError()
|
||||
{
|
||||
var cfg = new WebSocketTlsConfig { KeyFile = "server-key.pem" };
|
||||
var result = cfg.Validate();
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldNotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CertWithoutKey_HasError()
|
||||
{
|
||||
var cfg = new WebSocketTlsConfig { CertFile = "server.pem" };
|
||||
var result = cfg.Validate();
|
||||
result.IsValid.ShouldBeFalse();
|
||||
result.Errors.ShouldNotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_EmptyConfig_Valid()
|
||||
{
|
||||
// An empty configuration means "no TLS" — that is a valid state.
|
||||
var cfg = new WebSocketTlsConfig();
|
||||
var result = cfg.Validate();
|
||||
result.IsValid.ShouldBeTrue();
|
||||
result.Errors.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
// ── HasChangedFrom ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HasChangedFrom_SameConfig_ReturnsFalse()
|
||||
{
|
||||
var a = new WebSocketTlsConfig
|
||||
{
|
||||
CertFile = "server.pem",
|
||||
KeyFile = "server-key.pem",
|
||||
CaFile = "ca.pem",
|
||||
RequireClientCert = true,
|
||||
InsecureSkipVerify = false,
|
||||
};
|
||||
|
||||
var b = new WebSocketTlsConfig
|
||||
{
|
||||
CertFile = "server.pem",
|
||||
KeyFile = "server-key.pem",
|
||||
CaFile = "ca.pem",
|
||||
RequireClientCert = true,
|
||||
InsecureSkipVerify = false,
|
||||
};
|
||||
|
||||
a.HasChangedFrom(b).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasChangedFrom_DifferentCert_ReturnsTrue()
|
||||
{
|
||||
var a = new WebSocketTlsConfig { CertFile = "old.pem", KeyFile = "old-key.pem" };
|
||||
var b = new WebSocketTlsConfig { CertFile = "new.pem", KeyFile = "new-key.pem" };
|
||||
|
||||
a.HasChangedFrom(b).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasChangedFrom_NullOther_ReturnsTrue()
|
||||
{
|
||||
var cfg = new WebSocketTlsConfig { CertFile = "server.pem", KeyFile = "server-key.pem" };
|
||||
cfg.HasChangedFrom(null).ShouldBeTrue();
|
||||
}
|
||||
|
||||
// ── Defaults ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RequireClientCert_Default_False()
|
||||
{
|
||||
var cfg = new WebSocketTlsConfig();
|
||||
cfg.RequireClientCert.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user