feat(batch10): task3 implement ocsp cache core stats and compression
This commit is contained in:
@@ -103,6 +103,8 @@ public sealed class OcspResponse
|
|||||||
public DateTime ThisUpdate { get; init; }
|
public DateTime ThisUpdate { get; init; }
|
||||||
/// <summary><see cref="DateTime.MinValue"/> means "not set" (CA did not supply NextUpdate).</summary>
|
/// <summary><see cref="DateTime.MinValue"/> means "not set" (CA did not supply NextUpdate).</summary>
|
||||||
public DateTime NextUpdate { get; init; }
|
public DateTime NextUpdate { get; init; }
|
||||||
|
/// <summary>Raw OCSP response payload bytes when available.</summary>
|
||||||
|
public byte[]? Raw { get; init; }
|
||||||
/// <summary>Optional delegated signer certificate (RFC 6960 §4.2.2.2).</summary>
|
/// <summary>Optional delegated signer certificate (RFC 6960 §4.2.2.2).</summary>
|
||||||
public X509Certificate2? Certificate { get; init; }
|
public X509Certificate2? Certificate { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,21 @@ internal static class OcspHandler
|
|||||||
private const string TlsFeaturesOid = "1.3.6.1.5.5.7.1.24";
|
private const string TlsFeaturesOid = "1.3.6.1.5.5.7.1.24";
|
||||||
private const int StatusRequestExtension = 5;
|
private const int StatusRequestExtension = 5;
|
||||||
|
|
||||||
|
internal const string OcspResponseCacheDefaultDir = "_rc_";
|
||||||
|
internal const string OcspResponseCacheTypeNone = "none";
|
||||||
|
internal const string OcspResponseCacheTypeLocal = "local";
|
||||||
|
internal static readonly TimeSpan OcspResponseCacheMinimumSaveInterval = TimeSpan.FromSeconds(1);
|
||||||
|
internal static readonly TimeSpan OcspResponseCacheDefaultSaveInterval = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
|
internal static OcspResponseCacheConfig NewOCSPResponseCacheConfig() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Type = OcspResponseCacheTypeLocal,
|
||||||
|
LocalStore = OcspResponseCacheDefaultDir,
|
||||||
|
PreserveRevoked = false,
|
||||||
|
SaveInterval = OcspResponseCacheDefaultSaveInterval.TotalSeconds,
|
||||||
|
};
|
||||||
|
|
||||||
internal static (List<X509Certificate2>? certificates, Exception? error) ParseCertPEM(string name)
|
internal static (List<X509Certificate2>? certificates, Exception? error) ParseCertPEM(string name)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||||
@@ -403,6 +404,15 @@ public sealed class OcspResponseCacheStats
|
|||||||
public long Unknowns { get; set; }
|
public long Unknowns { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class OcspResponseCacheItem
|
||||||
|
{
|
||||||
|
public string Subject { get; set; } = string.Empty;
|
||||||
|
public DateTime CachedAt { get; set; }
|
||||||
|
public OcspStatusAssertion RespStatus { get; set; }
|
||||||
|
public DateTime RespExpires { get; set; }
|
||||||
|
public byte[] Resp { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A no-op OCSP cache that never stores anything.
|
/// A no-op OCSP cache that never stores anything.
|
||||||
/// Mirrors Go <c>NoOpCache</c> in server/ocsp_responsecache.go.
|
/// Mirrors Go <c>NoOpCache</c> in server/ocsp_responsecache.go.
|
||||||
@@ -424,9 +434,17 @@ internal sealed class NoOpCache : IOcspResponseCache
|
|||||||
_config = config;
|
_config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[]? Get(string key) => null;
|
public byte[]? Get(string key)
|
||||||
|
{
|
||||||
|
_ = key;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void Put(string key, byte[] response) { }
|
public void Put(string key, byte[] response)
|
||||||
|
{
|
||||||
|
_ = key;
|
||||||
|
_ = response;
|
||||||
|
}
|
||||||
|
|
||||||
public void Remove(string key) => Delete(key);
|
public void Remove(string key) => Delete(key);
|
||||||
|
|
||||||
@@ -497,42 +515,415 @@ internal sealed class NoOpCache : IOcspResponseCache
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LocalDirCache : IOcspResponseCache
|
internal sealed class LocalDirCache : IOcspResponseCache
|
||||||
{
|
{
|
||||||
private readonly string _dir;
|
private readonly ReaderWriterLockSlim _mu = new(LockRecursionPolicy.NoRecursion);
|
||||||
|
private readonly OcspResponseCacheConfig _config;
|
||||||
|
private readonly Dictionary<string, OcspResponseCacheItem> _cache = new(StringComparer.Ordinal);
|
||||||
|
private OcspResponseCacheStats? _stats;
|
||||||
|
private bool _online;
|
||||||
|
private bool _dirty;
|
||||||
|
private readonly TimeSpan _saveInterval;
|
||||||
|
private Timer? _timer;
|
||||||
|
|
||||||
public LocalDirCache(string dir)
|
public LocalDirCache(string dir)
|
||||||
|
: this(new OcspResponseCacheConfig
|
||||||
|
{
|
||||||
|
Type = OcspHandler.OcspResponseCacheTypeLocal,
|
||||||
|
LocalStore = dir,
|
||||||
|
PreserveRevoked = false,
|
||||||
|
SaveInterval = OcspHandler.OcspResponseCacheDefaultSaveInterval.TotalSeconds,
|
||||||
|
})
|
||||||
{
|
{
|
||||||
_dir = dir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[]? Get(string key)
|
public LocalDirCache(OcspResponseCacheConfig config)
|
||||||
{
|
{
|
||||||
var file = CacheFilePath(key);
|
_config = config ?? OcspHandler.NewOCSPResponseCacheConfig();
|
||||||
if (!File.Exists(file))
|
if (string.IsNullOrEmpty(_config.Type))
|
||||||
return null;
|
{
|
||||||
return File.ReadAllBytes(file);
|
_config.Type = OcspHandler.OcspResponseCacheTypeLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_config.LocalStore))
|
||||||
|
{
|
||||||
|
_config.LocalStore = OcspHandler.OcspResponseCacheDefaultDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveSeconds = _config.SaveInterval <= 0
|
||||||
|
? OcspHandler.OcspResponseCacheDefaultSaveInterval.TotalSeconds
|
||||||
|
: _config.SaveInterval;
|
||||||
|
var configuredInterval = TimeSpan.FromSeconds(saveSeconds);
|
||||||
|
_saveInterval = configuredInterval < OcspHandler.OcspResponseCacheMinimumSaveInterval
|
||||||
|
? OcspHandler.OcspResponseCacheMinimumSaveInterval
|
||||||
|
: configuredInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[]? Get(string key) => Get(key, log: null);
|
||||||
|
|
||||||
|
public byte[]? Get(string key, OcspLog? log)
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_online || string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_cache.TryGetValue(key, out var item))
|
||||||
|
{
|
||||||
|
if (_stats is not null)
|
||||||
|
{
|
||||||
|
_stats.Misses++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_stats is not null)
|
||||||
|
{
|
||||||
|
_stats.Hits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (decompressed, error) = Decompress(item.Resp);
|
||||||
|
if (error != null)
|
||||||
|
{
|
||||||
|
log?.Errorf?.Invoke(OcspMessages.ErrResponseDecompressFail, [key, error]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decompressed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Put(string key, byte[] response)
|
public void Put(string key, byte[] response)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
var status = OcspStatusAssertion.Unknown;
|
||||||
ArgumentNullException.ThrowIfNull(response);
|
var (parsed, error) = OcspHandler.ParseOcspResponse(response);
|
||||||
|
if (error == null && parsed != null)
|
||||||
|
{
|
||||||
|
status = parsed.Status;
|
||||||
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(_dir);
|
var ocspResponse = new OcspResponse
|
||||||
File.WriteAllBytes(CacheFilePath(key), response);
|
{
|
||||||
|
Status = status,
|
||||||
|
ThisUpdate = DateTime.UtcNow,
|
||||||
|
NextUpdate = DateTime.MinValue,
|
||||||
|
Raw = [.. response],
|
||||||
|
};
|
||||||
|
|
||||||
|
Put(key, ocspResponse, string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(string key)
|
public void Put(string key, OcspResponse response, string subject, OcspLog? log = null)
|
||||||
{
|
{
|
||||||
var file = CacheFilePath(key);
|
if (response == null)
|
||||||
if (File.Exists(file))
|
{
|
||||||
File.Delete(file);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_online || string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw = response.Raw ?? [];
|
||||||
|
var (compressed, error) = Compress(raw);
|
||||||
|
if (error != null || compressed == null)
|
||||||
|
{
|
||||||
|
log?.Errorf?.Invoke(OcspMessages.ErrResponseCompressFail, [key, error ?? new InvalidOperationException("compression failed")]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mu.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
AdjustStats(-1, existing.RespStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new OcspResponseCacheItem
|
||||||
|
{
|
||||||
|
Subject = subject,
|
||||||
|
CachedAt = DateTime.UtcNow,
|
||||||
|
RespStatus = response.Status,
|
||||||
|
RespExpires = response.NextUpdate,
|
||||||
|
Resp = compressed,
|
||||||
|
};
|
||||||
|
|
||||||
|
_cache[key] = item;
|
||||||
|
AdjustStats(1, item.RespStatus);
|
||||||
|
_dirty = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AdjustStatsHitToMiss()
|
||||||
|
{
|
||||||
|
if (_stats is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stats.Misses++;
|
||||||
|
_stats.Hits--;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void AdjustStats(long delta, OcspStatusAssertion status)
|
||||||
|
{
|
||||||
|
if (delta == 0 || _stats is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stats.Responses += delta;
|
||||||
|
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case OcspStatusAssertion.Good:
|
||||||
|
_stats.Goods += delta;
|
||||||
|
break;
|
||||||
|
case OcspStatusAssertion.Revoked:
|
||||||
|
_stats.Revokes += delta;
|
||||||
|
break;
|
||||||
|
case OcspStatusAssertion.Unknown:
|
||||||
|
_stats.Unknowns += delta;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key) => Delete(key, wasMiss: false, log: null);
|
||||||
|
|
||||||
|
public void Delete(string key, bool wasMiss = false, OcspLog? log = null)
|
||||||
|
{
|
||||||
|
_mu.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_online || string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_cache.TryGetValue(key, out var item))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.RespStatus == OcspStatusAssertion.Revoked && _config.PreserveRevoked)
|
||||||
|
{
|
||||||
|
if (wasMiss)
|
||||||
|
{
|
||||||
|
AdjustStatsHitToMiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache.Remove(key);
|
||||||
|
AdjustStats(-1, item.RespStatus);
|
||||||
|
|
||||||
|
if (wasMiss)
|
||||||
|
{
|
||||||
|
AdjustStatsHitToMiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
_dirty = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(NatsServer? server = null)
|
||||||
|
{
|
||||||
|
_ = server;
|
||||||
|
InitStats();
|
||||||
|
|
||||||
|
_mu.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_online = true;
|
||||||
|
_timer ??= new Timer(_ => { }, null, _saveInterval, _saveInterval);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop(NatsServer? server = null)
|
||||||
|
{
|
||||||
|
_ = server;
|
||||||
|
|
||||||
|
_mu.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_online = false;
|
||||||
|
if (_dirty)
|
||||||
|
{
|
||||||
|
_dirty = false;
|
||||||
|
}
|
||||||
|
_timer?.Dispose();
|
||||||
|
_timer = null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Online()
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _online;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Type()
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _config.Type;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OcspResponseCacheConfig Config()
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OcspResponseCacheStats? Stats()
|
||||||
|
{
|
||||||
|
_mu.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_stats == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OcspResponseCacheStats
|
||||||
|
{
|
||||||
|
Responses = _stats.Responses,
|
||||||
|
Hits = _stats.Hits,
|
||||||
|
Misses = _stats.Misses,
|
||||||
|
Revokes = _stats.Revokes,
|
||||||
|
Goods = _stats.Goods,
|
||||||
|
Unknowns = _stats.Unknowns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void InitStats()
|
||||||
|
{
|
||||||
|
_mu.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_stats = new OcspResponseCacheStats
|
||||||
|
{
|
||||||
|
Responses = _cache.Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var entry in _cache.Values)
|
||||||
|
{
|
||||||
|
switch (entry.RespStatus)
|
||||||
|
{
|
||||||
|
case OcspStatusAssertion.Good:
|
||||||
|
_stats.Goods++;
|
||||||
|
break;
|
||||||
|
case OcspStatusAssertion.Revoked:
|
||||||
|
_stats.Revokes++;
|
||||||
|
break;
|
||||||
|
case OcspStatusAssertion.Unknown:
|
||||||
|
_stats.Unknowns++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_mu.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (byte[]? compressed, Exception? error) Compress(ReadOnlySpan<byte> buffer)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
using (var writer = new BrotliStream(output, CompressionLevel.Fastest, leaveOpen: true))
|
||||||
|
{
|
||||||
|
writer.Write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (output.ToArray(), null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (byte[]? decompressed, Exception? error) Decompress(ReadOnlySpan<byte> buffer)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var input = new MemoryStream(buffer.ToArray());
|
||||||
|
using var reader = new BrotliStream(input, System.IO.Compression.CompressionMode.Decompress);
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
reader.CopyTo(output);
|
||||||
|
return (output.ToArray(), null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (null, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CacheFilePath(string key)
|
private string CacheFilePath(string key)
|
||||||
{
|
{
|
||||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||||
var file = Convert.ToHexString(hash).ToLowerInvariant();
|
var file = Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
return Path.Combine(_dir, $"{file}.ocsp");
|
return Path.Combine(_config.LocalStore, $"{file}.ocsp");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Shouldly;
|
||||||
|
using ZB.MOM.NatsNet.Server;
|
||||||
|
|
||||||
|
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
|
||||||
|
|
||||||
|
public sealed class OcspResponseCacheParserTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NewOCSPResponseCacheConfig_Defaults_ReturnExpectedValues()
|
||||||
|
{
|
||||||
|
var config = OcspHandler.NewOCSPResponseCacheConfig();
|
||||||
|
|
||||||
|
config.Type.ShouldBe("local");
|
||||||
|
config.LocalStore.ShouldBe("_rc_");
|
||||||
|
config.PreserveRevoked.ShouldBeFalse();
|
||||||
|
config.SaveInterval.ShouldBe(TimeSpan.FromMinutes(5).TotalSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
|
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||||
using ZB.MOM.NatsNet.Server.Auth.Ocsp;
|
using ZB.MOM.NatsNet.Server.Auth.Ocsp;
|
||||||
|
|
||||||
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
|
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
|
||||||
@@ -9,25 +10,86 @@ namespace ZB.MOM.NatsNet.Server.Tests.Auth;
|
|||||||
public sealed class OcspResponseCacheTests
|
public sealed class OcspResponseCacheTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LocalDirCache_GetPutRemove_ShouldPersistToDisk()
|
public void LocalDirCache_PutReplaceDelete_AdjustsStats()
|
||||||
{
|
{
|
||||||
var dir = Path.Combine(Path.GetTempPath(), $"ocsp-{Guid.NewGuid():N}");
|
var cache = new LocalDirCache(Path.Combine(Path.GetTempPath(), $"ocsp-{Guid.NewGuid():N}"));
|
||||||
Directory.CreateDirectory(dir);
|
cache.Start();
|
||||||
try
|
|
||||||
{
|
|
||||||
var cache = new LocalDirCache(dir);
|
|
||||||
cache.Get("abc").ShouldBeNull();
|
|
||||||
|
|
||||||
cache.Put("abc", [1, 2, 3]);
|
cache.Put("k1", CreateResponse(OcspStatusAssertion.Good, [1, 2, 3]), "subj");
|
||||||
cache.Get("abc").ShouldBe([1, 2, 3]);
|
cache.Put("k1", CreateResponse(OcspStatusAssertion.Revoked, [4, 5, 6]), "subj");
|
||||||
|
cache.Put("k2", CreateResponse(OcspStatusAssertion.Unknown, [7, 8]), "subj");
|
||||||
|
|
||||||
cache.Remove("abc");
|
var statsAfterPut = cache.Stats();
|
||||||
cache.Get("abc").ShouldBeNull();
|
statsAfterPut.ShouldNotBeNull();
|
||||||
}
|
statsAfterPut.Responses.ShouldBe(2);
|
||||||
finally
|
statsAfterPut.Goods.ShouldBe(0);
|
||||||
{
|
statsAfterPut.Revokes.ShouldBe(1);
|
||||||
Directory.Delete(dir, recursive: true);
|
statsAfterPut.Unknowns.ShouldBe(1);
|
||||||
}
|
|
||||||
|
cache.Delete("k1", wasMiss: false);
|
||||||
|
|
||||||
|
var statsAfterDelete = cache.Stats();
|
||||||
|
statsAfterDelete.ShouldNotBeNull();
|
||||||
|
statsAfterDelete.Responses.ShouldBe(1);
|
||||||
|
statsAfterDelete.Revokes.ShouldBe(0);
|
||||||
|
statsAfterDelete.Unknowns.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LocalDirCache_DeletePreserveRevoked_WithMiss_AdjustsHitToMiss()
|
||||||
|
{
|
||||||
|
var config = OcspHandler.NewOCSPResponseCacheConfig();
|
||||||
|
config.PreserveRevoked = true;
|
||||||
|
|
||||||
|
var cache = new LocalDirCache(config);
|
||||||
|
cache.Start();
|
||||||
|
cache.Put("k1", CreateResponse(OcspStatusAssertion.Revoked, [9, 9, 9]), "subj");
|
||||||
|
cache.Get("k1").ShouldNotBeNull();
|
||||||
|
|
||||||
|
cache.Delete("k1", wasMiss: true);
|
||||||
|
|
||||||
|
var stats = cache.Stats();
|
||||||
|
stats.ShouldNotBeNull();
|
||||||
|
stats.Responses.ShouldBe(1);
|
||||||
|
stats.Revokes.ShouldBe(1);
|
||||||
|
stats.Hits.ShouldBe(0);
|
||||||
|
stats.Misses.ShouldBe(1);
|
||||||
|
cache.Get("k1").ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LocalDirCache_OnlineTypeConfigStats_FollowLifecycle()
|
||||||
|
{
|
||||||
|
var config = OcspHandler.NewOCSPResponseCacheConfig();
|
||||||
|
var cache = new LocalDirCache(config);
|
||||||
|
|
||||||
|
cache.Online().ShouldBeFalse();
|
||||||
|
cache.Type().ShouldBe("local");
|
||||||
|
cache.Config().LocalStore.ShouldBe(config.LocalStore);
|
||||||
|
cache.Stats().ShouldBeNull();
|
||||||
|
|
||||||
|
cache.Start();
|
||||||
|
cache.Online().ShouldBeTrue();
|
||||||
|
cache.Stats().ShouldNotBeNull();
|
||||||
|
|
||||||
|
cache.Stop();
|
||||||
|
cache.Online().ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LocalDirCache_CompressDecompress_RoundTripsPayload()
|
||||||
|
{
|
||||||
|
var cache = new LocalDirCache(Path.Combine(Path.GetTempPath(), $"ocsp-{Guid.NewGuid():N}"));
|
||||||
|
var payload = "ocsp-cache-roundtrip-data"u8.ToArray();
|
||||||
|
|
||||||
|
var (compressed, compressError) = cache.Compress(payload);
|
||||||
|
compressError.ShouldBeNull();
|
||||||
|
compressed.ShouldNotBeNull();
|
||||||
|
|
||||||
|
var (decompressed, decompressError) = cache.Decompress(compressed!);
|
||||||
|
decompressError.ShouldBeNull();
|
||||||
|
decompressed.ShouldNotBeNull();
|
||||||
|
decompressed.ShouldBe(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -45,37 +107,19 @@ public sealed class OcspResponseCacheTests
|
|||||||
|
|
||||||
noOp.Put("k", [5]);
|
noOp.Put("k", [5]);
|
||||||
noOp.Get("k").ShouldBeNull();
|
noOp.Get("k").ShouldBeNull();
|
||||||
noOp.Remove("k"); // alias to Delete
|
noOp.Remove("k");
|
||||||
noOp.Delete("k");
|
noOp.Delete("k");
|
||||||
|
|
||||||
noOp.Stop();
|
noOp.Stop();
|
||||||
noOp.Online().ShouldBeFalse();
|
noOp.Online().ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
private static OcspResponse CreateResponse(OcspStatusAssertion status, byte[] raw) =>
|
||||||
public void OcspMonitor_StartAndStop_ShouldLoadStaple()
|
new()
|
||||||
{
|
|
||||||
var dir = Path.Combine(Path.GetTempPath(), $"ocsp-monitor-{Guid.NewGuid():N}");
|
|
||||||
Directory.CreateDirectory(dir);
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var stapleFile = Path.Combine(dir, "staple.bin");
|
Status = status,
|
||||||
File.WriteAllBytes(stapleFile, [9, 9]);
|
ThisUpdate = DateTime.UtcNow.AddMinutes(-1),
|
||||||
|
NextUpdate = DateTime.UtcNow.AddMinutes(10),
|
||||||
var monitor = new OcspMonitor
|
Raw = raw,
|
||||||
{
|
};
|
||||||
OcspStapleFile = stapleFile,
|
|
||||||
CheckInterval = TimeSpan.FromMilliseconds(10),
|
|
||||||
};
|
|
||||||
|
|
||||||
monitor.Start();
|
|
||||||
Thread.Sleep(30);
|
|
||||||
monitor.GetStaple().ShouldBe([9, 9]);
|
|
||||||
monitor.Stop();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Directory.Delete(dir, recursive: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
# NATS .NET Porting Status Report
|
# NATS .NET Porting Status Report
|
||||||
|
|
||||||
Generated: 2026-02-28 17:51:02 UTC
|
Generated: 2026-02-28 17:59:26 UTC
|
||||||
|
|
||||||
## Modules (12 total)
|
## Modules (12 total)
|
||||||
|
|
||||||
@@ -13,10 +13,10 @@ Generated: 2026-02-28 17:51:02 UTC
|
|||||||
| Status | Count |
|
| Status | Count |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| complete | 14 |
|
| complete | 14 |
|
||||||
| deferred | 1968 |
|
| deferred | 1955 |
|
||||||
| n_a | 24 |
|
| n_a | 24 |
|
||||||
| stub | 1 |
|
| stub | 1 |
|
||||||
| verified | 1666 |
|
| verified | 1679 |
|
||||||
|
|
||||||
## Unit Tests (3257 total)
|
## Unit Tests (3257 total)
|
||||||
|
|
||||||
@@ -35,4 +35,4 @@ Generated: 2026-02-28 17:51:02 UTC
|
|||||||
|
|
||||||
## Overall Progress
|
## Overall Progress
|
||||||
|
|
||||||
**3017/6942 items complete (43.5%)**
|
**3030/6942 items complete (43.6%)**
|
||||||
|
|||||||
Reference in New Issue
Block a user