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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user