feat: add MaxSubs enforcement, delivery-time deny filtering, auto-unsub cleanup

This commit is contained in:
Joseph Doherty
2026-02-23 00:53:15 -05:00
parent e9b6c7fdd3
commit 7a897c1087
5 changed files with 43 additions and 5 deletions

View File

@@ -45,8 +45,18 @@ public sealed class ClientPermissions : IDisposable
{
if (_subscribe == null)
return true;
if (!_subscribe.IsAllowed(subject))
return false;
if (queue != null && _subscribe.IsDenied(queue))
return false;
return true;
}
return _subscribe.IsAllowed(subject);
public bool IsDeliveryAllowed(string subject)
{
if (_subscribe == null)
return true;
return _subscribe.IsDeliveryAllowed(subject);
}
public void Dispose()
@@ -117,6 +127,21 @@ public sealed class PermissionSet : IDisposable
return allowed;
}
public bool IsDenied(string subject)
{
if (_deny == null) return false;
var result = _deny.Match(subject);
return result.PlainSubs.Length > 0 || result.QueueSubs.Length > 0;
}
public bool IsDeliveryAllowed(string subject)
{
if (_deny == null)
return true;
var result = _deny.Match(subject);
return result.PlainSubs.Length == 0 && result.QueueSubs.Length == 0;
}
public void Dispose()
{
_allow?.Dispose();

View File

@@ -23,6 +23,7 @@ public enum ClientClosedReason
ServerShutdown,
MsgHeaderViolation,
NoRespondersRequiresHeaders,
AuthenticationExpired,
}
public static class ClientClosedReasonExtensions
@@ -46,6 +47,7 @@ public static class ClientClosedReasonExtensions
ClientClosedReason.ServerShutdown => "Server Shutdown",
ClientClosedReason.MsgHeaderViolation => "Message Header Violation",
ClientClosedReason.NoRespondersRequiresHeaders => "No Responders Requires Headers",
ClientClosedReason.AuthenticationExpired => "Authentication Expired",
_ => reason.ToString(),
};
}

View File

@@ -523,7 +523,7 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
}
}
private static void DeliverMessage(Subscription sub, string subject, string? replyTo,
private void DeliverMessage(Subscription sub, string subject, string? replyTo,
ReadOnlyMemory<byte> headers, ReadOnlyMemory<byte> payload)
{
var client = sub.Client;
@@ -532,6 +532,16 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
// Check auto-unsub
var count = Interlocked.Increment(ref sub.MessageCount);
if (sub.MaxMessages > 0 && count > sub.MaxMessages)
{
// Clean up exhausted subscription from trie and client tracking
var subList = client.Account?.SubList ?? _globalAccount.SubList;
subList.Remove(sub);
client.RemoveSubscription(sub.Sid);
return;
}
// Deny-list delivery filter
if (client.Permissions?.IsDeliveryAllowed(subject) == false)
return;
client.SendMessage(subject, sub.Sid, replyTo, headers, payload);

View File

@@ -33,6 +33,7 @@ public static class NatsProtocol
public const string ErrPermissionsSubscribe = "Permissions Violation for Subscription";
public const string ErrSlowConsumer = "Slow Consumer";
public const string ErrNoRespondersRequiresHeaders = "No Responders Requires Headers Support";
public const string ErrMaxSubscriptionsExceeded = "Maximum Subscriptions Exceeded";
}
public sealed class ServerInfo

View File

@@ -5,10 +5,10 @@ public class ClientClosedReasonTests
[Fact]
public void All_expected_close_reasons_exist()
{
// Verify all 17 enum values exist and are distinct (None + 16 named reasons)
// Verify all 18 enum values exist and are distinct (None + 17 named reasons)
var values = Enum.GetValues<ClientClosedReason>();
values.Length.ShouldBe(17);
values.Distinct().Count().ShouldBe(17);
values.Length.ShouldBe(18);
values.Distinct().Count().ShouldBe(18);
}
[Theory]