// Copyright 2012-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); using System.Text; using System.Text.Json; using ZB.MOM.NatsNet.Server.Auth; using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Protocol; namespace ZB.MOM.NatsNet.Server; public sealed partial class ClientConnection { internal void RemoveReplySub(Subscription? sub) { if (sub?.Sid is not { Length: > 0 } sid || Server is not NatsServer server) return; var sidText = Encoding.ASCII.GetString(sid); var sep = sidText.IndexOf(' '); if (sep <= 0) return; var accountName = sidText[..sep]; var (account, _) = server.LookupAccount(accountName); account?.Sublist?.Remove(sub); lock (_mu) { Subs.Remove(sidText); } } internal Exception? ProcessAccountSub(byte[] arg) { _ = arg; // Gateway account-sub propagation is owned by gateway sessions. return null; } internal void ProcessAccountUnsub(byte[] arg) { _ = arg; // Gateway account-unsub propagation is owned by gateway sessions. } internal Exception? ProcessRoutedOriginClusterMsgArgs(byte[] arg) => ProtocolParser.ProcessRoutedOriginClusterMsgArgs(ParseCtx, arg); internal Exception? ProcessRoutedHeaderMsgArgs(byte[] arg) => ProtocolParser.ProcessRoutedHeaderMsgArgs(ParseCtx, arg); internal Exception? ProcessRoutedMsgArgs(byte[] arg) => ProtocolParser.ProcessRoutedMsgArgs(ParseCtx, arg); internal void ProcessInboundRoutedMsg(byte[] msg) { _in.Msgs++; _in.Bytes += Math.Max(0, msg.Length - 2); if (Opts.Verbose) SendOK(); var pa = ParseCtx.Pa; if (pa.Subject is null) return; var (acc, result) = GetAccAndResultFromCache(); if (acc is null) return; if ((result?.PSubs.Count ?? 0) + (result?.QSubs.Count ?? 0) > 0) ProcessMsgResults(acc, result, msg, null, pa.Subject, pa.Reply, PmrFlags.None); } internal Exception? SendRouteConnect(string clusterName, bool tlsRequired) { var user = string.Empty; var pass = string.Empty; var routeUrl = Route?.Url; if (routeUrl is not null && !string.IsNullOrEmpty(routeUrl.UserInfo)) { var userInfo = routeUrl.UserInfo.Split(':', 2); user = userInfo[0]; if (userInfo.Length > 1) pass = userInfo[1]; } if (Server is not NatsServer server) return new InvalidOperationException("route server unavailable"); var connect = new ConnectInfo { Echo = true, Verbose = false, Pedantic = false, User = user, Pass = pass, Tls = tlsRequired, Name = server.ID(), Headers = server.SupportsHeaders(), Cluster = clusterName, Dynamic = server.IsClusterNameDynamic(), Lnoc = true, }; var payload = JsonSerializer.Serialize(connect); EnqueueProto(Encoding.ASCII.GetBytes($"CONNECT {payload}\r\n")); return null; } internal void ProcessRouteInfo(ServerInfo info) { if (Server is not NatsServer server) return; lock (_mu) { Route ??= new Route(); if (Flags.IsSet(ClientFlags.InfoReceived)) { Opts.Import = info.Import; Opts.Export = info.Export; } Route.RemoteId = info.Id; Route.RemoteName = info.Name; Route.AuthRequired = info.AuthRequired; Route.TlsRequired = info.TlsRequired; Route.GatewayUrl = info.GatewayUrl ?? string.Empty; Route.Lnoc = info.Lnoc; Route.Lnocu = info.Lnocu; Route.JetStream = info.JetStream; Route.ConnectUrls = info.ClientConnectUrls?.ToList() ?? []; Route.WsConnUrls = info.WsConnectUrls?.ToList() ?? []; Route.LeafnodeUrl = info.LeafNodeUrls is { Length: 1 } leaf ? leaf[0] : string.Empty; Route.Hash = NatsServer.GetHash(info.Name); Route.IdHash = NatsServer.GetHash(info.Id); Opts.Protocol = info.Proto; Headers = server.SupportsHeaders() && info.Headers; Flags |= ClientFlags.InfoReceived; } if (NatsServer.NeedsCompression(server.GetOpts().Cluster.Compression.Mode)) _ = server.NegotiateRouteCompression(this, Route?.DidSolicit == true, Route?.AccName is { Length: > 0 } an ? Encoding.ASCII.GetString(an) : string.Empty, info.Compression ?? string.Empty, server.GetOpts()); server.UpdateRemoteRoutePerms(this, info); } internal bool CanImport(string subject) => PubAllowedFullCheck(subject, fullCheck: false, hasLock: true); internal bool CanExport(string subject) => CanSubscribe(subject); internal void SetRoutePermissions(RoutePermissions? perms) { if (perms is null) { Perms = null; MPerms = null; return; } SetPermissions(new Permissions { Publish = perms.Import?.Clone(), Subscribe = perms.Export?.Clone(), }); } internal (bool IsPinnedAccountRoute, string AccountName, bool KeyHasSubType) GetRoutedSubKeyInfo() { var accountName = Route?.AccName is { Length: > 0 } an ? Encoding.ASCII.GetString(an) : string.Empty; return (!string.IsNullOrEmpty(accountName), accountName, Route?.Lnocu == true); } internal void RemoveRemoteSubs() { if (Server is not NatsServer server) return; Dictionary subs; var grouped = new Dictionary>(StringComparer.Ordinal); var (pinned, accountName, keyHasSubType) = GetRoutedSubKeyInfo(); lock (_mu) { subs = Subs; Subs = new Dictionary(StringComparer.Ordinal); } foreach (var kvp in subs) { var keyAccount = pinned ? accountName : RouteHandler.GetAccNameFromRoutedSubKey(kvp.Value, kvp.Key, keyHasSubType); if (string.IsNullOrEmpty(keyAccount)) continue; if (!grouped.TryGetValue(keyAccount, out var list)) { list = []; grouped[keyAccount] = list; } list.Add(kvp.Value); } foreach (var (accName, list) in grouped) { var (acc, _) = server.LookupAccount(accName); acc?.Sublist?.RemoveBatch(list); } } internal List RemoveRemoteSubsForAcc(string name) { var removed = new List(); var (_, _, keyHasSubType) = GetRoutedSubKeyInfo(); lock (_mu) { foreach (var key in Subs.Keys.ToArray()) { var sub = Subs[key]; if (RouteHandler.GetAccNameFromRoutedSubKey(sub, key, keyHasSubType) != name) continue; removed.Add(sub); Subs.Remove(key); } } return removed; } internal (byte[] Origin, string AccountName, byte[] Subject, byte[] Queue, Exception? Error) ParseUnsubProto(byte[] arg, bool accInProto, bool hasOrigin) { _in.Subs++; var args = SplitArg(arg); var origin = Array.Empty(); var queue = Array.Empty(); var subjectIndex = 0; if (hasOrigin) { if (args.Count == 0) return (origin, string.Empty, Array.Empty(), queue, new FormatException($"parse error: '{Encoding.ASCII.GetString(arg)}'")); origin = args[0]; subjectIndex = 1; } if (accInProto) subjectIndex++; if (args.Count is not (>= 1) || args.Count < subjectIndex + 1 || args.Count > subjectIndex + 2) return (origin, string.Empty, Array.Empty(), queue, new FormatException($"parse error: '{Encoding.ASCII.GetString(arg)}'")); if (args.Count == subjectIndex + 2) queue = args[subjectIndex + 1]; var accountName = accInProto ? Encoding.ASCII.GetString(args[subjectIndex - 1]) : string.Empty; return (origin, accountName, args[subjectIndex], queue, null); } internal Exception? ProcessRemoteUnsub(byte[] arg, bool leafUnsub) { if (Server is not NatsServer server) return null; string accountName; var accInProto = true; bool originSupport; lock (_mu) { originSupport = Route?.Lnocu == true; if (Route?.AccName is { Length: > 0 } an) { accountName = Encoding.ASCII.GetString(an); accInProto = false; } else { accountName = string.Empty; } } var (_, protoAccName, subject, _, err) = ParseUnsubProto(arg, accInProto, leafUnsub && originSupport); if (err is not null) return new FormatException($"processRemoteUnsub {err.Message}"); if (accInProto) accountName = protoAccName; var (acc, _) = server.LookupAccount(accountName); if (acc is null) { Debugf("Unknown account {0} for subject {1}", accountName, Encoding.ASCII.GetString(subject)); return null; } Subscription? sub = null; var key = Encoding.ASCII.GetString(arg); lock (_mu) { if (IsClosed()) return null; if (Subs.TryGetValue(key, out sub)) { Subs.Remove(key); acc.Sublist?.Remove(sub); } } if (Opts.Verbose) SendOK(); return null; } internal Exception? ProcessRemoteSub(byte[] protoArg, bool hasOrigin) { _in.Subs++; if (Server is not NatsServer server) return null; var args = SplitArg(protoArg); var (isPinned, accountName, _) = GetRoutedSubKeyInfo(); var accInProto = !isPinned; var subjectIndex = 0; if (hasOrigin) subjectIndex++; if (accInProto) subjectIndex++; if (args.Count is not (>= 1) || (args.Count != subjectIndex + 1 && args.Count != subjectIndex + 3)) return new FormatException($"processRemoteSub Parse Error: '{Encoding.ASCII.GetString(protoArg)}'"); if (accInProto) accountName = Encoding.ASCII.GetString(args[subjectIndex - 1]); var subject = args[subjectIndex]; byte[]? queue = null; var qw = 1; if (args.Count == subjectIndex + 3) { queue = args[subjectIndex + 1]; _ = int.TryParse(Encoding.ASCII.GetString(args[subjectIndex + 2]), out qw); if (qw <= 0) qw = 1; } var (acc, _) = server.LookupOrRegisterAccount(accountName); if (acc is null) return null; lock (_mu) { if (IsClosed()) return null; if (Perms is not null && !CanExport(Encoding.ASCII.GetString(subject))) return null; if (SubsAtLimit()) { MaxSubsExceeded(); return null; } var key = Encoding.ASCII.GetString(protoArg); if (!Subs.ContainsKey(key)) { var sub = new Subscription { Subject = subject, Queue = queue, Sid = Encoding.ASCII.GetBytes(key), Qw = qw, }; Subs[key] = sub; acc.Sublist?.Insert(sub); } } if (Opts.Verbose) SendOK(); return null; } internal byte[] AddRouteSubOrUnsubProtoToBuf(byte[] buf, string accName, Subscription sub, bool isSubProto) { var list = new List(buf.Length + 64); list.AddRange(buf); if (isSubProto) list.AddRange(Encoding.ASCII.GetBytes("RS+ ")); else list.AddRange(Encoding.ASCII.GetBytes("RS- ")); if (Route?.AccName is not { Length: > 0 }) { list.AddRange(Encoding.ASCII.GetBytes(accName)); list.Add((byte)' '); } list.AddRange(sub.Subject); if (sub.Queue is { Length: > 0 } queue) { list.Add((byte)' '); list.AddRange(queue); if (isSubProto) { list.Add((byte)' '); list.AddRange(Encoding.ASCII.GetBytes(Math.Max(sub.Qw, 1).ToString())); } } list.Add((byte)'\r'); list.Add((byte)'\n'); return list.ToArray(); } internal void SendRouteSubProtos(IReadOnlyList subs, bool trace, Func? filter = null) => SendRouteSubOrUnSubProtos(subs, isSubProto: true, trace, filter); internal void SendRouteUnSubProtos(IReadOnlyList subs, bool trace, Func? filter = null) => SendRouteSubOrUnSubProtos(subs, isSubProto: false, trace, filter); internal void SendRouteSubOrUnSubProtos( IReadOnlyList subs, bool isSubProto, bool trace, Func? filter = null) { var buf = Array.Empty(); foreach (var sub in subs) { if (filter is not null && !filter(sub)) continue; var accountName = ServerConstants.DefaultGlobalAccount; var startLen = buf.Length; buf = AddRouteSubOrUnsubProtoToBuf(buf, accountName, sub, isSubProto); if (trace && buf.Length > startLen) TraceOutOp(string.Empty, buf.AsSpan(startLen, buf.Length - startLen - 2).ToArray()); } if (buf.Length > 0) EnqueueProto(buf); } internal bool ImportFilter(string subject) => CanImport(subject); internal bool IsSolicitedRoute() => Route?.DidSolicit == true; internal Exception? ProcessRouteConnect(byte[] arg) { if (arg is not { Length: > 0 }) return new FormatException("processRouteConnect parse error"); ConnectInfo? info; try { info = JsonSerializer.Deserialize(arg); } catch (Exception ex) { return ex; } if (info is null) return new FormatException("processRouteConnect missing CONNECT payload"); lock (_mu) { Opts.Name = info.Name; Opts.Headers = info.Headers; Route ??= new Route(); Route.Lnoc = info.Lnoc; Route.Lnocu = info.Lnocu; } return null; } }