Files
natsnet/dotnet/src/ZB.MOM.NatsNet.Server/ServerOptions.Methods.cs

2576 lines
101 KiB
C#

// 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.Text.Json;
using System.Threading;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Config;
namespace ZB.MOM.NatsNet.Server;
// Global flag for unknown field handling.
internal static class ConfigFlags
{
private static int _allowUnknownTopLevelField;
/// <summary>
/// Sets whether unknown top-level config fields should be allowed.
/// Mirrors <c>NoErrOnUnknownFields</c> in opts.go.
/// </summary>
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
{
/// <summary>
/// Toggles unknown top-level field handling for config parsing.
/// Mirrors <c>NoErrOnUnknownFields</c> in opts.go.
/// </summary>
public static void NoErrOnUnknownFields(bool noError) => ConfigFlags.NoErrOnUnknownFields(noError);
/// <summary>
/// Snapshot of command-line flags, populated during <see cref="ConfigureOptions"/>.
/// Mirrors <c>FlagSnapshot</c> in opts.go.
/// </summary>
public static ServerOptions? FlagSnapshot { get; internal set; }
/// <summary>
/// Deep-copies this <see cref="ServerOptions"/> instance.
/// Mirrors <c>Options.Clone()</c> in opts.go.
/// </summary>
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<string, string>(Metadata);
clone.TrustedKeys = [.. TrustedKeys];
clone.JsAccDefaultDomain = new Dictionary<string, string>(JsAccDefaultDomain);
clone.InConfig = new Dictionary<string, bool>(InConfig);
clone.InCmdLine = new Dictionary<string, bool>(InCmdLine);
clone.OperatorJwt = [.. OperatorJwt];
clone.ResolverPreloads = new Dictionary<string, string>(ResolverPreloads);
clone.ResolverPinnedAccounts = [.. ResolverPinnedAccounts];
return clone;
}
/// <summary>
/// Returns the SHA-256 digest of the configuration.
/// Mirrors <c>Options.ConfigDigest()</c> in opts.go.
/// </summary>
public string ConfigDigest() => ConfigDigestValue;
// -------------------------------------------------------------------------
// Merge / Baseline
// -------------------------------------------------------------------------
/// <summary>
/// Merges file-based options with command-line flag options.
/// Flag options override file options where set.
/// Mirrors <c>MergeOptions</c> in opts.go.
/// </summary>
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;
}
/// <summary>
/// Parses route URLs from a comma-separated string.
/// Mirrors <c>RoutesFromStr</c> in opts.go.
/// </summary>
public static List<Uri> RoutesFromStr(string routesStr)
{
var parts = routesStr.Split(',');
if (parts.Length == 0) return [];
var urls = new List<Uri>();
foreach (var r in parts)
{
var trimmed = r.Trim();
if (Uri.TryCreate(trimmed, UriKind.Absolute, out var u))
urls.Add(u);
}
return urls;
}
/// <summary>
/// Applies system-wide defaults to any unset options.
/// Mirrors <c>setBaselineOptions</c> in opts.go.
/// </summary>
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
}
/// <summary>
/// Normalizes an HTTP base path (ensure leading slash, clean redundant separators).
/// Mirrors <c>normalizeBasePath</c> in opts.go.
/// </summary>
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;
}
/// <summary>
/// Computes the default auth timeout based on TLS config presence.
/// Mirrors <c>getDefaultAuthTimeout</c> in opts.go.
/// </summary>
public static double GetDefaultAuthTimeout(object? tlsConfig, double tlsTimeout)
{
if (tlsConfig != null)
return tlsTimeout + 1.0;
return ServerConstants.AuthTimeout.TotalSeconds;
}
/// <summary>
/// Returns the user's home directory.
/// Mirrors <c>homeDir</c> in opts.go.
/// </summary>
public static string HomeDir() => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
/// <summary>
/// Expands environment variables and ~/ prefix in a path.
/// Mirrors <c>expandPath</c> in opts.go.
/// </summary>
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));
}
/// <summary>
/// Reads a PID from a file path if possible, otherwise returns the string as-is.
/// Mirrors <c>maybeReadPidFile</c> in opts.go.
/// </summary>
public static string MaybeReadPidFile(string pidStr)
{
try { return File.ReadAllText(pidStr).Trim(); }
catch { return pidStr; }
}
/// <summary>
/// Applies TLS overrides from command-line options.
/// Mirrors <c>overrideTLS</c> in opts.go.
/// </summary>
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;
}
/// <summary>
/// Overrides cluster options from the --cluster flag.
/// Mirrors <c>overrideCluster</c> in opts.go.
/// </summary>
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;
}
// -------------------------------------------------------------------------
// Batch 6: opts.go package-level parse/config helpers (F1)
// -------------------------------------------------------------------------
/// <summary>
/// Deep copies route/gateway URL lists.
/// Mirrors <c>deepCopyURLs</c> in opts.go.
/// </summary>
public static List<Uri>? DeepCopyURLs(IReadOnlyList<Uri>? urls)
{
if (urls == null)
return null;
var copied = new List<Uri>(urls.Count);
foreach (var u in urls)
copied.Add(new Uri(u.ToString(), UriKind.Absolute));
return copied;
}
/// <summary>
/// Loads server options from a config file.
/// Mirrors package-level <c>ProcessConfigFile</c> in opts.go.
/// </summary>
public static ServerOptions ProcessConfigFile(string configFile) =>
ServerOptionsConfiguration.ProcessConfigFile(configFile);
/// <summary>
/// Normalizes token-like values to plain CLR values.
/// Mirrors <c>unwrapValue</c> intent from opts.go.
/// </summary>
public static object? UnwrapValue(object? value) => NormalizeConfigValue(value);
/// <summary>
/// Converts a recovered panic/exception to an error list entry.
/// Mirrors <c>convertPanicToErrorList</c> in opts.go.
/// </summary>
public static void ConvertPanicToErrorList(Exception? panic, ICollection<Exception>? errors, string? context = null)
{
if (panic == null || errors == null)
return;
var message = string.IsNullOrWhiteSpace(context)
? "encountered panic while processing config"
: $"encountered panic while processing {context}";
errors.Add(new InvalidOperationException(message, panic));
}
/// <summary>
/// Converts a recovered panic/exception to a single error output.
/// Mirrors <c>convertPanicToError</c> in opts.go.
/// </summary>
public static void ConvertPanicToError(Exception? panic, ref Exception? error, string? context = null)
{
if (panic == null || error != null)
return;
var message = string.IsNullOrWhiteSpace(context)
? "encountered panic while processing config"
: $"encountered panic while processing {context}";
error = new InvalidOperationException(message, panic);
}
/// <summary>
/// Applies <c>system_account</c>/<c>system</c> config values.
/// Mirrors <c>configureSystemAccount</c> in opts.go.
/// </summary>
public static Exception? ConfigureSystemAccount(ServerOptions options, IReadOnlyDictionary<string, object?> config)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(config);
if (!TryGetFirst(config, ["system_account", "system"], out var value))
return null;
if (value is not string systemAccount)
return new InvalidOperationException("system account name must be a string");
options.SystemAccount = systemAccount;
return null;
}
/// <summary>
/// Builds a username/nkey identity map for duplicate detection.
/// Mirrors <c>setupUsersAndNKeysDuplicateCheckMap</c> in opts.go.
/// </summary>
public static HashSet<string> SetupUsersAndNKeysDuplicateCheckMap(ServerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var identities = new HashSet<string>(StringComparer.Ordinal);
if (options.Users != null)
{
foreach (var user in options.Users)
{
if (!string.IsNullOrWhiteSpace(user.Username))
identities.Add(user.Username);
}
}
if (options.Nkeys != null)
{
foreach (var user in options.Nkeys)
{
if (!string.IsNullOrWhiteSpace(user.Nkey))
identities.Add(user.Nkey);
}
}
return identities;
}
/// <summary>
/// Parses a duration from config value.
/// Mirrors <c>parseDuration</c> in opts.go.
/// </summary>
public static TimeSpan ParseDuration(
string field,
object? value,
ICollection<Exception>? errors = null,
ICollection<Exception>? warnings = null)
{
if (value is string s)
{
try
{
return NatsDurationJsonConverter.Parse(s);
}
catch (Exception ex)
{
errors?.Add(new InvalidOperationException($"error parsing {field}: {ex.Message}", ex));
return TimeSpan.Zero;
}
}
if (TryConvertToLong(value, out var legacySeconds))
{
warnings?.Add(new InvalidOperationException($"{field} should be converted to a duration"));
return TimeSpan.FromSeconds(legacySeconds);
}
errors?.Add(new InvalidOperationException($"{field} should be a duration string or number of seconds"));
return TimeSpan.Zero;
}
/// <summary>
/// Parses write timeout policy value.
/// Mirrors <c>parseWriteDeadlinePolicy</c> in opts.go.
/// </summary>
public static WriteTimeoutPolicy ParseWriteDeadlinePolicy(string value, ICollection<Exception>? errors = null) =>
value.ToLowerInvariant() switch
{
"default" => WriteTimeoutPolicy.Default,
"close" => WriteTimeoutPolicy.Close,
"retry" => WriteTimeoutPolicy.Retry,
_ => ParseWriteDeadlinePolicyFallback(value, errors),
};
/// <summary>
/// Parses <c>listen</c> values (<c>port</c> or <c>host:port</c>).
/// Mirrors <c>parseListen</c> in opts.go.
/// </summary>
public static (string Host, int Port) ParseListen(object? value)
{
if (TryConvertToLong(value, out var portOnly))
return (string.Empty, checked((int)portOnly));
if (value is not string address)
throw new InvalidOperationException($"expected port or host:port, got {value?.GetType().Name ?? "null"}");
if (!TrySplitHostPort(address, out var host, out var port))
throw new InvalidOperationException($"could not parse address string \"{address}\"");
return (host, port);
}
/// <summary>
/// Parses cluster block config.
/// Mirrors <c>parseCluster</c> in opts.go.
/// </summary>
public static Exception? ParseCluster(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null,
ICollection<Exception>? warnings = null)
{
ArgumentNullException.ThrowIfNull(options);
if (!TryGetMap(value, out var clusterMap))
return new InvalidOperationException($"Expected map to define cluster, got {value?.GetType().Name ?? "null"}");
foreach (var (rawKey, rawValue) in clusterMap)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "name":
{
var name = entry as string ?? string.Empty;
if (name.Contains(' '))
{
errors?.Add(new InvalidOperationException(ServerErrors.ErrClusterNameHasSpaces.Message));
break;
}
options.Cluster.Name = name;
break;
}
case "listen":
{
try
{
var (host, port) = ParseListen(entry);
options.Cluster.Host = host;
options.Cluster.Port = port;
}
catch (Exception ex)
{
errors?.Add(ex);
}
break;
}
case "port":
if (TryConvertToLong(entry, out var clusterPort))
options.Cluster.Port = checked((int)clusterPort);
break;
case "host":
case "net":
options.Cluster.Host = entry as string ?? string.Empty;
break;
case "authorization":
{
var auth = ParseSimpleAuthorization(entry, errors, warnings);
if (auth == null)
break;
if (auth.HasUsers)
{
errors?.Add(new InvalidOperationException("Cluster authorization does not allow multiple users"));
break;
}
if (!string.IsNullOrEmpty(auth.Token))
{
errors?.Add(new InvalidOperationException("Cluster authorization does not support tokens"));
break;
}
if (auth.HasCallout)
{
errors?.Add(new InvalidOperationException("Cluster authorization does not support callouts"));
break;
}
options.Cluster.Username = auth.Username;
options.Cluster.Password = auth.Password;
if (auth.TimeoutSeconds > 0)
options.Cluster.AuthTimeout = auth.TimeoutSeconds;
break;
}
case "routes":
if (TryGetArray(entry, out var routes))
options.Routes = ParseURLs(routes, "route", warnings, errors);
break;
case "cluster_advertise":
case "advertise":
options.Cluster.Advertise = entry as string ?? string.Empty;
break;
case "no_advertise":
if (TryConvertToBool(entry, out var noAdvertise))
{
options.Cluster.NoAdvertise = noAdvertise;
TrackExplicitVal(options.InConfig, "Cluster.NoAdvertise", noAdvertise);
}
break;
case "connect_retries":
if (TryConvertToLong(entry, out var retries))
options.Cluster.ConnectRetries = checked((int)retries);
break;
case "connect_backoff":
if (TryConvertToBool(entry, out var connectBackoff))
options.Cluster.ConnectBackoff = connectBackoff;
break;
case "compression":
{
var parseError = ParseCompression(
options.Cluster.Compression,
CompressionModes.S2Fast,
"compression",
entry);
if (parseError != null)
errors?.Add(parseError);
break;
}
case "ping_interval":
options.Cluster.PingInterval = ParseDuration("ping_interval", entry, errors, warnings);
break;
case "ping_max":
if (TryConvertToLong(entry, out var pingMax))
options.Cluster.MaxPingsOut = checked((int)pingMax);
break;
case "write_deadline":
options.Cluster.WriteDeadline = ParseDuration("write_deadline", entry, errors, warnings);
break;
case "write_timeout":
options.Cluster.WriteTimeout = ParseWriteDeadlinePolicy(entry as string ?? string.Empty, errors);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return null;
}
/// <summary>
/// Parses compression option values from bool/string/map forms.
/// Mirrors <c>parseCompression</c> in opts.go.
/// </summary>
public static Exception? ParseCompression(
CompressionOpts compression,
string chosenModeForOn,
string fieldName,
object? value)
{
ArgumentNullException.ThrowIfNull(compression);
switch (NormalizeConfigValue(value))
{
case string mode:
compression.Mode = mode;
return null;
case bool enabled:
compression.Mode = enabled ? chosenModeForOn : CompressionModes.Off;
return null;
default:
if (!TryGetMap(value, out var map))
return new InvalidOperationException(
$"field \"{fieldName}\" should be a boolean or a structure, got {value?.GetType().Name ?? "null"}");
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "mode":
compression.Mode = entry as string ?? string.Empty;
break;
case "rtt_thresholds":
case "thresholds":
case "rtts":
case "rtt":
if (!TryGetArray(entry, out var thresholds))
return new InvalidOperationException("rtt_thresholds should be an array");
foreach (var threshold in thresholds)
{
if (threshold is not string thresholdValue)
return new InvalidOperationException("rtt_thresholds entries should be duration strings");
compression.RttThresholds.Add(NatsDurationJsonConverter.Parse(thresholdValue));
}
break;
default:
return new InvalidOperationException($"unknown field \"{rawKey}\"");
}
}
return null;
}
}
/// <summary>
/// Parses URL arrays with duplicate detection.
/// Mirrors <c>parseURLs</c> in opts.go.
/// </summary>
public static List<Uri> ParseURLs(
IEnumerable<object?> values,
string type,
ICollection<Exception>? warnings = null,
ICollection<Exception>? errors = null)
{
var urls = new List<Uri>();
var dedupe = new HashSet<string>(StringComparer.Ordinal);
foreach (var rawValue in values)
{
if (NormalizeConfigValue(rawValue) is not string urlValue)
{
errors?.Add(new InvalidOperationException($"{type} url must be a string"));
continue;
}
if (!dedupe.Add(urlValue))
{
warnings?.Add(new InvalidOperationException($"Duplicate {type} entry detected: {urlValue}"));
continue;
}
try
{
urls.Add(ParseURL(urlValue, type));
}
catch (Exception ex)
{
errors?.Add(ex);
}
}
return urls;
}
/// <summary>
/// Parses a single URL entry.
/// Mirrors <c>parseURL</c> in opts.go.
/// </summary>
public static Uri ParseURL(string value, string type)
{
var trimmed = value.Trim();
if (!Uri.TryCreate(trimmed, UriKind.Absolute, out var parsed))
throw new InvalidOperationException($"error parsing {type} url [\"{trimmed}\"]");
return parsed;
}
/// <summary>
/// Parses gateway block config.
/// Mirrors <c>parseGateway</c> in opts.go.
/// </summary>
public static Exception? ParseGateway(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null,
ICollection<Exception>? warnings = null)
{
ArgumentNullException.ThrowIfNull(options);
if (!TryGetMap(value, out var gatewayMap))
return new InvalidOperationException($"Expected gateway to be a map, got {value?.GetType().Name ?? "null"}");
foreach (var (rawKey, rawValue) in gatewayMap)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "name":
{
var name = entry as string ?? string.Empty;
if (name.Contains(' '))
{
errors?.Add(new InvalidOperationException(ServerErrors.ErrGatewayNameHasSpaces.Message));
break;
}
options.Gateway.Name = name;
break;
}
case "listen":
{
try
{
var (host, port) = ParseListen(entry);
options.Gateway.Host = host;
options.Gateway.Port = port;
}
catch (Exception ex)
{
errors?.Add(ex);
}
break;
}
case "port":
if (TryConvertToLong(entry, out var gatewayPort))
options.Gateway.Port = checked((int)gatewayPort);
break;
case "host":
case "net":
options.Gateway.Host = entry as string ?? string.Empty;
break;
case "authorization":
{
var auth = ParseSimpleAuthorization(entry, errors, warnings);
if (auth == null)
break;
if (auth.HasUsers)
{
errors?.Add(new InvalidOperationException("Gateway authorization does not allow multiple users"));
break;
}
if (!string.IsNullOrEmpty(auth.Token))
{
errors?.Add(new InvalidOperationException("Gateway authorization does not support tokens"));
break;
}
if (auth.HasCallout)
{
errors?.Add(new InvalidOperationException("Gateway authorization does not support callouts"));
break;
}
options.Gateway.Username = auth.Username;
options.Gateway.Password = auth.Password;
if (auth.TimeoutSeconds > 0)
options.Gateway.AuthTimeout = auth.TimeoutSeconds;
break;
}
case "advertise":
options.Gateway.Advertise = entry as string ?? string.Empty;
break;
case "connect_retries":
if (TryConvertToLong(entry, out var retries))
options.Gateway.ConnectRetries = checked((int)retries);
break;
case "connect_backoff":
if (TryConvertToBool(entry, out var connectBackoff))
options.Gateway.ConnectBackoff = connectBackoff;
break;
case "reject_unknown":
case "reject_unknown_cluster":
if (TryConvertToBool(entry, out var rejectUnknown))
options.Gateway.RejectUnknown = rejectUnknown;
break;
case "write_deadline":
options.Gateway.WriteDeadline = ParseDuration("write_deadline", entry, errors, warnings);
break;
case "write_timeout":
options.Gateway.WriteTimeout = ParseWriteDeadlinePolicy(entry as string ?? string.Empty, errors);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return null;
}
// -------------------------------------------------------------------------
// Batch 6: opts.go package-level parse/config helpers (F2)
// -------------------------------------------------------------------------
/// <summary>
/// Parses JetStream account enablement/limits for an account-level block.
/// Mirrors <c>parseJetStreamForAccount</c> in opts.go.
/// </summary>
public static Exception? ParseJetStreamForAccount(
object? value,
Account account,
ICollection<Exception>? errors = null)
{
ArgumentNullException.ThrowIfNull(account);
var normalized = NormalizeConfigValue(value);
switch (normalized)
{
case bool enabled:
account.JetStreamLimits = enabled ? CreateDefaultJetStreamAccountTiers() : null;
return null;
case string mode:
switch (mode.Trim().ToLowerInvariant())
{
case "enabled":
case "enable":
account.JetStreamLimits = CreateDefaultJetStreamAccountTiers();
return null;
case "disabled":
case "disable":
account.JetStreamLimits = null;
return null;
default:
return new InvalidOperationException(
$"Expected 'enabled' or 'disabled' for string value, got '{mode}'");
}
default:
if (!TryGetMap(normalized, out var map))
{
return new InvalidOperationException(
$"Expected map, bool or string to define JetStream, got {normalized?.GetType().Name ?? "null"}");
}
var limits = CreateUnlimitedJetStreamAccountLimits();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "max_memory":
case "max_mem":
case "mem":
case "memory":
if (!TryConvertToLong(entry, out var maxMemory))
return new InvalidOperationException($"Expected a parseable size for \"{rawKey}\", got {entry}");
limits.MaxMemory = maxMemory;
break;
case "max_store":
case "max_file":
case "max_disk":
case "store":
case "disk":
if (!TryConvertToLong(entry, out var maxStore))
return new InvalidOperationException($"Expected a parseable size for \"{rawKey}\", got {entry}");
limits.MaxStore = maxStore;
break;
case "max_streams":
case "streams":
if (!TryConvertToLong(entry, out var maxStreams))
return new InvalidOperationException($"Expected a parseable size for \"{rawKey}\", got {entry}");
limits.MaxStreams = checked((int)maxStreams);
break;
case "max_consumers":
case "consumers":
if (!TryConvertToLong(entry, out var maxConsumers))
return new InvalidOperationException($"Expected a parseable size for \"{rawKey}\", got {entry}");
limits.MaxConsumers = checked((int)maxConsumers);
break;
case "max_bytes_required":
case "max_stream_bytes":
case "max_bytes":
if (!TryConvertToBool(entry, out var maxBytesRequired))
return new InvalidOperationException($"Expected a parseable bool for \"{rawKey}\", got {entry}");
limits.MaxBytesRequired = maxBytesRequired;
break;
case "mem_max_stream_bytes":
case "memory_max_stream_bytes":
if (!TryConvertToLong(entry, out var memoryMaxStreamBytes))
return new InvalidOperationException($"Expected a parseable size for \"{rawKey}\", got {entry}");
limits.MemoryMaxStreamBytes = memoryMaxStreamBytes;
break;
case "disk_max_stream_bytes":
case "store_max_stream_bytes":
if (!TryConvertToLong(entry, out var storeMaxStreamBytes))
return new InvalidOperationException($"Expected a parseable size for \"{rawKey}\", got {entry}");
limits.StoreMaxStreamBytes = storeMaxStreamBytes;
break;
case "max_ack_pending":
if (!TryConvertToLong(entry, out var maxAckPending))
return new InvalidOperationException($"Expected a parseable size for \"{rawKey}\", got {entry}");
limits.MaxAckPending = checked((int)maxAckPending);
break;
case "cluster_traffic":
{
var traffic = entry as string ?? string.Empty;
switch (traffic)
{
case "system":
case "":
account.NrgAccount = string.Empty;
break;
case "owner":
account.NrgAccount = account.Name;
break;
default:
return new InvalidOperationException(
$"Expected 'system' or 'owner' string value for \"{rawKey}\", got {entry}");
}
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
account.JetStreamLimits = new Dictionary<string, object>(StringComparer.Ordinal)
{
[string.Empty] = limits,
};
return null;
}
}
/// <summary>
/// Parses storage sizes from integer or suffixed-string values.
/// Mirrors <c>getStorageSize</c> in opts.go.
/// </summary>
public static long GetStorageSize(object? value)
{
if (TryConvertToLong(value, out var asLong))
return asLong;
if (NormalizeConfigValue(value) is not string raw)
throw new InvalidOperationException("must be int64 or string");
var s = raw.Trim();
if (s.Length == 0)
return 0;
var suffix = char.ToUpperInvariant(s[^1]);
var prefix = s[..^1];
if (!long.TryParse(prefix, out var parsed))
throw new InvalidOperationException($"invalid size value: {raw}");
return suffix switch
{
'K' => parsed << 10,
'M' => parsed << 20,
'G' => parsed << 30,
'T' => parsed << 40,
_ => throw new InvalidOperationException("sizes defined as strings must end in K, M, G, T"),
};
}
/// <summary>
/// Parses server-level JetStream limits.
/// Mirrors <c>parseJetStreamLimits</c> in opts.go.
/// </summary>
public static Exception? ParseJetStreamLimits(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null)
{
ArgumentNullException.ThrowIfNull(options);
if (!TryGetMap(value, out var map))
return new InvalidOperationException($"Expected a map to define JetStreamLimits, got {value?.GetType().Name ?? "null"}");
options.JetStreamLimits = new JsLimitOpts();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "max_ack_pending":
if (TryConvertToLong(entry, out var maxAckPending))
options.JetStreamLimits.MaxAckPending = checked((int)maxAckPending);
break;
case "max_ha_assets":
if (TryConvertToLong(entry, out var maxHaAssets))
options.JetStreamLimits.MaxHaAssets = checked((int)maxHaAssets);
break;
case "max_request_batch":
if (TryConvertToLong(entry, out var maxRequestBatch))
options.JetStreamLimits.MaxRequestBatch = checked((int)maxRequestBatch);
break;
case "duplicate_window":
options.JetStreamLimits.Duplicates = ParseDuration("duplicate_window", entry, errors, warnings: null);
break;
case "batch":
{
var parseError = ParseJetStreamLimitsBatch(entry, options, errors);
if (parseError != null)
return parseError;
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return null;
}
/// <summary>
/// Parses nested batch limits inside JetStream limits block.
/// Mirrors <c>parseJetStreamLimitsBatch</c> in opts.go.
/// </summary>
public static Exception? ParseJetStreamLimitsBatch(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null)
{
ArgumentNullException.ThrowIfNull(options);
if (!TryGetMap(value, out var map))
return new InvalidOperationException($"Expected a map to define batch limits, got {value?.GetType().Name ?? "null"}");
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "max_inflight_per_stream":
if (TryConvertToLong(entry, out var maxInflightPerStream))
options.JetStreamLimits.MaxBatchInflightPerStream = checked((int)maxInflightPerStream);
break;
case "max_inflight_total":
if (TryConvertToLong(entry, out var maxInflightTotal))
options.JetStreamLimits.MaxBatchInflightTotal = checked((int)maxInflightTotal);
break;
case "max_msgs":
if (TryConvertToLong(entry, out var maxBatchSize))
options.JetStreamLimits.MaxBatchSize = checked((int)maxBatchSize);
break;
case "timeout":
options.JetStreamLimits.MaxBatchTimeout = ParseDuration("timeout", entry, errors, warnings: null);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return null;
}
/// <summary>
/// Parses JetStream TPM options.
/// Mirrors <c>parseJetStreamTPM</c> in opts.go.
/// </summary>
public static Exception? ParseJetStreamTPM(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null)
{
ArgumentNullException.ThrowIfNull(options);
if (!TryGetMap(value, out var map))
return new InvalidOperationException($"Expected a map to define JetStream TPM, got {value?.GetType().Name ?? "null"}");
options.JetStreamTpm = new JsTpmOpts();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "keys_file":
options.JetStreamTpm.KeysFile = entry as string ?? string.Empty;
break;
case "encryption_password":
options.JetStreamTpm.KeyPassword = entry as string ?? string.Empty;
break;
case "srk_password":
options.JetStreamTpm.SrkPassword = entry as string ?? string.Empty;
break;
case "pcr":
if (TryConvertToLong(entry, out var pcr))
options.JetStreamTpm.Pcr = checked((int)pcr);
break;
case "cipher":
{
var parseError = SetJetStreamEkCipher(options, entry);
if (parseError != null)
return parseError;
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return null;
}
/// <summary>
/// Parses JetStream encryption cipher selection.
/// Mirrors <c>setJetStreamEkCipher</c> in opts.go.
/// </summary>
public static Exception? SetJetStreamEkCipher(ServerOptions options, object? value)
{
ArgumentNullException.ThrowIfNull(options);
var cipher = (NormalizeConfigValue(value) as string ?? string.Empty).ToLowerInvariant();
switch (cipher)
{
case "chacha":
case "chachapoly":
options.JetStreamCipher = StoreCipher.ChaCha;
return null;
case "aes":
options.JetStreamCipher = StoreCipher.Aes;
return null;
default:
return new InvalidOperationException($"Unknown cipher type: \"{value}\"");
}
}
/// <summary>
/// Parses top-level JetStream enablement/configuration.
/// Mirrors <c>parseJetStream</c> in opts.go.
/// </summary>
public static Exception? ParseJetStream(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null,
ICollection<Exception>? warnings = null)
{
ArgumentNullException.ThrowIfNull(options);
var normalized = NormalizeConfigValue(value);
switch (normalized)
{
case bool enabled:
options.JetStream = enabled;
return null;
case string mode:
switch (mode.Trim().ToLowerInvariant())
{
case "enabled":
case "enable":
options.JetStream = true;
return null;
case "disabled":
case "disable":
options.JetStream = false;
return null;
default:
return new InvalidOperationException(
$"Expected 'enabled' or 'disabled' for string value, got '{mode}'");
}
default:
if (!TryGetMap(normalized, out var map))
{
return new InvalidOperationException(
$"Expected map, bool or string to define JetStream, got {normalized?.GetType().Name ?? "null"}");
}
var enable = true;
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "strict":
if (!TryConvertToBool(entry, out var strictMode))
return new InvalidOperationException($"Expected bool for \"{rawKey}\", got {entry}");
options.NoJetStreamStrict = !strictMode;
break;
case "store":
case "store_dir":
case "storedir":
if (!string.IsNullOrEmpty(options.StoreDir))
return new InvalidOperationException("Duplicate 'store_dir' configuration");
options.StoreDir = entry as string ?? string.Empty;
break;
case "sync":
case "sync_interval":
if ((entry as string)?.Equals("always", StringComparison.OrdinalIgnoreCase) == true)
{
options.SyncInterval = TimeSpan.FromMinutes(2);
options.SyncAlways = true;
}
else
{
options.SyncInterval = ParseDuration(rawKey, entry, errors, warnings);
}
options.SyncSet = true;
break;
case "max_memory_store":
case "max_mem_store":
case "max_mem":
options.JetStreamMaxMemory = GetStorageSize(entry);
options.MaxMemSet = true;
break;
case "max_file_store":
case "max_file":
options.JetStreamMaxStore = GetStorageSize(entry);
options.MaxStoreSet = true;
break;
case "domain":
options.JetStreamDomain = entry as string ?? string.Empty;
break;
case "enable":
case "enabled":
if (TryConvertToBool(entry, out var toggle))
enable = toggle;
break;
case "key":
case "ek":
case "encryption_key":
options.JetStreamKey = entry as string ?? string.Empty;
break;
case "prev_key":
case "prev_ek":
case "prev_encryption_key":
options.JetStreamOldKey = entry as string ?? string.Empty;
break;
case "cipher":
{
var parseError = SetJetStreamEkCipher(options, entry);
if (parseError != null)
return parseError;
break;
}
case "extension_hint":
options.JetStreamExtHint = entry as string ?? string.Empty;
break;
case "limits":
{
var parseError = ParseJetStreamLimits(entry, options, errors);
if (parseError != null)
return parseError;
break;
}
case "tpm":
{
var parseError = ParseJetStreamTPM(entry, options, errors);
if (parseError != null)
return parseError;
break;
}
case "unique_tag":
options.JetStreamUniqueTag = (entry as string ?? string.Empty).Trim().ToLowerInvariant();
break;
case "max_outstanding_catchup":
options.JetStreamMaxCatchup = GetStorageSize(entry);
break;
case "max_buffered_size":
options.StreamMaxBufferedSize = GetStorageSize(entry);
break;
case "max_buffered_msgs":
if (TryConvertToLong(entry, out var bufferedMsgs))
options.StreamMaxBufferedMsgs = checked((int)bufferedMsgs);
break;
case "request_queue_limit":
if (TryConvertToLong(entry, out var requestQueueLimit))
options.JetStreamRequestQueueLimit = requestQueueLimit;
break;
case "meta_compact":
if (TryConvertToLong(entry, out var compact))
{
if (compact < 0)
return new InvalidOperationException($"Expected an absolute size for \"{rawKey}\", got {entry}");
options.JetStreamMetaCompact = checked((ulong)compact);
}
break;
case "meta_compact_size":
{
var compactSize = GetStorageSize(entry);
if (compactSize < 0)
return new InvalidOperationException($"Expected an absolute size for \"{rawKey}\", got {entry}");
options.JetStreamMetaCompactSize = checked((ulong)compactSize);
break;
}
case "meta_compact_sync":
if (TryConvertToBool(entry, out var compactSync))
options.JetStreamMetaCompactSync = compactSync;
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
options.JetStream = enable;
return null;
}
}
/// <summary>
/// Parses leaf-node configuration.
/// Mirrors <c>parseLeafNodes</c> in opts.go.
/// </summary>
public static Exception? ParseLeafNodes(
object? value,
ServerOptions options,
ICollection<Exception>? errors = null,
ICollection<Exception>? warnings = null)
{
ArgumentNullException.ThrowIfNull(options);
if (!TryGetMap(value, out var map))
return new InvalidOperationException($"Expected map to define a leafnode, got {value?.GetType().Name ?? "null"}");
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "listen":
{
try
{
var (host, port) = ParseListen(entry);
options.LeafNode.Host = host;
options.LeafNode.Port = port;
}
catch (Exception ex)
{
errors?.Add(ex);
}
break;
}
case "port":
if (TryConvertToLong(entry, out var leafPort))
options.LeafNode.Port = checked((int)leafPort);
break;
case "host":
case "net":
options.LeafNode.Host = entry as string ?? string.Empty;
break;
case "authorization":
{
var auth = ParseLeafAuthorization(entry, errors, warnings);
if (auth == null)
break;
options.LeafNode.Username = auth.Username;
options.LeafNode.Password = auth.Password;
options.LeafNode.ProxyRequired = auth.ProxyRequired;
options.LeafNode.AuthTimeout = auth.TimeoutSeconds;
options.LeafNode.Account = auth.AccountName;
options.LeafNode.Users = auth.Users;
options.LeafNode.Nkey = auth.Nkey;
break;
}
case "remotes":
options.LeafNode.Remotes = ParseRemoteLeafNodes(entry, errors, warnings);
break;
case "reconnect":
case "reconnect_delay":
case "reconnect_interval":
options.LeafNode.ReconnectInterval = ParseDuration("reconnect", entry, errors, warnings);
break;
case "tls":
{
var (config, tlsOpts, parseError) = GetTLSConfig(entry, requireClientCertificate: false);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
options.LeafNode.TlsConfig = config;
options.LeafNode.TlsTimeout = tlsOpts?.Timeout > 0
? tlsOpts.Timeout
: ServerConstants.DefaultLeafTlsTimeout.TotalSeconds;
options.LeafNode.TlsMap = tlsOpts?.Map ?? false;
options.LeafNode.TlsPinnedCerts = tlsOpts?.PinnedCerts;
options.LeafNode.TlsHandshakeFirst = tlsOpts?.HandshakeFirst ?? false;
options.LeafNode.TlsHandshakeFirstFallback = tlsOpts?.FallbackDelay ?? TimeSpan.Zero;
options.LeafNode.TlsConfigOpts = tlsOpts;
break;
}
case "leafnode_advertise":
case "advertise":
options.LeafNode.Advertise = entry as string ?? string.Empty;
break;
case "no_advertise":
if (TryConvertToBool(entry, out var noAdvertise))
{
options.LeafNode.NoAdvertise = noAdvertise;
TrackExplicitVal(options.InConfig, "LeafNode.NoAdvertise", noAdvertise);
}
break;
case "min_version":
case "minimum_version":
options.LeafNode.MinVersion = entry as string ?? string.Empty;
break;
case "compression":
{
var parseError = ParseCompression(
options.LeafNode.Compression,
CompressionModes.S2Auto,
"compression",
entry);
if (parseError != null)
errors?.Add(parseError);
break;
}
case "isolate_leafnode_interest":
case "isolate":
if (TryConvertToBool(entry, out var isolate))
options.LeafNode.IsolateLeafnodeInterest = isolate;
break;
case "write_deadline":
options.LeafNode.WriteDeadline = ParseDuration("write_deadline", entry, errors, warnings);
break;
case "write_timeout":
options.LeafNode.WriteTimeout = ParseWriteDeadlinePolicy(entry as string ?? string.Empty, errors);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return null;
}
/// <summary>
/// Parses leaf-node authorization block.
/// Mirrors <c>parseLeafAuthorization</c> in opts.go.
/// </summary>
public static LeafAuthorization? ParseLeafAuthorization(
object? value,
ICollection<Exception>? errors = null,
ICollection<Exception>? warnings = null)
{
if (!TryGetMap(value, out var map))
{
errors?.Add(new InvalidOperationException("leafnode authorization should be a map"));
return null;
}
var auth = new LeafAuthorization();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "user":
case "username":
auth.Username = entry as string ?? string.Empty;
break;
case "pass":
case "password":
auth.Password = entry as string ?? string.Empty;
break;
case "nkey":
auth.Nkey = entry as string ?? string.Empty;
break;
case "timeout":
switch (entry)
{
case long l:
auth.TimeoutSeconds = l;
break;
case double d:
auth.TimeoutSeconds = d;
break;
case string s:
auth.TimeoutSeconds = ParseDuration("timeout", s, errors, warnings).TotalSeconds;
break;
default:
errors?.Add(new InvalidOperationException("error parsing leafnode authorization config, 'timeout' wrong type"));
break;
}
if (auth.TimeoutSeconds > TimeSpan.FromMinutes(1).TotalSeconds)
{
warnings?.Add(new InvalidOperationException(
$"timeout of {entry} ({auth.TimeoutSeconds} seconds) is high, consider keeping it under 60 seconds"));
}
break;
case "users":
auth.Users = ParseLeafUsers(entry, errors);
break;
case "account":
auth.AccountName = entry as string ?? string.Empty;
break;
case "proxy_required":
if (TryConvertToBool(entry, out var proxyRequired))
auth.ProxyRequired = proxyRequired;
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return auth;
}
/// <summary>
/// Parses leaf-node authorization users list.
/// Mirrors <c>parseLeafUsers</c> in opts.go.
/// </summary>
public static List<User> ParseLeafUsers(object? value, ICollection<Exception>? errors = null)
{
if (!TryGetArray(value, out var usersArray))
throw new InvalidOperationException($"Expected users field to be an array, got {value?.GetType().Name ?? "null"}");
var users = new List<User>(usersArray.Count);
foreach (var userEntry in usersArray)
{
if (!TryGetMap(userEntry, out var userMap))
{
errors?.Add(new InvalidOperationException($"Expected user entry to be a map/struct, got {userEntry}"));
continue;
}
var user = new User();
foreach (var (rawKey, rawValue) in userMap)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "user":
case "username":
user.Username = entry as string ?? string.Empty;
break;
case "pass":
case "password":
user.Password = entry as string ?? string.Empty;
break;
case "account":
user.Account = new Account { Name = entry as string ?? string.Empty };
break;
case "proxy_required":
if (TryConvertToBool(entry, out var proxyRequired))
user.ProxyRequired = proxyRequired;
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
users.Add(user);
}
return users;
}
/// <summary>
/// Parses remote leaf-node definitions.
/// Mirrors <c>parseRemoteLeafNodes</c> in opts.go.
/// </summary>
public static List<RemoteLeafOpts> ParseRemoteLeafNodes(
object? value,
ICollection<Exception>? errors = null,
ICollection<Exception>? warnings = null)
{
if (!TryGetArray(value, out var remoteArray))
throw new InvalidOperationException($"Expected remotes field to be an array, got {value?.GetType().Name ?? "null"}");
var remotes = new List<RemoteLeafOpts>(remoteArray.Count);
foreach (var remoteEntry in remoteArray)
{
if (!TryGetMap(remoteEntry, out var remoteMap))
{
errors?.Add(new InvalidOperationException(
$"Expected remote leafnode entry to be a map/struct, got {remoteEntry}"));
continue;
}
var remote = new RemoteLeafOpts();
foreach (var (rawKey, rawValue) in remoteMap)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "no_randomize":
case "dont_randomize":
if (TryConvertToBool(entry, out var noRandomize))
remote.NoRandomize = noRandomize;
break;
case "url":
case "urls":
if (entry is string singleUrl)
{
remote.Urls.Add(ParseURL(singleUrl, "leafnode"));
}
else if (TryGetArray(entry, out var urlArray))
{
remote.Urls = ParseURLs(urlArray, "leafnode", warnings, errors);
}
break;
case "account":
case "local":
remote.LocalAccount = entry as string ?? string.Empty;
break;
case "creds":
case "credentials":
remote.Credentials = ExpandPath(entry as string ?? string.Empty);
break;
case "nkey":
case "seed":
remote.Nkey = entry as string ?? string.Empty;
break;
case "tls":
{
var (config, tlsOpts, parseError) = GetTLSConfig(entry, requireClientCertificate: false);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
remote.TlsConfig = config;
remote.TlsTimeout = tlsOpts?.Timeout > 0
? tlsOpts.Timeout
: ServerConstants.DefaultLeafTlsTimeout.TotalSeconds;
remote.TlsHandshakeFirst = tlsOpts?.HandshakeFirst ?? false;
remote.TlsConfigOpts = tlsOpts;
break;
}
case "hub":
if (TryConvertToBool(entry, out var hub))
remote.Hub = hub;
break;
case "deny_imports":
case "deny_import":
remote.DenyImports = ParseStringList(entry);
break;
case "deny_exports":
case "deny_export":
remote.DenyExports = ParseStringList(entry);
break;
case "ws_compress":
case "ws_compression":
case "websocket_compress":
case "websocket_compression":
if (TryConvertToBool(entry, out var wsCompression))
remote.Websocket.Compression = wsCompression;
break;
case "ws_no_masking":
case "websocket_no_masking":
if (TryConvertToBool(entry, out var wsNoMasking))
remote.Websocket.NoMasking = wsNoMasking;
break;
case "jetstream_cluster_migrate":
case "js_cluster_migrate":
if (TryConvertToBool(entry, out var migrateToggle))
{
remote.JetStreamClusterMigrate = migrateToggle;
}
else if (TryGetMap(entry, out var migrateMap))
{
remote.JetStreamClusterMigrate = true;
if (migrateMap.TryGetValue("leader_migrate_delay", out var delayValue))
{
remote.JetStreamClusterMigrateDelay = ParseDuration(
"leader_migrate_delay",
delayValue,
errors,
warnings);
}
}
else
{
errors?.Add(new InvalidOperationException(
$"Expected boolean or map for jetstream_cluster_migrate, got {entry?.GetType().Name ?? "null"}"));
}
break;
case "isolate_leafnode_interest":
case "isolate":
if (TryConvertToBool(entry, out var localIsolation))
remote.LocalIsolation = localIsolation;
break;
case "request_isolation":
if (TryConvertToBool(entry, out var requestIsolation))
remote.RequestIsolation = requestIsolation;
break;
case "compression":
{
var parseError = ParseCompression(
remote.Compression,
CompressionModes.S2Auto,
"compression",
entry);
if (parseError != null)
errors?.Add(parseError);
break;
}
case "first_info_timeout":
remote.FirstInfoTimeout = ParseDuration("first_info_timeout", entry, errors, warnings);
break;
case "disabled":
if (TryConvertToBool(entry, out var disabled))
remote.Disabled = disabled;
break;
case "proxy":
if (TryGetMap(entry, out var proxyMap))
{
foreach (var (proxyKeyRaw, proxyValueRaw) in proxyMap)
{
var proxyKey = proxyKeyRaw.ToLowerInvariant();
var proxyValue = NormalizeConfigValue(proxyValueRaw);
switch (proxyKey)
{
case "url":
remote.Proxy.Url = proxyValue as string ?? string.Empty;
break;
case "username":
remote.Proxy.Username = proxyValue as string ?? string.Empty;
break;
case "password":
remote.Proxy.Password = proxyValue as string ?? string.Empty;
break;
case "timeout":
remote.Proxy.Timeout = ParseDuration("proxy timeout", proxyValue, errors, warnings);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{proxyKeyRaw}\""));
break;
}
}
}
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
remotes.Add(remote);
}
return remotes;
}
/// <summary>
/// Parses TLS config block and builds runtime TLS options.
/// Mirrors <c>getTLSConfig</c> in opts.go.
/// </summary>
public static (SslServerAuthenticationOptions? Config, TlsConfigOpts? Options, Exception? Error) GetTLSConfig(
object? value,
bool requireClientCertificate = true)
{
if (!TryGetMap(value, out var map))
return (null, null, new InvalidOperationException("TLS options should be a map"));
var tlsOpts = new TlsConfigOpts();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "cert_file":
case "cert":
tlsOpts.CertFile = entry as string ?? string.Empty;
break;
case "key_file":
case "key":
tlsOpts.KeyFile = entry as string ?? string.Empty;
break;
case "ca_file":
case "ca":
tlsOpts.CaFile = entry as string ?? string.Empty;
break;
case "verify":
if (TryConvertToBool(entry, out var verify))
tlsOpts.Verify = verify;
break;
case "map":
if (TryConvertToBool(entry, out var mapCert))
tlsOpts.Map = mapCert;
break;
case "timeout":
if (entry is string timeoutString)
{
tlsOpts.Timeout = ParseDuration("tls timeout", timeoutString).TotalSeconds;
}
else if (TryConvertToDouble(entry, out var timeoutSeconds))
{
tlsOpts.Timeout = timeoutSeconds;
}
break;
case "handshake_first":
if (TryConvertToBool(entry, out var handshakeFirst))
tlsOpts.HandshakeFirst = handshakeFirst;
break;
case "handshake_first_fallback":
case "fallback_delay":
tlsOpts.FallbackDelay = ParseDuration("fallback_delay", entry);
break;
case "pinned_certs":
tlsOpts.PinnedCerts = new PinnedCertSet(ParseStringList(entry));
break;
case "min_version":
if (entry is string minVersion)
{
try
{
tlsOpts.MinVersion = TlsVersionJsonConverter.Parse(minVersion);
}
catch (Exception ex)
{
return (null, null, ex);
}
}
break;
}
}
var enabledProtocols = tlsOpts.MinVersion switch
{
SslProtocols.Tls13 => SslProtocols.Tls13,
SslProtocols.Tls12 => SslProtocols.Tls12 | SslProtocols.Tls13,
_ => SslProtocols.Tls12 | SslProtocols.Tls13,
};
var config = new SslServerAuthenticationOptions
{
EnabledSslProtocols = enabledProtocols,
ClientCertificateRequired = requireClientCertificate || tlsOpts.Verify,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
};
return (config, tlsOpts, null);
}
/// <summary>
/// Parses list of remote gateways.
/// Mirrors <c>parseGateways</c> in opts.go.
/// </summary>
public static List<RemoteGatewayOpts> ParseGateways(
object? value,
ICollection<Exception>? errors = null,
ICollection<Exception>? warnings = null)
{
if (!TryGetArray(value, out var gatewayArray))
throw new InvalidOperationException($"Expected gateways field to be an array, got {value?.GetType().Name ?? "null"}");
var gateways = new List<RemoteGatewayOpts>(gatewayArray.Count);
foreach (var gatewayEntry in gatewayArray)
{
if (!TryGetMap(gatewayEntry, out var gatewayMap))
{
errors?.Add(new InvalidOperationException($"Expected gateway entry to be a map/struct, got {gatewayEntry}"));
continue;
}
var gateway = new RemoteGatewayOpts();
foreach (var (rawKey, rawValue) in gatewayMap)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "name":
gateway.Name = entry as string ?? string.Empty;
break;
case "tls":
{
var (config, tlsOpts, parseError) = GetTLSConfig(entry);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
gateway.TlsConfig = config;
gateway.TlsTimeout = tlsOpts?.Timeout ?? 0;
gateway.TlsConfigOpts = tlsOpts;
break;
}
case "url":
gateway.Urls.Add(ParseURL(entry as string ?? string.Empty, "gateway"));
break;
case "urls":
if (TryGetArray(entry, out var urlArray))
gateway.Urls = ParseURLs(urlArray, "gateway", warnings, errors);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
gateways.Add(gateway);
}
return gateways;
}
/// <summary>
/// Maps parsed pub/sub permissions to route import/export permissions.
/// Mirrors <c>setClusterPermissions</c> in opts.go.
/// </summary>
public static void SetClusterPermissions(ClusterOpts options, Permissions permissions)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(permissions);
options.Permissions = new RoutePermissions
{
Import = permissions.Publish?.Clone(),
Export = permissions.Subscribe?.Clone(),
};
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
private static Dictionary<string, object> CreateDefaultJetStreamAccountTiers() =>
new(StringComparer.Ordinal)
{
[string.Empty] = CreateUnlimitedJetStreamAccountLimits(),
};
private static JetStreamAccountLimits CreateUnlimitedJetStreamAccountLimits() => new()
{
MaxMemory = -1,
MaxStore = -1,
MaxStreams = -1,
MaxConsumers = -1,
MaxAckPending = -1,
MemoryMaxStreamBytes = -1,
StoreMaxStreamBytes = -1,
MaxBytesRequired = false,
};
public sealed class LeafAuthorization
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string Nkey { get; set; } = string.Empty;
public double TimeoutSeconds { get; set; }
public List<User>? Users { get; set; }
public string AccountName { get; set; } = string.Empty;
public bool ProxyRequired { get; set; }
}
private static WriteTimeoutPolicy ParseWriteDeadlinePolicyFallback(string value, ICollection<Exception>? errors)
{
errors?.Add(new InvalidOperationException(
$"write_timeout must be 'default', 'close' or 'retry' (received '{value}')"));
return WriteTimeoutPolicy.Default;
}
private sealed record ParsedAuthorization(
string Username,
string Password,
string Token,
double TimeoutSeconds,
bool HasUsers,
bool HasCallout);
private static ParsedAuthorization? ParseSimpleAuthorization(
object? value,
ICollection<Exception>? errors,
ICollection<Exception>? warnings)
{
if (!TryGetMap(value, out var map))
{
errors?.Add(new InvalidOperationException("authorization should be a map"));
return null;
}
string user = string.Empty;
string pass = string.Empty;
string token = string.Empty;
double timeout = 0;
var hasUsers = false;
var hasCallout = false;
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "user":
case "username":
user = entry as string ?? string.Empty;
break;
case "pass":
case "password":
pass = entry as string ?? string.Empty;
break;
case "token":
token = entry as string ?? string.Empty;
break;
case "timeout":
case "auth_timeout":
if (entry is string timeoutAsString)
{
timeout = ParseDuration("auth_timeout", timeoutAsString, errors, warnings).TotalSeconds;
}
else if (TryConvertToDouble(entry, out var timeoutSeconds))
{
timeout = timeoutSeconds;
}
break;
case "users":
case "nkeys":
hasUsers = true;
break;
case "callout":
case "auth_callout":
hasCallout = true;
break;
}
}
return new ParsedAuthorization(user, pass, token, timeout, hasUsers, hasCallout);
}
private static bool TrySplitHostPort(string value, out string host, out int port)
{
host = string.Empty;
port = 0;
if (Uri.TryCreate($"tcp://{value}", UriKind.Absolute, out var uri))
{
host = uri.Host;
if (uri.Port >= 0)
{
port = uri.Port;
return true;
}
}
var idx = value.LastIndexOf(':');
if (idx <= 0 || idx >= value.Length - 1)
return false;
host = value[..idx];
return int.TryParse(value[(idx + 1)..], out port);
}
private static bool TryGetFirst(
IReadOnlyDictionary<string, object?> map,
IEnumerable<string> keys,
out object? value)
{
foreach (var key in keys)
{
if (map.TryGetValue(key, out value))
{
value = NormalizeConfigValue(value);
return true;
}
}
value = null;
return false;
}
private static bool TryGetMap(object? value, out IReadOnlyDictionary<string, object?> map)
{
var normalized = NormalizeConfigValue(value);
if (normalized is IReadOnlyDictionary<string, object?> readonlyMap)
{
map = readonlyMap;
return true;
}
if (normalized is Dictionary<string, object?> dict)
{
map = dict;
return true;
}
map = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
return false;
}
private static bool TryGetArray(object? value, out IReadOnlyList<object?> values)
{
var normalized = NormalizeConfigValue(value);
if (normalized is IReadOnlyList<object?> readonlyValues)
{
values = readonlyValues;
return true;
}
if (normalized is List<object?> listValues)
{
values = listValues;
return true;
}
values = [];
return false;
}
private static List<string> ParseStringList(object? value)
{
if (TryGetArray(value, out var items))
{
var result = new List<string>(items.Count);
foreach (var item in items)
{
var s = NormalizeConfigValue(item) as string;
if (!string.IsNullOrEmpty(s))
result.Add(s);
}
return result;
}
if (NormalizeConfigValue(value) is string sValue)
return [sValue];
return [];
}
private static object? NormalizeConfigValue(object? value)
{
if (value is not JsonElement element)
return value;
return element.ValueKind switch
{
JsonValueKind.Object => element.EnumerateObject()
.ToDictionary(p => p.Name, p => NormalizeConfigValue(p.Value), StringComparer.OrdinalIgnoreCase),
JsonValueKind.Array => element.EnumerateArray()
.Select(item => NormalizeConfigValue(item))
.ToList(),
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => element.GetRawText(),
};
}
private static bool TryConvertToLong(object? value, out long converted)
{
switch (NormalizeConfigValue(value))
{
case long longValue:
converted = longValue;
return true;
case int intValue:
converted = intValue;
return true;
case short shortValue:
converted = shortValue;
return true;
case byte byteValue:
converted = byteValue;
return true;
case double doubleValue when doubleValue >= long.MinValue && doubleValue <= long.MaxValue:
converted = checked((long)doubleValue);
return true;
case float floatValue when floatValue >= long.MinValue && floatValue <= long.MaxValue:
converted = checked((long)floatValue);
return true;
case string s when long.TryParse(s, out var parsed):
converted = parsed;
return true;
default:
converted = 0;
return false;
}
}
private static bool TryConvertToDouble(object? value, out double converted)
{
switch (NormalizeConfigValue(value))
{
case double d:
converted = d;
return true;
case float f:
converted = f;
return true;
case long l:
converted = l;
return true;
case int i:
converted = i;
return true;
case string s when double.TryParse(s, out var parsed):
converted = parsed;
return true;
default:
converted = 0;
return false;
}
}
private static bool TryConvertToBool(object? value, out bool converted)
{
switch (NormalizeConfigValue(value))
{
case bool b:
converted = b;
return true;
case string s when bool.TryParse(s, out var parsed):
converted = parsed;
return true;
default:
converted = false;
return false;
}
}
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<string, bool> 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<string, string>(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,
};
}