From f5a13bedff0f45af7e2b764a3eb62dc9b46b066a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 13:05:45 -0500 Subject: [PATCH] feat(batch10): task4 wire ocsp cache load save and server lifecycle --- .../Auth/Ocsp/OcspHandler.cs | 126 ++++++++++++++ .../Auth/Ocsp/OcspTypes.cs | 163 +++++++++++++++++- .../ZB.MOM.NatsNet.Server/NatsServer.Init.cs | 4 + .../NatsServer.Lifecycle.cs | 7 +- .../NatsServer.OcspResponseCache.cs | 128 ++++++++++++++ .../Auth/OcspResponseCacheParserTests.cs | 36 ++++ .../Auth/OcspResponseCacheTests.cs | 94 ++++++++++ .../NatsServerOcspCacheTests.cs | 122 +++++++++++++ porting.db | Bin 6545408 -> 6545408 bytes reports/current.md | 8 +- 10 files changed, 674 insertions(+), 14 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.OcspResponseCache.cs create mode 100644 dotnet/tests/ZB.MOM.NatsNet.Server.Tests/NatsServerOcspCacheTests.cs diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspHandler.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspHandler.cs index 1bbb38d..b54724b 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspHandler.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspHandler.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Formats.Asn1; +using System.Text.RegularExpressions; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.Json; @@ -24,6 +25,8 @@ internal static class OcspHandler internal const string OcspResponseCacheTypeLocal = "local"; internal static readonly TimeSpan OcspResponseCacheMinimumSaveInterval = TimeSpan.FromSeconds(1); internal static readonly TimeSpan OcspResponseCacheDefaultSaveInterval = TimeSpan.FromMinutes(5); + internal const string OcspResponseCacheDefaultFilename = "cache.json"; + internal const string OcspResponseCacheDefaultTempFilePrefix = "ocsprc-"; internal static OcspResponseCacheConfig NewOCSPResponseCacheConfig() => new() @@ -34,6 +37,82 @@ internal static class OcspHandler SaveInterval = OcspResponseCacheDefaultSaveInterval.TotalSeconds, }; + internal static (OcspResponseCacheConfig? config, Exception? error) ParseOCSPResponseCache(object? value) + { + if (value is not IDictionary map) + { + return (null, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrIllegalCacheOptsConfig, value ?? "null"))); + } + + var config = NewOCSPResponseCacheConfig(); + + foreach (var (key, raw) in map) + { + switch (key.ToLowerInvariant()) + { + case "type": + if (raw is not string cacheType) + { + return (null, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldGeneric, key))); + } + + var normalizedType = cacheType.ToLowerInvariant(); + if (normalizedType != OcspResponseCacheTypeLocal && normalizedType != OcspResponseCacheTypeNone) + { + return (null, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrUnknownCacheType, cacheType))); + } + + config.Type = normalizedType; + break; + + case "local_store": + if (raw is not string store) + { + return (null, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldGeneric, key))); + } + + config.LocalStore = store; + break; + + case "preserve_revoked": + if (raw is not bool preserveRevoked) + { + return (null, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldGeneric, key))); + } + + config.PreserveRevoked = preserveRevoked; + break; + + case "save_interval": + var (seconds, parseError) = ParseCacheSaveIntervalSeconds(raw); + if (parseError != null) + { + return (null, parseError); + } + + var parsedDuration = TimeSpan.FromSeconds(seconds); + if (parsedDuration < OcspResponseCacheMinimumSaveInterval) + { + parsedDuration = OcspResponseCacheMinimumSaveInterval; + } + + config.SaveInterval = parsedDuration.TotalSeconds; + break; + + default: + return (null, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldGeneric, key))); + } + } + + return (config, null); + } + internal static (List? certificates, Exception? error) ParseCertPEM(string name) { try @@ -395,6 +474,53 @@ internal static class OcspHandler string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldTypeConversion, "unexpected type"))); } + private static (double seconds, Exception? error) ParseCacheSaveIntervalSeconds(object? value) + { + return value switch + { + int i => (i, null), + long l => (l, null), + float f => (f, null), + double d => (d, null), + string s => ParseCacheDurationSeconds(s), + _ => (0, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldTypeConversion, "unexpected type"))), + }; + } + + private static (double seconds, Exception? error) ParseCacheDurationSeconds(string value) + { + if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out var parsed)) + { + return (parsed.TotalSeconds, null); + } + + var match = Regex.Match(value.Trim(), "^([0-9]*\\.?[0-9]+)\\s*(ms|s|m|h)$", RegexOptions.IgnoreCase); + if (!match.Success) + { + return (0, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldTypeConversion, "unexpected type"))); + } + + if (!double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var amount)) + { + return (0, new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingCacheOptFieldTypeConversion, "unexpected type"))); + } + + var unit = match.Groups[2].Value.ToLowerInvariant(); + var seconds = unit switch + { + "ms" => amount / 1000.0, + "s" => amount, + "m" => amount * 60.0, + "h" => amount * 3600.0, + _ => 0, + }; + + return (seconds, null); + } + private sealed class SerializedOcspResponse { public int Status { get; set; } diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs index 878d850..cfacf72 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Auth/Ocsp/OcspTypes.cs @@ -19,6 +19,7 @@ using System.Security.Cryptography; using System.IO.Compression; using System.Net.Http; using System.Text; +using System.Text.Json; using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider; namespace ZB.MOM.NatsNet.Server.Auth.Ocsp; @@ -752,14 +753,25 @@ internal sealed class LocalDirCache : IOcspResponseCache public void Start(NatsServer? server = null) { - _ = server; + if (server != null) + { + LoadCache(server); + } + InitStats(); _mu.EnterWriteLock(); try { _online = true; - _timer ??= new Timer(_ => { }, null, _saveInterval, _saveInterval); + if (server != null) + { + _timer ??= new Timer(_ => SaveCache(server), null, _saveInterval, _saveInterval); + } + else + { + _timer ??= new Timer(_ => { }, null, _saveInterval, _saveInterval); + } } finally { @@ -769,16 +781,10 @@ internal sealed class LocalDirCache : IOcspResponseCache public void Stop(NatsServer? server = null) { - _ = server; - _mu.EnterWriteLock(); try { _online = false; - if (_dirty) - { - _dirty = false; - } _timer?.Dispose(); _timer = null; } @@ -786,6 +792,138 @@ internal sealed class LocalDirCache : IOcspResponseCache { _mu.ExitWriteLock(); } + + if (server != null) + { + SaveCache(server); + } + } + + internal void LoadCache(NatsServer server) + { + var storePath = CacheStorePath(); + Dictionary? loaded; + + try + { + var bytes = File.ReadAllBytes(storePath); + loaded = JsonSerializer.Deserialize>(bytes); + } + catch (FileNotFoundException) + { + loaded = null; + } + catch (DirectoryNotFoundException) + { + loaded = null; + } + catch (Exception ex) + { + server.Warnf(OcspMessages.ErrLoadCacheFail, ex); + _mu.EnterWriteLock(); + try + { + _cache.Clear(); + _dirty = true; + } + finally + { + _mu.ExitWriteLock(); + } + return; + } + + _mu.EnterWriteLock(); + try + { + _cache.Clear(); + if (loaded != null) + { + foreach (var (key, item) in loaded) + { + _cache[key] = item; + } + } + + _dirty = false; + } + finally + { + _mu.ExitWriteLock(); + } + } + + internal void SaveCache(NatsServer server) + { + bool dirty; + + _mu.EnterReadLock(); + try + { + dirty = _dirty; + } + finally + { + _mu.ExitReadLock(); + } + + if (!dirty) + { + return; + } + + var storePath = CacheStorePath(); + var directory = Path.GetDirectoryName(storePath); + if (string.IsNullOrEmpty(directory)) + { + server.Errorf(OcspMessages.ErrSaveCacheFail, "cache directory path is invalid"); + return; + } + + try + { + Directory.CreateDirectory(directory); + } + catch (Exception ex) + { + server.Errorf(OcspMessages.ErrSaveCacheFail, ex); + return; + } + + var tempPath = Path.Combine(directory, $"{OcspHandler.OcspResponseCacheDefaultTempFilePrefix}{Path.GetRandomFileName()}"); + + try + { + _mu.EnterWriteLock(); + try + { + var payload = JsonSerializer.SerializeToUtf8Bytes(_cache, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllBytes(tempPath, payload); + File.Move(tempPath, storePath, overwrite: true); + _dirty = false; + } + finally + { + _mu.ExitWriteLock(); + } + } + catch (Exception ex) + { + server.Errorf(OcspMessages.ErrSaveCacheFail, ex); + } + finally + { + try + { + if (File.Exists(tempPath)) + { + File.Delete(tempPath); + } + } + catch + { + } + } } public bool Online() @@ -925,6 +1063,15 @@ internal sealed class LocalDirCache : IOcspResponseCache var file = Convert.ToHexString(hash).ToLowerInvariant(); return Path.Combine(_config.LocalStore, $"{file}.ocsp"); } + + private string CacheStorePath() + { + var directory = string.IsNullOrEmpty(_config.LocalStore) + ? OcspHandler.OcspResponseCacheDefaultDir + : _config.LocalStore; + + return Path.Combine(directory, OcspHandler.OcspResponseCacheDefaultFilename); + } } /// diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs index f48937d..e7c0d95 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Init.cs @@ -418,6 +418,8 @@ public sealed partial class NatsServer return (null, ocspError); } + s.InitOCSPResponseCache(); + // Gateway (stub — session 16). // s.NewGateway(opts) — deferred @@ -995,6 +997,8 @@ public sealed partial class NatsServer SetDefaultSystemAccount(); } + StartOCSPResponseCache(); + // Signal startup complete. _startupComplete.TrySetResult(); diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs index 6f6bd3d..92e403e 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Lifecycle.cs @@ -155,7 +155,7 @@ public sealed partial class NatsServer _mu.ExitWriteLock(); } - if (_ocsprc != null) { /* stub — stop OCSP cache in session 23 */ } + StopOCSPResponseCache(); DisposeSignalHandlers(); @@ -860,7 +860,10 @@ public sealed partial class NatsServer } /// Stub — Raft leader transfer (session 20). Returns false (no leaders to transfer). - private bool TransferRaftLeaders() => false; + private bool TransferRaftLeaders() + { + return false; + } /// Stub — LDM shutdown event (session 12). private void SendLDMShutdownEventLocked() diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.OcspResponseCache.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.OcspResponseCache.cs new file mode 100644 index 0000000..4bc1b24 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.OcspResponseCache.cs @@ -0,0 +1,128 @@ +// Copyright 2023-2026 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider; +using ZB.MOM.NatsNet.Server.Auth.Ocsp; + +namespace ZB.MOM.NatsNet.Server; + +public sealed partial class NatsServer +{ + internal void InitOCSPResponseCache() + { + _mu.EnterReadLock(); + try + { + if (!_ocspPeerVerify) + { + return; + } + } + finally + { + _mu.ExitReadLock(); + } + + var opts = GetOpts(); + opts.OcspCacheConfig ??= OcspHandler.NewOCSPResponseCacheConfig(); + var config = opts.OcspCacheConfig; + + IOcspResponseCache cache; + var cacheType = (config.Type ?? string.Empty).Trim().ToLowerInvariant(); + switch (cacheType) + { + case "": + case OcspHandler.OcspResponseCacheTypeLocal: + config.Type = OcspHandler.OcspResponseCacheTypeLocal; + cache = new LocalDirCache(config); + break; + + case OcspHandler.OcspResponseCacheTypeNone: + cache = new NoOpCache(config); + break; + + default: + Fatalf(OcspMessages.ErrBadCacheTypeConfig, config.Type); + return; + } + + _mu.EnterWriteLock(); + try + { + _ocsprc = cache; + } + finally + { + _mu.ExitWriteLock(); + } + } + + internal void StartOCSPResponseCache() + { + IOcspResponseCache? cache; + + _mu.EnterReadLock(); + try + { + if (!_ocspPeerVerify) + { + return; + } + + cache = _ocsprc; + } + finally + { + _mu.ExitReadLock(); + } + + if (cache == null) + { + return; + } + + switch (cache) + { + case NoOpCache noOpCache: + noOpCache.Start(this); + Noticef("OCSP peer cache online [{0}]", noOpCache.Type()); + break; + + case LocalDirCache localDirCache: + localDirCache.Start(this); + Noticef("OCSP peer cache online [{0}]", localDirCache.Type()); + break; + } + } + + internal void StopOCSPResponseCache() + { + IOcspResponseCache? cache; + + _mu.EnterReadLock(); + try + { + cache = _ocsprc; + } + finally + { + _mu.ExitReadLock(); + } + + if (cache == null) + { + return; + } + + switch (cache) + { + case NoOpCache noOpCache: + noOpCache.Stop(this); + break; + + case LocalDirCache localDirCache: + localDirCache.Stop(this); + break; + } + } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspResponseCacheParserTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspResponseCacheParserTests.cs index eb95421..a607559 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspResponseCacheParserTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspResponseCacheParserTests.cs @@ -15,4 +15,40 @@ public sealed class OcspResponseCacheParserTests config.PreserveRevoked.ShouldBeFalse(); config.SaveInterval.ShouldBe(TimeSpan.FromMinutes(5).TotalSeconds); } + + [Fact] + public void ParseOCSPResponseCache_StringDurationBelowMinimum_ClampsToOneSecond() + { + var input = new Dictionary + { + ["type"] = "local", + ["save_interval"] = "500ms", + }; + + var (config, error) = OcspHandler.ParseOCSPResponseCache(input); + + error.ShouldBeNull(); + config.ShouldNotBeNull(); + config.SaveInterval.ShouldBe(1); + } + + [Fact] + public void ParseOCSPResponseCache_NoneType_AcceptsConfiguration() + { + var input = new Dictionary + { + ["type"] = "none", + ["local_store"] = "_rc_", + ["preserve_revoked"] = true, + ["save_interval"] = 25.0, + }; + + var (config, error) = OcspHandler.ParseOCSPResponseCache(input); + + error.ShouldBeNull(); + config.ShouldNotBeNull(); + config.Type.ShouldBe("none"); + config.PreserveRevoked.ShouldBeTrue(); + config.SaveInterval.ShouldBe(25.0); + } } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspResponseCacheTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspResponseCacheTests.cs index 42f4a18..d035a3d 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspResponseCacheTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Auth/OcspResponseCacheTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0 using Shouldly; +using System.Text.Json; using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider; using ZB.MOM.NatsNet.Server.Auth.Ocsp; @@ -92,6 +93,84 @@ public sealed class OcspResponseCacheTests decompressed.ShouldBe(payload); } + [Fact] + public void LocalDirCache_LoadCache_MissingFile_LeavesCacheEmpty() + { + var dir = CreateTempDir(); + try + { + var cache = new LocalDirCache(dir); + var server = NewServer(new ServerOptions { NoSystemAccount = true }); + + cache.LoadCache(server); + cache.Start(); + + var stats = cache.Stats(); + stats.ShouldNotBeNull(); + stats.Responses.ShouldBe(0); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + [Fact] + public void LocalDirCache_LoadCache_ValidFile_PopulatesCache() + { + var dir = CreateTempDir(); + try + { + var cache = new LocalDirCache(dir); + var cacheFile = Path.Combine(dir, "cache.json"); + var seed = new Dictionary + { + ["k1"] = new() + { + Subject = "subj", + CachedAt = DateTime.UtcNow, + RespStatus = OcspStatusAssertion.Good, + RespExpires = DateTime.UtcNow.AddMinutes(5), + Resp = cache.Compress([1, 2, 3]).compressed!, + }, + }; + + File.WriteAllBytes(cacheFile, JsonSerializer.SerializeToUtf8Bytes(seed)); + + var server = NewServer(new ServerOptions { NoSystemAccount = true }); + cache.LoadCache(server); + cache.Start(); + + cache.Get("k1").ShouldBe([1, 2, 3]); + cache.Stats()!.Responses.ShouldBe(1); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + [Fact] + public void LocalDirCache_SaveCache_DirtyWritesCacheFile() + { + var dir = CreateTempDir(); + try + { + var cache = new LocalDirCache(dir); + var server = NewServer(new ServerOptions { NoSystemAccount = true }); + cache.Start(); + cache.Put("k1", CreateResponse(OcspStatusAssertion.Good, [4, 5, 6]), "subj"); + + cache.SaveCache(server); + + File.Exists(Path.Combine(dir, "cache.json")).ShouldBeTrue(); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + [Fact] public void NoOpCache_LifecycleAndStats_ShouldNoOpSafely() { @@ -122,4 +201,19 @@ public sealed class OcspResponseCacheTests NextUpdate = DateTime.UtcNow.AddMinutes(10), Raw = raw, }; + + private static NatsServer NewServer(ServerOptions options) + { + var (server, err) = NatsServer.NewServer(options); + err.ShouldBeNull(); + server.ShouldNotBeNull(); + return server!; + } + + private static string CreateTempDir() + { + var dir = Path.Combine(Path.GetTempPath(), "ocsp-cache-" + Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + return dir; + } } diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/NatsServerOcspCacheTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/NatsServerOcspCacheTests.cs new file mode 100644 index 0000000..5d854f9 --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/NatsServerOcspCacheTests.cs @@ -0,0 +1,122 @@ +using System.Reflection; +using Shouldly; +using ZB.MOM.NatsNet.Server.Auth.Ocsp; + +namespace ZB.MOM.NatsNet.Server.Tests; + +public sealed class NatsServerOcspCacheTests +{ + [Fact] + public void InitOCSPResponseCache_LocalType_CreatesLocalDirCache() + { + var dir = CreateTempDir(); + try + { + var server = NewServer(new ServerOptions + { + NoSystemAccount = true, + OcspCacheConfig = new OcspResponseCacheConfig + { + Type = "local", + LocalStore = dir, + SaveInterval = 5, + }, + }); + + SetPrivateField(server, "_ocspPeerVerify", true); + + server.InitOCSPResponseCache(); + + GetOcspCache(server).ShouldBeOfType(); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + [Fact] + public void InitOCSPResponseCache_NoneType_CreatesNoOpCache() + { + var server = NewServer(new ServerOptions + { + NoSystemAccount = true, + OcspCacheConfig = new OcspResponseCacheConfig + { + Type = "none", + LocalStore = "_rc_", + SaveInterval = 5, + }, + }); + + SetPrivateField(server, "_ocspPeerVerify", true); + + server.InitOCSPResponseCache(); + + GetOcspCache(server).ShouldBeOfType(); + } + + [Fact] + public void StartStopOCSPResponseCache_WhenInitialized_TogglesOnlineState() + { + var dir = CreateTempDir(); + try + { + var server = NewServer(new ServerOptions + { + NoSystemAccount = true, + OcspCacheConfig = new OcspResponseCacheConfig + { + Type = "local", + LocalStore = dir, + SaveInterval = 5, + }, + }); + + SetPrivateField(server, "_ocspPeerVerify", true); + server.InitOCSPResponseCache(); + var cache = GetOcspCache(server).ShouldBeOfType(); + + server.StartOCSPResponseCache(); + cache.Online().ShouldBeTrue(); + + server.StopOCSPResponseCache(); + cache.Online().ShouldBeFalse(); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + private static NatsServer NewServer(ServerOptions options) + { + var (server, err) = NatsServer.NewServer(options); + err.ShouldBeNull(); + server.ShouldNotBeNull(); + return server!; + } + + private static IOcspResponseCache GetOcspCache(NatsServer server) + { + var field = typeof(NatsServer).GetField("_ocsprc", BindingFlags.Instance | BindingFlags.NonPublic); + field.ShouldNotBeNull(); + var value = field!.GetValue(server); + value.ShouldNotBeNull(); + return (IOcspResponseCache)value!; + } + + private static void SetPrivateField(NatsServer server, string fieldName, object value) + { + var field = typeof(NatsServer).GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); + field.ShouldNotBeNull(); + field!.SetValue(server, value); + } + + private static string CreateTempDir() + { + var dir = Path.Combine(Path.GetTempPath(), "nats-ocsp-cache-" + Path.GetRandomFileName()); + Directory.CreateDirectory(dir); + return dir; + } +} diff --git a/porting.db b/porting.db index c7e8bedaf53e112db9deda3e63f4e903defb6e12..40df406e9db30b07648853d5b5044e15e9eb4ea0 100644 GIT binary patch delta 1475 zcmZwFTTC2P90u^2vwL7>cXoGxT^7pD0xiYbVp-U2VWC(q(jwYI*V4+ZGQbi^ngyZN z0EJMrr5d6LhJVt8rkI%cV4yW7lW3wW7h_^l8%^{6{&-(_QhVfV2l(KN=!3~wI$Hr{OgOZ<%<7P1?ONVVFkR7DSaPdYhodN-Nmxaf*{z9hh1!~`AjmsmFUA$LE&NF()Tjc)hzV&~?})l-udc*03rB}K=*PMZFROJp73RkYx< zY1BQykJ-1ht0&7Tt)HjTPXoM$LT^|!%J{)#rRpo(x3!lySwVAByV+SJlU2ob;XarSzGxo}kvniLErg zx6w~?Zq-B6DY&=}H+sZMJ123dQ24uos*aum#AF|;YH~`PWK{y0C@Ejb5T<}32V-vXfd3P`jdC;Pw*#|A|*%hDyqT^=@0!E(fIpM{gx~a(Tzv? zb!O4&UV~aWTDPhjb*rs&E13@Imf#q1nC;i?lI=a4qP(xD)(NZAGG$4TZ^|BtGUug1 z!#CdiG(LG?GE){brq#Hb{T*Tb(&e6Tue?lmqix5{fs7M=(jT;C?g z+0E|u-8k*)gV75y{@t;QF?}({{~qBE4d3B6gX4^G;de#Emj^y;|ET@Ieovk5xXXtd zy=jqVX3Fd`liE`*f?P-AV!~~hX_lg?wc;c-JW^qut`gm)Tx;1TIyiqoeU9j+K}Y!X zb4LVy+@{#1Dxj80b(wzc5VJf?KXr;yWA2uEs#DC?RNK2)^`XzdNSZ&OR%J`0%Ca?v zT9B>lsQheQLwU1xc_1_*z@LnrRj|Nz`x2XB75*w;zcuE4=q)gtb+}8e zG3m#zq(kG!%?RE4ZN5S0hoqZok4uxb9Fa@u>7dJ^qetW?bidfWgn~UX{$i}5M}A6q z|6tjTy;gvxdgRI%7Z&wma#O^;oW^=zjy>*`2}&OGMCeMF^wXh~TukMihUJP6)zV+R zGD4>g%XL)pd(r3E!2%!n!PXP0z=kvYnU+dD;yEmP=;yhrHHW8Zuq3gXt`w*+oh!uY zYDg{9lZ9$wrEd@WSJ+JMm)m4L)sBib`nXYjOLmi*J$pfTwb85^UBXjO-xsPfooZG0 z3_L!wU;aqmgLH7GN|Ks!>iCX<{DgvB$b%r{!#oH<0TeBSh{Gya4HfV@tbs~c3svw2tb=N(fj412)WQaM3pT>r zPzRgf9jJ$QVKcl3@52`O02-hXnxGlBLJPFQHrNgy!rXh>2?^K*yI~LPg*Kg^aGv%X z)A%ep@1FAqKiern`q`gO#^k@y$!X^Vjkl{!11&R-Zkb7rPKHb?!vbFi4Eu!b@t^Yh VeBbz#mC@0`-QAYX+}_t={0E4$S&aYy diff --git a/reports/current.md b/reports/current.md index fefdb53..718b7bc 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-28 17:59:26 UTC +Generated: 2026-02-28 18:05:45 UTC ## Modules (12 total) @@ -13,10 +13,10 @@ Generated: 2026-02-28 17:59:26 UTC | Status | Count | |--------|-------| | complete | 14 | -| deferred | 1955 | +| deferred | 1949 | | n_a | 24 | | stub | 1 | -| verified | 1679 | +| verified | 1685 | ## Unit Tests (3257 total) @@ -35,4 +35,4 @@ Generated: 2026-02-28 17:59:26 UTC ## Overall Progress -**3030/6942 items complete (43.6%)** +**3036/6942 items complete (43.7%)**