feat(batch10): task4 wire ocsp cache load save and server lifecycle
This commit is contained in:
@@ -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<string, object?> 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<X509Certificate2>? 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; }
|
||||
|
||||
@@ -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<string, OcspResponseCacheItem>? loaded;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(storePath);
|
||||
loaded = JsonSerializer.Deserialize<Dictionary<string, OcspResponseCacheItem>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>Stub — Raft leader transfer (session 20). Returns false (no leaders to transfer).</summary>
|
||||
private bool TransferRaftLeaders() => false;
|
||||
private bool TransferRaftLeaders()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Stub — LDM shutdown event (session 12).</summary>
|
||||
private void SendLDMShutdownEventLocked()
|
||||
|
||||
128
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.OcspResponseCache.cs
Normal file
128
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.OcspResponseCache.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, object?>
|
||||
{
|
||||
["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<string, object?>
|
||||
{
|
||||
["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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, OcspResponseCacheItem>
|
||||
{
|
||||
["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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LocalDirCache>();
|
||||
}
|
||||
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<NoOpCache>();
|
||||
}
|
||||
|
||||
[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<LocalDirCache>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -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%)**
|
||||
|
||||
Reference in New Issue
Block a user