feat(batch10): task4 wire ocsp cache load save and server lifecycle

This commit is contained in:
Joseph Doherty
2026-02-28 13:05:45 -05:00
parent 98cf350383
commit f5a13bedff
10 changed files with 674 additions and 14 deletions

View File

@@ -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; }

View File

@@ -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>

View File

@@ -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();

View File

@@ -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()

View 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;
}
}
}