// Copyright 2018-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Adapted from server/accounts.go in the NATS server Go source. using ZB.MOM.NatsNet.Server.Auth; using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Internal.DataStructures; using System.Text; using System.Text.Json; namespace ZB.MOM.NatsNet.Server; // ============================================================================ // Account — full implementation // Mirrors Go `Account` struct in server/accounts.go lines 52-119. // ============================================================================ /// /// Represents a NATS account, tracking clients, subscriptions, imports, exports, /// and subject mappings. Implements so that /// can interact with it without a hard dependency. /// Mirrors Go Account struct in server/accounts.go. /// public sealed partial class Account : INatsAccount { // ------------------------------------------------------------------------- // Constants // ------------------------------------------------------------------------- /// /// jwt.NoLimit equivalent: -1 means no limit applied. /// private const int NoLimit = -1; // ------------------------------------------------------------------------- // Identity fields // ------------------------------------------------------------------------- /// Account name. Mirrors Go Name string. public string Name { get; set; } = string.Empty; /// NKey public key. Mirrors Go Nkey string. public string Nkey { get; set; } = string.Empty; /// JWT issuer key. Mirrors Go Issuer string. public string Issuer { get; set; } = string.Empty; /// Raw JWT claim string. Mirrors Go claimJWT string. internal string ClaimJwt { get; set; } = string.Empty; /// Time of last update from resolver. Mirrors Go updated time.Time. internal DateTime Updated { get; set; } // ------------------------------------------------------------------------- // Locks // ------------------------------------------------------------------------- /// Primary account read/write lock. Mirrors Go mu sync.RWMutex. private readonly ReaderWriterLockSlim _mu = new(LockRecursionPolicy.NoRecursion); /// Send-queue mutex. Mirrors Go sqmu sync.Mutex. private readonly object _sqmu = new(); /// Leaf-node list lock. Mirrors Go lmu sync.RWMutex. private readonly ReaderWriterLockSlim _lmu = new(LockRecursionPolicy.NoRecursion); /// Event ID mutex. Mirrors Go eventIdsMu sync.Mutex. private readonly object _eventIdsMu = new(); /// JetStream migration/clear-observer mutex. Mirrors Go jscmMu sync.Mutex. private readonly object _jscmMu = new(); // ------------------------------------------------------------------------- // Subscription index // ------------------------------------------------------------------------- /// /// Subscription trie for this account. Mirrors Go sl *Sublist. /// Set by the server when the account is registered. /// internal SubscriptionIndex? Sublist { get; set; } // ------------------------------------------------------------------------- // Internal client and send queue (stubs) // ------------------------------------------------------------------------- /// /// Internal account client. Mirrors Go ic *client. /// TODO: session 12 — full internal client wiring. /// internal ClientConnection? InternalClient { get; set; } /// /// Per-account send queue. Mirrors Go sq *sendq. /// internal SendQueue? SendQueue { get; set; } internal SendQueue? GetSendQueue() { lock (_sqmu) { return SendQueue; } } internal void SetSendQueue(SendQueue? sendQueue) { lock (_sqmu) { SendQueue = sendQueue; } } // ------------------------------------------------------------------------- // Eventing timers // ------------------------------------------------------------------------- /// Expiration timer. Mirrors Go etmr *time.Timer. private Timer? _etmr; /// Connection-count timer. Mirrors Go ctmr *time.Timer. private Timer? _ctmr; // ------------------------------------------------------------------------- // Remote server tracking // ------------------------------------------------------------------------- /// /// Per-server connection and leaf-node counts. /// Key is server ID. Mirrors Go strack map[string]sconns. /// private Dictionary? _strack; /// /// Remote client count (sum of strack[*].Conns). /// Mirrors Go nrclients int32. /// Protected by . /// private int _nrclients; /// /// System client count. /// Mirrors Go sysclients int32. /// Protected by . /// private int _sysclients; /// /// Local leaf-node count. /// Mirrors Go nleafs int32. /// Protected by . /// private int _nleafs; /// /// Remote leaf-node count (sum of strack[*].Leafs). /// Mirrors Go nrleafs int32. /// Protected by . /// private int _nrleafs; // ------------------------------------------------------------------------- // Client set // ------------------------------------------------------------------------- /// /// Active local clients. Mirrors Go clients map[*client]struct{}. /// Protected by . /// private HashSet? _clients; // ------------------------------------------------------------------------- // Route and leaf-queue maps // ------------------------------------------------------------------------- /// /// Route map: subject → reference count. /// Mirrors Go rm map[string]int32. /// Protected by . /// private Dictionary? _rm; /// /// Leaf queue weights: subject → weight. /// Mirrors Go lqws map[string]int32. /// Protected by . /// private Dictionary? _lqws; // ------------------------------------------------------------------------- // User revocations // ------------------------------------------------------------------------- /// /// Revoked user nkeys: key → revocation timestamp (Unix seconds). /// Mirrors Go usersRevoked map[string]int64. /// Protected by . /// internal Dictionary? UsersRevoked { get; set; } // ------------------------------------------------------------------------- // Subject mappings // ------------------------------------------------------------------------- /// /// Ordered list of subject mappings. Mirrors Go mappings []*mapping. /// Protected by . /// private List _mappings = []; /// /// Atomic flag: 1 when is non-empty. /// Mirrors Go hasMapped atomic.Bool. /// private int _hasMapped; // 0 = false, 1 = true // ------------------------------------------------------------------------- // Leaf nodes // ------------------------------------------------------------------------- /// /// Ordered list of local leaf-node clients. /// Mirrors Go lleafs []*client. /// Protected by . /// private List _lleafs = []; /// /// Cluster name → count of leaf-node connections from that cluster. /// Mirrors Go leafClusters map[string]uint64. /// Protected by . /// private Dictionary? _leafClusters; // ------------------------------------------------------------------------- // Import / export maps // ------------------------------------------------------------------------- /// Import tracking. Mirrors Go imports importMap. internal ImportMap Imports { get; set; } = new(); /// Export tracking. Mirrors Go exports exportMap. internal ExportMap Exports { get; set; } = new(); // ------------------------------------------------------------------------- // JetStream (stubs) // ------------------------------------------------------------------------- /// /// JetStream account state. Mirrors Go js *jsAccount. /// TODO: session 19 — JetStream implementation. /// internal JsAccount? JetStream { get; set; } /// /// Per-domain JetStream limits. Mirrors Go jsLimits map[string]JetStreamAccountLimits. /// TODO: session 19 — JetStream implementation. /// internal Dictionary? JetStreamLimits { get; set; } // ------------------------------------------------------------------------- // Misc identity fields // ------------------------------------------------------------------------- /// Non-routed gateway account name. Mirrors Go nrgAccount string. internal string NrgAccount { get; set; } = string.Empty; // ------------------------------------------------------------------------- // Limits (embedded `limits` struct in Go) // ------------------------------------------------------------------------- /// /// Maximum payload size (-1 = unlimited). Mirrors Go embedded limits.mpay int32. /// internal int MaxPayload { get; set; } = NoLimit; /// /// Maximum subscriptions (-1 = unlimited). Mirrors Go embedded limits.msubs int32. /// internal int MaxSubscriptions { get; set; } = NoLimit; /// /// Maximum connections (-1 = unlimited). Mirrors Go embedded limits.mconns int32. /// internal int MaxConnections { get; set; } = NoLimit; /// /// Maximum leaf nodes (-1 = unlimited). Mirrors Go embedded limits.mleafs int32. /// internal int MaxLeafNodes { get; set; } = NoLimit; /// /// When true, bearer tokens are not allowed. /// Mirrors Go embedded limits.disallowBearer bool. /// internal bool DisallowBearer { get; set; } // ------------------------------------------------------------------------- // Expiration (atomic) // ------------------------------------------------------------------------- /// /// 1 when the account JWT has expired. Mirrors Go expired atomic.Bool. /// private int _expired; // 0 = not expired, 1 = expired // ------------------------------------------------------------------------- // Miscellaneous // ------------------------------------------------------------------------- /// /// When true, this account's config could not be fully resolved. /// Mirrors Go incomplete bool. /// internal bool Incomplete { get; set; } /// /// Signing keys for JWT validation. /// Mirrors Go signingKeys map[string]jwt.Scope. /// Value is object? because JWT Scope is not yet fully ported. /// internal Dictionary? SigningKeys { get; set; } /// /// External authorization configuration stub. /// Mirrors Go extAuth *jwt.ExternalAuthorization. /// TODO: session 11 — JWT full integration. /// internal object? ExternalAuth { get; set; } /// /// The server this account is registered with, or null if not yet registered. /// Stored as object? to avoid circular reference. /// Mirrors Go srv *Server. /// internal object? Server { get; set; } /// /// Loop detection subject for leaf nodes. /// Mirrors Go lds string. /// internal string LoopDetectionSubject { get; set; } = string.Empty; /// /// Service reply prefix (wildcard subscription root). /// Mirrors Go siReply []byte. /// internal byte[]? ServiceImportReply { get; set; } /// /// Gateway reply mapping table used for routed reply restoration. /// Mirrors Go gwReplyMapping. /// internal GwReplyMapping GwReplyMapping { get; } = new(); /// /// Subscription ID counter for internal use. /// Mirrors Go isid uint64. /// private ulong _isid; /// /// Default permissions for users with no explicit permissions. /// Mirrors Go defaultPerms *Permissions. /// internal Permissions? DefaultPerms { get; set; } /// /// Account tags from JWT. Mirrors Go tags jwt.TagList. /// Stored as string array pending full JWT integration. /// internal string[] Tags { get; set; } = []; /// /// Human-readable name tag (distinct from ). /// Mirrors Go nameTag string. /// internal string NameTag { get; set; } = string.Empty; /// /// Unix-nanosecond timestamp of last max-subscription-limit log. /// Mirrors Go lastLimErr int64. /// private long _lastLimErr; /// /// Route pool index (-1 = dedicated, -2 = transitioning, ≥ 0 = shared). /// Mirrors Go routePoolIdx int. /// internal int RoutePoolIdx { get; set; } /// /// Message-tracing destination subject. /// Mirrors Go traceDest string. /// Protected by . /// private string _traceDest = string.Empty; /// /// Tracing sampling percentage (0 = header-triggered, 1-100 = rate). /// Mirrors Go traceDestSampling int. /// Protected by . /// private int _traceDestSampling; /// /// Sets account-level message trace destination subject. /// Mirrors writes to Go acc.traceDest during config parsing. /// internal void SetMessageTraceDestination(string subject) { _mu.EnterWriteLock(); try { _traceDest = subject ?? string.Empty; } finally { _mu.ExitWriteLock(); } } /// /// Returns account-level message trace destination subject. /// Mirrors reads of Go acc.traceDest during config parsing. /// internal string GetMessageTraceDestination() { _mu.EnterReadLock(); try { return _traceDest; } finally { _mu.ExitReadLock(); } } /// /// Sets account-level message trace sampling percentage. /// Mirrors writes to Go acc.traceDestSampling during config parsing. /// internal void SetMessageTraceSampling(int sampling) { _mu.EnterWriteLock(); try { _traceDestSampling = sampling; } finally { _mu.ExitWriteLock(); } } /// /// Returns account-level message trace sampling percentage. /// Mirrors reads of Go acc.traceDestSampling during config parsing. /// internal int GetMessageTraceSampling() { _mu.EnterReadLock(); try { return _traceDestSampling; } finally { _mu.ExitReadLock(); } } /// /// Sets account-level message trace destination subject. /// Mirrors Go (a *Account) setTraceDest(dest string). /// internal void SetTraceDest(string dest) => SetMessageTraceDestination(dest); /// /// Returns trace destination and sampling. /// Mirrors Go (a *Account) getTraceDestAndSampling() (string, int). /// internal (string Destination, int Sampling) GetTraceDestAndSampling() { _mu.EnterReadLock(); try { return (_traceDest, _traceDestSampling); } finally { _mu.ExitReadLock(); } } // ------------------------------------------------------------------------- // Factory // ------------------------------------------------------------------------- /// /// Creates a new unlimited account with the given name. /// Mirrors Go NewAccount(name string) *Account. /// public static Account NewAccount(string name) => new() { Name = name, MaxPayload = NoLimit, MaxSubscriptions = NoLimit, MaxConnections = NoLimit, MaxLeafNodes = NoLimit, }; // ------------------------------------------------------------------------- // Object overrides // ------------------------------------------------------------------------- /// /// Returns the account name. Mirrors Go (a *Account) String() string. /// public override string ToString() => Name; /// /// Returns the account name. /// Mirrors Go (a *Account) String() string. /// public string String() => Name; // ------------------------------------------------------------------------- // Shallow copy for config reload // ------------------------------------------------------------------------- /// /// Copies identity and config fields from the options-struct account (a) /// into the live server account (na). The write lock on na must /// be held by the caller; this (the options account) requires no lock. /// Mirrors Go (a *Account) shallowCopy(na *Account). /// internal void ShallowCopy(Account na) { na.Nkey = Nkey; na.Issuer = Issuer; na._traceDest = _traceDest; na._traceDestSampling = _traceDestSampling; na.NrgAccount = NrgAccount; // Stream imports — shallow-clone each entry. if (Imports.Streams != null) { na.Imports.Streams = new List(Imports.Streams.Count); foreach (var si in Imports.Streams) { // Struct-style shallow copy via record-style clone. na.Imports.Streams.Add(new StreamImportEntry { Account = si.Account, From = si.From, To = si.To, Transform = si.Transform, ReverseTransform = si.ReverseTransform, Claim = si.Claim, UsePublishedSubject = si.UsePublishedSubject, Invalid = si.Invalid, AllowTrace = si.AllowTrace, }); } } // Service imports — shallow-clone each inner list. if (Imports.Services != null) { na.Imports.Services = new Dictionary>(Imports.Services.Count); foreach (var (k, list) in Imports.Services) { var cloned = new List(list.Count); foreach (var si in list) { cloned.Add(new ServiceImportEntry { Account = si.Account, Claim = si.Claim, ServiceExport = si.ServiceExport, SubscriptionId = si.SubscriptionId, From = si.From, To = si.To, Transform = si.Transform, Timestamp = si.Timestamp, ResponseType = si.ResponseType, Latency = si.Latency, M1 = si.M1, RequestingClient = si.RequestingClient, UsePublishedSubject = si.UsePublishedSubject, IsResponse = si.IsResponse, Invalid = si.Invalid, Share = si.Share, Tracking = si.Tracking, DidDeliver = si.DidDeliver, AllowTrace = si.AllowTrace, TrackingHeader = si.TrackingHeader, }); } na.Imports.Services[k] = cloned; } } // Stream exports — shallow-clone each entry. if (Exports.Streams != null) { na.Exports.Streams = new Dictionary(Exports.Streams.Count); foreach (var (k, se) in Exports.Streams) { na.Exports.Streams[k] = se == null ? null! : new StreamExport { TokenRequired = se.TokenRequired, AccountPosition = se.AccountPosition, Approved = se.Approved, ActivationsRevoked = se.ActivationsRevoked, }; } } // Service exports — shallow-clone each entry. if (Exports.Services != null) { na.Exports.Services = new Dictionary(Exports.Services.Count); foreach (var (k, se) in Exports.Services) { na.Exports.Services[k] = se == null ? null! : new ServiceExportEntry { Account = se.Account, ResponseType = se.ResponseType, Latency = se.Latency, ResponseTimer = se.ResponseTimer, ResponseThreshold = se.ResponseThreshold, AllowTrace = se.AllowTrace, TokenRequired = se.TokenRequired, AccountPosition = se.AccountPosition, Approved = se.Approved, ActivationsRevoked = se.ActivationsRevoked, }; } } // Mappings and limits — copy by reference / value. na._mappings = _mappings; Interlocked.Exchange(ref na._hasMapped, _mappings.Count > 0 ? 1 : 0); // JetStream limits — shared reference. // TODO: session 19 — deep copy JetStream limits when ported. na.JetStreamLimits = JetStreamLimits; // Server-config account limits. na.MaxPayload = MaxPayload; na.MaxSubscriptions = MaxSubscriptions; na.MaxConnections = MaxConnections; na.MaxLeafNodes = MaxLeafNodes; na.DisallowBearer = DisallowBearer; } // ------------------------------------------------------------------------- // Event ID generation // ------------------------------------------------------------------------- /// /// Generates a unique event identifier using its own dedicated lock. /// Mirrors Go (a *Account) nextEventID() string. /// internal string NextEventId() { lock (_eventIdsMu) { return Guid.NewGuid().ToString("N"); } } // ------------------------------------------------------------------------- // Client accessors // ------------------------------------------------------------------------- /// /// Returns a snapshot list of clients. Lock must be held by the caller. /// Mirrors Go (a *Account) getClientsLocked() []*client. /// internal List GetClientsLocked() { if (_clients == null || _clients.Count == 0) return []; return [.. _clients]; } /// /// Returns a thread-safe snapshot list of clients. /// Mirrors Go (a *Account) getClients() []*client. /// internal List GetClients() { _mu.EnterReadLock(); try { return GetClientsLocked(); } finally { _mu.ExitReadLock(); } } /// /// Returns a snapshot of non-internal clients. Lock must be held by the caller. /// Mirrors Go (a *Account) getExternalClientsLocked() []*client. /// internal List GetExternalClientsLocked() { if (_clients == null || _clients.Count == 0) return []; var result = new List(_clients.Count); foreach (var c in _clients) { if (!IsInternalClientKind(c.Kind)) result.Add(c); } return result; } // ------------------------------------------------------------------------- // Remote server tracking // ------------------------------------------------------------------------- /// /// Updates the remote-server tracking table for this account based on an /// incoming message, and returns the set of /// local clients that must be disconnected because a connection limit has /// been exceeded (after accounting for remote connections). /// Mirrors Go (a *Account) updateRemoteServer(m *AccountNumConns) []*client. /// internal List UpdateRemoteServer(AccountNumConns m) { _mu.EnterWriteLock(); try { _strack ??= new Dictionary(); _strack.TryGetValue(m.Server.Id, out var prev); _strack[m.Server.Id] = new SConns { Conns = m.Conns, Leafs = m.LeafNodes, }; _nrclients += m.Conns - (prev?.Conns ?? 0); _nrleafs += m.LeafNodes - (prev?.Leafs ?? 0); var localCount = _clients?.Count ?? 0; // Check if total connections exceed the limit. bool maxConnsExceeded = MaxConnections != NoLimit && (localCount - _sysclients + _nrclients) > MaxConnections; List toDisconnect = []; if (maxConnsExceeded) { var external = GetExternalClientsLocked(); // Sort: newest connections first (reverse chronological). // TODO: session 12 — sort by c.Start once ClientConnection has a Start field. // For now we cannot sort without the start time, so take from end. int over = (localCount - _sysclients + _nrclients) - MaxConnections; if (over < external.Count) toDisconnect.AddRange(external.GetRange(0, over)); else toDisconnect.AddRange(external); } // Check if total leaf nodes exceed the limit. bool maxLeafsExceeded = MaxLeafNodes != NoLimit && (_nleafs + _nrleafs) > MaxLeafNodes; if (maxLeafsExceeded) { _lmu.EnterReadLock(); try { int over = _nleafs + _nrleafs - MaxLeafNodes; if (over > 0) { int start = Math.Max(0, _lleafs.Count - over); toDisconnect.AddRange(_lleafs.GetRange(start, _lleafs.Count - start)); } } finally { _lmu.ExitReadLock(); } } return toDisconnect; } finally { _mu.ExitWriteLock(); } } /// /// Removes tracking for a remote server that has shut down. /// Mirrors Go (a *Account) removeRemoteServer(sid string). /// internal void RemoveRemoteServer(string sid) { _mu.EnterWriteLock(); try { if (_strack != null && _strack.TryGetValue(sid, out var prev)) { _strack.Remove(sid); _nrclients -= prev.Conns; _nrleafs -= prev.Leafs; } } finally { _mu.ExitWriteLock(); } } /// /// Returns the number of remote servers that have at least one connection or /// leaf-node for this account. /// Mirrors Go (a *Account) expectedRemoteResponses() int32. /// internal int ExpectedRemoteResponses() { int expected = 0; _mu.EnterReadLock(); try { if (_strack != null) { foreach (var sc in _strack.Values) { if (sc.Conns > 0 || sc.Leafs > 0) expected++; } } } finally { _mu.ExitReadLock(); } return expected; } // ------------------------------------------------------------------------- // Eventing // ------------------------------------------------------------------------- /// /// Clears eventing state including timers, clients, and remote tracking. /// Mirrors Go (a *Account) clearEventing(). /// internal void ClearEventing() { _mu.EnterWriteLock(); try { _nrclients = 0; ClearTimerLocked(ref _etmr); ClearTimerLocked(ref _ctmr); _clients = null; _strack = null; } finally { _mu.ExitWriteLock(); } } // ------------------------------------------------------------------------- // Name accessors // ------------------------------------------------------------------------- /// /// Returns the account name, thread-safely. /// Mirrors Go (a *Account) GetName() string. /// public string GetName() { _mu.EnterReadLock(); try { return Name; } finally { _mu.ExitReadLock(); } } /// /// Returns the if set, otherwise . /// Acquires a read lock. /// Mirrors Go (a *Account) getNameTag() string. /// internal string GetNameTag() { _mu.EnterReadLock(); try { return GetNameTagLocked(); } finally { _mu.ExitReadLock(); } } /// /// Returns the if set, otherwise . /// Lock must be held by the caller. /// Mirrors Go (a *Account) getNameTagLocked() string. /// internal string GetNameTagLocked() => string.IsNullOrEmpty(NameTag) ? Name : NameTag; // ------------------------------------------------------------------------- // Connection counts // ------------------------------------------------------------------------- /// /// Returns the total number of active clients across all servers (local minus /// system accounts plus remote). /// Mirrors Go (a *Account) NumConnections() int. /// public int NumConnections() { _mu.EnterReadLock(); try { return (_clients?.Count ?? 0) - _sysclients + _nrclients; } finally { _mu.ExitReadLock(); } } /// /// Returns the number of client and leaf-node connections that are on /// remote servers. /// Mirrors Go (a *Account) NumRemoteConnections() int. /// public int NumRemoteConnections() { _mu.EnterReadLock(); try { return _nrclients + _nrleafs; } finally { _mu.ExitReadLock(); } } /// /// Returns the number of non-system, non-leaf clients on this server. /// Mirrors Go (a *Account) NumLocalConnections() int. /// public int NumLocalConnections() { _mu.EnterReadLock(); try { return NumLocalConnectionsLocked(); } finally { _mu.ExitReadLock(); } } /// /// Returns local non-system, non-leaf client count. Lock must be held. /// Mirrors Go (a *Account) numLocalConnections() int. /// internal int NumLocalConnectionsLocked() => (_clients?.Count ?? 0) - _sysclients - _nleafs; /// /// Returns local non-system, non-leaf client count. Lock must be held. /// Mirrors Go (a *Account) numLocalConnections() int. /// internal int NumLocalConnectionsInternal() => NumLocalConnectionsLocked(); /// /// Returns all local connections including leaf nodes (minus system clients). /// Mirrors Go (a *Account) numLocalAndLeafConnections() int. /// internal int NumLocalAndLeafConnections() { _mu.EnterReadLock(); try { return (_clients?.Count ?? 0) - _sysclients; } finally { _mu.ExitReadLock(); } } /// /// Returns the local leaf-node count. /// Mirrors Go (a *Account) numLocalLeafNodes() int. /// internal int NumLocalLeafNodes() => _nleafs; // ------------------------------------------------------------------------- // Connection limit checks // ------------------------------------------------------------------------- /// /// Returns true if the total (local + remote) client count has reached or /// exceeded the configured limit. /// Mirrors Go (a *Account) MaxTotalConnectionsReached() bool. /// public bool MaxTotalConnectionsReached() { _mu.EnterReadLock(); try { if (MaxConnections == NoLimit) return false; return (_clients?.Count ?? 0) - _sysclients + _nrclients >= MaxConnections; } finally { _mu.ExitReadLock(); } } /// /// Returns the configured maximum connections limit. /// Mirrors Go (a *Account) MaxActiveConnections() int. /// public int MaxActiveConnections() { _mu.EnterReadLock(); try { return MaxConnections; } finally { _mu.ExitReadLock(); } } // ------------------------------------------------------------------------- // Leaf-node limit checks // ------------------------------------------------------------------------- /// /// Returns true if the total (local + remote) leaf-node count has reached or /// exceeded the configured limit. /// Mirrors Go (a *Account) MaxTotalLeafNodesReached() bool. /// public bool MaxTotalLeafNodesReached() { _mu.EnterReadLock(); try { return MaxTotalLeafNodesReachedLocked(); } finally { _mu.ExitReadLock(); } } /// /// Lock must be held by the caller. /// Mirrors Go (a *Account) maxTotalLeafNodesReached() bool. /// internal bool MaxTotalLeafNodesReachedLocked() { if (MaxLeafNodes == NoLimit) return false; return _nleafs + _nrleafs >= MaxLeafNodes; } /// /// Returns true if total leaf-node count reached the configured maximum. /// Lock must be held by the caller. /// Mirrors Go (a *Account) maxTotalLeafNodesReached() bool. /// internal bool MaxTotalLeafNodesReachedInternal() => MaxTotalLeafNodesReachedLocked(); /// /// Returns the total leaf-node count (local + remote). /// Mirrors Go (a *Account) NumLeafNodes() int. /// public int NumLeafNodes() { _mu.EnterReadLock(); try { return _nleafs + _nrleafs; } finally { _mu.ExitReadLock(); } } /// /// Returns the remote leaf-node count. /// Mirrors Go (a *Account) NumRemoteLeafNodes() int. /// public int NumRemoteLeafNodes() { _mu.EnterReadLock(); try { return _nrleafs; } finally { _mu.ExitReadLock(); } } /// /// Returns the configured maximum leaf-nodes limit. /// Mirrors Go (a *Account) MaxActiveLeafNodes() int. /// public int MaxActiveLeafNodes() { _mu.EnterReadLock(); try { return MaxLeafNodes; } finally { _mu.ExitReadLock(); } } // ------------------------------------------------------------------------- // Subscription counts // ------------------------------------------------------------------------- /// /// Returns the number of route-map entries (subjects sent across routes). /// Mirrors Go (a *Account) RoutedSubs() int. /// public int RoutedSubs() { _mu.EnterReadLock(); try { return _rm?.Count ?? 0; } finally { _mu.ExitReadLock(); } } /// /// Returns the total number of subscriptions in this account's subscription index. /// Mirrors Go (a *Account) TotalSubs() int. /// public int TotalSubs() { _mu.EnterReadLock(); try { if (Sublist == null) return 0; return (int)Sublist.Count(); } finally { _mu.ExitReadLock(); } } /// /// Returns true when there is at least one matching subscription for . /// Mirrors Go (a *Account) SubscriptionInterest(subject string) bool. /// public bool SubscriptionInterest(string subject) => Interest(subject) > 0; /// /// Returns total number of plain and queue subscriptions matching . /// Mirrors Go (a *Account) Interest(subject string) int. /// public int Interest(string subject) { _mu.EnterReadLock(); try { if (Sublist == null) return 0; var (np, nq) = Sublist.NumInterest(subject); return np + nq; } finally { _mu.ExitReadLock(); } } /// /// Increments the leaf-node count for a remote cluster. /// Mirrors Go (a *Account) registerLeafNodeCluster(cluster string). /// internal void RegisterLeafNodeCluster(string cluster) { _mu.EnterWriteLock(); try { _leafClusters ??= new Dictionary(StringComparer.Ordinal); _leafClusters.TryGetValue(cluster, out var current); _leafClusters[cluster] = current + 1; } finally { _mu.ExitWriteLock(); } } /// /// Returns true when this account already tracks one or more leaf nodes from . /// Mirrors Go (a *Account) hasLeafNodeCluster(cluster string) bool. /// internal bool HasLeafNodeCluster(string cluster) { _mu.EnterReadLock(); try { return _leafClusters != null && _leafClusters.TryGetValue(cluster, out var count) && count > 0; } finally { _mu.ExitReadLock(); } } /// /// Returns true when the account is leaf-cluster isolated to . /// Mirrors Go (a *Account) isLeafNodeClusterIsolated(cluster string) bool. /// internal bool IsLeafNodeClusterIsolated(string cluster) { _mu.EnterReadLock(); try { if (string.IsNullOrEmpty(cluster)) return false; if (_leafClusters == null || _leafClusters.Count > 1) return false; return _leafClusters.TryGetValue(cluster, out var count) && count == (ulong)_nleafs; } finally { _mu.ExitReadLock(); } } // ------------------------------------------------------------------------- // Subscription limit error throttle // ------------------------------------------------------------------------- /// /// Returns true when it is appropriate to log a max-subscription-limit error. /// Rate-limited to at most once per . /// Mirrors Go (a *Account) shouldLogMaxSubErr() bool. /// internal bool ShouldLogMaxSubErr() { _mu.EnterReadLock(); long last = Interlocked.Read(ref _lastLimErr); _mu.ExitReadLock(); long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L; // nanoseconds long threshold = (long)AccountEventConstants.DefaultMaxSubLimitReportThreshold.TotalMilliseconds * 1_000_000L; if (now - last < threshold) return false; _mu.EnterWriteLock(); try { Interlocked.Exchange(ref _lastLimErr, now); } finally { _mu.ExitWriteLock(); } return true; } // ------------------------------------------------------------------------- // Expiration // ------------------------------------------------------------------------- /// /// Returns true when the account JWT has expired. /// Mirrors Go (a *Account) IsExpired() bool. /// public bool IsExpired() => Interlocked.CompareExchange(ref _expired, 0, 0) == 1; /// /// Returns true when this account is backed by a JWT claim. /// Lock must be held by the caller. /// Mirrors Go (a *Account) isClaimAccount() bool. /// internal bool IsClaimAccount() => !string.IsNullOrEmpty(ClaimJwt); /// /// Invoked when the expiration timer fires: marks expired and collects clients. /// Mirrors Go (a *Account) expiredTimeout(). /// private void ExpiredTimeout() { Interlocked.Exchange(ref _expired, 1); var clients = GetClients(); foreach (var c in clients) { if (!IsInternalClientKind(c.Kind)) { // TODO: session 12 — call c.AccountAuthExpired() once fully ported. } } } /// /// Starts or resets the JWT expiration timer. /// Mirrors Go (a *Account) setExpirationTimer(d time.Duration). /// internal void SetExpirationTimer(TimeSpan d) { _etmr = new Timer(_ => ExpiredTimeout(), null, d, Timeout.InfiniteTimeSpan); } /// /// Stops the expiration timer. Returns true if it was active. /// Lock must be held by the caller. /// Mirrors Go (a *Account) clearExpirationTimer() bool. /// internal bool ClearExpirationTimer() { if (_etmr == null) return true; _etmr.Dispose(); _etmr = null; return true; } // ------------------------------------------------------------------------- // Subject mappings — public API // ------------------------------------------------------------------------- /// /// Adds a simple 1:1 subject mapping from to /// with weight 100. /// Mirrors Go (a *Account) AddMapping(src, dest string) error. /// public Exception? AddMapping(string src, string dest) => AddWeightedMappings(src, MapDest.New(dest, 100)); /// /// Adds weighted subject mappings for one or more destinations. /// Total weights must not exceed 100 per cluster group. If the total is /// less than 100 and the source was not listed as a destination, the /// remainder is automatically routed back to the source. /// Weights are converted to cumulative form and sorted ascending so that /// random selection can use a single linear scan. /// Mirrors Go (a *Account) AddWeightedMappings(src string, dests ...*MapDest) error. /// public Exception? AddWeightedMappings(string src, params MapDest[] dests) { _mu.EnterWriteLock(); try { if (!SubscriptionIndex.IsValidSubject(src)) return ServerErrors.ErrBadSubject; bool hasWildcard = SubscriptionIndex.SubjectHasWildcard(src); var m = new SubjectMapping { Source = src, HasWildcard = hasWildcard, Destinations = new List(dests.Length + 1), }; var seen = new HashSet(dests.Length); var totals = new Dictionary(); // cluster → cumulative weight foreach (var d in dests) { if (!seen.Add(d.Subject)) return new InvalidOperationException($"duplicate entry for \"{d.Subject}\""); if (d.Weight > 100) return new InvalidOperationException("individual weights need to be <= 100"); totals.TryGetValue(d.Cluster, out byte tw); int next = tw + d.Weight; if (next > 100) return new InvalidOperationException("total weight needs to be <= 100"); totals[d.Cluster] = (byte)next; // Validate the transform is valid. var validateErr = ValidateMapping(src, d.Subject); if (validateErr != null) return validateErr; var (tr, trErr) = SubjectTransform.New(src, d.Subject); if (trErr != null) return trErr; if (string.IsNullOrEmpty(d.Cluster)) { m.Destinations.Add(new Destination { Transform = tr, Weight = d.Weight }); } else { m.ClusterDestinations ??= new Dictionary>(); if (!m.ClusterDestinations.TryGetValue(d.Cluster, out var clusterList)) { clusterList = []; m.ClusterDestinations[d.Cluster] = clusterList; } clusterList.Add(new Destination { Transform = tr, Weight = d.Weight }); } } // Process each destination list: fill remainder and convert to cumulative weights. var destErr = ProcessDestinations(src, hasWildcard, seen, m.Destinations); if (destErr != null) return destErr; if (m.ClusterDestinations != null) { var clusterKeys = new List(m.ClusterDestinations.Keys); foreach (var cluster in clusterKeys) { destErr = ProcessDestinations(src, hasWildcard, seen, m.ClusterDestinations[cluster]); if (destErr != null) return destErr; } } // Replace existing entry for the same source, or append. for (int i = 0; i < _mappings.Count; i++) { if (_mappings[i].Source == src) { _mappings[i] = m; return null; } } _mappings.Add(m); Interlocked.Exchange(ref _hasMapped, _mappings.Count > 0 ? 1 : 0); UpdateLeafNodesEx(src, 1, force: true); return null; } finally { _mu.ExitWriteLock(); } } /// /// Removes a subject mapping entry by source subject. /// Returns true if an entry was removed. /// Mirrors Go (a *Account) RemoveMapping(src string) bool. /// public bool RemoveMapping(string src) { _mu.EnterWriteLock(); try { for (int i = 0; i < _mappings.Count; i++) { if (_mappings[i].Source == src) { // Swap with last element to avoid shifting (order may change). _mappings[i] = _mappings[^1]; _mappings.RemoveAt(_mappings.Count - 1); Interlocked.Exchange(ref _hasMapped, _mappings.Count > 0 ? 1 : 0); UpdateLeafNodesEx(src, -1, force: true); return true; } } return false; } finally { _mu.ExitWriteLock(); } } /// /// Returns true when there is at least one subject mapping entry. /// Mirrors Go (a *Account) hasMappings() bool. /// internal bool HasMappings() => Interlocked.CompareExchange(ref _hasMapped, 0, 0) == 1; /// /// Selects a mapped destination subject using weighted random selection. /// Returns (, false) when no mapping matches. /// Mirrors Go (a *Account) selectMappedSubject(dest string) (string, bool). /// internal (string dest, bool mapped) SelectMappedSubject(string dest) { if (!HasMappings()) return (dest, false); _mu.EnterReadLock(); try { // Tokenise the destination for wildcard subset matching. string[]? tts = null; SubjectMapping? m = null; foreach (var rm in _mappings) { if (!rm.HasWildcard && rm.Source == dest) { m = rm; break; } // Lazy tokenise for subset matching. tts ??= TokenizeSubjectForMapping(dest); if (SubjectTransform.IsSubsetMatch(tts, rm.Source)) { m = rm; break; } } if (m == null) return (dest, false); // Select the destination list (cluster-scoped or global). List dests = m.Destinations; if (m.ClusterDestinations != null && m.ClusterDestinations.Count > 0) { string clusterName = GetCachedClusterName(); if (!string.IsNullOrEmpty(clusterName) && m.ClusterDestinations.TryGetValue(clusterName, out var cdests)) { dests = cdests; } } if (dests.Count == 0) return (dest, false); // Optimise single-entry case where the full weight is 100. Destination? selected = null; if (dests.Count == 1 && dests[0].Weight == 100) { selected = dests[0]; } else { byte w = (byte)(Random.Shared.Next() % 100); foreach (var d in dests) { if (w < d.Weight) { selected = d; break; } } } if (selected == null) return (dest, false); string ndest; if (selected.Transform == null) { ndest = dest; } else if (tts != null) { ndest = selected.Transform.TransformTokenizedSubject(tts); } else { ndest = selected.Transform.TransformSubject(dest); } return (ndest, true); } finally { _mu.ExitReadLock(); } } // ------------------------------------------------------------------------- // Service export configuration // ------------------------------------------------------------------------- /// /// Configures an exported service with singleton response semantics. /// Mirrors Go (a *Account) AddServiceExport(subject string, accounts []*Account) error. /// public Exception? AddServiceExport(string subject, IReadOnlyList? accounts = null) => AddServiceExportWithResponseAndAccountPos(subject, ServiceRespType.Singleton, accounts, 0); /// /// Configures an exported service with singleton response semantics and account-position auth. /// Mirrors Go (a *Account) addServiceExportWithAccountPos(...). /// public Exception? AddServiceExportWithAccountPos(string subject, IReadOnlyList? accounts, uint accountPos) => AddServiceExportWithResponseAndAccountPos(subject, ServiceRespType.Singleton, accounts, accountPos); /// /// Configures an exported service with explicit response type. /// Mirrors Go (a *Account) AddServiceExportWithResponse(...). /// public Exception? AddServiceExportWithResponse(string subject, ServiceRespType respType, IReadOnlyList? accounts = null) => AddServiceExportWithResponseAndAccountPos(subject, respType, accounts, 0); /// /// Configures an exported service with explicit response type and account-position auth. /// Mirrors Go (a *Account) addServiceExportWithResponseAndAccountPos(...). /// public Exception? AddServiceExportWithResponseAndAccountPos(string subject, ServiceRespType respType, IReadOnlyList? accounts, uint accountPos) { if (!SubscriptionIndex.IsValidSubject(subject)) return ServerErrors.ErrBadSubject; _mu.EnterWriteLock(); try { Exports.Services ??= new Dictionary(StringComparer.Ordinal); if (!Exports.Services.TryGetValue(subject, out var serviceExport) || serviceExport == null) serviceExport = new ServiceExportEntry(); if (respType != ServiceRespType.Singleton) serviceExport.ResponseType = respType; if (accounts != null || accountPos > 0) { var authErr = SetExportAuth(serviceExport, subject, accounts, accountPos); if (authErr != null) return authErr; } serviceExport.Account = this; serviceExport.ResponseThreshold = ServerConstants.DefaultServiceExportResponseThreshold; Exports.Services[subject] = serviceExport; return null; } finally { _mu.ExitWriteLock(); } } /// /// Enables latency tracking for with default sampling. /// Mirrors Go (a *Account) TrackServiceExport(service, results string) error. /// public Exception? TrackServiceExport(string service, string results) => TrackServiceExportWithSampling(service, results, ServerConstants.DefaultServiceLatencySampling); /// /// Enables latency tracking for with explicit sampling. /// Mirrors Go (a *Account) TrackServiceExportWithSampling(...). /// public Exception? TrackServiceExportWithSampling(string service, string results, int sampling) { if (sampling != 0 && (sampling < 1 || sampling > 100)) return ServerErrors.ErrBadSampling; if (!SubscriptionIndex.IsValidPublishSubject(results)) return ServerErrors.ErrBadPublishSubject; if (IsExportService(results)) return ServerErrors.ErrBadPublishSubject; _mu.EnterWriteLock(); try { if (Exports.Services == null) return ServerErrors.ErrMissingService; if (!Exports.Services.TryGetValue(service, out var serviceExport)) return ServerErrors.ErrMissingService; serviceExport ??= new ServiceExportEntry(); if (serviceExport.ResponseType != ServiceRespType.Singleton) return ServerErrors.ErrBadServiceType; serviceExport.Latency = new InternalServiceLatency { Sampling = sampling, Subject = results, }; Exports.Services[service] = serviceExport; if (Imports.Services != null) { foreach (var imports in Imports.Services.Values) { foreach (var import in imports) { if (import?.Account?.Name != Name) continue; if (SubjectTransform.IsSubsetMatch(SubjectTransform.TokenizeSubject(import.To), service)) import.Latency = serviceExport.Latency; } } } return null; } finally { _mu.ExitWriteLock(); } } /// /// Disables latency tracking for the exported service. /// Mirrors Go (a *Account) UnTrackServiceExport(service string). /// public void UnTrackServiceExport(string service) { _mu.EnterWriteLock(); try { if (Exports.Services == null || !Exports.Services.TryGetValue(service, out var serviceExport) || serviceExport?.Latency == null) return; serviceExport.Latency = null; if (Imports.Services == null) return; foreach (var imports in Imports.Services.Values) { foreach (var import in imports) { if (import?.Account?.Name != Name) continue; if (SubjectTransform.IsSubsetMatch(SubjectTransform.TokenizeSubject(import.To), service)) { import.Latency = null; import.M1 = null; } } } } finally { _mu.ExitWriteLock(); } } /// /// Publishes a service-latency metric for an import. /// Mirrors Go (a *Account) sendLatencyResult(...). /// internal void SendLatencyResult(ServiceImportEntry si, ServiceLatency sl) { sl.Type = AccountEventConstants.ServiceLatencyType; sl.Id = NextEventId(); sl.Time = DateTime.UtcNow; string? latencySubject; _mu.EnterWriteLock(); try { latencySubject = si.Latency?.Subject; si.RequestingClient = null; } finally { _mu.ExitWriteLock(); } if (string.IsNullOrWhiteSpace(latencySubject) || Server is not NatsServer server) return; var payload = JsonSerializer.SerializeToUtf8Bytes(sl); _ = server.SendInternalAccountMsg(this, latencySubject, payload); } /// /// Publishes a bad-request latency metric (missing or invalid request shape). /// Mirrors Go (a *Account) sendBadRequestTrackingLatency(...). /// internal void SendBadRequestTrackingLatency(ServiceImportEntry si, ClientConnection requestor, Dictionary? header) { var sl = new ServiceLatency { Status = 400, Error = "Bad Request", Requestor = CreateClientInfo(requestor, si.Share), RequestHeader = header, RequestStart = DateTime.UtcNow.Subtract(requestor.GetRttValue()), }; SendLatencyResult(si, sl); } /// /// Publishes timeout latency when requestor interest is lost before response delivery. /// Mirrors Go (a *Account) sendReplyInterestLostTrackLatency(...). /// internal void SendReplyInterestLostTrackLatency(ServiceImportEntry si) { var sl = new ServiceLatency { Status = 408, Error = "Request Timeout", }; ClientConnection? requestor; bool share; long timestamp; _mu.EnterReadLock(); try { requestor = si.RequestingClient; share = si.Share; timestamp = si.Timestamp; sl.RequestHeader = si.TrackingHeader; } finally { _mu.ExitReadLock(); } if (requestor != null) sl.Requestor = CreateClientInfo(requestor, share); var reqRtt = sl.Requestor?.Rtt ?? TimeSpan.Zero; sl.RequestStart = UnixNanoToDateTime(timestamp - TimeSpanToUnixNanos(reqRtt)); SendLatencyResult(si, sl); } /// /// Publishes backend failure latency for response-service imports. /// Mirrors Go (a *Account) sendBackendErrorTrackingLatency(...). /// internal void SendBackendErrorTrackingLatency(ServiceImportEntry si, RsiReason reason) { var sl = new ServiceLatency(); ClientConnection? requestor; bool share; long timestamp; _mu.EnterReadLock(); try { requestor = si.RequestingClient; share = si.Share; timestamp = si.Timestamp; sl.RequestHeader = si.TrackingHeader; } finally { _mu.ExitReadLock(); } if (requestor != null) sl.Requestor = CreateClientInfo(requestor, share); var reqRtt = sl.Requestor?.Rtt ?? TimeSpan.Zero; sl.RequestStart = UnixNanoToDateTime(timestamp - TimeSpanToUnixNanos(reqRtt)); if (reason == RsiReason.NoDelivery) { sl.Status = 503; sl.Error = "Service Unavailable"; } else if (reason == RsiReason.Timeout) { sl.Status = 504; sl.Error = "Service Timeout"; } SendLatencyResult(si, sl); } /// /// Sends request/response latency metrics. Returns true when complete, false when waiting for remote-half merge. /// Mirrors Go (a *Account) sendTrackingLatency(...). /// internal bool SendTrackingLatency(ServiceImportEntry si, ClientConnection? responder) { _mu.EnterReadLock(); var requestor = si.RequestingClient; _mu.ExitReadLock(); if (requestor == null) return true; var nowUnixNanos = UtcNowUnixNanos(); var serviceRtt = UnixNanosToTimeSpan(Math.Max(0, nowUnixNanos - si.Timestamp)); var sl = new ServiceLatency { Status = 200, Requestor = CreateClientInfo(requestor, si.Share), Responder = responder == null ? null : CreateClientInfo(responder, true), RequestHeader = si.TrackingHeader, }; var respRtt = sl.Responder?.Rtt ?? TimeSpan.Zero; var reqRtt = sl.Requestor?.Rtt ?? TimeSpan.Zero; sl.RequestStart = UnixNanoToDateTime(si.Timestamp - TimeSpanToUnixNanos(reqRtt)); sl.ServiceLatencyDuration = serviceRtt > respRtt ? serviceRtt - respRtt : TimeSpan.Zero; sl.TotalLatency = reqRtt + serviceRtt; if (respRtt > TimeSpan.Zero) { sl.SystemLatency = DateTime.UtcNow - UnixNanoToDateTime(nowUnixNanos); if (sl.SystemLatency < TimeSpan.Zero) sl.SystemLatency = TimeSpan.Zero; sl.TotalLatency += sl.SystemLatency; } if (responder != null && responder.Kind != ClientKind.Client) { if (si.M1 != null) { SendLatencyResult(si, sl); return true; } _mu.EnterWriteLock(); try { si.M1 = sl; } finally { _mu.ExitWriteLock(); } return false; } SendLatencyResult(si, sl); return true; } /// /// Returns the lowest response threshold configured across all service exports. /// Mirrors Go (a *Account) lowestServiceExportResponseTime() time.Duration. /// internal TimeSpan LowestServiceExportResponseTime() { var lowest = TimeSpan.FromMinutes(5); _mu.EnterReadLock(); try { if (Exports.Services == null) return lowest; foreach (var export in Exports.Services.Values) { if (export != null && export.ResponseThreshold < lowest) lowest = export.ResponseThreshold; } return lowest; } finally { _mu.ExitReadLock(); } } /// /// Adds a service import with claim authorization context. /// Mirrors Go (a *Account) AddServiceImportWithClaim(...). /// public Exception? AddServiceImportWithClaim(Account destination, string from, string to, object? imClaim) => AddServiceImportWithClaimInternal(destination, from, to, imClaim, false); /// /// Internal service-import add path with optional authorization bypass. /// Mirrors Go (a *Account) addServiceImportWithClaim(..., internal bool). /// internal Exception? AddServiceImportWithClaimInternal(Account destination, string from, string to, object? imClaim, bool internalRequest) { if (destination == null) return ServerErrors.ErrMissingAccount; if (string.IsNullOrEmpty(to)) to = from; if (!SubscriptionIndex.IsValidSubject(from) || !SubscriptionIndex.IsValidSubject(to)) return SubscriptionIndex.ErrInvalidSubject; if (!internalRequest && !destination.CheckServiceExportApproved(this, to, imClaim)) return ServerErrors.ErrServiceImportAuthorization; var cycleErr = ServiceImportFormsCycle(destination, from); if (cycleErr != null) return cycleErr; var (_, addErr) = AddServiceImportInternal(destination, from, to, imClaim); return addErr; } /// /// Checks whether adding a service import forms an account cycle. /// Mirrors Go (a *Account) serviceImportFormsCycle(...). /// internal Exception? ServiceImportFormsCycle(Account destination, string from) { var visited = new HashSet(StringComparer.Ordinal) { Name }; return destination.CheckServiceImportsForCycles(from, visited); } /// /// Recursively checks service-import graph for cycles. /// Mirrors Go (a *Account) checkServiceImportsForCycles(...). /// internal Exception? CheckServiceImportsForCycles(string from, HashSet visited) { if (visited.Count >= AccountConstants.MaxCycleSearchDepth) return ServerErrors.ErrCycleSearchDepth; List? snapshot = null; _mu.EnterReadLock(); try { if (Imports.Services == null || Imports.Services.Count == 0) return null; snapshot = []; foreach (var entries in Imports.Services.Values) snapshot.AddRange(entries); } finally { _mu.ExitReadLock(); } foreach (var import in snapshot) { if (import?.Account == null) continue; if (!SubscriptionIndex.SubjectsCollide(from, import.To)) continue; if (visited.Contains(import.Account.Name)) return ServerErrors.ErrImportFormsCycle; visited.Add(Name); var nextFrom = SubscriptionIndex.SubjectIsSubsetMatch(import.From, from) ? import.From : from; var err = import.Account.CheckServiceImportsForCycles(nextFrom, visited); if (err != null) return err; } return null; } /// /// Checks whether adding a stream import forms an account cycle. /// Mirrors Go (a *Account) streamImportFormsCycle(...). /// internal Exception? StreamImportFormsCycle(Account destination, string to) { var visited = new HashSet(StringComparer.Ordinal) { Name }; return destination.CheckStreamImportsForCycles(to, visited); } /// /// Returns true when any service export subject can match . /// Mirrors Go (a *Account) hasServiceExportMatching(to string) bool. /// internal bool HasServiceExportMatching(string to) { if (Exports.Services == null) return false; foreach (var subject in Exports.Services.Keys) { if (SubscriptionIndex.SubjectIsSubsetMatch(to, subject)) return true; } return false; } /// /// Returns true when any stream export subject can match . /// Mirrors Go (a *Account) hasStreamExportMatching(to string) bool. /// internal bool HasStreamExportMatching(string to) { if (Exports.Streams == null) return false; foreach (var subject in Exports.Streams.Keys) { if (SubscriptionIndex.SubjectIsSubsetMatch(to, subject)) return true; } return false; } /// /// Recursively checks stream-import graph for cycles. /// Mirrors Go (a *Account) checkStreamImportsForCycles(...). /// internal Exception? CheckStreamImportsForCycles(string to, HashSet visited) { if (visited.Count >= AccountConstants.MaxCycleSearchDepth) return ServerErrors.ErrCycleSearchDepth; _mu.EnterReadLock(); var hasMatchingExport = HasStreamExportMatching(to); var streams = Imports.Streams == null ? null : new List(Imports.Streams); _mu.ExitReadLock(); if (!hasMatchingExport || streams == null || streams.Count == 0) return null; foreach (var stream in streams) { if (stream?.Account == null) continue; if (!SubscriptionIndex.SubjectsCollide(to, stream.To)) continue; if (visited.Contains(stream.Account.Name)) return ServerErrors.ErrImportFormsCycle; visited.Add(Name); var nextTo = SubscriptionIndex.SubjectIsSubsetMatch(stream.To, to) ? stream.To : to; var err = stream.Account.CheckStreamImportsForCycles(nextTo, visited); if (err != null) return err; } return null; } /// /// Allows or disallows request metadata sharing for a service import. /// Mirrors Go (a *Account) SetServiceImportSharing(...). /// public Exception? SetServiceImportSharing(Account destination, string to, bool allow) => SetServiceImportSharingInternal(destination, to, true, allow); /// /// Internal service-import sharing setter with optional claim-account check bypass. /// Mirrors Go (a *Account) setServiceImportSharing(...). /// internal Exception? SetServiceImportSharingInternal(Account destination, string to, bool check, bool allow) { _mu.EnterWriteLock(); try { if (check && IsClaimAccount()) return new InvalidOperationException("claim based accounts can not be updated directly"); if (Imports.Services == null) return new InvalidOperationException("service import not found"); foreach (var imports in Imports.Services.Values) { foreach (var import in imports) { if (import?.Account?.Name == destination.Name && import.To == to) { import.Share = allow; return null; } } } return new InvalidOperationException("service import not found"); } finally { _mu.ExitWriteLock(); } } /// /// Adds a service import from this account to . /// Mirrors Go (a *Account) AddServiceImport(destination, from, to string) error. /// public Exception? AddServiceImport(Account destination, string from, string to) => AddServiceImportWithClaim(destination, from, to, null); /// /// Number of pending reverse-response map entries. /// Mirrors Go (a *Account) NumPendingReverseResponses() int. /// public int NumPendingReverseResponses() { _mu.EnterReadLock(); try { return Imports.ReverseResponseMap?.Count ?? 0; } finally { _mu.ExitReadLock(); } } /// /// Total number of pending response imports across all service exports. /// Mirrors Go (a *Account) NumPendingAllResponses() int. /// public int NumPendingAllResponses() => NumPendingResponses(string.Empty); /// /// Number of pending response imports, optionally filtered by exported service subject. /// Mirrors Go (a *Account) NumPendingResponses(filter string) int. /// public int NumPendingResponses(string filter) { _mu.EnterReadLock(); try { if (string.IsNullOrEmpty(filter)) return Exports.Responses?.Count ?? 0; var export = GetServiceExport(filter); if (export == null || Exports.Responses == null) return 0; var count = 0; foreach (var import in Exports.Responses.Values) { if (ReferenceEquals(import.ServiceExport, export)) count++; } return count; } finally { _mu.ExitReadLock(); } } /// /// Number of configured service-import subject keys. /// Mirrors Go (a *Account) NumServiceImports() int. /// public int NumServiceImports() { _mu.EnterReadLock(); try { return Imports.Services?.Count ?? 0; } finally { _mu.ExitReadLock(); } } /// /// Removes a response service import and performs reverse-map cleanup. /// Mirrors Go (a *Account) removeRespServiceImport(...). /// internal void RemoveRespServiceImport(ServiceImportEntry? serviceImport, RsiReason reason) { if (serviceImport == null) return; Account? destination; string from; string to; bool tracking; bool delivered; ClientConnection? requestor; byte[]? sid; _mu.EnterWriteLock(); try { if (Exports.Responses != null) Exports.Responses.Remove(serviceImport.From); destination = serviceImport.Account; from = serviceImport.From; to = serviceImport.To; tracking = serviceImport.Tracking; delivered = serviceImport.DidDeliver; requestor = serviceImport.RequestingClient; sid = serviceImport.SubscriptionId; } finally { _mu.ExitWriteLock(); } if (sid is { Length: > 0 } && InternalClient != null) InternalClient.RemoveSubBySid(sid); if (tracking && requestor != null && !delivered) SendBackendErrorTrackingLatency(serviceImport, reason); destination?.CheckForReverseEntry(to, serviceImport, false); } /// /// Gets a service import for a specific destination account and subject key. /// Lock must be held by caller. /// Mirrors Go (a *Account) getServiceImportForAccountLocked(...). /// internal ServiceImportEntry? GetServiceImportForAccountLocked(string destinationAccountName, string subject) { if (Imports.Services == null || !Imports.Services.TryGetValue(subject, out var serviceImports)) return null; if (serviceImports.Count == 1 && serviceImports[0].Account?.Name == destinationAccountName) return serviceImports[0]; foreach (var serviceImport in serviceImports) { if (serviceImport.Account?.Name == destinationAccountName) return serviceImport; } return null; } /// /// Removes a service import mapping by destination account name and subject key. /// Mirrors Go (a *Account) removeServiceImport(dstAccName, subject string). /// internal void RemoveServiceImport(string destinationAccountName, string subject) { ServiceImportEntry? removed = null; byte[]? sid = null; _mu.EnterWriteLock(); try { if (Imports.Services == null || !Imports.Services.TryGetValue(subject, out var serviceImports)) return; if (serviceImports.Count == 1) { if (serviceImports[0].Account?.Name == destinationAccountName) { removed = serviceImports[0]; Imports.Services.Remove(subject); } } else { for (var i = 0; i < serviceImports.Count; i++) { if (serviceImports[i].Account?.Name == destinationAccountName) { removed = serviceImports[i]; serviceImports.RemoveAt(i); Imports.Services[subject] = serviceImports; break; } } } if (removed?.SubscriptionId is { Length: > 0 }) sid = removed.SubscriptionId; } finally { _mu.ExitWriteLock(); } if (sid != null && InternalClient != null) InternalClient.RemoveSubBySid(sid); } /// /// Adds an entry to the reverse-response map for response cleanup. /// Mirrors Go (a *Account) addReverseRespMapEntry(...). /// internal void AddReverseRespMapEntry(Account account, string reply, string from) { _mu.EnterWriteLock(); try { Imports.ReverseResponseMap ??= new Dictionary>(StringComparer.Ordinal); if (!Imports.ReverseResponseMap.TryGetValue(reply, out var entries)) { entries = []; Imports.ReverseResponseMap[reply] = entries; } entries.Add(new ServiceRespEntry { Account = account, MappedSubject = from, }); } finally { _mu.ExitWriteLock(); } } /// /// Checks reverse-response entries for wildcard replies. /// Mirrors Go (a *Account) checkForReverseEntries(...). /// internal void CheckForReverseEntries(string reply, bool checkInterest, bool recursed) { if (!SubscriptionIndex.SubjectHasWildcard(reply)) { CheckForReverseEntry(reply, null, checkInterest, recursed); return; } List replies; _mu.EnterReadLock(); try { if (Imports.ReverseResponseMap == null || Imports.ReverseResponseMap.Count == 0) return; replies = [.. Imports.ReverseResponseMap.Keys]; } finally { _mu.ExitReadLock(); } var replyTokens = SubjectTransform.TokenizeSubject(reply); foreach (var candidate in replies) { if (SubjectTransform.IsSubsetMatch(SubjectTransform.TokenizeSubject(candidate), reply)) CheckForReverseEntry(candidate, null, checkInterest, recursed); else if (SubjectTransform.IsSubsetMatch(replyTokens, candidate)) CheckForReverseEntry(candidate, null, checkInterest, recursed); } } /// /// Checks and optionally removes reverse-response entries. /// Mirrors Go (a *Account) checkForReverseEntry(...). /// internal void CheckForReverseEntry(string reply, ServiceImportEntry? serviceImport, bool checkInterest) => CheckForReverseEntry(reply, serviceImport, checkInterest, false); /// /// Internal reverse-entry checker with recursion protection. /// Mirrors Go (a *Account) _checkForReverseEntry(...). /// internal void CheckForReverseEntry(string reply, ServiceImportEntry? serviceImport, bool checkInterest, bool recursed) { List? responseEntries; _mu.EnterReadLock(); try { if (Imports.ReverseResponseMap == null || Imports.ReverseResponseMap.Count == 0) return; if (SubscriptionIndex.SubjectHasWildcard(reply)) { if (recursed) return; } else if (!Imports.ReverseResponseMap.TryGetValue(reply, out responseEntries) || responseEntries == null) { return; } else if (checkInterest && Sublist != null && Sublist.HasInterest(reply)) { return; } } finally { _mu.ExitReadLock(); } if (SubscriptionIndex.SubjectHasWildcard(reply)) { CheckForReverseEntries(reply, checkInterest, true); return; } _mu.EnterWriteLock(); try { if (Imports.ReverseResponseMap == null || !Imports.ReverseResponseMap.TryGetValue(reply, out responseEntries) || responseEntries == null) return; if (serviceImport == null) { Imports.ReverseResponseMap.Remove(reply); } else { responseEntries.RemoveAll(entry => entry.MappedSubject == serviceImport.From); if (responseEntries.Count == 0) Imports.ReverseResponseMap.Remove(reply); else Imports.ReverseResponseMap[reply] = responseEntries; } } finally { _mu.ExitWriteLock(); } } /// /// Returns true when a service import is overshadowed by an existing subject key. /// Mirrors Go (a *Account) serviceImportShadowed(from string) bool. /// internal bool ServiceImportShadowed(string from) { _mu.EnterReadLock(); try { if (Imports.Services == null) return false; if (Imports.Services.ContainsKey(from)) return true; foreach (var subject in Imports.Services.Keys) { if (SubscriptionIndex.SubjectIsSubsetMatch(from, subject)) return true; } return false; } finally { _mu.ExitReadLock(); } } /// /// Returns true when a service import already exists for destination account + source subject. /// Mirrors Go (a *Account) serviceImportExists(dstAccName, from string) bool. /// internal bool ServiceImportExists(string destinationAccountName, string from) { _mu.EnterReadLock(); try { return GetServiceImportForAccountLocked(destinationAccountName, from) != null; } finally { _mu.ExitReadLock(); } } /// /// Creates (or returns existing) internal account client. /// Lock must be held. /// Mirrors Go (a *Account) internalClient() *client. /// internal ClientConnection? InternalAccountClient() { if (InternalClient == null && Server is NatsServer server) { InternalClient = server.CreateInternalAccountClient(); InternalClient.SetAccount(this); } return InternalClient; } /// /// Creates internal account-scoped subscription. /// Mirrors Go (a *Account) subscribeInternal(...). /// internal (Subscription? Sub, Exception? Error) SubscribeInternal(string subject) => SubscribeInternalEx(subject, false); /// /// Unsubscribes from an internal account subscription. /// Mirrors Go (a *Account) unsubscribeInternal(sub *subscription). /// internal void UnsubscribeInternal(Subscription? sub) { if (sub?.Sid == null) return; _mu.EnterReadLock(); var internalClient = InternalClient; _mu.ExitReadLock(); internalClient?.RemoveSubBySid(sub.Sid); } /// /// Creates internal subscription for service-import responses. /// Mirrors Go (a *Account) subscribeServiceImportResponse(subject string). /// internal (Subscription? Sub, Exception? Error) SubscribeServiceImportResponse(string subject) => SubscribeInternalEx(subject, true); /// /// Extended internal subscription helper. /// Mirrors Go (a *Account) subscribeInternalEx(...). /// internal (Subscription? Sub, Exception? Error) SubscribeInternalEx(string subject, bool responseImport) { ClientConnection? client; string sidText; _mu.EnterWriteLock(); try { _isid++; client = InternalAccountClient(); sidText = _isid.ToString(); } finally { _mu.ExitWriteLock(); } if (client == null) return (null, new InvalidOperationException("no internal account client")); return client.ProcessSubEx(Encoding.ASCII.GetBytes(subject), null, Encoding.ASCII.GetBytes(sidText), false, false, responseImport); } /// /// Adds an internal subscription that matches a service import's from subject. /// Mirrors Go (a *Account) addServiceImportSub(si *serviceImport) error. /// internal Exception? AddServiceImportSub(ServiceImportEntry serviceImport) { if (serviceImport == null) return ServerErrors.ErrMissingService; ClientConnection? client; string sidText; string subject; _mu.EnterWriteLock(); try { client = InternalAccountClient(); if (client == null) return null; if (serviceImport.SubscriptionId is { Length: > 0 }) return new InvalidOperationException("duplicate call to create subscription for service import"); _isid++; sidText = _isid.ToString(); serviceImport.SubscriptionId = Encoding.ASCII.GetBytes(sidText); subject = serviceImport.From; } finally { _mu.ExitWriteLock(); } var (_, err) = client.ProcessSubEx(Encoding.ASCII.GetBytes(subject), null, Encoding.ASCII.GetBytes(sidText), true, true, false); return err; } /// /// Removes all subscriptions associated with service imports. /// Mirrors Go (a *Account) removeAllServiceImportSubs(). /// internal void RemoveAllServiceImportSubs() { List subscriptionIds = []; ClientConnection? internalClient; _mu.EnterWriteLock(); try { if (Imports.Services != null) { foreach (var imports in Imports.Services.Values) { foreach (var serviceImport in imports) { if (serviceImport.SubscriptionId is { Length: > 0 }) { subscriptionIds.Add(serviceImport.SubscriptionId); serviceImport.SubscriptionId = null; } } } } internalClient = InternalClient; InternalClient = null; } finally { _mu.ExitWriteLock(); } if (internalClient == null) return; foreach (var sid in subscriptionIds) internalClient.RemoveSubBySid(sid); internalClient.CloseConnection(ClosedState.InternalClient); } /// /// Adds subscriptions for all registered service imports. /// Mirrors Go (a *Account) addAllServiceImportSubs(). /// internal void AddAllServiceImportSubs() { List imports = []; _mu.EnterReadLock(); try { if (Imports.Services != null) { foreach (var entries in Imports.Services.Values) imports.AddRange(entries); } } finally { _mu.ExitReadLock(); } foreach (var serviceImport in imports) _ = AddServiceImportSub(serviceImport); } /// /// Processes a service-import response routed to this account. /// Mirrors Go (a *Account) processServiceImportResponse(...). /// internal void ProcessServiceImportResponse(string subject, byte[] msg) { ServiceImportEntry? serviceImport; _mu.EnterReadLock(); try { if (IsExpired() || Exports.Responses == null || Exports.Responses.Count == 0) return; if (!Exports.Responses.TryGetValue(subject, out serviceImport)) return; if (serviceImport == null || serviceImport.Invalid) return; } finally { _mu.ExitReadLock(); } // The client-side response processing pipeline is still under active porting. serviceImport.DidDeliver = msg.Length >= 0; } /// /// Creates response wildcard prefix for service replies. /// Lock must be held by caller. /// Mirrors Go (a *Account) createRespWildcard(). /// internal void CreateRespWildcard() { const string alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; Span prefix = stackalloc byte[14]; prefix[0] = (byte)'_'; prefix[1] = (byte)'R'; prefix[2] = (byte)'_'; prefix[3] = (byte)'.'; ulong random = (ulong)Random.Shared.NextInt64(); for (var i = 4; i < prefix.Length; i++) { prefix[i] = (byte)alphabet[(int)(random % (ulong)alphabet.Length)]; random /= (ulong)alphabet.Length; } ServiceImportReply = [.. prefix, (byte)'.']; } /// /// Generates a new service reply subject. /// Mirrors Go (a *Account) newServiceReply(tracking bool) []byte. /// internal byte[] NewServiceReply(bool tracking) { bool createdPrefix = false; byte[] replyPrefix; _mu.EnterWriteLock(); try { if (ServiceImportReply == null) { CreateRespWildcard(); createdPrefix = true; } replyPrefix = ServiceImportReply ?? Encoding.ASCII.GetBytes("_R_."); } finally { _mu.ExitWriteLock(); } if (createdPrefix) _ = SubscribeServiceImportResponse(Encoding.ASCII.GetString([.. replyPrefix, (byte)'>'])); const string alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; Span randomPart = stackalloc byte[20]; ulong random = (ulong)Random.Shared.NextInt64(); for (var i = 0; i < randomPart.Length; i++) { randomPart[i] = (byte)alphabet[(int)(random % (ulong)alphabet.Length)]; random /= (ulong)alphabet.Length; } var reply = new List(replyPrefix.Length + randomPart.Length + 2); reply.AddRange(replyPrefix); reply.AddRange(randomPart.ToArray()); if (tracking) { reply.Add((byte)'.'); reply.Add((byte)'T'); } return [.. reply]; } /// /// Returns the response threshold for an exported service. /// Mirrors Go (a *Account) ServiceExportResponseThreshold(...). /// public (TimeSpan Threshold, Exception? Error) ServiceExportResponseThreshold(string export) { _mu.EnterReadLock(); try { var serviceExport = GetServiceExport(export); if (serviceExport == null) return (TimeSpan.Zero, new InvalidOperationException($"no export defined for \"{export}\"")); return (serviceExport.ResponseThreshold, null); } finally { _mu.ExitReadLock(); } } /// /// Sets max response delivery time for an exported service. /// Mirrors Go (a *Account) SetServiceExportResponseThreshold(...). /// public Exception? SetServiceExportResponseThreshold(string export, TimeSpan maxTime) { _mu.EnterWriteLock(); try { if (IsClaimAccount()) return new InvalidOperationException("claim based accounts can not be updated directly"); var serviceExport = GetServiceExport(export); if (serviceExport == null) return new InvalidOperationException($"no export defined for \"{export}\""); serviceExport.ResponseThreshold = maxTime; return null; } finally { _mu.ExitWriteLock(); } } /// /// Enables/disables cross-account trace propagation on a service export. /// Mirrors Go (a *Account) SetServiceExportAllowTrace(...). /// public Exception? SetServiceExportAllowTrace(string export, bool allowTrace) { _mu.EnterWriteLock(); try { var serviceExport = GetServiceExport(export); if (serviceExport == null) return new InvalidOperationException($"no export defined for \"{export}\""); serviceExport.AllowTrace = allowTrace; return null; } finally { _mu.ExitWriteLock(); } } /// /// Creates internal response service import entry. /// Mirrors Go (a *Account) addRespServiceImport(...). /// internal ServiceImportEntry AddRespServiceImport(Account destination, string to, ServiceImportEntry originalServiceImport, bool tracking, Dictionary? header) { var newReply = Encoding.ASCII.GetString(originalServiceImport.Account?.NewServiceReply(tracking) ?? NewServiceReply(tracking)); ServiceImportEntry responseImport; _mu.EnterWriteLock(); try { responseImport = new ServiceImportEntry { Account = destination, ServiceExport = originalServiceImport.ServiceExport, From = newReply, To = to, ResponseType = originalServiceImport.ResponseType, IsResponse = true, Share = originalServiceImport.Share, Timestamp = UtcNowUnixNanos(), Tracking = tracking && originalServiceImport.ResponseType == ServiceRespType.Singleton, TrackingHeader = header, Latency = tracking && originalServiceImport.ResponseType == ServiceRespType.Singleton ? originalServiceImport.Latency : null, }; Exports.Responses ??= new Dictionary(StringComparer.Ordinal); Exports.Responses[newReply] = responseImport; } finally { _mu.ExitWriteLock(); } destination.AddReverseRespMapEntry(this, to, newReply); return responseImport; } /// /// Adds stream import with optional claim context. /// Mirrors Go (a *Account) AddStreamImportWithClaim(...). /// public Exception? AddStreamImportWithClaim(Account account, string from, string prefix, object? importClaim) => AddStreamImportWithClaimInternal(account, from, prefix, false, importClaim); /// /// Internal stream import add helper. /// Mirrors Go (a *Account) addStreamImportWithClaim(...). /// internal Exception? AddStreamImportWithClaimInternal(Account account, string from, string prefix, bool allowTrace, object? importClaim) { if (account == null) return ServerErrors.ErrMissingAccount; if (!account.CheckStreamImportAuthorized(this, from, importClaim)) return ServerErrors.ErrStreamImportAuthorization; if (!string.IsNullOrEmpty(prefix)) { if (SubscriptionIndex.SubjectHasWildcard(prefix)) return ServerErrors.ErrStreamImportBadPrefix; if (!prefix.EndsWith(".", StringComparison.Ordinal)) prefix += '.'; } return AddMappedStreamImportWithClaimInternal(account, from, prefix + from, allowTrace, importClaim); } /// /// Convenience helper for mapped stream imports without claim. /// Mirrors Go (a *Account) AddMappedStreamImport(...). /// public Exception? AddMappedStreamImport(Account account, string from, string to) => AddMappedStreamImportWithClaim(account, from, to, null); /// /// Adds mapped stream import with optional claim. /// Mirrors Go (a *Account) AddMappedStreamImportWithClaim(...). /// public Exception? AddMappedStreamImportWithClaim(Account account, string from, string to, object? importClaim) => AddMappedStreamImportWithClaimInternal(account, from, to, false, importClaim); /// /// Internal mapped stream import add helper. /// Mirrors Go (a *Account) addMappedStreamImportWithClaim(...). /// internal Exception? AddMappedStreamImportWithClaimInternal(Account account, string from, string to, bool allowTrace, object? importClaim) { if (account == null) return ServerErrors.ErrMissingAccount; if (!account.CheckStreamImportAuthorized(this, from, importClaim)) return ServerErrors.ErrStreamImportAuthorization; if (string.IsNullOrEmpty(to)) to = from; var cycleErr = StreamImportFormsCycle(account, to) ?? StreamImportFormsCycle(account, from); if (cycleErr != null) return cycleErr; ISubjectTransformer? transform = null; var usePublishedSubject = false; if (SubscriptionIndex.SubjectHasWildcard(from)) { if (to == from) { usePublishedSubject = true; } else { var (created, err) = SubjectTransform.New(from, to); if (err != null) return new InvalidOperationException($"failed to create mapping transform for stream import subject from \"{from}\" to \"{to}\": {err.Message}"); transform = created; } } _mu.EnterWriteLock(); try { if (IsStreamImportDuplicate(account, from)) return ServerErrors.ErrStreamImportDuplicate; Imports.Streams ??= []; Imports.Streams.Add(new StreamImportEntry { Account = account, From = from, To = to, Transform = transform, Claim = importClaim, UsePublishedSubject = usePublishedSubject, AllowTrace = allowTrace, }); return null; } finally { _mu.ExitWriteLock(); } } /// /// Checks if stream import duplicate exists. Lock should be held. /// Mirrors Go (a *Account) isStreamImportDuplicate(...). /// internal bool IsStreamImportDuplicate(Account account, string from) { if (Imports.Streams == null) return false; foreach (var streamImport in Imports.Streams) { if (ReferenceEquals(streamImport.Account, account) && streamImport.From == from) return true; } return false; } /// /// Adds stream import from a specific account. /// Mirrors Go (a *Account) AddStreamImport(...). /// public Exception? AddStreamImport(Account account, string from, string prefix) => AddStreamImportWithClaimInternal(account, from, prefix, false, null); /// /// Adds stream export, optionally restricted to explicit accounts. /// Mirrors Go (a *Account) AddStreamExport(...). /// public Exception? AddStreamExport(string subject, IReadOnlyList? accounts = null) => AddStreamExportWithAccountPos(subject, accounts, 0); /// /// Adds stream export with account-position matching. /// Mirrors Go (a *Account) addStreamExportWithAccountPos(...). /// public Exception? AddStreamExportWithAccountPos(string subject, IReadOnlyList? accounts, uint accountPos) { if (!SubscriptionIndex.IsValidSubject(subject)) return ServerErrors.ErrBadSubject; _mu.EnterWriteLock(); try { Exports.Streams ??= new Dictionary(StringComparer.Ordinal); Exports.Streams.TryGetValue(subject, out var export); export ??= new StreamExport(); if (accounts != null || accountPos > 0) { var authErr = SetExportAuth(export, subject, accounts, accountPos); if (authErr != null) return authErr; } Exports.Streams[subject] = export; return null; } finally { _mu.ExitWriteLock(); } } /// /// Checks stream import authorization with account lock. /// Mirrors Go (a *Account) checkStreamImportAuthorized(...). /// internal bool CheckStreamImportAuthorized(Account account, string subject, object? importClaim) { _mu.EnterReadLock(); try { return CheckStreamImportAuthorizedNoLock(account, subject, importClaim); } finally { _mu.ExitReadLock(); } } /// /// Checks stream import authorization assuming lock is already held. /// Mirrors Go (a *Account) checkStreamImportAuthorizedNoLock(...). /// internal bool CheckStreamImportAuthorizedNoLock(Account account, string subject, object? importClaim) { if (Exports.Streams == null || !SubscriptionIndex.IsValidSubject(subject)) return false; return CheckStreamExportApproved(account, subject, importClaim); } /// /// Gets wildcard-matching service export for subject. /// Lock should be held. /// Mirrors Go (a *Account) getWildcardServiceExport(from string). /// internal ServiceExportEntry? GetWildcardServiceExport(string from) { if (Exports.Services == null) return null; var tokens = SubjectTransform.TokenizeSubject(from); foreach (var (subject, serviceExport) in Exports.Services) { if (SubjectTransform.IsSubsetMatch(tokens, subject)) return serviceExport; } return null; } /// /// Handles stream import activation expiration. /// Mirrors Go (a *Account) streamActivationExpired(...). /// internal void StreamActivationExpired(Account exportAccount, string subject) { _mu.EnterWriteLock(); try { if (IsExpired() || Imports.Streams == null) return; foreach (var streamImport in Imports.Streams) { if (ReferenceEquals(streamImport.Account, exportAccount) && streamImport.From == subject) { streamImport.Invalid = true; return; } } } finally { _mu.ExitWriteLock(); } } /// /// Handles service import activation expiration. /// Mirrors Go (a *Account) serviceActivationExpired(...). /// internal void ServiceActivationExpired(Account destinationAccount, string subject) { _mu.EnterWriteLock(); try { if (IsExpired() || Imports.Services == null) return; var serviceImport = GetServiceImportForAccountLocked(destinationAccount.Name, subject); if (serviceImport != null) serviceImport.Invalid = true; } finally { _mu.ExitWriteLock(); } } /// /// Re-evaluates import validity when an activation token expiration timer fires. /// Mirrors Go (a *Account) activationExpired(...). /// internal void ActivationExpired(Account exportAccount, string subject, object? kind) { var normalizedKind = NormalizeExportKind(kind); if (string.Equals(normalizedKind, "stream", StringComparison.Ordinal)) { StreamActivationExpired(exportAccount, subject); } else if (string.Equals(normalizedKind, "service", StringComparison.Ordinal)) { ServiceActivationExpired(exportAccount, subject); } } /// /// Validates an import activation claim/token. /// Mirrors Go (a *Account) checkActivation(...). /// internal bool CheckActivation(Account importAccount, object? claim, ExportAuth? exportAuth, bool expirationTimer) { if (claim == null) return false; if (!TryReadStringMember(claim, "Token", out var token) || string.IsNullOrWhiteSpace(token)) return false; if (!TryDecodeJwtPayload(token, out var activationPayload)) return false; if (!IsIssuerClaimTrusted(activationPayload)) return false; if (TryReadLongMember(activationPayload, "exp", out var expires) && expires > 0) { var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (expires <= now) return false; if (expirationTimer) { var delay = TimeSpan.FromSeconds(expires - now); string importSubject = ReadActivationImportSubject(activationPayload); object? claimType = TryReadMember(claim, "Type", out var typeValue) ? typeValue : null; _ = new Timer( _ => importAccount.ActivationExpired(this, importSubject, claimType), null, delay, Timeout.InfiniteTimeSpan); } } if (exportAuth == null) return true; string subject = TryReadStringMember(activationPayload, "sub", out var sub) ? sub : string.Empty; long issuedAt = TryReadLongMember(activationPayload, "iat", out var iat) ? iat : 0; return !IsRevoked(exportAuth.ActivationsRevoked, subject, issuedAt); } /// /// Returns true when activation issuer details are trusted for this account. /// Mirrors Go (a *Account) isIssuerClaimTrusted(...). /// internal bool IsIssuerClaimTrusted(object? claims) { if (claims == null) return false; string issuerAccount = TryReadStringMember(claims, "IssuerAccount", out var ia) ? ia : TryReadStringMember(claims, "issuer_account", out var iaAlt) ? iaAlt : string.Empty; // If issuer-account is omitted, issuer defaults to the account itself. if (string.IsNullOrEmpty(issuerAccount)) return true; if (!string.Equals(Name, issuerAccount, StringComparison.Ordinal)) { if (Server is NatsServer server) { string importSubject = ReadActivationImportSubject(claims); string importType = TryReadStringMember(claims, "import_type", out var it) ? it : string.Empty; server.Errorf( "Invalid issuer account {0} in activation claim (subject: {1} - type: {2}) for account {3}", issuerAccount, importSubject, importType, Name); } return false; } string issuer = TryReadStringMember(claims, "Issuer", out var issuerValue) ? issuerValue : TryReadStringMember(claims, "iss", out var issValue) ? issValue : string.Empty; _mu.EnterReadLock(); try { (_, var ok) = HasIssuerNoLock(issuer); return ok; } finally { _mu.ExitReadLock(); } } /// /// Checks whether another account is approved to import this service export. /// Mirrors Go (a *Account) checkServiceImportAuthorized(...). /// internal bool CheckServiceImportAuthorized(Account account, string subject, object? importClaim) { _mu.EnterReadLock(); try { return CheckServiceImportAuthorizedNoLock(account, subject, importClaim); } finally { _mu.ExitReadLock(); } } /// /// Lock-free helper for service import authorization checks. /// Mirrors Go (a *Account) checkServiceImportAuthorizedNoLock(...). /// internal bool CheckServiceImportAuthorizedNoLock(Account account, string subject, object? importClaim) { if (Exports.Services == null) return false; return CheckServiceExportApproved(account, subject, importClaim); } /// /// Returns whether bearer tokens should be rejected for this account. /// Mirrors Go (a *Account) failBearer() bool. /// internal bool FailBearer() { _mu.EnterReadLock(); try { return DisallowBearer; } finally { _mu.ExitReadLock(); } } /// /// Updates expiration state/timer from claim data. /// Mirrors Go (a *Account) checkExpiration(...). /// internal void CheckExpiration(object? claimsData) { long expires = claimsData != null && TryReadLongMember(claimsData, "Expires", out var exp) ? exp : claimsData != null && TryReadLongMember(claimsData, "exp", out var expUnix) ? expUnix : 0; _mu.EnterWriteLock(); try { ClearExpirationTimer(); if (expires == 0) { Interlocked.Exchange(ref _expired, 0); return; } long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (expires <= now) { Interlocked.Exchange(ref _expired, 1); return; } SetExpirationTimer(TimeSpan.FromSeconds(expires - now)); Interlocked.Exchange(ref _expired, 0); } finally { _mu.ExitWriteLock(); } } /// /// Returns signer scope for issuer, if present. /// Mirrors Go (a *Account) hasIssuer(...). /// internal (object? Scope, bool Ok) HasIssuer(string issuer) { _mu.EnterReadLock(); try { return HasIssuerNoLock(issuer); } finally { _mu.ExitReadLock(); } } /// /// Lock-free signer lookup. /// Mirrors Go (a *Account) hasIssuerNoLock(...). /// internal (object? Scope, bool Ok) HasIssuerNoLock(string issuer) { if (SigningKeys == null || string.IsNullOrEmpty(issuer)) return (null, false); return SigningKeys.TryGetValue(issuer, out var scope) ? (scope, true) : (null, false); } /// /// Returns the leaf-node loop-detection subject. /// Mirrors Go (a *Account) getLDSubject() string. /// internal string GetLDSubject() { _mu.EnterReadLock(); try { return LoopDetectionSubject; } finally { _mu.ExitReadLock(); } } /// /// Returns account label used in trace output. /// Mirrors Go (a *Account) traceLabel() string. /// internal string TraceLabel() { if (string.IsNullOrEmpty(NameTag)) return Name; return $"{Name}/{NameTag}"; } /// /// Returns true when external auth is configured. /// Mirrors Go (a *Account) hasExternalAuth() bool. /// internal bool HasExternalAuth() { _mu.EnterReadLock(); try { return ExternalAuth != null; } finally { _mu.ExitReadLock(); } } /// /// Returns true when is configured as an external-auth user. /// Mirrors Go (a *Account) isExternalAuthUser(userID string) bool. /// internal bool IsExternalAuthUser(string userId) { _mu.EnterReadLock(); try { foreach (var authUser in ReadStringListMember(ExternalAuth, "AuthUsers", "auth_users")) { if (string.Equals(userId, authUser, StringComparison.Ordinal)) return true; } return false; } finally { _mu.ExitReadLock(); } } /// /// Returns configured external-auth xkey, or empty when unset. /// Mirrors Go (a *Account) externalAuthXKey() string. /// internal string ExternalAuthXKey() { _mu.EnterReadLock(); try { if (TryReadStringMember(ExternalAuth, "XKey", out var xkey) && !string.IsNullOrEmpty(xkey)) return xkey; if (TryReadStringMember(ExternalAuth, "xkey", out var xkeyAlt) && !string.IsNullOrEmpty(xkeyAlt)) return xkeyAlt; return string.Empty; } finally { _mu.ExitReadLock(); } } /// /// Returns whether external auth allows account switching to . /// Mirrors Go (a *Account) isAllowedAcount(acc string) bool. /// internal bool IsAllowedAcount(string account) { _mu.EnterReadLock(); try { var allowed = ReadStringListMember(ExternalAuth, "AllowedAccounts", "allowed_accounts"); if (allowed.Count == 1 && string.Equals(allowed[0], "*", StringComparison.Ordinal)) return true; foreach (var candidate in allowed) { if (string.Equals(candidate, account, StringComparison.Ordinal)) return true; } return false; } finally { _mu.ExitReadLock(); } } // ------------------------------------------------------------------------- // Export checks // ------------------------------------------------------------------------- /// /// Returns true if the given service subject is exported (exact or wildcard match). /// Mirrors Go (a *Account) IsExportService(service string) bool. /// public bool IsExportService(string service) { _mu.EnterReadLock(); try { if (Exports.Services == null) return false; if (Exports.Services.ContainsKey(service)) return true; var tokens = SubjectTransform.TokenizeSubject(service); foreach (var subj in Exports.Services.Keys) { if (SubjectTransform.IsSubsetMatch(tokens, subj)) return true; } return false; } finally { _mu.ExitReadLock(); } } /// /// Returns true if the given service export has latency tracking enabled. /// Mirrors Go (a *Account) IsExportServiceTracking(service string) bool. /// public bool IsExportServiceTracking(string service) { _mu.EnterReadLock(); try { if (Exports.Services == null) return false; if (Exports.Services.TryGetValue(service, out var ea)) { if (ea == null) return false; if (ea.Latency != null) return true; } var tokens = SubjectTransform.TokenizeSubject(service); foreach (var (subj, se) in Exports.Services) { if (SubjectTransform.IsSubsetMatch(tokens, subj) && se?.Latency != null) return true; } return false; } finally { _mu.ExitReadLock(); } } /// /// Checks whether another account is approved to import this stream export. /// Lock must be held on entry (read is sufficient). /// Mirrors Go (a *Account) checkStreamExportApproved(...) bool. /// internal bool CheckStreamExportApproved(Account account, string subject, object? imClaim) { if (Exports.Streams == null) return false; if (Exports.Streams.TryGetValue(subject, out var ea)) { if (ea == null) return true; return CheckAuth(ea, account, imClaim, null); } var tokens = SubjectTransform.TokenizeSubject(subject); foreach (var (subj, se) in Exports.Streams) { if (SubjectTransform.IsSubsetMatch(tokens, subj)) { if (se == null) return true; return CheckAuth(se, account, imClaim, tokens); } } return false; } /// /// Checks whether another account is approved to import this service export. /// Lock must be held on entry (read is sufficient). /// Mirrors Go (a *Account) checkServiceExportApproved(...) bool. /// internal bool CheckServiceExportApproved(Account account, string subject, object? imClaim) { if (Exports.Services == null) return false; if (Exports.Services.TryGetValue(subject, out var se)) { if (se == null) return true; return CheckAuth(se, account, imClaim, null); } var tokens = SubjectTransform.TokenizeSubject(subject); foreach (var (subj, entry) in Exports.Services) { if (SubjectTransform.IsSubsetMatch(tokens, subj)) { if (entry == null) return true; return CheckAuth(entry, account, imClaim, tokens); } } return false; } // ------------------------------------------------------------------------- // User revocation check // ------------------------------------------------------------------------- /// /// Returns true if the user identified by with the /// given timestamp has been revoked. /// Mirrors Go (a *Account) checkUserRevoked(nkey string, issuedAt int64) bool. /// public bool CheckUserRevoked(string nkey, long issuedAt) { _mu.EnterReadLock(); try { return IsRevoked(UsersRevoked, nkey, issuedAt); } finally { _mu.ExitReadLock(); } } // ------------------------------------------------------------------------- // Config-reload comparison helpers // ------------------------------------------------------------------------- /// /// Returns true if this account's stream imports equal 's. /// Acquires this account's read lock; must not be /// concurrently accessed. /// Mirrors Go (a *Account) checkStreamImportsEqual(b *Account) bool. /// internal bool CheckStreamImportsEqual(Account b) { _mu.EnterReadLock(); try { var aStreams = Imports.Streams; var bStreams = b.Imports.Streams; int aLen = aStreams?.Count ?? 0; int bLen = bStreams?.Count ?? 0; if (aLen != bLen) return false; if (aLen == 0) return true; // Build an index from (accName+from+to) → entry for b. var bIndex = new Dictionary(bLen); foreach (var bim in bStreams!) { string key = (bim.Account?.Name ?? string.Empty) + bim.From + bim.To; bIndex[key] = bim; } foreach (var aim in aStreams!) { string key = (aim.Account?.Name ?? string.Empty) + aim.From + aim.To; if (!bIndex.TryGetValue(key, out var bim)) return false; if (aim.AllowTrace != bim.AllowTrace) return false; } return true; } finally { _mu.ExitReadLock(); } } /// /// Returns true if this account's stream exports equal 's. /// Acquires this account's read lock; must not be /// concurrently accessed. /// Mirrors Go (a *Account) checkStreamExportsEqual(b *Account) bool. /// internal bool CheckStreamExportsEqual(Account b) { _mu.EnterReadLock(); try { var aStreams = Exports.Streams; var bStreams = b.Exports.Streams; int aLen = aStreams?.Count ?? 0; int bLen = bStreams?.Count ?? 0; if (aLen != bLen) return false; if (aLen == 0) return true; foreach (var (subj, aea) in aStreams!) { if (!bStreams!.TryGetValue(subj, out var bea)) return false; if (!IsStreamExportEqual(aea, bea)) return false; } return true; } finally { _mu.ExitReadLock(); } } /// /// Returns true if this account's service exports equal 's. /// Acquires this account's read lock; must not be /// concurrently accessed. /// Mirrors Go (a *Account) checkServiceExportsEqual(b *Account) bool. /// internal bool CheckServiceExportsEqual(Account b) { _mu.EnterReadLock(); try { var aServices = Exports.Services; var bServices = b.Exports.Services; int aLen = aServices?.Count ?? 0; int bLen = bServices?.Count ?? 0; if (aLen != bLen) return false; if (aLen == 0) return true; foreach (var (subj, aea) in aServices!) { if (!bServices!.TryGetValue(subj, out var bea)) return false; if (!IsServiceExportEqual(aea, bea)) return false; } return true; } finally { _mu.ExitReadLock(); } } // ------------------------------------------------------------------------- // Leaf-node helpers // ------------------------------------------------------------------------- /// /// Notifies leaf nodes of a subscription change. /// Stub — full implementation in session 15. /// Mirrors Go (a *Account) updateLeafNodes(sub, delta). /// internal void UpdateLeafNodes(object sub, int delta) { if (delta == 0 || sub is not Subscription s || s.Subject.Length == 0) return; var subject = Encoding.UTF8.GetString(s.Subject); var queue = s.Queue is { Length: > 0 } ? Encoding.UTF8.GetString(s.Queue) : string.Empty; _mu.EnterWriteLock(); try { _rm ??= new Dictionary(StringComparer.Ordinal); if (!_rm.TryGetValue(subject, out var rc)) rc = 0; rc += delta; if (rc <= 0) _rm.Remove(subject); else _rm[subject] = rc; if (!string.IsNullOrEmpty(queue)) { _lqws ??= new Dictionary(StringComparer.Ordinal); var key = $"{subject} {queue}"; var qw = s.Qw != 0 ? s.Qw : 1; if (!_lqws.TryGetValue(key, out var qv)) qv = 0; qv += delta * qw; if (qv <= 0) _lqws.Remove(key); else _lqws[key] = qv; } } finally { _mu.ExitWriteLock(); } List leafs; _lmu.EnterReadLock(); try { leafs = [.. _lleafs]; } finally { _lmu.ExitReadLock(); } foreach (var leaf in leafs) leaf.FlushSignal(); } internal void UpdateLeafNodesEx(string subject, int delta, bool force = false) { if (string.IsNullOrWhiteSpace(subject) || delta == 0) return; var heldWriteLock = _mu.IsWriteLockHeld; if (!heldWriteLock) _mu.EnterWriteLock(); try { _rm ??= new Dictionary(StringComparer.Ordinal); _rm.TryGetValue(subject, out var interest); interest += delta; if (interest <= 0) _rm.Remove(subject); else _rm[subject] = interest; } finally { if (!heldWriteLock) _mu.ExitWriteLock(); } List leafs; _lmu.EnterReadLock(); try { leafs = [.. _lleafs]; } finally { _lmu.ExitReadLock(); } foreach (var leaf in leafs) { if (force) { if (delta > 0) leaf.ForceAddToSmap(subject); else leaf.ForceRemoveFromSmap(subject); } else { leaf.FlushSignal(); } } } // ------------------------------------------------------------------------- // addClient / removeClient // ------------------------------------------------------------------------- /// /// Registers a client with this account, updating system and leaf counters. /// Returns the previous total client count. /// Mirrors Go (a *Account) addClient(c *client) int. /// private int AddClientInternal(ClientConnection c) { _mu.EnterWriteLock(); int prev; try { _clients ??= new HashSet(); prev = _clients.Count; if (!_clients.Add(c)) { // Client was already present — do nothing. return prev; } if (IsInternalClientKind(c.Kind)) { _sysclients++; } else if (c.Kind == ClientKind.Leaf) { _nleafs++; } } finally { _mu.ExitWriteLock(); } // Add leaf to the leaf list (uses separate lock). if (c.Kind == ClientKind.Leaf) { _lmu.EnterWriteLock(); try { _lleafs.Add(c); } finally { _lmu.ExitWriteLock(); } } // TODO: session 12 — notify server via c.srv.accConnsUpdate(a). return prev; } /// /// Unregisters a client from this account, updating system and leaf counters. /// Returns the previous total client count. /// Mirrors Go (a *Account) removeClient(c *client) int. /// private int RemoveClientInternal(ClientConnection c) { _mu.EnterWriteLock(); int prev; bool wasLeaf = false; try { prev = _clients?.Count ?? 0; if (_clients == null || !_clients.Remove(c)) return prev; if (IsInternalClientKind(c.Kind)) { _sysclients--; } else if (c.Kind == ClientKind.Leaf) { _nleafs--; wasLeaf = true; // Cluster accounting for hub leaf nodes. if (c.IsHubLeafNode()) { var cluster = c.RemoteCluster(); if (!string.IsNullOrWhiteSpace(cluster) && _leafClusters != null && _leafClusters.TryGetValue(cluster, out var current)) { if (current <= 1) _leafClusters.Remove(cluster); else _leafClusters[cluster] = current - 1; } } } } finally { _mu.ExitWriteLock(); } if (wasLeaf) { RemoveLeafNode(c); } // TODO: session 12 — notify server via c.srv.accConnsUpdate(a). return prev; } /// /// Removes a leaf-node client from the ordered leaf list. /// Uses internally. /// Mirrors Go (a *Account) removeLeafNode(c *client). /// private void RemoveLeafNode(ClientConnection c) { _lmu.EnterWriteLock(); try { int idx = _lleafs.IndexOf(c); if (idx < 0) return; int last = _lleafs.Count - 1; _lleafs[idx] = _lleafs[last]; _lleafs.RemoveAt(last); } finally { _lmu.ExitWriteLock(); } } // ------------------------------------------------------------------------- // INatsAccount implementation // ------------------------------------------------------------------------- /// /// Returns true when the account is valid (not expired). /// Mirrors Go INatsAccount.IsValid. /// bool INatsAccount.IsValid => !IsExpired(); /// /// Delegates to . /// Mirrors Go INatsAccount.MaxTotalConnectionsReached(). /// bool INatsAccount.MaxTotalConnectionsReached() => MaxTotalConnectionsReached(); /// /// Delegates to . /// Mirrors Go INatsAccount.MaxTotalLeafNodesReached(). /// bool INatsAccount.MaxTotalLeafNodesReached() => MaxTotalLeafNodesReached(); /// /// Registers a client connection. Returns the previous client count. /// Mirrors Go INatsAccount.AddClient(c). /// int INatsAccount.AddClient(ClientConnection c) => AddClientInternal(c); /// /// Unregisters a client connection. Returns the previous client count. /// Mirrors Go INatsAccount.RemoveClient(c). /// int INatsAccount.RemoveClient(ClientConnection c) => RemoveClientInternal(c); // ------------------------------------------------------------------------- // Static helpers // ------------------------------------------------------------------------- /// /// Returns true when the user identified by with /// the given timestamp has been revoked. /// Also checks the wildcard entry (jwt.All = "*"). /// Mirrors Go package-level isRevoked(...) bool. /// internal static bool IsRevoked( Dictionary? revocations, string subject, long issuedAt) { if (revocations == null || revocations.Count == 0) return false; // Check specific key. if (revocations.TryGetValue(subject, out long ts) && ts >= issuedAt) return true; // Check wildcard revocation ("*" = jwt.All). if (revocations.TryGetValue("*", out long tsAll) && tsAll >= issuedAt) return true; return false; } /// /// Returns true if the reply is a tracked reply (ends with "..T"). /// Mirrors Go package-level isTrackedReply(reply []byte) bool. /// internal static bool IsTrackedReply(ReadOnlySpan reply) { int lreply = reply.Length - 1; return lreply > 3 && reply[lreply - 1] == '.' && reply[lreply] == 'T'; } /// /// Validates a mapping destination subject without creating a full transform. /// Mirrors Go ValidateMapping(src, dest string) error in sublist.go. /// Returns null on success; an exception on failure. /// internal static Exception? ValidateMapping(string src, string dest) { if (string.IsNullOrEmpty(dest)) return null; bool sfwc = false; foreach (var token in dest.Split('.')) { int length = token.Length; if (length == 0 || sfwc) return new MappingDestinationException(token, ServerErrors.ErrInvalidMappingDestinationSubject); // If it looks like a mapping function, ensure it is a known one. if (length > 4 && token[0] == '{' && token[1] == '{' && token[length - 2] == '}' && token[length - 1] == '}') { var (tt, _, _, _, terr) = SubjectTransform.IndexPlaceHolders(token); if (terr != null) return terr; if (tt == TransformType.BadTransform) return new MappingDestinationException(token, ServerErrors.ErrUnknownMappingDestinationFunction); continue; } if (length == 1 && token[0] == '>') { sfwc = true; } else if (token.IndexOfAny(['\t', '\n', '\f', '\r', ' ']) >= 0) { return ServerErrors.ErrInvalidMappingDestinationSubject; } } // Verify the full transform can be constructed. var (_, err) = SubjectTransform.New(src, dest); return err; } /// /// Returns true if the given is an internal kind /// (System, JetStream, or Account — not a real user connection). /// Mirrors Go isInternalClient(kind int) bool. /// private static bool IsInternalClientKind(ClientKind kind) => kind is ClientKind.System or ClientKind.JetStream or ClientKind.Account; // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- /// /// Builds the cumulative-weight destination list from a list of raw-weight /// entries. If the total weight is less than 100 /// and the source was not explicitly listed as a destination, a pass-through /// entry is auto-added for the remainder. /// Mirrors Go processDestinations(dests []*destination) ([]*destination, error). /// private static Exception? ProcessDestinations( string src, bool hasWildcard, HashSet seen, List dests) { byte totalWeight = 0; foreach (var d in dests) totalWeight += d.Weight; bool haveSrc = seen.Contains(src); // Auto-fill the remaining weight with a pass-through to the source. if (totalWeight != 100 && !haveSrc) { string passThroughDest = src; if (hasWildcard) passThroughDest = SubjectTransform.TransformTokenize(src); var (tr, err) = SubjectTransform.New(src, passThroughDest); if (err != null) return err; byte aw = dests.Count == 0 ? (byte)100 : (byte)(100 - totalWeight); dests.Add(new Destination { Transform = tr, Weight = aw }); } // Sort ascending by raw weight so the cumulative scan is correct. dests.Sort((a, b) => a.Weight.CompareTo(b.Weight)); // Convert raw weights to cumulative weights. byte cumulative = 0; foreach (var d in dests) { cumulative += d.Weight; d.Weight = cumulative; } return null; } /// /// Adds a service import entry to the import map. /// Mirrors Go (a *Account) addServiceImport(...). /// private (ServiceImportEntry? Import, Exception? Error) AddServiceImportInternal(Account destination, string from, string to, object? claim) { _mu.EnterWriteLock(); try { Imports.Services ??= new Dictionary>(StringComparer.Ordinal); var serviceImport = new ServiceImportEntry { Account = destination, Claim = claim, From = from, To = to, }; if (!Imports.Services.TryGetValue(from, out var entries)) { entries = []; Imports.Services[from] = entries; } entries.Add(serviceImport); return (serviceImport, null); } finally { _mu.ExitWriteLock(); } } /// /// Resolves a service export by exact or wildcard subject match. /// Mirrors Go (a *Account) getServiceExport(service string) *serviceExport. /// private ServiceExportEntry? GetServiceExport(string service) { if (Exports.Services == null) return null; if (Exports.Services.TryGetValue(service, out var serviceExport)) return serviceExport; var tokens = SubjectTransform.TokenizeSubject(service); foreach (var (subject, export) in Exports.Services) { if (SubjectTransform.IsSubsetMatch(tokens, subject)) return export; } return null; } private static ClientInfo? CreateClientInfo(ClientConnection? client, bool _) { if (client == null) return null; return new ClientInfo { Id = client.Cid, Account = client.Account()?.Name ?? string.Empty, Name = client.Opts.Name ?? string.Empty, Rtt = client.GetRttValue(), Start = client.Start == default ? string.Empty : client.Start.ToUniversalTime().ToString("O"), Kind = client.Kind.ToString(), ClientType = client.ClientType().ToString(), }; } private static long UtcNowUnixNanos() => TimeSpanToUnixNanos(DateTime.UtcNow - DateTime.UnixEpoch); private static long TimeSpanToUnixNanos(TimeSpan value) => value.Ticks * 100L; private static TimeSpan UnixNanosToTimeSpan(long unixNanos) => TimeSpan.FromTicks(unixNanos / 100L); private static DateTime UnixNanoToDateTime(long unixNanos) { if (unixNanos <= 0) return DateTime.UnixEpoch; return DateTime.UnixEpoch.AddTicks(unixNanos / 100L); } private static bool TryDecodeJwtPayload(string token, out JsonElement payload) { payload = default; if (string.IsNullOrWhiteSpace(token)) return false; var parts = token.Split('.'); if (parts.Length < 2) return false; string base64 = parts[1] .Replace("-", "+", StringComparison.Ordinal) .Replace("_", "/", StringComparison.Ordinal); int mod = base64.Length % 4; if (mod > 0) base64 = base64.PadRight(base64.Length + (4 - mod), '='); byte[] bytes; try { bytes = Convert.FromBase64String(base64); } catch { return false; } try { using var doc = JsonDocument.Parse(bytes); payload = doc.RootElement.Clone(); return payload.ValueKind == JsonValueKind.Object; } catch { return false; } } private static bool TryReadMember(object source, string name, out object? value) { value = null; if (source == null) return false; if (source is JsonElement element) { if (element.ValueKind != JsonValueKind.Object) return false; foreach (var property in element.EnumerateObject()) { if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) { value = property.Value; return true; } } return false; } if (source is IDictionary dictionary && dictionary.TryGetValue(name, out var dictionaryValue)) { value = dictionaryValue; return true; } if (source is IDictionary stringDictionary && stringDictionary.TryGetValue(name, out var stringDictionaryValue)) { value = stringDictionaryValue; return true; } var propertyInfo = source .GetType() .GetProperty(name, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase); if (propertyInfo == null) return false; value = propertyInfo.GetValue(source); return true; } private static bool TryReadStringMember(object? source, string name, out string value) { value = string.Empty; if (source == null || !TryReadMember(source, name, out var member)) return false; if (member is JsonElement element) { if (element.ValueKind == JsonValueKind.String) { value = element.GetString() ?? string.Empty; return true; } if (element.ValueKind == JsonValueKind.Number) { value = element.ToString(); return true; } return false; } value = member?.ToString() ?? string.Empty; return true; } private static bool TryReadLongMember(object source, string name, out long value) { value = 0; if (!TryReadMember(source, name, out var member)) return false; if (member is JsonElement element) { if (element.ValueKind == JsonValueKind.Number) return element.TryGetInt64(out value); if (element.ValueKind == JsonValueKind.String) return long.TryParse(element.GetString(), out value); return false; } switch (member) { case byte b: value = b; return true; case sbyte sb: value = sb; return true; case short s: value = s; return true; case ushort us: value = us; return true; case int i: value = i; return true; case uint ui: value = ui; return true; case long l: value = l; return true; case ulong ul when ul <= long.MaxValue: value = (long)ul; return true; case string str: return long.TryParse(str, out value); default: return false; } } private static IReadOnlyList ReadStringListMember(object? source, params string[] names) { if (source == null) return []; foreach (var name in names) { if (!TryReadMember(source, name, out var member) || member == null) continue; if (member is IEnumerable enumerableStrings) return [.. enumerableStrings]; if (member is JsonElement element && element.ValueKind == JsonValueKind.Array) { var results = new List(); foreach (var item in element.EnumerateArray()) { if (item.ValueKind == JsonValueKind.String) results.Add(item.GetString() ?? string.Empty); else results.Add(item.ToString()); } return results; } if (member is IEnumerable objectEnumerable) { var results = new List(); foreach (var item in objectEnumerable) results.Add(item?.ToString() ?? string.Empty); return results; } } return []; } private static string NormalizeExportKind(object? kind) { if (kind is JsonElement element) return element.ToString().Trim().ToLowerInvariant(); return kind?.ToString()?.Trim().ToLowerInvariant() ?? string.Empty; } private static string ReadActivationImportSubject(object claimOrPayload) { if (TryReadStringMember(claimOrPayload, "ImportSubject", out var importSubject) && !string.IsNullOrEmpty(importSubject)) return importSubject; if (TryReadStringMember(claimOrPayload, "import_subject", out var importSubjectSnake) && !string.IsNullOrEmpty(importSubjectSnake)) return importSubjectSnake; if (claimOrPayload is JsonElement element && element.ValueKind == JsonValueKind.Object && element.TryGetProperty("nats", out var natsObj) && natsObj.ValueKind == JsonValueKind.Object && natsObj.TryGetProperty("import_subject", out var natsImportSubject) && natsImportSubject.ValueKind == JsonValueKind.String) { return natsImportSubject.GetString() ?? string.Empty; } return string.Empty; } /// /// Tokenises a subject string into an array, using the same split logic /// as btsep-based tokenisation in the Go source. /// private static string[] TokenizeSubjectForMapping(string subject) { var parts = new List(); int start = 0; for (int i = 0; i < subject.Length; i++) { if (subject[i] == '.') { parts.Add(subject[start..i]); start = i + 1; } } parts.Add(subject[start..]); return [.. parts]; } /// /// Returns the cached cluster name for cluster-scoped mapping selection. /// Delegates to the server when available; returns empty string as a stub. /// Mirrors Go a.srv.cachedClusterName(). /// TODO: session 09 — wire up Server.CachedClusterName(). /// private string GetCachedClusterName() { // TODO: session 09 — Server.CachedClusterName(). return string.Empty; } /// /// Clears the connection-heartbeat timer. Caller must hold the account lock. /// Mirrors Go (a *Account) clearConnectionTimer() in server/events.go. /// internal void ClearConnectionHeartbeatTimer() { ClearTimerLocked(ref _ctmr); } /// /// Starts or resets the connection-heartbeat timer. /// Caller must hold the account lock. /// Mirrors Go inline timer setup in sendAccConnsUpdate(). /// internal void SetConnectionHeartbeatTimer(long delayMs, Action callback) { if (_ctmr == null) _ctmr = new Timer(_ => callback(), null, delayMs, Timeout.Infinite); else _ctmr.Change(delayMs, Timeout.Infinite); } /// /// Stops and nulls out a timer. Lock must be held by the caller. /// Mirrors Go clearTimer(t **time.Timer). /// private static void ClearTimerLocked(ref Timer? t) { t?.Dispose(); t = null; } /// /// Checks whether is authorised to use /// (either via explicit approval or token requirement). /// Mirrors Go (a *Account) checkAuth(...) bool. /// private bool CheckAuth( ExportAuth ea, Account account, object? imClaim, string[]? tokens) { if (ea.Approved != null && ea.Approved.ContainsKey(account.Name)) return true; if (ea.TokenRequired) { return CheckActivation(account, imClaim, ea, expirationTimer: true); } // No approved list and no token required → public export. if (ea.Approved == null) return true; // AccountPosition embedding check. if (ea.AccountPosition > 0 && tokens != null) { int pos = (int)ea.AccountPosition - 1; if (pos < tokens.Length && tokens[pos] == account.Name) return true; } return false; } /// /// Applies account-based authorization rules to an export descriptor. /// Mirrors Go setExportAuth(&se.exportAuth, ...). /// private static Exception? SetExportAuth(ExportAuth auth, string subject, IReadOnlyList? accounts, uint accountPos) { if (!SubscriptionIndex.IsValidSubject(subject)) return ServerErrors.ErrBadSubject; auth.AccountPosition = accountPos; if (accounts == null || accounts.Count == 0) { auth.Approved = null; return null; } var approved = new Dictionary(accounts.Count, StringComparer.Ordinal); foreach (var account in accounts) { if (account == null) continue; approved[account.Name] = account; } auth.Approved = approved; return null; } // ------------------------------------------------------------------------- // Export equality helpers // ------------------------------------------------------------------------- private static bool IsStreamExportEqual(StreamExport? a, StreamExport? b) { if (a == null && b == null) return true; if ((a == null) != (b == null)) return false; return IsExportAuthEqual(a!, b!); } private static bool IsServiceExportEqual(ServiceExportEntry? a, ServiceExportEntry? b) { if (a == null && b == null) return true; if ((a == null) != (b == null)) return false; if (!IsExportAuthEqual(a!, b!)) return false; if ((a!.Account?.Name ?? string.Empty) != (b!.Account?.Name ?? string.Empty)) return false; if (a.ResponseType != b.ResponseType) return false; if (a.AllowTrace != b.AllowTrace) return false; // Latency comparison. if ((a.Latency == null) != (b.Latency == null)) return false; if (a.Latency != null) { if (a.Latency.Sampling != b.Latency!.Sampling) return false; if (a.Latency.Subject != b.Latency.Subject) return false; } return true; } private static bool IsExportAuthEqual(ExportAuth a, ExportAuth b) { if (a.TokenRequired != b.TokenRequired) return false; if (a.AccountPosition != b.AccountPosition) return false; int aApproved = a.Approved?.Count ?? 0; int bApproved = b.Approved?.Count ?? 0; if (aApproved != bApproved) return false; if (a.Approved != null) { foreach (var (ak, av) in a.Approved) { if (!b.Approved!.TryGetValue(ak, out var bv) || av.Name != bv.Name) return false; } } int aRevoked = a.ActivationsRevoked?.Count ?? 0; int bRevoked = b.ActivationsRevoked?.Count ?? 0; if (aRevoked != bRevoked) return false; if (a.ActivationsRevoked != null) { foreach (var (ak, av) in a.ActivationsRevoked) { if (!b.ActivationsRevoked!.TryGetValue(ak, out var bv) || av != bv) return false; } } return true; } }