// 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,
};
}
}