From f08fc5d6a736c3dd1f701ca3aa34af2c137c9633 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 26 Feb 2026 11:51:01 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20port=20session=2003=20=E2=80=94=20Confi?= =?UTF-8?q?guration=20&=20Options=20types,=20Clone,=20MergeOptions,=20SetB?= =?UTF-8?q?aseline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ServerOptionTypes.cs: all supporting types — ClusterOpts, GatewayOpts, LeafNodeOpts, WebsocketOpts, MqttOpts, RemoteLeafOpts, RemoteGatewayOpts, CompressionOpts, TlsConfigOpts, JsLimitOpts, JsTpmOpts, AuthCalloutOpts, ProxiesConfig, IAuthentication, IAccountResolver, enums (WriteTimeoutPolicy, StoreCipher, OcspMode) - ServerOptions.cs: full Options struct with ~100 properties across 10 subsystems (general, logging, networking, TLS, cluster, gateway, leafnode, websocket, MQTT, JetStream) - ServerOptions.Methods.cs: Clone (deep copy), MergeOptions, SetBaselineOptions, RoutesFromStr, NormalizeBasePath, OverrideTls, OverrideCluster, ExpandPath, HomeDir, MaybeReadPidFile, GetDefaultAuthTimeout, ConfigFlags.NoErrOnUnknownFields - 17 tests covering defaults, random port, merge, clone, expand path, auth timeout, routes parsing, normalize path, cluster override, config flags - Config file parsing (processConfigFileLine 765-line function) deferred to follow-up - All 130 tests pass (129 unit + 1 integration) - DB: features 344/3673 complete, tests 148/3257 complete (9.1% overall) --- .../ServerOptionTypes.cs | 477 +++++++++++++++ .../ServerOptions.Methods.cs | 569 ++++++++++++++++++ .../ZB.MOM.NatsNet.Server/ServerOptions.cs | 234 +++++++ .../ServerOptionsTests.cs | 333 ++++++++++ porting.db | Bin 2465792 -> 2469888 bytes reports/current.md | 16 +- reports/report_11c0b92.md | 39 ++ 7 files changed, 1661 insertions(+), 7 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/ServerOptionTypes.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.cs create mode 100644 dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ServerOptionsTests.cs create mode 100644 reports/report_11c0b92.md 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 2e12d22104ee1a0b2135a883da6c82e6e53b9c3b..3c7bf3a8699c4b359d02baa0237a426781d90b99 100644 GIT binary patch delta 13526 zcmcgyd013Ow(q-k_pQ4$Alg7TaG?=FQ5F#eQ4kez14MB_1x14^f@0joZg-7KydCSs zWHO1GWlUnoh_*{qk~oRuBqpvgVV$xw)I#8N*2?6J7OS{6I>2v1b2oSr~d~@xdmli|I84LjMva#!1&MnBOpOX46dRrl;jd3q>`s+;P*VJ}#b) zqT;!5j%Dv~!@2%kPwqZ$0@uWOxSiZ%+;ir6=Bef~7^~8cz|$(-4~m(z-Itm`zd@%O zo3);8_x5_bx7T2N5*^(vDDA$z+V1UbBxjS^#pn0BD(c5hL><;nC$OEW#dbeoQ2xYxL&+yU-UZacSzTgc7eD!E}? zF4u#LK zR6HO)DsC6ohzrFTVx>4t%oTfxv7$|+!S~=}a1ERTuYlv=_uv573-*9rU<>eo|ZLro^r&`BZM_P-lxy{xLYd340wX;>SvX<{GA6hP0+AJ?v9kKl9p6yDC8xjWq7xXa8wW*6gT8klNkG?UNtU}BgAf&P*HlzxYPjXq58r#0F` zH_|n9DGCex53NoU+?hl>%=#;J)t6DgT=d76?`RU;N*$WJFP(uk{plF1g^8f+tEb81 zpf8usr==G7RW3aNy0Yk)zF=42uhut$!k>b|xuCEmC~OW2n_zV=-N!c|k4~UqVHVxZ zVrC*J)X5qVK49cqp453d=I-92nk* z?hbor(kZZ_5AB3@BsoRnijYCOHv#rcqHCX~3g|W~{}0m>)G-rR3LDC}IWl#OI!9fn zzNF)5nY#Osg*gig8j>=hZJI3?Z(i_|KC}uy?n8&cD}87id=*POUsHPf3TP&lfqxoE zhxrz8%w~!eSC|qWER2QeG^6;;IrKfsH)H_4f$|*`m^UfbWMZ0p>doi|1|w`6?lC$6!WgrTcjs+(T`nlskhB=@L7mfpG;l{pVip_nWR)*BG z-5@0k z8V?GBFn6;y62Y?Ec_|VyD*_fz0K4JupJbi>b{&&I1awRUaX|$S&_*DIJgkt16~;^i zkF{6mL$Fh{;Ru_!HaSH~#Ms%Bz|RKkgH<3kh;`4?N)Wb}J1#}Sj)fPiz*>V}biZno zL0sp+0Nq`*VPPgJNlKA)hn$@Z(qZOcDh{IY0hlxebdL@|k^9|^S}|%q**zj9rVI98 z-4w7BPJ2G+Ki5>?GQiiSYC{n`dmWP^WmA;XS3MQvQt;wXXTaaDRfDWPq2?v_1ydMR zYhpfTqUi|v9jaFPOez)gzyje_>vhX#mR$2JejoQf`vYC;%c!ApsLt*lS{<@K8`+;t z(N5m5kj6AE^gDgMRWb+Dv3wKXfi zsEd@jDN+h{>GJ8I)&Ny%K{o;&qSaz(Er!yOa8xa*@k8T}IHKU3SNS*)8tzcM7zWwg zGqpv?mLzNonuADqdIoTl4!Xc)GY!*Ks4YYtWDV5gEE3+HjRNcCnP9L1Kj1#ll^9g0 zHU0wsRSgRD<24}Ppp-t%pfp-*KuXCgQlw-&isko#IR>b+4#dX>!>7Bp073ok$IDT7 ze*=8E4%t;-2a*hWZ`6V0p#Ruiurpxv~2c%�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 QSXzod>hMP;`k-e2H}>IFX8-^I delta 13183 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 Sb?nK!c3nUB<|l9X+W!X|8!4y& 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%)**