487 lines
15 KiB
C#
487 lines
15 KiB
C#
// 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<string, Subscription> subs;
|
|
var grouped = new Dictionary<string, List<Subscription>>(StringComparer.Ordinal);
|
|
var (pinned, accountName, keyHasSubType) = GetRoutedSubKeyInfo();
|
|
|
|
lock (_mu)
|
|
{
|
|
subs = Subs;
|
|
Subs = new Dictionary<string, Subscription>(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<Subscription> RemoveRemoteSubsForAcc(string name)
|
|
{
|
|
var removed = new List<Subscription>();
|
|
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<byte>();
|
|
var queue = Array.Empty<byte>();
|
|
var subjectIndex = 0;
|
|
|
|
if (hasOrigin)
|
|
{
|
|
if (args.Count == 0)
|
|
return (origin, string.Empty, Array.Empty<byte>(), 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<byte>(), 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<byte>(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<Subscription> subs, bool trace, Func<Subscription, bool>? filter = null) =>
|
|
SendRouteSubOrUnSubProtos(subs, isSubProto: true, trace, filter);
|
|
|
|
internal void SendRouteUnSubProtos(IReadOnlyList<Subscription> subs, bool trace, Func<Subscription, bool>? filter = null) =>
|
|
SendRouteSubOrUnSubProtos(subs, isSubProto: false, trace, filter);
|
|
|
|
internal void SendRouteSubOrUnSubProtos(
|
|
IReadOnlyList<Subscription> subs,
|
|
bool isSubProto,
|
|
bool trace,
|
|
Func<Subscription, bool>? filter = null)
|
|
{
|
|
var buf = Array.Empty<byte>();
|
|
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<ConnectInfo>(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;
|
|
}
|
|
}
|