feat(config+ws): add TLS cert reload, WS compression negotiation, WS JWT auth (E9+E10+E11)

E9: TLS Certificate Reload
- Add TlsCertificateProvider with Interlocked-swappable cert field
- New connections get current cert, existing connections keep theirs
- ConfigReloader.ReloadTlsCertificate rebuilds SslServerAuthenticationOptions
- NatsServer.ApplyConfigChanges triggers TLS reload on TLS config changes
- 11 tests covering cert swap, versioning, thread safety, config diff

E10: WebSocket Compression Negotiation (RFC 7692)
- Add WsDeflateNegotiator to parse Sec-WebSocket-Extensions parameters
- Parse server_no_context_takeover, client_no_context_takeover,
  server_max_window_bits, client_max_window_bits
- WsDeflateParams record struct with ToResponseHeaderValue()
- NATS always enforces no_context_takeover (matching Go server)
- WsUpgrade returns negotiated WsDeflateParams in upgrade result
- 22 tests covering parameter parsing, clamping, response headers

E11: WebSocket JWT Authentication
- Extract JWT from Authorization header (Bearer token), cookie, or ?jwt= query param
- Priority: Authorization header > cookie > query parameter
- WsUpgrade.TryUpgradeAsync now parses query string from request URI
- Add FailUnauthorizedAsync for 401 responses
- 24 tests covering all JWT extraction sources and priority ordering
This commit is contained in:
Joseph Doherty
2026-02-24 16:03:46 -05:00
parent c6ecbbfbcc
commit 02531dda58
9 changed files with 1284 additions and 14 deletions

View File

@@ -50,8 +50,9 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
private readonly Account _globalAccount;
private readonly Account _systemAccount;
private InternalEventSystem? _eventSystem;
private readonly SslServerAuthenticationOptions? _sslOptions;
private SslServerAuthenticationOptions? _sslOptions;
private readonly TlsRateLimiter? _tlsRateLimiter;
private readonly TlsCertificateProvider? _tlsCertProvider;
private readonly SubjectTransform[] _subjectTransforms;
private readonly RouteManager? _routeManager;
@@ -148,6 +149,8 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
public void WaitForShutdown() => _shutdownComplete.Task.GetAwaiter().GetResult();
internal TlsCertificateProvider? TlsCertProviderForTest => _tlsCertProvider;
internal Task AcquireReloadLockForTestAsync() => _reloadMu.WaitAsync();
internal void ReleaseReloadLockForTest() => _reloadMu.Release();
@@ -427,7 +430,9 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
if (options.HasTls)
{
_tlsCertProvider = new TlsCertificateProvider(options.TlsCert!, options.TlsKey);
_sslOptions = TlsHelper.BuildServerAuthOptions(options);
_tlsCertProvider.SwapSslOptions(_sslOptions);
// OCSP stapling: build a certificate context so the runtime can
// fetch and cache a fresh OCSP response and staple it during the
@@ -1377,6 +1382,16 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
Connections = ClientCount,
TotalConnections = Interlocked.Read(ref _stats.TotalConnections),
Subscriptions = SubList.Count,
Sent = new Events.DataStats
{
Msgs = Interlocked.Read(ref _stats.OutMsgs),
Bytes = Interlocked.Read(ref _stats.OutBytes),
},
Received = new Events.DataStats
{
Msgs = Interlocked.Read(ref _stats.InMsgs),
Bytes = Interlocked.Read(ref _stats.InBytes),
},
InMsgs = Interlocked.Read(ref _stats.InMsgs),
OutMsgs = Interlocked.Read(ref _stats.OutMsgs),
InBytes = Interlocked.Read(ref _stats.InBytes),
@@ -1672,11 +1687,13 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
{
bool hasLoggingChanges = false;
bool hasAuthChanges = false;
bool hasTlsChanges = false;
foreach (var change in changes)
{
if (change.IsLoggingChange) hasLoggingChanges = true;
if (change.IsAuthChange) hasAuthChanges = true;
if (change.IsTlsChange) hasTlsChanges = true;
}
// Copy reloadable values from newOpts to _options
@@ -1689,6 +1706,18 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
_logger.LogInformation("Logging configuration reloaded");
}
if (hasTlsChanges)
{
// Reload TLS certificates: new connections get the new cert,
// existing connections keep their original cert.
// Reference: golang/nats-server/server/reload.go — tlsOption.Apply.
if (ConfigReloader.ReloadTlsCertificate(_options, _tlsCertProvider))
{
_sslOptions = _tlsCertProvider!.GetCurrentSslOptions();
_logger.LogInformation("TLS configuration reloaded");
}
}
if (hasAuthChanges)
{
// Rebuild auth service with new options, then propagate changes to connected clients
@@ -1837,6 +1866,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
reg.Dispose();
_quitCts.Dispose();
_tlsRateLimiter?.Dispose();
_tlsCertProvider?.Dispose();
_listener?.Dispose();
_wsListener?.Dispose();
_routeManager?.DisposeAsync().AsTask().GetAwaiter().GetResult();