feat(batch16): port client helper and permission core features

This commit is contained in:
Joseph Doherty
2026-02-28 18:53:59 -05:00
parent 0cc5d89199
commit 46ea749ad0
4 changed files with 364 additions and 11 deletions

View File

@@ -209,6 +209,12 @@ public sealed partial class ClientConnection
/// </summary>
public override string ToString() => _ncs;
/// <summary>
/// Returns the cached connection string identifier.
/// Mirrors Go <c>client.String()</c>.
/// </summary>
public string String() => ToString();
/// <summary>
/// Returns the nonce presented to the client during connection.
/// Mirrors Go <c>client.GetNonce()</c>.
@@ -243,6 +249,12 @@ public sealed partial class ClientConnection
lock (_mu) { return _nc as SslStream; }
}
/// <summary>
/// Returns TLS connection state if the connection is TLS-secured, otherwise <c>null</c>.
/// Mirrors Go <c>client.GetTLSConnectionState()</c>.
/// </summary>
public SslStream? GetTLSConnectionState() => GetTlsStream();
// =========================================================================
// Client type classification (features 403-404)
// =========================================================================
@@ -649,6 +661,140 @@ public sealed partial class ClientConnection
return cp;
}
/// <summary>
/// Builds public permissions from internal permission indexes.
/// Mirrors Go <c>client.publicPermissions()</c>.
/// </summary>
internal Permissions? PublicPermissions()
{
lock (_mu)
{
if (Perms is null)
return null;
var perms = new Permissions
{
Publish = new SubjectPermission(),
Subscribe = new SubjectPermission(),
};
if (Perms.Pub.Allow is not null)
{
var subs = new List<Subscription>(32);
Perms.Pub.Allow.All(subs);
perms.Publish.Allow = [];
foreach (var sub in subs)
perms.Publish.Allow.Add(Encoding.ASCII.GetString(sub.Subject));
}
if (Perms.Pub.Deny is not null)
{
var subs = new List<Subscription>(32);
Perms.Pub.Deny.All(subs);
perms.Publish.Deny = [];
foreach (var sub in subs)
perms.Publish.Deny.Add(Encoding.ASCII.GetString(sub.Subject));
}
if (Perms.Sub.Allow is not null)
{
var subs = new List<Subscription>(32);
Perms.Sub.Allow.All(subs);
perms.Subscribe.Allow = [];
foreach (var sub in subs)
{
if (sub.Queue is { Length: > 0 })
perms.Subscribe.Allow.Add($"{Encoding.ASCII.GetString(sub.Subject)} {Encoding.ASCII.GetString(sub.Queue)}");
else
perms.Subscribe.Allow.Add(Encoding.ASCII.GetString(sub.Subject));
}
}
if (Perms.Sub.Deny is not null)
{
var subs = new List<Subscription>(32);
Perms.Sub.Deny.All(subs);
perms.Subscribe.Deny = [];
foreach (var sub in subs)
{
if (sub.Queue is { Length: > 0 })
perms.Subscribe.Deny.Add($"{Encoding.ASCII.GetString(sub.Subject)} {Encoding.ASCII.GetString(sub.Queue)}");
else
perms.Subscribe.Deny.Add(Encoding.ASCII.GetString(sub.Subject));
}
}
if (Perms.Resp is not null)
{
perms.Response = new ResponsePermission
{
MaxMsgs = Perms.Resp.MaxMsgs,
Expires = Perms.Resp.Expires,
};
}
return perms;
}
}
/// <summary>
/// Merges deny permissions into publish/subscribe deny lists.
/// Lock is expected on entry.
/// Mirrors Go <c>client.mergeDenyPermissions()</c>.
/// </summary>
internal void MergeDenyPermissions(DenyType what, IReadOnlyList<string> denySubjects)
{
if (denySubjects.Count == 0)
return;
Perms ??= new ClientPermissions();
List<Perm> targets = what switch
{
DenyType.Pub => [Perms.Pub],
DenyType.Sub => [Perms.Sub],
DenyType.Both => [Perms.Pub, Perms.Sub],
_ => [],
};
foreach (var target in targets)
{
target.Deny ??= SubscriptionIndex.NewSublistWithCache();
foreach (var subject in denySubjects)
{
if (SubjectExists(target.Deny, subject))
continue;
target.Deny.Insert(new Subscription { Subject = Encoding.ASCII.GetBytes(subject) });
}
}
}
/// <summary>
/// Merges deny permissions under the client lock.
/// Mirrors Go <c>client.mergeDenyPermissionsLocked()</c>.
/// </summary>
internal void MergeDenyPermissionsLocked(DenyType what, IReadOnlyList<string> denySubjects)
{
lock (_mu)
{
MergeDenyPermissions(what, denySubjects);
}
}
private static bool SubjectExists(SubscriptionIndex index, string subject)
{
var result = index.Match(subject);
foreach (var qGroup in result.QSubs)
foreach (var sub in qGroup)
if (Encoding.ASCII.GetString(sub.Subject) == subject)
return true;
foreach (var sub in result.PSubs)
if (Encoding.ASCII.GetString(sub.Subject) == subject)
return true;
return false;
}
// =========================================================================
// setExpiration / loadMsgDenyFilter (features 423-424)
// =========================================================================
@@ -676,6 +822,49 @@ public sealed partial class ClientConnection
_expTimer = new Timer(_ => ClaimExpiration(), null, d, Timeout.InfiniteTimeSpan);
}
/// <summary>
/// Applies JWT expiration with optional validity cap.
/// Mirrors Go <c>client.setExpiration()</c>.
/// </summary>
internal void SetExpiration(long claimsExpiresUnixSeconds, TimeSpan validFor)
{
if (claimsExpiresUnixSeconds == 0)
{
if (validFor != TimeSpan.Zero)
SetExpirationTimer(validFor);
return;
}
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var expiresAt = TimeSpan.Zero;
if (claimsExpiresUnixSeconds > now)
expiresAt = TimeSpan.FromSeconds(claimsExpiresUnixSeconds - now);
if (validFor != TimeSpan.Zero && validFor < expiresAt)
SetExpirationTimer(validFor);
else
SetExpirationTimer(expiresAt);
}
/// <summary>
/// Loads message deny filter from current deny subject array.
/// Lock is expected on entry.
/// Mirrors Go <c>client.loadMsgDenyFilter()</c>.
/// </summary>
internal void LoadMsgDenyFilter()
{
MPerms = new MsgDeny
{
Deny = SubscriptionIndex.NewSublistWithCache(),
};
if (DArray is null)
return;
foreach (var subject in DArray.Keys)
MPerms.Deny.Insert(new Subscription { Subject = Encoding.ASCII.GetBytes(subject) });
}
// =========================================================================
// msgParts (feature 470)
// =========================================================================
@@ -1018,7 +1207,7 @@ public sealed partial class ClientConnection
internal void EnqueueProto(ReadOnlySpan<byte> proto)
{
// TODO: Full write-loop queuing when Server is ported (session 09).
// Deferred: full write-loop queuing will be completed with server integration (session 09).
if (_nc is not null)
{
try { _nc.Write(proto); }
@@ -1110,7 +1299,7 @@ public sealed partial class ClientConnection
internal bool ConnectionTypeAllowed(string ct)
{
// TODO: Full implementation when JWT is integrated.
// Deferred: full implementation will be completed with JWT integration.
return true;
}
@@ -1179,13 +1368,13 @@ public sealed partial class ClientConnection
internal async Task<bool> DoTlsServerHandshakeAsync(SslServerAuthenticationOptions opts, CancellationToken ct = default)
{
// TODO: Full TLS when Server is ported.
// Deferred: full TLS flow will be completed with server integration.
return false;
}
internal async Task<bool> DoTlsClientHandshakeAsync(SslClientAuthenticationOptions opts, CancellationToken ct = default)
{
// TODO: Full TLS when Server is ported.
// Deferred: full TLS flow will be completed with server integration.
return false;
}
@@ -1373,10 +1562,10 @@ public sealed partial class ClientConnection
// IsMqtt / IsWebSocket helpers (used by clientType, not separately tracked)
// =========================================================================
internal bool IsMqtt() => false; // TODO: set in session 22 (MQTT)
internal bool IsWebSocket() => false; // TODO: set in session 23 (WebSocket)
internal bool IsHubLeafNode() => false; // TODO: set in session 15 (leaf nodes)
internal string RemoteCluster() => string.Empty; // TODO: session 14/15
internal bool IsMqtt() => false; // Deferred to session 22 (MQTT).
internal bool IsWebSocket() => false; // Deferred to session 23 (WebSocket).
internal bool IsHubLeafNode() => false; // Deferred to session 15 (leaf nodes).
internal string RemoteCluster() => string.Empty; // Deferred to sessions 14/15.
}
// ============================================================================
@@ -1449,11 +1638,17 @@ public interface INatsAccount
/// <summary>Thrown when account connection limits are exceeded.</summary>
public sealed class TooManyAccountConnectionsException : Exception
{
public TooManyAccountConnectionsException() : base("Too Many Account Connections") { }
public TooManyAccountConnectionsException() : base("Too Many Account Connections")
{
// Intentionally empty.
}
}
/// <summary>Thrown when an account is invalid or null.</summary>
public sealed class BadAccountException : Exception
{
public BadAccountException() : base("Bad Account") { }
public BadAccountException() : base("Bad Account")
{
// Intentionally empty.
}
}