// Copyright 2013-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/monitor.go in the NATS server Go source. using System.Net; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json.Serialization; using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server; // ============================================================================ // GatewayzOptions — query options for the Gatewayz endpoint // Mirrors Go GatewayzOptions struct in server/monitor.go. // ============================================================================ /// /// Options that control the output of a Gatewayz monitoring query. /// Mirrors Go GatewayzOptions struct in server/monitor.go. /// public sealed class GatewayzOptions { /// When non-empty, limits output to the gateway with this name. Mirrors Go Name. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; /// When true, includes accounts with their interest. Mirrors Go Accounts. [JsonPropertyName("accounts")] public bool Accounts { get; set; } /// Limits accounts to this specific name (implies ). Mirrors Go AccountName. [JsonPropertyName("account_name")] public string AccountName { get; set; } = string.Empty; /// When true, subscription subjects are included in account results. Mirrors Go AccountSubscriptions. [JsonPropertyName("subscriptions")] public bool AccountSubscriptions { get; set; } /// When true, verbose subscription details are included. Mirrors Go AccountSubscriptionsDetail. [JsonPropertyName("subscriptions_detail")] public bool AccountSubscriptionsDetail { get; set; } } // ============================================================================ // Gatewayz — top-level gateway monitoring response // Mirrors Go Gatewayz struct in server/monitor.go. // ============================================================================ /// /// Top-level response type for the /gatewayz monitoring endpoint. /// Mirrors Go Gatewayz struct in server/monitor.go. /// public sealed class Gatewayz { [JsonPropertyName("server_id")] public string Id { get; set; } = string.Empty; [JsonPropertyName("now")] public DateTime Now { get; set; } [JsonPropertyName("name")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; set; } [JsonPropertyName("host")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Host { get; set; } [JsonPropertyName("port")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int Port { get; set; } [JsonPropertyName("outbound_gateways")] public Dictionary OutboundGateways { get; set; } = new(); [JsonPropertyName("inbound_gateways")] public Dictionary> InboundGateways { get; set; } = new(); } // ============================================================================ // RemoteGatewayz — information about a single remote gateway connection // Mirrors Go RemoteGatewayz struct in server/monitor.go. // ============================================================================ /// /// Information about a single outbound or inbound gateway connection. /// Mirrors Go RemoteGatewayz struct in server/monitor.go. /// public sealed class RemoteGatewayz { /// True if the gateway was explicitly configured (not implicit). Mirrors Go IsConfigured. [JsonPropertyName("configured")] public bool IsConfigured { get; set; } /// Connection details. Mirrors Go Connection *ConnInfo. [JsonPropertyName("connection")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ConnInfo? Connection { get; set; } /// Per-account interest information. Mirrors Go Accounts []*AccountGatewayz. [JsonPropertyName("accounts")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? Accounts { get; set; } } // ============================================================================ // AccountGatewayz — per-account interest mode on a gateway // Mirrors Go AccountGatewayz struct in server/monitor.go. // ============================================================================ /// /// Per-account interest mode information for a gateway connection. /// Mirrors Go AccountGatewayz struct in server/monitor.go. /// public sealed class AccountGatewayz { [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; [JsonPropertyName("interest_mode")] public string InterestMode { get; set; } = string.Empty; [JsonPropertyName("no_interest_count")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int NoInterestCount { get; set; } [JsonPropertyName("interest_only_threshold")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int InterestOnlyThreshold { get; set; } [JsonPropertyName("num_subs")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int TotalSubscriptions { get; set; } [JsonPropertyName("num_queue_subs")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public int NumQueueSubscriptions { get; set; } [JsonPropertyName("subscriptions_list")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? Subs { get; set; } [JsonPropertyName("subscriptions_list_detail")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? SubsDetail { get; set; } } // ============================================================================ // ExtImport — external account import detail for /accountz // Mirrors Go ExtImport struct in server/monitor.go. // ============================================================================ /// /// External view of a service import entry, as returned by the /accountz endpoint. /// Mirrors Go ExtImport struct in server/monitor.go. /// Note: The JWT Import embedded struct fields are inlined here since the /// nats.io/jwt library is not yet ported. /// public sealed class ExtImport { /// Whether this import is invalid. Mirrors Go Invalid bool. [JsonPropertyName("invalid")] public bool Invalid { get; set; } /// Whether the requestor's client info is shared. Mirrors Go Share bool. [JsonPropertyName("share")] public bool Share { get; set; } /// Whether latency tracking is enabled. Mirrors Go Tracking bool. [JsonPropertyName("tracking")] public bool Tracking { get; set; } /// Headers used when latency is triggered by a header. Mirrors Go TrackingHdr http.Header. [JsonPropertyName("tracking_header")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? TrackingHeader { get; set; } /// /// Latency configuration from the exporting account's JWT claim. /// Mirrors Go Latency *jwt.ServiceLatency. /// Sampling and subject are stored directly since jwt lib is not ported. /// [JsonPropertyName("latency")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ExtServiceLatency? Latency { get; set; } /// First-leg latency measurement. Mirrors Go M1 *ServiceLatency. [JsonPropertyName("m1")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ServiceLatency? M1 { get; set; } // Inlined jwt.Import fields. /// Subject of the imported service. Mirrors Go jwt.Import.Subject. [JsonPropertyName("subject")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Subject { get; set; } /// Account that exports the service. Mirrors Go jwt.Import.Account. [JsonPropertyName("account")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Account { get; set; } /// Local subject used on the importing account. Mirrors Go jwt.Import.LocalSubject. [JsonPropertyName("local_subject")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? LocalSubject { get; set; } } // ============================================================================ // ExtServiceLatency — external representation of service latency config // Used by ExtImport in place of jwt.ServiceLatency. // ============================================================================ /// /// External representation of service latency configuration, used in . /// Mirrors Go jwt.ServiceLatency from nats.io/jwt/v2. /// public sealed class ExtServiceLatency { [JsonPropertyName("sampling")] public int Sampling { get; set; } [JsonPropertyName("results")] public string Results { get; set; } = string.Empty; } // ============================================================================ // MonitorHelpers — standalone helper functions // Mirrors standalone functions in server/monitor.go. // ============================================================================ /// /// Standalone helper functions used by the monitoring subsystem. /// Mirrors package-level functions from server/monitor.go. /// internal static class MonitorHelpers { // ------------------------------------------------------------------------- // newSubsDetailList // ------------------------------------------------------------------------- /// /// Builds a verbose subscription detail list for a client connection. /// Client must be locked by caller. /// Mirrors Go newSubsDetailList. /// internal static List NewSubsDetailList(ClientConnection client) { var result = new List(client.Subs?.Count ?? 0); if (client.Subs == null) return result; foreach (var sub in client.Subs.Values) result.Add(NewClientSubDetail(sub, client.Cid)); return result; } // ------------------------------------------------------------------------- // newSubsList // ------------------------------------------------------------------------- /// /// Builds a plain subscription subject list for a client connection. /// Client must be locked by caller. /// Mirrors Go newSubsList. /// internal static List NewSubsList(ClientConnection client) { var result = new List(client.Subs?.Count ?? 0); if (client.Subs == null) return result; foreach (var sub in client.Subs.Values) result.Add(Encoding.UTF8.GetString(sub.Subject)); return result; } // ------------------------------------------------------------------------- // createProxyInfo // ------------------------------------------------------------------------- /// /// Returns a if the connection has a proxy key set, or null. /// Client lock must be held on entry. /// Mirrors Go createProxyInfo. /// internal static ProxyInfo? CreateProxyInfo(ClientConnection c) { if (string.IsNullOrEmpty(c.ProxyKey)) return null; return new ProxyInfo { Key = c.ProxyKey }; } // ------------------------------------------------------------------------- // makePeerCerts // ------------------------------------------------------------------------- /// /// Converts a list of X.509 peer certificates into summary records. /// Each record contains subject string, SPKI SHA-256 hex, and certificate SHA-256 hex. /// Mirrors Go makePeerCerts. /// internal static List MakePeerCerts(IReadOnlyList peerCerts) { var result = new List(peerCerts.Count); foreach (var cert in peerCerts) { var spkiHash = SHA256.HashData(cert.PublicKey.ExportSubjectPublicKeyInfo()); var certHash = SHA256.HashData(cert.RawData); result.Add(new TlsPeerCert { Subject = cert.Subject, SubjectPkiSha256 = Convert.ToHexString(spkiHash).ToLowerInvariant(), CertSha256 = Convert.ToHexString(certHash).ToLowerInvariant(), }); } return result; } // ------------------------------------------------------------------------- // decodeBool // ------------------------------------------------------------------------- /// /// Parses a boolean query-string parameter from an HTTP listener request. /// Writes a 400 status and returns an error if the value cannot be parsed. /// Mirrors Go decodeBool. /// internal static (bool Value, Exception? Error) DecodeBool( HttpListenerResponse response, System.Collections.Specialized.NameValueCollection query, string param) { var str = query[param] ?? string.Empty; if (str.Length == 0) return (false, null); if (bool.TryParse(str, out var val)) return (val, null); if (str == "1") return (true, null); if (str == "0") return (false, null); var err = new FormatException($"Error decoding boolean for '{param}': {str}"); response.StatusCode = 400; return (false, err); } // ------------------------------------------------------------------------- // decodeUint64 // ------------------------------------------------------------------------- /// /// Parses a uint64 query-string parameter from an HTTP listener request. /// Mirrors Go decodeUint64. /// internal static (ulong Value, Exception? Error) DecodeUint64( HttpListenerResponse response, System.Collections.Specialized.NameValueCollection query, string param) { var str = query[param] ?? string.Empty; if (str.Length == 0) return (0, null); if (ulong.TryParse(str, out var val)) return (val, null); var err = new FormatException($"Error decoding uint64 for '{param}': {str}"); response.StatusCode = 400; return (0, err); } // ------------------------------------------------------------------------- // decodeInt // ------------------------------------------------------------------------- /// /// Parses an int query-string parameter from an HTTP listener request. /// Mirrors Go decodeInt. /// internal static (int Value, Exception? Error) DecodeInt( HttpListenerResponse response, System.Collections.Specialized.NameValueCollection query, string param) { var str = query[param] ?? string.Empty; if (str.Length == 0) return (0, null); if (int.TryParse(str, out var val)) return (val, null); var err = new FormatException($"Error decoding int for '{param}': {str}"); response.StatusCode = 400; return (0, err); } // ------------------------------------------------------------------------- // decodeState // ------------------------------------------------------------------------- /// /// Parses the connection-state filter query parameter. /// Mirrors Go decodeState. /// internal static (ConnState Value, Exception? Error) DecodeState( HttpListenerResponse response, System.Collections.Specialized.NameValueCollection query) { var str = query["state"] ?? string.Empty; if (str.Length == 0) return (ConnState.ConnOpen, null); switch (str.ToLowerInvariant()) { case "open": return (ConnState.ConnOpen, null); case "closed": return (ConnState.ConnClosed, null); case "any": case "all": return (ConnState.ConnAll, null); } var err = new FormatException($"Error decoding state for {str}"); response.StatusCode = 400; return (default, err); } // ------------------------------------------------------------------------- // decodeSubs // ------------------------------------------------------------------------- /// /// Parses the subs query parameter into subs and subsDet flags. /// Mirrors Go decodeSubs. /// internal static (bool Subs, bool SubsDetail, Exception? Error) DecodeSubs( HttpListenerResponse response, System.Collections.Specialized.NameValueCollection query) { var raw = query["subs"] ?? string.Empty; if (raw.Equals("detail", StringComparison.OrdinalIgnoreCase)) return (false, true, null); var (subs, err) = DecodeBool(response, query, "subs"); return (subs, false, err); } // ------------------------------------------------------------------------- // newSubDetail // ------------------------------------------------------------------------- /// /// Creates a including account name from the owning client. /// Client must be locked on entry. /// Mirrors Go newSubDetail. /// internal static SubDetail NewSubDetail(Internal.Subscription sub, ClientConnection client) { var sd = NewClientSubDetail(sub, client.Cid); var acc = client.Account() as Account; sd.Account = acc?.GetName(); sd.AccountTag = acc?.GetNameTag(); return sd; } // ------------------------------------------------------------------------- // newClientSubDetail // ------------------------------------------------------------------------- /// /// Creates a from a subscription (no account name). /// Mirrors Go newClientSubDetail. /// internal static SubDetail NewClientSubDetail(Internal.Subscription sub, ulong cid) { return new SubDetail { Subject = Encoding.UTF8.GetString(sub.Subject), Queue = sub.Queue is { Length: > 0 } ? Encoding.UTF8.GetString(sub.Queue) : null, Sid = sub.Sid is { Length: > 0 } ? Encoding.UTF8.GetString(sub.Sid) : string.Empty, Cid = cid, }; } // ------------------------------------------------------------------------- // myUptime // ------------------------------------------------------------------------- /// /// Formats a as a human-readable uptime string /// (e.g. "2d3h14m5s", "45m30s"). /// Mirrors Go myUptime. /// internal static string MyUptime(TimeSpan d) { var tsecs = (long)d.TotalSeconds; var tmins = tsecs / 60; var thrs = tmins / 60; var tdays = thrs / 24; var tyrs = tdays / 365; if (tyrs > 0) return $"{tyrs}y{tdays % 365}d{thrs % 24}h{tmins % 60}m{tsecs % 60}s"; if (tdays > 0) return $"{tdays}d{thrs % 24}h{tmins % 60}m{tsecs % 60}s"; if (thrs > 0) return $"{thrs}h{tmins % 60}m{tsecs % 60}s"; if (tmins > 0) return $"{tmins}m{tsecs % 60}s"; return $"{tsecs}s"; } // ------------------------------------------------------------------------- // tlsCertNotAfter // ------------------------------------------------------------------------- /// /// Returns the expiry date of the first certificate in the given collection, /// or if the collection is empty. /// Mirrors Go tlsCertNotAfter. /// internal static DateTime TlsCertNotAfter(X509CertificateCollection? certs) { if (certs == null || certs.Count == 0) return DateTime.MinValue; if (certs[0] is X509Certificate2 cert2) return cert2.NotAfter.ToUniversalTime(); try { var parsed = new X509Certificate2(certs[0]); return parsed.NotAfter.ToUniversalTime(); } catch { return DateTime.MinValue; } } // ------------------------------------------------------------------------- // urlsToStrings // ------------------------------------------------------------------------- /// /// Converts a list of objects to their Host:Port string form. /// Mirrors Go urlsToStrings. /// internal static string[] UrlsToStrings(IReadOnlyList urls) { var result = new string[urls.Count]; for (int i = 0; i < urls.Count; i++) result[i] = urls[i].Authority; // "host:port" return result; } // ------------------------------------------------------------------------- // getPinnedCertsAsSlice // ------------------------------------------------------------------------- /// /// Converts a to a plain string array. /// Returns null if the set is empty. /// Mirrors Go getPinnedCertsAsSlice. /// internal static string[]? GetPinnedCertsAsSlice(PinnedCertSet? certs) { if (certs == null || certs.Count == 0) return null; var result = new string[certs.Count]; certs.CopyTo(result); return result; } // ------------------------------------------------------------------------- // getMonitorGWOptions // ------------------------------------------------------------------------- /// /// Extracts gateway name filter and accounts flag from . /// When AccountName is set but Accounts is false, the accounts flag is /// implicitly promoted to true. /// Mirrors Go getMonitorGWOptions. /// internal static (string Name, bool Accounts) GetMonitorGWOptions(GatewayzOptions? opts) { if (opts == null) return (string.Empty, false); var name = opts.Name; var accs = opts.Accounts; if (!accs && !string.IsNullOrEmpty(opts.AccountName)) accs = true; return (name, accs); } // ------------------------------------------------------------------------- // createOutboundRemoteGatewayz // ------------------------------------------------------------------------- /// /// Builds a from an outbound gateway client connection. /// Client lock is acquired internally. /// Mirrors Go createOutboundRemoteGatewayz. /// Note: Per-account interest detail (outsim) requires a running gateway layer; /// account lists are empty in the current partial port. /// internal static (string Name, RemoteGatewayz? Rgw) CreateOutboundRemoteGatewayz( ClientConnection c, GatewayzOptions? opts, DateTime now, bool doAccs) { lock (c) { var name = c.Gateway?.Name; if (string.IsNullOrEmpty(name)) return (string.Empty, null); var isConfigured = c.Gateway?.Cfg != null && !c.Gateway.Cfg.IsImplicit(); var rgw = new RemoteGatewayz { IsConfigured = isConfigured, Connection = new ConnInfo(), Accounts = doAccs ? [] : null, }; return (name, rgw); } } // ------------------------------------------------------------------------- // createOutboundAccountsGatewayz // ------------------------------------------------------------------------- /// /// Returns the per-account interest list for an outbound gateway connection. /// Mirrors Go createOutboundAccountsGatewayz. /// Note: Requires fully-ported gateway outsim state; returns empty list in current port. /// internal static List CreateOutboundAccountsGatewayz( GatewayzOptions? opts, ClientConnection c) { // outsim not yet ported — return empty. return []; } // ------------------------------------------------------------------------- // createAccountOutboundGatewayz // ------------------------------------------------------------------------- /// /// Creates an entry for a named outbound account. /// Mirrors Go createAccountOutboundGatewayz. /// Note: Requires fully-ported outsie state; returns Optimistic defaults. /// internal static AccountGatewayz CreateAccountOutboundGatewayz( GatewayzOptions? opts, string name, object? outsie) { // outsie not yet ported — return Optimistic defaults. return new AccountGatewayz { Name = name, InterestMode = GatewayInterestMode.Optimistic.String(), }; } // ------------------------------------------------------------------------- // createInboundAccountsGatewayz // ------------------------------------------------------------------------- /// /// Returns the per-account interest list for an inbound gateway connection. /// Mirrors Go createInboundAccountsGatewayz. /// Note: Requires fully-ported gateway insim state; returns empty list. /// internal static List CreateInboundAccountsGatewayz( GatewayzOptions? opts, ClientConnection c) { // insim not yet ported — return empty. return []; } // ------------------------------------------------------------------------- // createInboundAccountGatewayz // ------------------------------------------------------------------------- /// /// Creates an entry for a named inbound account. /// Mirrors Go createInboundAccountGatewayz. /// Note: Requires fully-ported insie state; returns Optimistic defaults. /// internal static AccountGatewayz CreateInboundAccountGatewayz(string name, object? insie) { // insie not yet ported — return Optimistic defaults. return new AccountGatewayz { Name = name, InterestMode = GatewayInterestMode.Optimistic.String(), }; } // ------------------------------------------------------------------------- // ResponseHandler / handleResponse // ------------------------------------------------------------------------- /// /// Writes a JSON (or JSONP) HTTP monitoring response with 200 OK. /// Mirrors Go ResponseHandler. /// internal static void ResponseHandler( HttpListenerResponse response, HttpListenerRequest request, byte[] data) { HandleResponse(200, response, request, data); } /// /// Writes a JSON (or JSONP if the callback query param is set) HTTP monitoring response. /// Mirrors Go handleResponse. /// internal static void HandleResponse( int statusCode, HttpListenerResponse response, HttpListenerRequest request, byte[] data) { var callback = request.QueryString["callback"] ?? string.Empty; response.StatusCode = statusCode; if (callback.Length > 0) { response.ContentType = "application/javascript"; var prefix = Encoding.UTF8.GetBytes($"{callback}("); var suffix = Encoding.UTF8.GetBytes(")"); response.OutputStream.Write(prefix); response.OutputStream.Write(data); response.OutputStream.Write(suffix); } else { response.ContentType = "application/json"; response.Headers["Access-Control-Allow-Origin"] = "*"; response.OutputStream.Write(data); } } // ------------------------------------------------------------------------- // newExtServiceLatency // ------------------------------------------------------------------------- /// /// Converts an to an /// for the /accountz response. Returns null if input is null. /// Mirrors Go newExtServiceLatency. /// internal static ExtServiceLatency? NewExtServiceLatency(InternalServiceLatency? l) { if (l == null) return null; return new ExtServiceLatency { Sampling = l.Sampling, Results = l.Subject, }; } // ------------------------------------------------------------------------- // newExtImport // ------------------------------------------------------------------------- /// /// Converts a to an /// for the /accountz response. /// Mirrors Go newExtImport. /// internal static ExtImport NewExtImport(ServiceImportEntry? v) { if (v == null) return new ExtImport { Invalid = true }; return new ExtImport { Invalid = v.Invalid, Share = v.Share, Tracking = v.Tracking, TrackingHeader = v.TrackingHeader, Latency = NewExtServiceLatency(v.Latency), M1 = v.M1, Subject = v.To, Account = v.Account?.Name, LocalSubject = v.From, }; } }