diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs new file mode 100644 index 0000000..713b43c --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs @@ -0,0 +1,477 @@ +// Copyright 2012-2025 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/opts.go in the NATS server Go source. + +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; + +namespace ZB.MOM.NatsNet.Server; + +/// +/// Write timeout behavior policy. +/// Mirrors WriteTimeoutPolicy in opts.go. +/// +public enum WriteTimeoutPolicy : byte +{ + Default = 0, + Close = 1, + Retry = 2, +} + +/// +/// Store encryption cipher selection. +/// Mirrors StoreCipher in opts.go. +/// +public enum StoreCipher +{ + ChaCha = 0, + Aes = 1, + NoCipher = 2, +} + +/// +/// OCSP stapling mode. +/// Mirrors OCSPMode in opts.go. +/// +public enum OcspMode : byte +{ + Auto = 0, + Always = 1, + Never = 2, + Must = 3, +} + +/// +/// Set of pinned certificate SHA256 hashes (lowercase hex-encoded DER SubjectPublicKeyInfo). +/// Mirrors PinnedCertSet in opts.go. +/// +public class PinnedCertSet : HashSet +{ + public PinnedCertSet() : base(StringComparer.OrdinalIgnoreCase) { } + public PinnedCertSet(IEnumerable collection) : base(collection, StringComparer.OrdinalIgnoreCase) { } +} + +/// +/// Compression options for route/leaf connections. +/// Mirrors CompressionOpts in opts.go. +/// +public class CompressionOpts +{ + public string Mode { get; set; } = string.Empty; + public List RttThresholds { get; set; } = []; +} + +/// +/// Compression mode string constants. +/// +public static class CompressionModes +{ + public const string Off = "off"; + public const string Accept = "accept"; + public const string S2Fast = "s2_fast"; + public const string S2Better = "s2_better"; + public const string S2Best = "s2_best"; + public const string S2Auto = "s2_auto"; +} + +/// +/// TLS configuration parsed from config file. +/// Mirrors TLSConfigOpts in opts.go. +/// +public class TlsConfigOpts +{ + public string CertFile { get; set; } = string.Empty; + public string KeyFile { get; set; } = string.Empty; + public string CaFile { get; set; } = string.Empty; + public bool Verify { get; set; } + public bool Insecure { get; set; } + public bool Map { get; set; } + public bool TlsCheckKnownUrls { get; set; } + public bool HandshakeFirst { get; set; } + public TimeSpan FallbackDelay { get; set; } + public double Timeout { get; set; } + public long RateLimit { get; set; } + public bool AllowInsecureCiphers { get; set; } + public List CurvePreferences { get; set; } = []; + public PinnedCertSet? PinnedCerts { get; set; } + public string CertMatch { get; set; } = string.Empty; + public bool CertMatchSkipInvalid { get; set; } + public List CaCertsMatch { get; set; } = []; + public List Certificates { get; set; } = []; + public SslProtocols MinVersion { get; set; } +} + +/// +/// Certificate and key file pair. +/// Mirrors TLSCertPairOpt in opts.go. +/// +public class TlsCertPairOpt +{ + public string CertFile { get; set; } = string.Empty; + public string KeyFile { get; set; } = string.Empty; +} + +/// +/// OCSP stapling configuration. +/// Mirrors OCSPConfig in opts.go. +/// +public class OcspConfig +{ + public OcspMode Mode { get; set; } + public List OverrideUrls { get; set; } = []; +} + +/// +/// OCSP response cache configuration. +/// Mirrors OCSPResponseCacheConfig in opts.go. +/// +public class OcspResponseCacheConfig +{ + public string Type { get; set; } = string.Empty; + public string LocalStore { get; set; } = string.Empty; + public bool PreserveRevoked { get; set; } + public double SaveInterval { get; set; } +} + +/// +/// Cluster configuration options. +/// Mirrors ClusterOpts in opts.go. +/// +public class ClusterOpts +{ + public string Name { get; set; } = string.Empty; + public string Host { get; set; } = string.Empty; + public int Port { get; set; } + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public double AuthTimeout { get; set; } + public double TlsTimeout { get; set; } + public SslServerAuthenticationOptions? TlsConfig { get; set; } + public bool TlsMap { get; set; } + public bool TlsCheckKnownUrls { get; set; } + public PinnedCertSet? TlsPinnedCerts { get; set; } + public bool TlsHandshakeFirst { get; set; } + public TimeSpan TlsHandshakeFirstFallback { get; set; } + public string ListenStr { get; set; } = string.Empty; + public string Advertise { get; set; } = string.Empty; + public bool NoAdvertise { get; set; } + public int ConnectRetries { get; set; } + public bool ConnectBackoff { get; set; } + public int PoolSize { get; set; } + public List PinnedAccounts { get; set; } = []; + public CompressionOpts Compression { get; set; } = new(); + public TimeSpan PingInterval { get; set; } + public int MaxPingsOut { get; set; } + public TimeSpan WriteDeadline { get; set; } + public WriteTimeoutPolicy WriteTimeout { get; set; } + internal TlsConfigOpts? TlsConfigOpts { get; set; } +} + +/// +/// Gateway configuration options. +/// Mirrors GatewayOpts in opts.go. +/// +public class GatewayOpts +{ + public string Name { get; set; } = string.Empty; + public string Host { get; set; } = string.Empty; + public int Port { get; set; } + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public double AuthTimeout { get; set; } + public SslServerAuthenticationOptions? TlsConfig { get; set; } + public double TlsTimeout { get; set; } + public bool TlsMap { get; set; } + public bool TlsCheckKnownUrls { get; set; } + public PinnedCertSet? TlsPinnedCerts { get; set; } + public string Advertise { get; set; } = string.Empty; + public int ConnectRetries { get; set; } + public bool ConnectBackoff { get; set; } + public List Gateways { get; set; } = []; + public bool RejectUnknown { get; set; } + public TimeSpan WriteDeadline { get; set; } + public WriteTimeoutPolicy WriteTimeout { get; set; } + internal TlsConfigOpts? TlsConfigOpts { get; set; } +} + +/// +/// Remote gateway connection options. +/// Mirrors RemoteGatewayOpts in opts.go. +/// +public class RemoteGatewayOpts +{ + public string Name { get; set; } = string.Empty; + public SslServerAuthenticationOptions? TlsConfig { get; set; } + public double TlsTimeout { get; set; } + public List Urls { get; set; } = []; + internal TlsConfigOpts? TlsConfigOpts { get; set; } +} + +/// +/// Leaf node configuration options. +/// Mirrors LeafNodeOpts in opts.go. +/// +public class LeafNodeOpts +{ + public string Host { get; set; } = string.Empty; + public int Port { get; set; } + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public bool ProxyRequired { get; set; } + public string Nkey { get; set; } = string.Empty; + public string Account { get; set; } = string.Empty; + public double AuthTimeout { get; set; } + public SslServerAuthenticationOptions? TlsConfig { get; set; } + public double TlsTimeout { get; set; } + public bool TlsMap { get; set; } + public PinnedCertSet? TlsPinnedCerts { get; set; } + public bool TlsHandshakeFirst { get; set; } + public TimeSpan TlsHandshakeFirstFallback { get; set; } + public string Advertise { get; set; } = string.Empty; + public bool NoAdvertise { get; set; } + public TimeSpan ReconnectInterval { get; set; } + public TimeSpan WriteDeadline { get; set; } + public WriteTimeoutPolicy WriteTimeout { get; set; } + public CompressionOpts Compression { get; set; } = new(); + public List Remotes { get; set; } = []; + public string MinVersion { get; set; } = string.Empty; + public bool IsolateLeafnodeInterest { get; set; } + internal TlsConfigOpts? TlsConfigOpts { get; set; } +} + +/// +/// Signature handler delegate for NKey authentication. +/// Mirrors SignatureHandler in opts.go. +/// +public delegate (string jwt, byte[] signature, Exception? err) SignatureHandler(byte[] nonce); + +/// +/// Options for connecting to a remote leaf node. +/// Mirrors RemoteLeafOpts in opts.go. +/// +public class RemoteLeafOpts +{ + public string LocalAccount { get; set; } = string.Empty; + public bool NoRandomize { get; set; } + public List Urls { get; set; } = []; + public string Credentials { get; set; } = string.Empty; + public string Nkey { get; set; } = string.Empty; + public SignatureHandler? SignatureCb { get; set; } + public bool Tls { get; set; } + public SslServerAuthenticationOptions? TlsConfig { get; set; } + public double TlsTimeout { get; set; } + public bool TlsHandshakeFirst { get; set; } + public bool Hub { get; set; } + public List DenyImports { get; set; } = []; + public List DenyExports { get; set; } = []; + public TimeSpan FirstInfoTimeout { get; set; } + public CompressionOpts Compression { get; set; } = new(); + public RemoteLeafWebsocketOpts Websocket { get; set; } = new(); + public RemoteLeafProxyOpts Proxy { get; set; } = new(); + public bool JetStreamClusterMigrate { get; set; } + public TimeSpan JetStreamClusterMigrateDelay { get; set; } + public bool LocalIsolation { get; set; } + public bool RequestIsolation { get; set; } + public bool Disabled { get; set; } + internal TlsConfigOpts? TlsConfigOpts { get; set; } +} + +/// WebSocket sub-options for a remote leaf connection. +public class RemoteLeafWebsocketOpts +{ + public bool Compression { get; set; } + public bool NoMasking { get; set; } +} + +/// HTTP proxy sub-options for a remote leaf connection. +public class RemoteLeafProxyOpts +{ + public string Url { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public TimeSpan Timeout { get; set; } +} + +/// +/// WebSocket configuration options. +/// Mirrors WebsocketOpts in opts.go. +/// +public class WebsocketOpts +{ + public string Host { get; set; } = string.Empty; + public int Port { get; set; } + public string Advertise { get; set; } = string.Empty; + public string NoAuthUser { get; set; } = string.Empty; + public string JwtCookie { get; set; } = string.Empty; + public string UsernameCookie { get; set; } = string.Empty; + public string PasswordCookie { get; set; } = string.Empty; + public string TokenCookie { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Token { get; set; } = string.Empty; + public double AuthTimeout { get; set; } + public bool NoTls { get; set; } + public SslServerAuthenticationOptions? TlsConfig { get; set; } + public bool TlsMap { get; set; } + public PinnedCertSet? TlsPinnedCerts { get; set; } + public bool SameOrigin { get; set; } + public List AllowedOrigins { get; set; } = []; + public bool Compression { get; set; } + public TimeSpan HandshakeTimeout { get; set; } + public TimeSpan PingInterval { get; set; } + public Dictionary Headers { get; set; } = new(); + internal TlsConfigOpts? TlsConfigOpts { get; set; } +} + +/// +/// MQTT configuration options. +/// Mirrors MQTTOpts in opts.go. +/// +public class MqttOpts +{ + public string Host { get; set; } = string.Empty; + public int Port { get; set; } + public string NoAuthUser { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Token { get; set; } = string.Empty; + public string JsDomain { get; set; } = string.Empty; + public int StreamReplicas { get; set; } + public int ConsumerReplicas { get; set; } + public bool ConsumerMemoryStorage { get; set; } + public TimeSpan ConsumerInactiveThreshold { get; set; } + public double AuthTimeout { get; set; } + public SslServerAuthenticationOptions? TlsConfig { get; set; } + public bool TlsMap { get; set; } + public double TlsTimeout { get; set; } + public PinnedCertSet? TlsPinnedCerts { get; set; } + public TimeSpan AckWait { get; set; } + public TimeSpan JsApiTimeout { get; set; } + public ushort MaxAckPending { get; set; } + internal TlsConfigOpts? TlsConfigOpts { get; set; } + internal bool RejectQoS2Pub { get; set; } + internal bool DowngradeQoS2Sub { get; set; } +} + +/// +/// JetStream server-level limits. +/// Mirrors JSLimitOpts in opts.go. +/// +public class JsLimitOpts +{ + public int MaxRequestBatch { get; set; } + public int MaxAckPending { get; set; } + public int MaxHaAssets { get; set; } + public TimeSpan Duplicates { get; set; } + public int MaxBatchInflightPerStream { get; set; } + public int MaxBatchInflightTotal { get; set; } + public int MaxBatchSize { get; set; } + public TimeSpan MaxBatchTimeout { get; set; } +} + +/// +/// TPM configuration for JetStream encryption. +/// Mirrors JSTpmOpts in opts.go. +/// +public class JsTpmOpts +{ + public string KeysFile { get; set; } = string.Empty; + public string KeyPassword { get; set; } = string.Empty; + public string SrkPassword { get; set; } = string.Empty; + public int Pcr { get; set; } +} + +/// +/// Auth callout options for external authentication. +/// Mirrors AuthCallout in opts.go. +/// +public class AuthCalloutOpts +{ + public string Issuer { get; set; } = string.Empty; + public string Account { get; set; } = string.Empty; + public List AuthUsers { get; set; } = []; + public string XKey { get; set; } = string.Empty; + public List AllowedAccounts { get; set; } = []; +} + +/// +/// Proxy configuration for trusted proxies. +/// Mirrors ProxiesConfig in opts.go. +/// +public class ProxiesConfig +{ + public List Trusted { get; set; } = []; +} + +/// +/// A single trusted proxy identified by public key. +/// Mirrors ProxyConfig in opts.go. +/// +public class ProxyConfig +{ + public string Key { get; set; } = string.Empty; +} + +/// +/// Parsed authorization section from config file. +/// Mirrors the unexported authorization struct in opts.go. +/// +internal class AuthorizationConfig +{ + public string User { get; set; } = string.Empty; + public string Pass { get; set; } = string.Empty; + public string Token { get; set; } = string.Empty; + public string Nkey { get; set; } = string.Empty; + public string Acc { get; set; } = string.Empty; + public bool ProxyRequired { get; set; } + public double Timeout { get; set; } + public AuthCalloutOpts? Callout { get; set; } +} + +/// +/// Custom authentication interface. +/// Mirrors the Authentication interface in auth.go. +/// +public interface IAuthentication +{ + bool Check(IClientAuthentication client); +} + +/// +/// Client-side of authentication check. +/// Mirrors ClientAuthentication in auth.go. +/// +public interface IClientAuthentication +{ + string? GetOpts(); + bool IsTls(); + string? GetTlsConnectionState(); + string RemoteAddress(); +} + +/// +/// Account resolver interface for dynamic account loading. +/// Mirrors AccountResolver in accounts.go. +/// +public interface IAccountResolver +{ + (string jwt, Exception? err) Fetch(string name); + Exception? Store(string name, string jwt); + bool IsReadOnly(); + Exception? Start(object server); + bool IsTrackingUpdate(); + Exception? Reload(); + void Close(); +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs new file mode 100644 index 0000000..a356eb0 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs @@ -0,0 +1,569 @@ +// Copyright 2012-2025 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/opts.go in the NATS server Go source. + +using System.Runtime.InteropServices; +using System.Threading; + +namespace ZB.MOM.NatsNet.Server; + +// Global flag for unknown field handling. +internal static class ConfigFlags +{ + private static int _allowUnknownTopLevelField; + + /// + /// Sets whether unknown top-level config fields should be allowed. + /// Mirrors NoErrOnUnknownFields in opts.go. + /// + public static void NoErrOnUnknownFields(bool noError) => + Interlocked.Exchange(ref _allowUnknownTopLevelField, noError ? 1 : 0); + + public static bool AllowUnknownTopLevelField => + Interlocked.CompareExchange(ref _allowUnknownTopLevelField, 0, 0) != 0; +} + +public sealed partial class ServerOptions +{ + /// + /// Snapshot of command-line flags, populated during . + /// Mirrors FlagSnapshot in opts.go. + /// + public static ServerOptions? FlagSnapshot { get; internal set; } + + /// + /// Deep-copies this instance. + /// Mirrors Options.Clone() in opts.go. + /// + public ServerOptions Clone() + { + // Start with a shallow memberwise clone. + var clone = (ServerOptions)MemberwiseClone(); + + // Deep-copy reference types that need isolation. + if (Routes.Count > 0) + clone.Routes = Routes.Select(u => new Uri(u.ToString())).ToList(); + + clone.Cluster = CloneClusterOpts(Cluster); + clone.Gateway = CloneGatewayOpts(Gateway); + clone.LeafNode = CloneLeafNodeOpts(LeafNode); + clone.Websocket = CloneWebsocketOpts(Websocket); + clone.Mqtt = CloneMqttOpts(Mqtt); + + clone.Tags = [.. Tags]; + clone.Metadata = new Dictionary(Metadata); + clone.TrustedKeys = [.. TrustedKeys]; + clone.JsAccDefaultDomain = new Dictionary(JsAccDefaultDomain); + clone.InConfig = new Dictionary(InConfig); + clone.InCmdLine = new Dictionary(InCmdLine); + clone.OperatorJwt = [.. OperatorJwt]; + clone.ResolverPreloads = new Dictionary(ResolverPreloads); + clone.ResolverPinnedAccounts = [.. ResolverPinnedAccounts]; + + return clone; + } + + /// + /// Returns the SHA-256 digest of the configuration. + /// Mirrors Options.ConfigDigest() in opts.go. + /// + public string ConfigDigest() => ConfigDigestValue; + + // ------------------------------------------------------------------------- + // Merge / Baseline + // ------------------------------------------------------------------------- + + /// + /// Merges file-based options with command-line flag options. + /// Flag options override file options where set. + /// Mirrors MergeOptions in opts.go. + /// + public static ServerOptions MergeOptions(ServerOptions? fileOpts, ServerOptions? flagOpts) + { + if (fileOpts == null) return flagOpts ?? new ServerOptions(); + if (flagOpts == null) return fileOpts; + + var opts = fileOpts.Clone(); + + if (flagOpts.Port != 0) opts.Port = flagOpts.Port; + if (!string.IsNullOrEmpty(flagOpts.Host)) opts.Host = flagOpts.Host; + if (flagOpts.DontListen) opts.DontListen = true; + if (!string.IsNullOrEmpty(flagOpts.ClientAdvertise)) opts.ClientAdvertise = flagOpts.ClientAdvertise; + if (!string.IsNullOrEmpty(flagOpts.Username)) opts.Username = flagOpts.Username; + if (!string.IsNullOrEmpty(flagOpts.Password)) opts.Password = flagOpts.Password; + if (!string.IsNullOrEmpty(flagOpts.Authorization)) opts.Authorization = flagOpts.Authorization; + if (flagOpts.HttpPort != 0) opts.HttpPort = flagOpts.HttpPort; + if (!string.IsNullOrEmpty(flagOpts.HttpBasePath)) opts.HttpBasePath = flagOpts.HttpBasePath; + if (flagOpts.Debug) opts.Debug = true; + if (flagOpts.Trace) opts.Trace = true; + if (flagOpts.Logtime) opts.Logtime = true; + if (!string.IsNullOrEmpty(flagOpts.LogFile)) opts.LogFile = flagOpts.LogFile; + if (!string.IsNullOrEmpty(flagOpts.PidFile)) opts.PidFile = flagOpts.PidFile; + if (!string.IsNullOrEmpty(flagOpts.PortsFileDir)) opts.PortsFileDir = flagOpts.PortsFileDir; + if (flagOpts.ProfPort != 0) opts.ProfPort = flagOpts.ProfPort; + if (!string.IsNullOrEmpty(flagOpts.Cluster.ListenStr)) opts.Cluster.ListenStr = flagOpts.Cluster.ListenStr; + if (flagOpts.Cluster.NoAdvertise) opts.Cluster.NoAdvertise = true; + if (flagOpts.Cluster.ConnectRetries != 0) opts.Cluster.ConnectRetries = flagOpts.Cluster.ConnectRetries; + if (!string.IsNullOrEmpty(flagOpts.Cluster.Advertise)) opts.Cluster.Advertise = flagOpts.Cluster.Advertise; + if (!string.IsNullOrEmpty(flagOpts.RoutesStr)) MergeRoutes(opts, flagOpts); + if (flagOpts.JetStream) opts.JetStream = true; + if (!string.IsNullOrEmpty(flagOpts.StoreDir)) opts.StoreDir = flagOpts.StoreDir; + + return opts; + } + + /// + /// Parses route URLs from a comma-separated string. + /// Mirrors RoutesFromStr in opts.go. + /// + public static List RoutesFromStr(string routesStr) + { + var parts = routesStr.Split(','); + if (parts.Length == 0) return []; + + var urls = new List(); + foreach (var r in parts) + { + var trimmed = r.Trim(); + if (Uri.TryCreate(trimmed, UriKind.Absolute, out var u)) + urls.Add(u); + } + return urls; + } + + /// + /// Applies system-wide defaults to any unset options. + /// Mirrors setBaselineOptions in opts.go. + /// + public void SetBaselineOptions() + { + if (string.IsNullOrEmpty(Host)) + Host = ServerConstants.DefaultHost; + if (string.IsNullOrEmpty(HttpHost)) + HttpHost = Host; + if (Port == 0) + Port = ServerConstants.DefaultPort; + else if (Port == ServerConstants.RandomPort) + Port = 0; + if (MaxConn == 0) + MaxConn = ServerConstants.DefaultMaxConnections; + if (PingInterval == TimeSpan.Zero) + PingInterval = ServerConstants.DefaultPingInterval; + if (MaxPingsOut == 0) + MaxPingsOut = ServerConstants.DefaultPingMaxOut; + if (TlsTimeout == 0) + TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds; + if (AuthTimeout == 0) + AuthTimeout = GetDefaultAuthTimeout(TlsConfig, TlsTimeout); + + // Cluster defaults + if (Cluster.Port != 0 || !string.IsNullOrEmpty(Cluster.ListenStr)) + { + if (string.IsNullOrEmpty(Cluster.Host)) + Cluster.Host = ServerConstants.DefaultHost; + if (Cluster.TlsTimeout == 0) + Cluster.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds; + if (Cluster.AuthTimeout == 0) + Cluster.AuthTimeout = GetDefaultAuthTimeout(Cluster.TlsConfig, Cluster.TlsTimeout); + if (Cluster.PoolSize == 0) + Cluster.PoolSize = ServerConstants.DefaultRoutePoolSize; + + // Add system account to pinned accounts if pool is enabled. + if (Cluster.PoolSize > 0) + { + var sysAccName = SystemAccount; + if (string.IsNullOrEmpty(sysAccName) && !NoSystemAccount) + sysAccName = ServerConstants.DefaultSystemAccount; + if (!string.IsNullOrEmpty(sysAccName) && !Cluster.PinnedAccounts.Contains(sysAccName)) + Cluster.PinnedAccounts.Add(sysAccName); + } + + // Default compression to "accept". + if (string.IsNullOrEmpty(Cluster.Compression.Mode)) + Cluster.Compression.Mode = CompressionModes.Accept; + } + + // LeafNode defaults + if (LeafNode.Port != 0) + { + if (string.IsNullOrEmpty(LeafNode.Host)) + LeafNode.Host = ServerConstants.DefaultHost; + if (LeafNode.TlsTimeout == 0) + LeafNode.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds; + if (LeafNode.AuthTimeout == 0) + LeafNode.AuthTimeout = GetDefaultAuthTimeout(LeafNode.TlsConfig, LeafNode.TlsTimeout); + if (string.IsNullOrEmpty(LeafNode.Compression.Mode)) + LeafNode.Compression.Mode = CompressionModes.S2Auto; + } + + // Remote leafnode defaults + foreach (var r in LeafNode.Remotes) + { + foreach (var u in r.Urls) + { + if (u.IsDefaultPort || string.IsNullOrEmpty(u.GetComponents(UriComponents.Port, UriFormat.Unescaped))) + { + var builder = new UriBuilder(u) { Port = ServerConstants.DefaultLeafNodePort }; + r.Urls[r.Urls.IndexOf(u)] = builder.Uri; + } + } + if (string.IsNullOrEmpty(r.Compression.Mode)) + r.Compression.Mode = CompressionModes.S2Auto; + if (r.FirstInfoTimeout <= TimeSpan.Zero) + r.FirstInfoTimeout = ServerConstants.DefaultLeafNodeInfoWait; + } + + if (LeafNode.ReconnectInterval == TimeSpan.Zero) + LeafNode.ReconnectInterval = ServerConstants.DefaultLeafNodeReconnect; + + // Protocol limits + if (MaxControlLine == 0) + MaxControlLine = ServerConstants.MaxControlLineSize; + if (MaxPayload == 0) + MaxPayload = ServerConstants.MaxPayloadSize; + if (MaxPending == 0) + MaxPending = ServerConstants.MaxPendingSize; + if (WriteDeadline == TimeSpan.Zero) + WriteDeadline = ServerConstants.DefaultFlushDeadline; + if (MaxClosedClients == 0) + MaxClosedClients = ServerConstants.DefaultMaxClosedClients; + if (LameDuckDuration == TimeSpan.Zero) + LameDuckDuration = ServerConstants.DefaultLameDuckDuration; + if (LameDuckGracePeriod == TimeSpan.Zero) + LameDuckGracePeriod = ServerConstants.DefaultLameDuckGracePeriod; + + // Gateway defaults + if (Gateway.Port != 0) + { + if (string.IsNullOrEmpty(Gateway.Host)) + Gateway.Host = ServerConstants.DefaultHost; + if (Gateway.TlsTimeout == 0) + Gateway.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds; + if (Gateway.AuthTimeout == 0) + Gateway.AuthTimeout = GetDefaultAuthTimeout(Gateway.TlsConfig, Gateway.TlsTimeout); + } + + // Error reporting + if (ConnectErrorReports == 0) + ConnectErrorReports = ServerConstants.DefaultConnectErrorReports; + if (ReconnectErrorReports == 0) + ReconnectErrorReports = ServerConstants.DefaultReconnectErrorReports; + + // WebSocket defaults + if (Websocket.Port != 0) + { + if (string.IsNullOrEmpty(Websocket.Host)) + Websocket.Host = ServerConstants.DefaultHost; + } + + // MQTT defaults + if (Mqtt.Port != 0) + { + if (string.IsNullOrEmpty(Mqtt.Host)) + Mqtt.Host = ServerConstants.DefaultHost; + if (Mqtt.TlsTimeout == 0) + Mqtt.TlsTimeout = ServerConstants.TlsTimeout.TotalSeconds; + } + + // JetStream defaults + if (JetStreamMaxMemory == 0 && !MaxMemSet) + JetStreamMaxMemory = -1; + if (JetStreamMaxStore == 0 && !MaxStoreSet) + JetStreamMaxStore = -1; + if (SyncInterval == TimeSpan.Zero && !SyncSet) + SyncInterval = TimeSpan.FromMinutes(2); // defaultSyncInterval + if (JetStreamRequestQueueLimit <= 0) + JetStreamRequestQueueLimit = 4096; // JSDefaultRequestQueueLimit + } + + /// + /// Normalizes an HTTP base path (ensure leading slash, clean redundant separators). + /// Mirrors normalizeBasePath in opts.go. + /// + public static string NormalizeBasePath(string p) + { + if (string.IsNullOrEmpty(p)) return "/"; + if (p[0] != '/') p = "/" + p; + // Simple path clean: collapse repeated slashes and remove trailing slash. + while (p.Contains("//")) p = p.Replace("//", "/"); + return p.Length > 1 && p.EndsWith('/') ? p[..^1] : p; + } + + /// + /// Computes the default auth timeout based on TLS config presence. + /// Mirrors getDefaultAuthTimeout in opts.go. + /// + public static double GetDefaultAuthTimeout(object? tlsConfig, double tlsTimeout) + { + if (tlsConfig != null) + return tlsTimeout + 1.0; + return ServerConstants.AuthTimeout.TotalSeconds; + } + + /// + /// Returns the user's home directory. + /// Mirrors homeDir in opts.go. + /// + public static string HomeDir() => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + /// + /// Expands environment variables and ~/ prefix in a path. + /// Mirrors expandPath in opts.go. + /// + public static string ExpandPath(string p) + { + p = Environment.ExpandEnvironmentVariables(p); + if (!p.StartsWith('~')) return p; + return Path.Combine(HomeDir(), p[1..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + } + + /// + /// Reads a PID from a file path if possible, otherwise returns the string as-is. + /// Mirrors maybeReadPidFile in opts.go. + /// + public static string MaybeReadPidFile(string pidStr) + { + try { return File.ReadAllText(pidStr).Trim(); } + catch { return pidStr; } + } + + /// + /// Applies TLS overrides from command-line options. + /// Mirrors overrideTLS in opts.go. + /// + public Exception? OverrideTls() + { + if (string.IsNullOrEmpty(TlsCert)) + return new InvalidOperationException("TLS Server certificate must be present and valid"); + if (string.IsNullOrEmpty(TlsKey)) + return new InvalidOperationException("TLS Server private key must be present and valid"); + + // TLS config generation is deferred to GenTlsConfig (session 06+). + // For now, mark that TLS is enabled. + Tls = true; + return null; + } + + /// + /// Overrides cluster options from the --cluster flag. + /// Mirrors overrideCluster in opts.go. + /// + public Exception? OverrideCluster() + { + if (string.IsNullOrEmpty(Cluster.ListenStr)) + { + Cluster.Port = 0; + return null; + } + + var listenStr = Cluster.ListenStr; + var wantsRandom = false; + if (listenStr.EndsWith(":-1")) + { + wantsRandom = true; + listenStr = listenStr[..^3] + ":0"; + } + + if (!Uri.TryCreate(listenStr, UriKind.Absolute, out var clusterUri)) + return new InvalidOperationException($"could not parse cluster URL: {Cluster.ListenStr}"); + + Cluster.Host = clusterUri.Host; + Cluster.Port = wantsRandom ? -1 : clusterUri.Port; + + var userInfo = clusterUri.UserInfo; + if (!string.IsNullOrEmpty(userInfo)) + { + var parts = userInfo.Split(':', 2); + if (parts.Length != 2) + return new InvalidOperationException("expected cluster password to be set"); + Cluster.Username = parts[0]; + Cluster.Password = parts[1]; + } + else + { + Cluster.Username = string.Empty; + Cluster.Password = string.Empty; + } + + return null; + } + + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + + private static void MergeRoutes(ServerOptions opts, ServerOptions flagOpts) + { + var routeUrls = RoutesFromStr(flagOpts.RoutesStr); + if (routeUrls.Count == 0) return; + opts.Routes = routeUrls; + opts.RoutesStr = flagOpts.RoutesStr; + } + + internal static void TrackExplicitVal(Dictionary pm, string name, bool val) => + pm[name] = val; + + private static ClusterOpts CloneClusterOpts(ClusterOpts src) => new() + { + Name = src.Name, + Host = src.Host, + Port = src.Port, + Username = src.Username, + Password = src.Password, + AuthTimeout = src.AuthTimeout, + TlsTimeout = src.TlsTimeout, + TlsConfig = src.TlsConfig, + TlsMap = src.TlsMap, + TlsCheckKnownUrls = src.TlsCheckKnownUrls, + TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null, + TlsHandshakeFirst = src.TlsHandshakeFirst, + TlsHandshakeFirstFallback = src.TlsHandshakeFirstFallback, + ListenStr = src.ListenStr, + Advertise = src.Advertise, + NoAdvertise = src.NoAdvertise, + ConnectRetries = src.ConnectRetries, + ConnectBackoff = src.ConnectBackoff, + PoolSize = src.PoolSize, + PinnedAccounts = [.. src.PinnedAccounts], + Compression = new CompressionOpts { Mode = src.Compression.Mode, RttThresholds = [.. src.Compression.RttThresholds] }, + PingInterval = src.PingInterval, + MaxPingsOut = src.MaxPingsOut, + WriteDeadline = src.WriteDeadline, + WriteTimeout = src.WriteTimeout, + }; + + private static GatewayOpts CloneGatewayOpts(GatewayOpts src) => new() + { + Name = src.Name, + Host = src.Host, + Port = src.Port, + Username = src.Username, + Password = src.Password, + AuthTimeout = src.AuthTimeout, + TlsConfig = src.TlsConfig, + TlsTimeout = src.TlsTimeout, + TlsMap = src.TlsMap, + TlsCheckKnownUrls = src.TlsCheckKnownUrls, + TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null, + Advertise = src.Advertise, + ConnectRetries = src.ConnectRetries, + ConnectBackoff = src.ConnectBackoff, + Gateways = src.Gateways.Select(g => new RemoteGatewayOpts + { + Name = g.Name, + TlsConfig = g.TlsConfig, + TlsTimeout = g.TlsTimeout, + Urls = g.Urls.Select(u => new Uri(u.ToString())).ToList(), + }).ToList(), + RejectUnknown = src.RejectUnknown, + WriteDeadline = src.WriteDeadline, + WriteTimeout = src.WriteTimeout, + }; + + private static LeafNodeOpts CloneLeafNodeOpts(LeafNodeOpts src) => new() + { + Host = src.Host, + Port = src.Port, + Username = src.Username, + Password = src.Password, + ProxyRequired = src.ProxyRequired, + Nkey = src.Nkey, + Account = src.Account, + AuthTimeout = src.AuthTimeout, + TlsConfig = src.TlsConfig, + TlsTimeout = src.TlsTimeout, + TlsMap = src.TlsMap, + TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null, + TlsHandshakeFirst = src.TlsHandshakeFirst, + TlsHandshakeFirstFallback = src.TlsHandshakeFirstFallback, + Advertise = src.Advertise, + NoAdvertise = src.NoAdvertise, + ReconnectInterval = src.ReconnectInterval, + WriteDeadline = src.WriteDeadline, + WriteTimeout = src.WriteTimeout, + Compression = new CompressionOpts { Mode = src.Compression.Mode, RttThresholds = [.. src.Compression.RttThresholds] }, + Remotes = src.Remotes.Select(r => new RemoteLeafOpts + { + LocalAccount = r.LocalAccount, + NoRandomize = r.NoRandomize, + Urls = r.Urls.Select(u => new Uri(u.ToString())).ToList(), + Credentials = r.Credentials, + Nkey = r.Nkey, + Tls = r.Tls, + TlsTimeout = r.TlsTimeout, + TlsHandshakeFirst = r.TlsHandshakeFirst, + Hub = r.Hub, + DenyImports = [.. r.DenyImports], + DenyExports = [.. r.DenyExports], + FirstInfoTimeout = r.FirstInfoTimeout, + Compression = new CompressionOpts { Mode = r.Compression.Mode, RttThresholds = [.. r.Compression.RttThresholds] }, + JetStreamClusterMigrate = r.JetStreamClusterMigrate, + JetStreamClusterMigrateDelay = r.JetStreamClusterMigrateDelay, + LocalIsolation = r.LocalIsolation, + RequestIsolation = r.RequestIsolation, + Disabled = r.Disabled, + }).ToList(), + MinVersion = src.MinVersion, + IsolateLeafnodeInterest = src.IsolateLeafnodeInterest, + }; + + private static WebsocketOpts CloneWebsocketOpts(WebsocketOpts src) => new() + { + Host = src.Host, + Port = src.Port, + Advertise = src.Advertise, + NoAuthUser = src.NoAuthUser, + JwtCookie = src.JwtCookie, + UsernameCookie = src.UsernameCookie, + PasswordCookie = src.PasswordCookie, + TokenCookie = src.TokenCookie, + Username = src.Username, + Password = src.Password, + Token = src.Token, + AuthTimeout = src.AuthTimeout, + NoTls = src.NoTls, + TlsConfig = src.TlsConfig, + TlsMap = src.TlsMap, + TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null, + SameOrigin = src.SameOrigin, + AllowedOrigins = [.. src.AllowedOrigins], + Compression = src.Compression, + HandshakeTimeout = src.HandshakeTimeout, + PingInterval = src.PingInterval, + Headers = new Dictionary(src.Headers), + }; + + private static MqttOpts CloneMqttOpts(MqttOpts src) => new() + { + Host = src.Host, + Port = src.Port, + NoAuthUser = src.NoAuthUser, + Username = src.Username, + Password = src.Password, + Token = src.Token, + JsDomain = src.JsDomain, + StreamReplicas = src.StreamReplicas, + ConsumerReplicas = src.ConsumerReplicas, + ConsumerMemoryStorage = src.ConsumerMemoryStorage, + ConsumerInactiveThreshold = src.ConsumerInactiveThreshold, + AuthTimeout = src.AuthTimeout, + TlsConfig = src.TlsConfig, + TlsMap = src.TlsMap, + TlsTimeout = src.TlsTimeout, + TlsPinnedCerts = src.TlsPinnedCerts != null ? new PinnedCertSet(src.TlsPinnedCerts) : null, + AckWait = src.AckWait, + JsApiTimeout = src.JsApiTimeout, + MaxAckPending = src.MaxAckPending, + }; +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs new file mode 100644 index 0000000..22921b8 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs @@ -0,0 +1,234 @@ +// Copyright 2012-2025 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/opts.go in the NATS server Go source. + +using System.Net.Security; +using System.Security.Authentication; +using System.Threading; + +namespace ZB.MOM.NatsNet.Server; + +/// +/// Server configuration options block. +/// Mirrors Options struct in opts.go. +/// +public sealed partial class ServerOptions +{ + // ------------------------------------------------------------------------- + // General / Startup + // ------------------------------------------------------------------------- + + public string ConfigFile { get; set; } = string.Empty; + public string ServerName { get; set; } = string.Empty; + public string Host { get; set; } = string.Empty; + public int Port { get; set; } + public bool DontListen { get; set; } + public string ClientAdvertise { get; set; } = string.Empty; + public bool CheckConfig { get; set; } + public string PidFile { get; set; } = string.Empty; + public string PortsFileDir { get; set; } = string.Empty; + + // ------------------------------------------------------------------------- + // Logging & Debugging + // ------------------------------------------------------------------------- + + public bool Trace { get; set; } + public bool Debug { get; set; } + public bool TraceVerbose { get; set; } + public bool TraceHeaders { get; set; } + public bool NoLog { get; set; } + public bool NoSigs { get; set; } + public bool Logtime { get; set; } + public bool LogtimeUtc { get; set; } + public string LogFile { get; set; } = string.Empty; + public long LogSizeLimit { get; set; } + public long LogMaxFiles { get; set; } + public bool Syslog { get; set; } + public string RemoteSyslog { get; set; } = string.Empty; + public int ProfPort { get; set; } + public int ProfBlockRate { get; set; } + public int MaxTracedMsgLen { get; set; } + + // ------------------------------------------------------------------------- + // Networking & Limits + // ------------------------------------------------------------------------- + + public int MaxConn { get; set; } + public int MaxSubs { get; set; } + public byte MaxSubTokens { get; set; } + public int MaxControlLine { get; set; } + public int MaxPayload { get; set; } + public long MaxPending { get; set; } + public bool NoFastProducerStall { get; set; } + public bool ProxyRequired { get; set; } + public bool ProxyProtocol { get; set; } + public int MaxClosedClients { get; set; } + + // ------------------------------------------------------------------------- + // Connectivity + // ------------------------------------------------------------------------- + + public TimeSpan PingInterval { get; set; } + public int MaxPingsOut { get; set; } + public TimeSpan WriteDeadline { get; set; } + public WriteTimeoutPolicy WriteTimeout { get; set; } + public TimeSpan LameDuckDuration { get; set; } + public TimeSpan LameDuckGracePeriod { get; set; } + + // ------------------------------------------------------------------------- + // HTTP / Monitoring + // ------------------------------------------------------------------------- + + public string HttpHost { get; set; } = string.Empty; + public int HttpPort { get; set; } + public string HttpBasePath { get; set; } = string.Empty; + public int HttpsPort { get; set; } + + // ------------------------------------------------------------------------- + // Authentication & Authorization + // ------------------------------------------------------------------------- + + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Authorization { get; set; } = string.Empty; + public double AuthTimeout { get; set; } + public string NoAuthUser { get; set; } = string.Empty; + public string DefaultSentinel { get; set; } = string.Empty; + public string SystemAccount { get; set; } = string.Empty; + public bool NoSystemAccount { get; set; } + public AuthCalloutOpts? AuthCallout { get; set; } + public bool AlwaysEnableNonce { get; set; } + public IAuthentication? CustomClientAuthentication { get; set; } + public IAuthentication? CustomRouterAuthentication { get; set; } + + // ------------------------------------------------------------------------- + // Sublist + // ------------------------------------------------------------------------- + + public bool NoSublistCache { get; set; } + public bool NoHeaderSupport { get; set; } + public bool DisableShortFirstPing { get; set; } + + // ------------------------------------------------------------------------- + // TLS (Client) + // ------------------------------------------------------------------------- + + public double TlsTimeout { get; set; } + public bool Tls { get; set; } + public bool TlsVerify { get; set; } + public bool TlsMap { get; set; } + public string TlsCert { get; set; } = string.Empty; + public string TlsKey { get; set; } = string.Empty; + public string TlsCaCert { get; set; } = string.Empty; + public SslServerAuthenticationOptions? TlsConfig { get; set; } + public PinnedCertSet? TlsPinnedCerts { get; set; } + public long TlsRateLimit { get; set; } + public bool TlsHandshakeFirst { get; set; } + public TimeSpan TlsHandshakeFirstFallback { get; set; } + public bool AllowNonTls { get; set; } + + // ------------------------------------------------------------------------- + // Cluster / Gateway / Leaf / WebSocket / MQTT + // ------------------------------------------------------------------------- + + public ClusterOpts Cluster { get; set; } = new(); + public GatewayOpts Gateway { get; set; } = new(); + public LeafNodeOpts LeafNode { get; set; } = new(); + public WebsocketOpts Websocket { get; set; } = new(); + public MqttOpts Mqtt { get; set; } = new(); + + // ------------------------------------------------------------------------- + // Routing + // ------------------------------------------------------------------------- + + public List Routes { get; set; } = []; + public string RoutesStr { get; set; } = string.Empty; + + // ------------------------------------------------------------------------- + // JetStream + // ------------------------------------------------------------------------- + + public bool JetStream { get; set; } + public bool NoJetStreamStrict { get; set; } + public long JetStreamMaxMemory { get; set; } + public long JetStreamMaxStore { get; set; } + public string JetStreamDomain { get; set; } = string.Empty; + public string JetStreamExtHint { get; set; } = string.Empty; + public string JetStreamKey { get; set; } = string.Empty; + public string JetStreamOldKey { get; set; } = string.Empty; + public StoreCipher JetStreamCipher { get; set; } + public string JetStreamUniqueTag { get; set; } = string.Empty; + public JsLimitOpts JetStreamLimits { get; set; } = new(); + public JsTpmOpts JetStreamTpm { get; set; } = new(); + public long JetStreamMaxCatchup { get; set; } + public long JetStreamRequestQueueLimit { get; set; } + public ulong JetStreamMetaCompact { get; set; } + public ulong JetStreamMetaCompactSize { get; set; } + public bool JetStreamMetaCompactSync { get; set; } + public int StreamMaxBufferedMsgs { get; set; } + public long StreamMaxBufferedSize { get; set; } + public string StoreDir { get; set; } = string.Empty; + public TimeSpan SyncInterval { get; set; } + public bool SyncAlways { get; set; } + public Dictionary JsAccDefaultDomain { get; set; } = new(); + public bool DisableJetStreamBanner { get; set; } + + // ------------------------------------------------------------------------- + // Security & Trust + // ------------------------------------------------------------------------- + + public List TrustedKeys { get; set; } = []; + public SslServerAuthenticationOptions? AccountResolverTlsConfig { get; set; } + public IAccountResolver? AccountResolver { get; set; } + public OcspConfig? OcspConfig { get; set; } + public OcspResponseCacheConfig? OcspCacheConfig { get; set; } + + // ------------------------------------------------------------------------- + // Tagging & Metadata + // ------------------------------------------------------------------------- + + public List Tags { get; set; } = []; + public Dictionary Metadata { get; set; } = new(); + + // ------------------------------------------------------------------------- + // Proxies + // ------------------------------------------------------------------------- + + public ProxiesConfig? Proxies { get; set; } + + // ------------------------------------------------------------------------- + // Connectivity error reporting + // ------------------------------------------------------------------------- + + public int ConnectErrorReports { get; set; } + public int ReconnectErrorReports { get; set; } + + // ------------------------------------------------------------------------- + // Internal / Private fields + // ------------------------------------------------------------------------- + + internal Dictionary InConfig { get; set; } = new(); + internal Dictionary InCmdLine { get; set; } = new(); + internal List OperatorJwt { get; set; } = []; + internal Dictionary ResolverPreloads { get; set; } = new(); + internal HashSet ResolverPinnedAccounts { get; set; } = []; + internal TimeSpan GatewaysSolicitDelay { get; set; } + internal int OverrideProto { get; set; } + internal bool MaxMemSet { get; set; } + internal bool MaxStoreSet { get; set; } + internal bool SyncSet { get; set; } + internal bool AuthBlockDefined { get; set; } + internal string ConfigDigestValue { get; set; } = string.Empty; + internal TlsConfigOpts? TlsConfigOpts { get; set; } +} diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs new file mode 100644 index 0000000..85dd93a --- /dev/null +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs @@ -0,0 +1,333 @@ +// Copyright 2012-2025 The NATS Authors +// Licensed under the Apache License, Version 2.0 + +using Shouldly; +using ZB.MOM.NatsNet.Server; + +namespace ZB.MOM.NatsNet.Server.Tests; + +/// +/// Tests for ServerOptions — mirrors tests from server/opts_test.go. +/// +public class ServerOptionsTests +{ + /// + /// Mirrors TestDefaultOptions — verifies baseline defaults are applied. + /// + [Fact] // T:2512 + public void DefaultOptions_ShouldSetBaselineDefaults() + { + var opts = new ServerOptions(); + opts.SetBaselineOptions(); + + opts.Host.ShouldBe(ServerConstants.DefaultHost); + opts.Port.ShouldBe(ServerConstants.DefaultPort); + opts.MaxConn.ShouldBe(ServerConstants.DefaultMaxConnections); + opts.HttpHost.ShouldBe(ServerConstants.DefaultHost); + opts.PingInterval.ShouldBe(ServerConstants.DefaultPingInterval); + opts.MaxPingsOut.ShouldBe(ServerConstants.DefaultPingMaxOut); + opts.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds); + opts.AuthTimeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds); + opts.MaxControlLine.ShouldBe(ServerConstants.MaxControlLineSize); + opts.MaxPayload.ShouldBe(ServerConstants.MaxPayloadSize); + opts.MaxPending.ShouldBe(ServerConstants.MaxPendingSize); + opts.WriteDeadline.ShouldBe(ServerConstants.DefaultFlushDeadline); + opts.MaxClosedClients.ShouldBe(ServerConstants.DefaultMaxClosedClients); + opts.LameDuckDuration.ShouldBe(ServerConstants.DefaultLameDuckDuration); + opts.LameDuckGracePeriod.ShouldBe(ServerConstants.DefaultLameDuckGracePeriod); + opts.LeafNode.ReconnectInterval.ShouldBe(ServerConstants.DefaultLeafNodeReconnect); + opts.ConnectErrorReports.ShouldBe(ServerConstants.DefaultConnectErrorReports); + opts.ReconnectErrorReports.ShouldBe(ServerConstants.DefaultReconnectErrorReports); + opts.JetStreamMaxMemory.ShouldBe(-1); + opts.JetStreamMaxStore.ShouldBe(-1); + opts.SyncInterval.ShouldBe(TimeSpan.FromMinutes(2)); + opts.JetStreamRequestQueueLimit.ShouldBe(4096); + } + + /// + /// Mirrors TestOptions_RandomPort — RANDOM_PORT should become 0 after baseline. + /// + [Fact] // T:2513 + public void RandomPort_ShouldResolveToZero() + { + var opts = new ServerOptions { Port = ServerConstants.RandomPort }; + opts.SetBaselineOptions(); + opts.Port.ShouldBe(0); + } + + /// + /// Mirrors TestMergeOverrides — flag options override file options. + /// + [Fact] // T:2516 + public void MergeOverrides_ShouldLetFlagsWin() + { + var fileOpts = new ServerOptions + { + Host = "0.0.0.0", + Port = 4222, + Username = "fileuser", + Password = "filepass", + }; + + var flagOpts = new ServerOptions + { + Port = 9999, + Username = "flaguser", + }; + + var merged = ServerOptions.MergeOptions(fileOpts, flagOpts); + + merged.Port.ShouldBe(9999); + merged.Username.ShouldBe("flaguser"); + // Host not overridden (empty in flags) + merged.Host.ShouldBe("0.0.0.0"); + // Password not overridden (empty in flags) + merged.Password.ShouldBe("filepass"); + } + + /// + /// Mirrors TestOptionsClone — deep copy should produce independent objects. + /// + [Fact] // T:2537 + public void Clone_ShouldDeepCopyAllFields() + { + var opts = new ServerOptions + { + ConfigFile = "./configs/test.conf", + Host = "127.0.0.1", + Port = 2222, + Username = "derek", + Password = "porkchop", + AuthTimeout = 1.0, + Debug = true, + Trace = true, + Logtime = false, + HttpPort = ServerConstants.DefaultHttpPort, + HttpBasePath = ServerConstants.DefaultHttpBasePath, + PidFile = "/tmp/nats-server/nats-server.pid", + ProfPort = 6789, + Syslog = true, + RemoteSyslog = "udp://foo.com:33", + MaxControlLine = 2048, + MaxPayload = 65536, + MaxConn = 100, + PingInterval = TimeSpan.FromSeconds(60), + MaxPingsOut = 3, + Cluster = new ClusterOpts + { + NoAdvertise = true, + ConnectRetries = 2, + WriteDeadline = TimeSpan.FromSeconds(3), + }, + Gateway = new GatewayOpts + { + Name = "A", + Gateways = + [ + new RemoteGatewayOpts { Name = "B", Urls = [new Uri("nats://host:5222")] }, + new RemoteGatewayOpts { Name = "C" }, + ], + }, + WriteDeadline = TimeSpan.FromSeconds(3), + Routes = [new Uri("nats://localhost:4222")], + }; + + var clone = opts.Clone(); + + // Values should match. + clone.ConfigFile.ShouldBe(opts.ConfigFile); + clone.Host.ShouldBe(opts.Host); + clone.Port.ShouldBe(opts.Port); + clone.Username.ShouldBe(opts.Username); + clone.Gateway.Name.ShouldBe("A"); + clone.Gateway.Gateways.Count.ShouldBe(2); + clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222"); + + // Mutating clone should not affect original. + clone.Username = "changed"; + opts.Username.ShouldBe("derek"); + + // Mutating original gateway URLs should not affect clone. + opts.Gateway.Gateways[0].Urls[0] = new Uri("nats://other:9999"); + clone.Gateway.Gateways[0].Urls[0].Authority.ShouldBe("host:5222"); + } + + /// + /// Mirrors TestOptionsCloneNilLists — clone of empty lists should produce empty lists. + /// + [Fact] // T:2538 + public void CloneNilLists_ShouldProduceEmptyLists() + { + var opts = new ServerOptions(); + var clone = opts.Clone(); + clone.Routes.ShouldNotBeNull(); + } + + /// + /// Mirrors TestExpandPath — tilde and env var expansion. + /// + [Fact] // T:2563 + public void ExpandPath_ShouldExpandTildeAndEnvVars() + { + // Absolute path should not change. + var result = ServerOptions.ExpandPath("/foo/bar"); + result.ShouldBe("/foo/bar"); + + // Tilde expansion. + var home = ServerOptions.HomeDir(); + var expanded = ServerOptions.ExpandPath("~/test"); + expanded.ShouldBe(Path.Combine(home, "test")); + } + + /// + /// Mirrors TestDefaultAuthTimeout — verifies auth timeout logic. + /// + [Fact] // T:2572 + public void DefaultAuthTimeout_ShouldBe2sWithoutTls() + { + var timeout = ServerOptions.GetDefaultAuthTimeout(null, 0); + timeout.ShouldBe(ServerConstants.AuthTimeout.TotalSeconds); + } + + [Fact] + public void DefaultAuthTimeout_ShouldBeTlsTimeoutPlusOneWithTls() + { + // When TLS is configured, auth timeout = tls_timeout + 1 + var timeout = ServerOptions.GetDefaultAuthTimeout(new object(), 4.0); + timeout.ShouldBe(5.0); + } + + /// + /// Tests RoutesFromStr parsing. + /// + [Fact] // T:2576 (partial) + public void RoutesFromStr_ShouldParseCommaSeparatedUrls() + { + var routes = ServerOptions.RoutesFromStr("nats://localhost:4222,nats://localhost:4223"); + routes.Count.ShouldBe(2); + routes[0].Authority.ShouldBe("localhost:4222"); + routes[1].Authority.ShouldBe("localhost:4223"); + } + + /// + /// Tests NormalizeBasePath. + /// + [Fact] // T:2581 + public void NormalizeBasePath_ShouldCleanPaths() + { + ServerOptions.NormalizeBasePath("").ShouldBe("/"); + ServerOptions.NormalizeBasePath("/").ShouldBe("/"); + ServerOptions.NormalizeBasePath("foo").ShouldBe("/foo"); + ServerOptions.NormalizeBasePath("/foo/").ShouldBe("/foo"); + ServerOptions.NormalizeBasePath("//foo//bar//").ShouldBe("/foo/bar"); + } + + /// + /// Tests ConfigFlags.NoErrOnUnknownFields. + /// + [Fact] // T:2502 + public void NoErrOnUnknownFields_ShouldToggleFlag() + { + ConfigFlags.NoErrOnUnknownFields(true); + ConfigFlags.AllowUnknownTopLevelField.ShouldBeTrue(); + + ConfigFlags.NoErrOnUnknownFields(false); + ConfigFlags.AllowUnknownTopLevelField.ShouldBeFalse(); + } + + /// + /// Tests SetBaseline with cluster port set. + /// + [Fact] + public void SetBaseline_WithClusterPort_ShouldApplyClusterDefaults() + { + var opts = new ServerOptions + { + Cluster = new ClusterOpts { Port = 6222 }, + }; + opts.SetBaselineOptions(); + + opts.Cluster.Host.ShouldBe(ServerConstants.DefaultHost); + opts.Cluster.TlsTimeout.ShouldBe(ServerConstants.TlsTimeout.TotalSeconds); + opts.Cluster.PoolSize.ShouldBe(ServerConstants.DefaultRoutePoolSize); + opts.Cluster.Compression.Mode.ShouldBe(CompressionModes.Accept); + // System account should be auto-pinned. + opts.Cluster.PinnedAccounts.ShouldContain(ServerConstants.DefaultSystemAccount); + } + + /// + /// Tests SetBaseline with leaf node port set. + /// + [Fact] + public void SetBaseline_WithLeafNodePort_ShouldApplyLeafDefaults() + { + var opts = new ServerOptions + { + LeafNode = new LeafNodeOpts { Port = 7422 }, + }; + opts.SetBaselineOptions(); + + opts.LeafNode.Host.ShouldBe(ServerConstants.DefaultHost); + opts.LeafNode.Compression.Mode.ShouldBe(CompressionModes.S2Auto); + } + + /// + /// Tests OverrideCluster with valid URL. + /// + [Fact] // T:2518 + public void OverrideCluster_ShouldParseUrlAndSetHostPort() + { + var opts = new ServerOptions + { + Cluster = new ClusterOpts { ListenStr = "nats://user:pass@127.0.0.1:6222" }, + }; + var err = opts.OverrideCluster(); + err.ShouldBeNull(); + opts.Cluster.Host.ShouldBe("127.0.0.1"); + opts.Cluster.Port.ShouldBe(6222); + opts.Cluster.Username.ShouldBe("user"); + opts.Cluster.Password.ShouldBe("pass"); + } + + /// + /// Tests OverrideCluster with empty string disables clustering. + /// + [Fact] + public void OverrideCluster_EmptyString_ShouldDisableClustering() + { + var opts = new ServerOptions + { + Cluster = new ClusterOpts { Port = 6222, ListenStr = "" }, + }; + var err = opts.OverrideCluster(); + err.ShouldBeNull(); + opts.Cluster.Port.ShouldBe(0); + } + + /// + /// Tests MaybeReadPidFile returns original string when file doesn't exist. + /// + [Fact] // T:2585 + public void MaybeReadPidFile_NonExistent_ShouldReturnOriginalString() + { + ServerOptions.MaybeReadPidFile("12345").ShouldBe("12345"); + } + + /// + /// Tests that MergeOptions handles null inputs. + /// + [Fact] + public void MergeOptions_NullInputs_ShouldReturnNonNull() + { + var result = ServerOptions.MergeOptions(null, null); + result.ShouldNotBeNull(); + + var fileOpts = new ServerOptions { Port = 1234 }; + var r1 = ServerOptions.MergeOptions(fileOpts, null); + r1.Port.ShouldBe(1234); + + var flagOpts = new ServerOptions { Port = 5678 }; + var r2 = ServerOptions.MergeOptions(null, flagOpts); + r2.Port.ShouldBe(5678); + } +} diff --git a/porting.db b/porting.db index 2e12d22..3c7bf3a 100644 Binary files a/porting.db and b/porting.db differ diff --git a/reports/current.md b/reports/current.md index b385c39..9062e7b 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-26 14:39:36 UTC +Generated: 2026-02-26 16:51:01 UTC ## Modules (12 total) @@ -13,17 +13,19 @@ Generated: 2026-02-26 14:39:36 UTC | Status | Count | |--------|-------| -| complete | 328 | -| n_a | 79 | -| not_started | 3266 | +| complete | 344 | +| n_a | 82 | +| not_started | 3180 | +| stub | 67 | ## Unit Tests (3257 total) | Status | Count | |--------|-------| -| complete | 139 | +| complete | 148 | | n_a | 49 | -| not_started | 3069 | +| not_started | 2980 | +| stub | 80 | ## Library Mappings (36 total) @@ -34,4 +36,4 @@ Generated: 2026-02-26 14:39:36 UTC ## Overall Progress -**606/6942 items complete (8.7%)** +**634/6942 items complete (9.1%)** diff --git a/reports/report_11c0b92.md b/reports/report_11c0b92.md new file mode 100644 index 0000000..9062e7b --- /dev/null +++ b/reports/report_11c0b92.md @@ -0,0 +1,39 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-26 16:51:01 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| complete | 11 | +| not_started | 1 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| complete | 344 | +| n_a | 82 | +| not_started | 3180 | +| stub | 67 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| complete | 148 | +| n_a | 49 | +| not_started | 2980 | +| stub | 80 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**634/6942 items complete (9.1%)**