// 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.Text.RegularExpressions;
using System.Threading;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Config;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
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
{
///
/// Toggles unknown top-level field handling for config parsing.
/// Mirrors NoErrOnUnknownFields in opts.go.
///
public static void NoErrOnUnknownFields(bool noError) => ConfigFlags.NoErrOnUnknownFields(noError);
///
/// 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;
}
// -------------------------------------------------------------------------
// Batch 6: opts.go package-level parse/config helpers (F1)
// -------------------------------------------------------------------------
///
/// Deep copies route/gateway URL lists.
/// Mirrors deepCopyURLs in opts.go.
///
public static List? DeepCopyURLs(IReadOnlyList? urls)
{
if (urls == null)
return null;
var copied = new List(urls.Count);
foreach (var u in urls)
copied.Add(new Uri(u.ToString(), UriKind.Absolute));
return copied;
}
///
/// Loads server options from a config file.
/// Mirrors package-level ProcessConfigFile in opts.go.
///
public static ServerOptions ProcessConfigFile(string configFile) =>
ServerOptionsConfiguration.ProcessConfigFile(configFile);
///
/// Receiver-style config loader that updates this instance with values from
/// .
/// Mirrors Go Options.ProcessConfigFile .
///
public Exception? ProcessConfigFileOverload2510(string configFile)
{
ConfigFile = configFile;
if (string.IsNullOrEmpty(configFile))
return null;
try
{
var data = File.ReadAllText(configFile);
ConfigDigestValue = ComputeConfigDigest(data);
return ProcessConfigString(data);
}
catch (Exception ex)
{
return ex;
}
}
///
/// Receiver-style config loader from in-memory content.
/// Mirrors Go Options.ProcessConfigString .
///
public Exception? ProcessConfigString(string data)
{
try
{
using var doc = JsonDocument.Parse(
data,
new JsonDocumentOptions
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip,
});
if (doc.RootElement.ValueKind != JsonValueKind.Object)
return new InvalidOperationException("configuration root must be an object");
var normalized = NormalizeConfigValue(doc.RootElement);
var configMap = normalized as IReadOnlyDictionary
?? normalized as Dictionary;
if (configMap == null)
return new InvalidOperationException("configuration root must be a key/value object");
return ProcessConfigFileInternal(string.Empty, configMap);
}
catch (Exception ex)
{
return ex;
}
}
///
/// Internal receiver config pipeline that processes each top-level config key.
/// Mirrors Go Options.processConfigFile .
///
public Exception? ProcessConfigFileInternal(string configFile, IReadOnlyDictionary config)
{
var errors = new List();
var warnings = new List();
if (config.Count == 0)
warnings.Add(new InvalidOperationException($"{configFile}: config has no values or is empty"));
var sysErr = ConfigureSystemAccount(this, config);
if (sysErr != null)
errors.Add(sysErr);
foreach (var (key, value) in config)
ProcessConfigFileLine(key, value, errors, warnings);
if (AuthCallout?.AllowedAccounts is { Count: > 0 })
{
var configuredAccounts = new HashSet(
Accounts.Select(a => a.Name),
StringComparer.Ordinal);
foreach (var account in AuthCallout.AllowedAccounts)
{
if (!configuredAccounts.Contains(account))
{
errors.Add(new InvalidOperationException(
$"auth_callout allowed account \"{account}\" not found in configured accounts"));
}
}
}
if (errors.Count == 0 && warnings.Count == 0)
return null;
return new ProcessConfigException(errors, warnings);
}
///
/// Processes a single top-level config key.
/// Mirrors Go Options.processConfigFileLine .
///
public void ProcessConfigFileLine(
string key,
object? value,
ICollection errors,
ICollection warnings)
{
try
{
var normalized = NormalizeConfigValue(value);
switch (key.ToLowerInvariant())
{
case "listen":
{
var (host, port) = ParseListen(normalized);
Host = host;
Port = port;
break;
}
case "client_advertise":
if (normalized is string ca)
ClientAdvertise = ca;
else
errors.Add(new InvalidOperationException("client_advertise must be a string"));
break;
case "port":
if (TryConvertToLong(normalized, out var p))
Port = checked((int)p);
else
errors.Add(new InvalidOperationException("port must be an integer"));
break;
case "server_name":
if (normalized is not string sn)
{
errors.Add(new InvalidOperationException("server_name must be a string"));
}
else if (sn.Contains(' '))
{
errors.Add(ServerErrors.ErrServerNameHasSpaces);
}
else
{
ServerName = sn;
}
break;
case "host":
case "net":
if (normalized is string configuredHost)
Host = configuredHost;
else
errors.Add(new InvalidOperationException($"{key} must be a string"));
break;
case "debug":
if (TryConvertToBool(normalized, out var debug))
{
Debug = debug;
TrackExplicitVal(InConfig, nameof(Debug), Debug);
}
else
{
errors.Add(new InvalidOperationException("debug must be a boolean"));
}
break;
case "trace":
if (TryConvertToBool(normalized, out var trace))
{
Trace = trace;
TrackExplicitVal(InConfig, nameof(Trace), Trace);
}
else
{
errors.Add(new InvalidOperationException("trace must be a boolean"));
}
break;
case "trace_verbose":
if (TryConvertToBool(normalized, out var traceVerbose))
{
TraceVerbose = traceVerbose;
Trace = traceVerbose;
TrackExplicitVal(InConfig, nameof(TraceVerbose), TraceVerbose);
TrackExplicitVal(InConfig, nameof(Trace), Trace);
}
else
{
errors.Add(new InvalidOperationException("trace_verbose must be a boolean"));
}
break;
case "trace_headers":
if (TryConvertToBool(normalized, out var traceHeaders))
{
TraceHeaders = traceHeaders;
Trace = traceHeaders;
TrackExplicitVal(InConfig, nameof(TraceHeaders), TraceHeaders);
TrackExplicitVal(InConfig, nameof(Trace), Trace);
}
else
{
errors.Add(new InvalidOperationException("trace_headers must be a boolean"));
}
break;
case "logtime":
if (TryConvertToBool(normalized, out var logtime))
{
Logtime = logtime;
TrackExplicitVal(InConfig, nameof(Logtime), Logtime);
}
else
{
errors.Add(new InvalidOperationException("logtime must be a boolean"));
}
break;
case "logtime_utc":
if (TryConvertToBool(normalized, out var logtimeUtc))
{
LogtimeUtc = logtimeUtc;
TrackExplicitVal(InConfig, nameof(LogtimeUtc), LogtimeUtc);
}
else
{
errors.Add(new InvalidOperationException("logtime_utc must be a boolean"));
}
break;
case "disable_sublist_cache":
case "no_sublist_cache":
if (TryConvertToBool(normalized, out var noSublistCache))
NoSublistCache = noSublistCache;
else
errors.Add(new InvalidOperationException($"{key} must be a boolean"));
break;
case "accounts":
{
var err = ParseAccounts(normalized, this, errors, warnings);
if (err != null)
errors.Add(err);
break;
}
case "default_sentinel":
if (normalized is string sentinel)
DefaultSentinel = sentinel;
else
errors.Add(new InvalidOperationException("default_sentinel must be a string"));
break;
case "authorization":
{
var (auth, err) = ParseAuthorization(normalized, errors, warnings);
if (err != null)
{
errors.Add(err);
break;
}
if (auth == null)
break;
AuthBlockDefined = true;
Username = auth.User;
Password = auth.Pass;
ProxyRequired = auth.ProxyRequired;
Authorization = auth.Token;
AuthTimeout = auth.TimeoutSeconds;
AuthCallout = auth.Callout;
if ((!string.IsNullOrEmpty(auth.User) || !string.IsNullOrEmpty(auth.Pass)) &&
!string.IsNullOrEmpty(auth.Token))
{
errors.Add(new InvalidOperationException("Cannot have a user/pass and token"));
}
break;
}
case "cluster":
{
var err = ParseCluster(normalized, this, errors, warnings);
if (err != null)
errors.Add(err);
break;
}
case "gateway":
{
var err = ParseGateway(normalized, this, errors, warnings);
if (err != null)
errors.Add(err);
break;
}
case "leafnodes":
{
var err = ParseLeafNodes(normalized, this, errors, warnings);
if (err != null)
errors.Add(err);
break;
}
case "routes":
if (normalized is string routesString)
{
RoutesStr = routesString;
Routes = RoutesFromStr(routesString);
break;
}
if (TryGetArray(normalized, out var routes))
{
Routes = ParseURLs(routes, "route", warnings, errors);
}
else
{
errors.Add(new InvalidOperationException("routes must be a string or array"));
}
break;
case "jetstream":
{
var err = ParseJetStream(normalized, this, errors, warnings);
if (err != null)
errors.Add(err);
break;
}
case "websocket":
{
var err = ParseWebsocket(normalized, this, errors, warnings);
if (err != null)
errors.Add(err);
break;
}
case "mqtt":
{
var err = ParseMQTT(normalized, this, errors, warnings);
if (err != null)
errors.Add(err);
break;
}
case "proxies":
{
var (proxies, err) = ParseProxies(normalized);
if (err != null)
errors.Add(err);
else
Proxies = proxies;
break;
}
case "system_account":
case "system":
{
var err = ConfigureSystemAccount(
this,
new Dictionary(StringComparer.OrdinalIgnoreCase)
{
[key] = normalized,
});
if (err != null)
errors.Add(err);
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors.Add(new InvalidOperationException($"unknown field \"{key}\""));
break;
}
}
catch (Exception ex)
{
errors.Add(ex);
}
}
///
/// Normalizes token-like values to plain CLR values.
/// Mirrors unwrapValue intent from opts.go.
///
public static object? UnwrapValue(object? value) => NormalizeConfigValue(value);
///
/// Converts a recovered panic/exception to an error list entry.
/// Mirrors convertPanicToErrorList in opts.go.
///
public static void ConvertPanicToErrorList(Exception? panic, ICollection? 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));
}
///
/// Converts a recovered panic/exception to a single error output.
/// Mirrors convertPanicToError in opts.go.
///
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);
}
///
/// Applies system_account /system config values.
/// Mirrors configureSystemAccount in opts.go.
///
public static Exception? ConfigureSystemAccount(ServerOptions options, IReadOnlyDictionary 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;
}
///
/// Builds a username/nkey identity map for duplicate detection.
/// Mirrors setupUsersAndNKeysDuplicateCheckMap in opts.go.
///
public static HashSet SetupUsersAndNKeysDuplicateCheckMap(ServerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
var identities = new HashSet(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;
}
///
/// Parses a duration from config value.
/// Mirrors parseDuration in opts.go.
///
public static TimeSpan ParseDuration(
string field,
object? value,
ICollection? errors = null,
ICollection? 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;
}
///
/// Parses write timeout policy value.
/// Mirrors parseWriteDeadlinePolicy in opts.go.
///
public static WriteTimeoutPolicy ParseWriteDeadlinePolicy(string value, ICollection? errors = null) =>
value.ToLowerInvariant() switch
{
"default" => WriteTimeoutPolicy.Default,
"close" => WriteTimeoutPolicy.Close,
"retry" => WriteTimeoutPolicy.Retry,
_ => ParseWriteDeadlinePolicyFallback(value, errors),
};
///
/// Parses listen values (port or host:port ).
/// Mirrors parseListen in opts.go.
///
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);
}
///
/// Parses cluster block config.
/// Mirrors parseCluster in opts.go.
///
public static Exception? ParseCluster(
object? value,
ServerOptions options,
ICollection? errors = null,
ICollection? 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;
}
///
/// Parses compression option values from bool/string/map forms.
/// Mirrors parseCompression in opts.go.
///
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;
}
}
///
/// Parses URL arrays with duplicate detection.
/// Mirrors parseURLs in opts.go.
///
public static List ParseURLs(
IEnumerable values,
string type,
ICollection? warnings = null,
ICollection? errors = null)
{
var urls = new List();
var dedupe = new HashSet(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;
}
///
/// Parses a single URL entry.
/// Mirrors parseURL in opts.go.
///
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;
}
///
/// Parses gateway block config.
/// Mirrors parseGateway in opts.go.
///
public static Exception? ParseGateway(
object? value,
ServerOptions options,
ICollection? errors = null,
ICollection? 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)
// -------------------------------------------------------------------------
///
/// Parses JetStream account enablement/limits for an account-level block.
/// Mirrors parseJetStreamForAccount in opts.go.
///
public static Exception? ParseJetStreamForAccount(
object? value,
Account account,
ICollection? 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(StringComparer.Ordinal)
{
[string.Empty] = limits,
};
return null;
}
}
///
/// Parses storage sizes from integer or suffixed-string values.
/// Mirrors getStorageSize in opts.go.
///
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"),
};
}
///
/// Parses server-level JetStream limits.
/// Mirrors parseJetStreamLimits in opts.go.
///
public static Exception? ParseJetStreamLimits(
object? value,
ServerOptions options,
ICollection? 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;
}
///
/// Parses nested batch limits inside JetStream limits block.
/// Mirrors parseJetStreamLimitsBatch in opts.go.
///
public static Exception? ParseJetStreamLimitsBatch(
object? value,
ServerOptions options,
ICollection? 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;
}
///
/// Parses JetStream TPM options.
/// Mirrors parseJetStreamTPM in opts.go.
///
public static Exception? ParseJetStreamTPM(
object? value,
ServerOptions options,
ICollection? 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;
}
///
/// Parses JetStream encryption cipher selection.
/// Mirrors setJetStreamEkCipher in opts.go.
///
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}\"");
}
}
///
/// Parses top-level JetStream enablement/configuration.
/// Mirrors parseJetStream in opts.go.
///
public static Exception? ParseJetStream(
object? value,
ServerOptions options,
ICollection? errors = null,
ICollection? 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;
}
}
///
/// Parses leaf-node configuration.
/// Mirrors parseLeafNodes in opts.go.
///
public static Exception? ParseLeafNodes(
object? value,
ServerOptions options,
ICollection? errors = null,
ICollection? 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;
}
///
/// Parses leaf-node authorization block.
/// Mirrors parseLeafAuthorization in opts.go.
///
public static LeafAuthorization? ParseLeafAuthorization(
object? value,
ICollection? errors = null,
ICollection? 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;
}
///
/// Parses leaf-node authorization users list.
/// Mirrors parseLeafUsers in opts.go.
///
public static List ParseLeafUsers(object? value, ICollection? 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(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;
}
///
/// Parses remote leaf-node definitions.
/// Mirrors parseRemoteLeafNodes in opts.go.
///
public static List ParseRemoteLeafNodes(
object? value,
ICollection? errors = null,
ICollection? 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(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;
}
///
/// Parses TLS config block and builds runtime TLS options.
/// Mirrors getTLSConfig in opts.go.
///
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);
}
///
/// Parses list of remote gateways.
/// Mirrors parseGateways in opts.go.
///
public static List ParseGateways(
object? value,
ICollection? errors = null,
ICollection? 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(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;
}
///
/// Maps parsed pub/sub permissions to route import/export permissions.
/// Mirrors setClusterPermissions in opts.go.
///
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(),
};
}
// -------------------------------------------------------------------------
// Batch 6: opts.go package-level parse/config helpers (F3)
// -------------------------------------------------------------------------
///
/// Returns true for reserved account names.
/// Mirrors isReservedAccount in opts.go.
///
public static bool IsReservedAccount(string name) =>
string.Equals(name, ServerConstants.DefaultGlobalAccount, StringComparison.Ordinal);
///
/// Parsed account export entry used during account-config parsing.
///
public sealed class AccountExportConfig
{
public Account? Account { get; set; }
public string Subject { get; set; } = string.Empty;
public List AccountNames { get; set; } = [];
public ServiceRespType ResponseType { get; set; } = ServiceRespType.Singleton;
public object? Latency { get; set; }
public TimeSpan ResponseThreshold { get; set; }
public uint AccountTokenPosition { get; set; }
public bool AllowTrace { get; set; }
}
///
/// Parsed account stream-import entry used during account-config parsing.
///
public sealed class AccountImportStreamConfig
{
public Account? Account { get; set; }
public string AccountName { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string Prefix { get; set; } = string.Empty;
public string To { get; set; } = string.Empty;
public bool AllowTrace { get; set; }
}
///
/// Parsed account service-import entry used during account-config parsing.
///
public sealed class AccountImportServiceConfig
{
public Account? Account { get; set; }
public string AccountName { get; set; } = string.Empty;
public string Subject { get; set; } = string.Empty;
public string To { get; set; } = string.Empty;
public bool Share { get; set; }
}
///
/// Parsed authorization block used for top-level/config-subtree authorization parsing.
///
public sealed class ParsedAuthorizationBlock
{
public string User { get; set; } = string.Empty;
public string Pass { get; set; } = string.Empty;
public string Token { get; set; } = string.Empty;
public double TimeoutSeconds { get; set; }
public bool ProxyRequired { get; set; }
public List Users { get; set; } = [];
public List Nkeys { get; set; } = [];
public Permissions? DefaultPermissions { get; set; }
public AuthCalloutOpts? Callout { get; set; }
}
///
/// Parses a weighted account mapping destination entry.
/// Mirrors parseAccountMapDest in opts.go.
///
public static MapDest? ParseAccountMapDest(object? value, ICollection? errors = null)
{
if (!TryGetMap(value, out var map))
{
errors?.Add(new InvalidOperationException("Expected an entry for the mapping destination"));
return null;
}
var mdest = new MapDest();
var sawWeight = false;
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "dest":
case "destination":
if (entry is string subject)
{
mdest.Subject = subject;
}
else
{
errors?.Add(new InvalidOperationException("mapping destination must be a string"));
return null;
}
break;
case "weight":
{
long parsedWeight;
switch (entry)
{
case string weightString:
{
var normalizedWeight = weightString.Trim().TrimEnd('%');
if (!long.TryParse(normalizedWeight, out parsedWeight))
{
errors?.Add(new InvalidOperationException(
$"Invalid weight \"{weightString}\" for mapping destination"));
return null;
}
break;
}
default:
if (!TryConvertToLong(entry, out parsedWeight))
{
errors?.Add(new InvalidOperationException(
$"Unknown entry type for weight \"{entry?.GetType().Name ?? "null"}\""));
return null;
}
break;
}
if (parsedWeight is > 100 or < 0)
{
errors?.Add(new InvalidOperationException(
$"Invalid weight {parsedWeight} for mapping destination"));
return null;
}
mdest.Weight = (byte)parsedWeight;
sawWeight = true;
break;
}
case "cluster":
mdest.Cluster = entry as string ?? string.Empty;
break;
default:
errors?.Add(new InvalidOperationException(
$"Unknown field \"{rawKey}\" for mapping destination"));
return null;
}
}
if (!sawWeight)
{
errors?.Add(new InvalidOperationException(
$"Missing weight for mapping destination \"{mdest.Subject}\""));
return null;
}
return mdest;
}
///
/// Parses account subject mappings.
/// Mirrors parseAccountMappings in opts.go.
///
public static Exception? ParseAccountMappings(
object? value,
Account account,
ICollection? errors = null)
{
ArgumentNullException.ThrowIfNull(account);
if (!TryGetMap(value, out var map))
return new InvalidOperationException($"Expected account mappings map, got {value?.GetType().Name ?? "null"}");
foreach (var (subject, rawDestination) in map)
{
if (!SubscriptionIndex.IsValidSubject(subject))
{
errors?.Add(new InvalidOperationException($"Subject \"{subject}\" is not a valid subject"));
continue;
}
var destination = NormalizeConfigValue(rawDestination);
switch (destination)
{
case string mappedSubject:
{
var mapError = account.AddMapping(subject, mappedSubject);
if (mapError != null)
{
errors?.Add(new InvalidOperationException(
$"Error adding mapping for \"{subject}\" to \"{mappedSubject}\": {mapError.Message}",
mapError));
}
break;
}
default:
if (TryGetArray(destination, out var destinationArray))
{
var destinations = new List(destinationArray.Count);
foreach (var entry in destinationArray)
{
var parsedDestination = ParseAccountMapDest(entry, errors);
if (parsedDestination != null)
destinations.Add(parsedDestination);
}
if (destinations.Count == 0)
break;
var mapError = account.AddWeightedMappings(subject, [.. destinations]);
if (mapError != null)
{
errors?.Add(new InvalidOperationException(
$"Error adding mapping for \"{subject}\": {mapError.Message}",
mapError));
}
break;
}
if (TryGetMap(destination, out _))
{
var parsedDestination = ParseAccountMapDest(destination, errors);
if (parsedDestination == null)
break;
var mapError = account.AddWeightedMappings(subject, parsedDestination);
if (mapError != null)
{
errors?.Add(new InvalidOperationException(
$"Error adding mapping for \"{subject}\": {mapError.Message}",
mapError));
}
break;
}
errors?.Add(new InvalidOperationException(
$"Unknown type {destination?.GetType().Name ?? "null"} for mapping destination"));
break;
}
}
return null;
}
///
/// Parses account-level connection/subscription/payload limits.
/// Mirrors parseAccountLimits in opts.go.
///
public static Exception? ParseAccountLimits(
object? value,
Account account,
ICollection? errors = null)
{
ArgumentNullException.ThrowIfNull(account);
if (!TryGetMap(value, out var map))
return new InvalidOperationException(
$"Expected account limits to be a map/struct, got {value?.GetType().Name ?? "null"}");
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
if (!TryConvertToLong(entry, out var numericValue))
{
errors?.Add(new InvalidOperationException(
$"Expected numeric value parsing account limit \"{rawKey}\""));
continue;
}
switch (key)
{
case "max_connections":
case "max_conn":
account.MaxConnections = checked((int)numericValue);
break;
case "max_subscriptions":
case "max_subs":
account.MaxSubscriptions = checked((int)numericValue);
break;
case "max_payload":
case "max_pay":
account.MaxPayload = checked((int)numericValue);
break;
case "max_leafnodes":
case "max_leafs":
account.MaxLeafNodes = checked((int)numericValue);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
{
errors?.Add(new InvalidOperationException(
$"Unknown field \"{rawKey}\" parsing account limits"));
}
break;
}
}
return null;
}
///
/// Parses account message trace destination/sampling configuration.
/// Mirrors parseAccountMsgTrace in opts.go.
///
public static Exception? ParseAccountMsgTrace(object? value, string topKey, Account account)
{
ArgumentNullException.ThrowIfNull(account);
static Exception? ProcessDestination(Account targetAccount, string key, object? rawValue)
{
if (rawValue is not string destination)
{
return new InvalidOperationException(
$"Field \"{key}\" should be a string, got {rawValue?.GetType().Name ?? "null"}");
}
if (!SubscriptionIndex.IsValidPublishSubject(destination))
return new InvalidOperationException($"Trace destination \"{destination}\" is not valid");
targetAccount.SetMessageTraceDestination(destination);
return null;
}
static Exception? ProcessSampling(Account targetAccount, int sampling)
{
if (sampling is <= 0 or > 100)
{
return new InvalidOperationException(
$"Trace destination sampling value {sampling} is invalid, needs to be [1..100]");
}
targetAccount.SetMessageTraceSampling(sampling);
return null;
}
var normalized = NormalizeConfigValue(value);
switch (normalized)
{
case string:
return ProcessDestination(account, topKey, normalized);
default:
if (!TryGetMap(normalized, out var map))
{
return new InvalidOperationException(
$"Expected account message trace \"{topKey}\" to be a string or map/struct, got {normalized?.GetType().Name ?? "null"}");
}
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "dest":
{
var destinationError = ProcessDestination(account, rawKey, entry);
if (destinationError != null)
return destinationError;
break;
}
case "sampling":
{
int parsedSampling;
switch (entry)
{
case string samplingString:
{
var normalizedSampling = samplingString.Trim().TrimEnd('%');
if (!int.TryParse(normalizedSampling, out parsedSampling))
{
return new InvalidOperationException(
$"Invalid trace destination sampling value \"{samplingString}\"");
}
break;
}
default:
if (!TryConvertToLong(entry, out var longSampling))
{
return new InvalidOperationException(
$"Trace destination sampling field \"{rawKey}\" should be an integer or a percentage, got {entry?.GetType().Name ?? "null"}");
}
parsedSampling = checked((int)longSampling);
break;
}
var samplingError = ProcessSampling(account, parsedSampling);
if (samplingError != null)
return samplingError;
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
{
return new InvalidOperationException(
$"Unknown field \"{rawKey}\" parsing account message trace map/struct \"{topKey}\"");
}
break;
}
}
return null;
}
}
///
/// Parses the top-level accounts block.
/// Mirrors parseAccounts in opts.go.
///
public static Exception? ParseAccounts(
object? value,
ServerOptions options,
ICollection? errors = null,
ICollection? warnings = null)
{
ArgumentNullException.ThrowIfNull(options);
var pendingImportStreams = new List();
var pendingImportServices = new List();
var pendingExportStreams = new List();
var pendingExportServices = new List();
var normalized = NormalizeConfigValue(value);
if (TryGetArray(normalized, out var accountArray))
{
var seen = new HashSet(StringComparer.Ordinal);
foreach (var accountEntry in accountArray)
{
var accountName = NormalizeConfigValue(accountEntry) as string ?? string.Empty;
if (string.IsNullOrWhiteSpace(accountName))
{
errors?.Add(new InvalidOperationException("Expected account name to be a string"));
continue;
}
if (IsReservedAccount(accountName))
{
errors?.Add(new InvalidOperationException($"\"{accountName}\" is a reserved account"));
continue;
}
if (!seen.Add(accountName))
{
errors?.Add(new InvalidOperationException($"Duplicate Account Entry: {accountName}"));
continue;
}
options.Accounts.Add(Account.NewAccount(accountName));
}
return null;
}
if (!TryGetMap(normalized, out var accountsMap))
{
return new InvalidOperationException(
$"Expected accounts to be an array or map, got {normalized?.GetType().Name ?? "null"}");
}
options.Users ??= [];
options.Nkeys ??= [];
var identities = SetupUsersAndNKeysDuplicateCheckMap(options);
foreach (var (accountName, accountValueRaw) in accountsMap)
{
var accountValue = NormalizeConfigValue(accountValueRaw);
if (!TryGetMap(accountValue, out var accountMap))
{
errors?.Add(new InvalidOperationException("Expected map entries for accounts"));
continue;
}
if (IsReservedAccount(accountName))
{
errors?.Add(new InvalidOperationException($"\"{accountName}\" is a reserved account"));
continue;
}
var account = Account.NewAccount(accountName);
options.Accounts.Add(account);
var parsedUsers = new List();
var parsedNkeys = new List();
foreach (var (rawKey, rawValue) in accountMap)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "nkey":
{
var accountKey = entry as string ?? string.Empty;
if (!IsLikelyPublicNkey(accountKey, 'A'))
{
errors?.Add(new InvalidOperationException(
$"Not a valid public nkey for an account: \"{accountKey}\""));
break;
}
account.Nkey = accountKey;
break;
}
case "imports":
{
var (importStreams, importServices, importError) = ParseAccountImports(entry, account, errors);
if (importError != null)
{
errors?.Add(importError);
break;
}
pendingImportStreams.AddRange(importStreams);
pendingImportServices.AddRange(importServices);
break;
}
case "exports":
{
var (exportStreams, exportServices, exportError) = ParseAccountExports(entry, account, errors);
if (exportError != null)
{
errors?.Add(exportError);
break;
}
pendingExportStreams.AddRange(exportStreams);
pendingExportServices.AddRange(exportServices);
break;
}
case "jetstream":
{
var jsError = ParseJetStreamForAccount(entry, account, errors);
if (jsError != null)
errors?.Add(jsError);
break;
}
case "users":
{
var (accountNkeys, accountUsers, usersError) = ParseUsers(entry, errors);
if (usersError != null)
{
errors?.Add(usersError);
break;
}
parsedUsers = accountUsers;
parsedNkeys = accountNkeys;
break;
}
case "default_permissions":
{
var permissions = ParsePermissionsValue(entry, errors);
if (permissions != null)
account.DefaultPerms = permissions;
break;
}
case "mappings":
case "maps":
{
var mappingsError = ParseAccountMappings(entry, account, errors);
if (mappingsError != null)
errors?.Add(mappingsError);
break;
}
case "limits":
{
var limitsError = ParseAccountLimits(entry, account, errors);
if (limitsError != null)
errors?.Add(limitsError);
break;
}
case "msg_trace":
case "trace_dest":
{
var traceError = ParseAccountMsgTrace(entry, key, account);
if (traceError != null)
{
errors?.Add(traceError);
break;
}
if (!string.IsNullOrEmpty(account.GetMessageTraceDestination()) &&
account.GetMessageTraceSampling() == 0)
{
account.SetMessageTraceSampling(100);
}
else if (account.GetMessageTraceSampling() > 0 &&
string.IsNullOrEmpty(account.GetMessageTraceDestination()))
{
warnings?.Add(new InvalidOperationException(
"Trace destination sampling ignored since no destination was set"));
account.SetMessageTraceSampling(0);
}
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
if (parsedNkeys.Count > 0 || parsedUsers.Count > 0)
{
if (!string.IsNullOrEmpty(options.Username))
errors?.Add(new InvalidOperationException("Cannot have a single user/pass and accounts"));
if (!string.IsNullOrEmpty(options.Authorization))
errors?.Add(new InvalidOperationException("Cannot have a token and accounts"));
}
ApplyDefaultPermissions(parsedUsers, parsedNkeys, account.DefaultPerms);
foreach (var nkeyUser in parsedNkeys)
{
if (!identities.Add(nkeyUser.Nkey))
{
errors?.Add(new InvalidOperationException($"Duplicate nkey \"{nkeyUser.Nkey}\" detected"));
continue;
}
nkeyUser.Account = account;
options.Nkeys.Add(nkeyUser);
}
foreach (var user in parsedUsers)
{
if (!identities.Add(user.Username))
{
errors?.Add(new InvalidOperationException($"Duplicate user \"{user.Username}\" detected"));
continue;
}
user.Account = account;
options.Users.Add(user);
}
}
if (errors is { Count: > 0 })
return null;
var accountLookup = options.Accounts.ToDictionary(a => a.Name, StringComparer.Ordinal);
foreach (var streamExport in pendingExportStreams)
{
if (streamExport.Account == null)
continue;
streamExport.Account.Exports.Streams ??= new Dictionary(StringComparer.Ordinal);
var export = new StreamExport
{
AccountPosition = streamExport.AccountTokenPosition,
};
if (streamExport.AccountNames.Count > 0)
{
export.Approved = new Dictionary(StringComparer.Ordinal);
foreach (var accountName in streamExport.AccountNames)
{
if (!accountLookup.TryGetValue(accountName, out var approvedAccount))
{
errors?.Add(new InvalidOperationException(
$"\"{accountName}\" account not defined for stream export"));
continue;
}
export.Approved[accountName] = approvedAccount;
}
}
streamExport.Account.Exports.Streams[streamExport.Subject] = export;
}
foreach (var serviceExport in pendingExportServices)
{
if (serviceExport.Account == null)
continue;
serviceExport.Account.Exports.Services ??= new Dictionary(StringComparer.Ordinal);
var export = new ServiceExportEntry
{
Account = serviceExport.Account,
ResponseType = serviceExport.ResponseType,
Latency = serviceExport.Latency as InternalServiceLatency,
ResponseThreshold = serviceExport.ResponseThreshold,
AllowTrace = serviceExport.AllowTrace,
AccountPosition = serviceExport.AccountTokenPosition,
};
if (serviceExport.AccountNames.Count > 0)
{
export.Approved = new Dictionary(StringComparer.Ordinal);
foreach (var accountName in serviceExport.AccountNames)
{
if (!accountLookup.TryGetValue(accountName, out var approvedAccount))
{
errors?.Add(new InvalidOperationException(
$"\"{accountName}\" account not defined for service export"));
continue;
}
export.Approved[accountName] = approvedAccount;
}
}
serviceExport.Account.Exports.Services[serviceExport.Subject] = export;
}
foreach (var streamImport in pendingImportStreams)
{
if (streamImport.Account == null)
continue;
if (!accountLookup.TryGetValue(streamImport.AccountName, out var importedAccount))
{
errors?.Add(new InvalidOperationException(
$"\"{streamImport.AccountName}\" account not defined for stream import"));
continue;
}
streamImport.Account.Imports.Streams ??= [];
var targetSubject = !string.IsNullOrEmpty(streamImport.To)
? streamImport.To
: !string.IsNullOrEmpty(streamImport.Prefix)
? streamImport.Prefix
: streamImport.Subject;
streamImport.Account.Imports.Streams.Add(new StreamImportEntry
{
Account = importedAccount,
From = streamImport.Subject,
To = targetSubject,
AllowTrace = streamImport.AllowTrace,
});
}
foreach (var serviceImport in pendingImportServices)
{
if (serviceImport.Account == null)
continue;
if (!accountLookup.TryGetValue(serviceImport.AccountName, out var importedAccount))
{
errors?.Add(new InvalidOperationException(
$"\"{serviceImport.AccountName}\" account not defined for service import"));
continue;
}
var targetSubject = string.IsNullOrEmpty(serviceImport.To)
? serviceImport.Subject
: serviceImport.To;
serviceImport.Account.Imports.Services ??= new Dictionary>(StringComparer.Ordinal);
if (!serviceImport.Account.Imports.Services.TryGetValue(targetSubject, out var importsForSubject))
{
importsForSubject = [];
serviceImport.Account.Imports.Services[targetSubject] = importsForSubject;
}
importsForSubject.Add(new ServiceImportEntry
{
Account = importedAccount,
From = serviceImport.Subject,
To = targetSubject,
Share = serviceImport.Share,
});
}
return null;
}
///
/// Parses account exports list.
/// Mirrors parseAccountExports in opts.go.
///
public static (List Streams, List Services, Exception? Error) ParseAccountExports(
object? value,
Account account,
ICollection? errors = null)
{
ArgumentNullException.ThrowIfNull(account);
if (!TryGetArray(value, out var exportArray))
{
return ([], [], new InvalidOperationException(
$"Exports should be an array, got {value?.GetType().Name ?? "null"}"));
}
var streamExports = new List();
var serviceExports = new List();
foreach (var exportValue in exportArray)
{
var (stream, service, parseError) = ParseExportStreamOrService(exportValue, errors);
if (parseError != null)
{
errors?.Add(parseError);
continue;
}
if (stream != null)
{
stream.Account = account;
streamExports.Add(stream);
}
if (service != null)
{
service.Account = account;
serviceExports.Add(service);
}
}
return (streamExports, serviceExports, null);
}
///
/// Parses account imports list.
/// Mirrors parseAccountImports in opts.go.
///
public static (List Streams, List Services, Exception? Error) ParseAccountImports(
object? value,
Account account,
ICollection? errors = null)
{
ArgumentNullException.ThrowIfNull(account);
if (!TryGetArray(value, out var importArray))
{
return ([], [], new InvalidOperationException(
$"Imports should be an array, got {value?.GetType().Name ?? "null"}"));
}
var streamImports = new List();
var serviceImports = new List();
var serviceSubjects = new Dictionary>(StringComparer.Ordinal);
foreach (var importValue in importArray)
{
var (stream, service, parseError) = ParseImportStreamOrService(importValue, errors);
if (parseError != null)
{
errors?.Add(parseError);
continue;
}
if (service != null)
{
var targetSubject = string.IsNullOrEmpty(service.To) ? service.Subject : service.To;
if (!serviceSubjects.TryGetValue(targetSubject, out var seenAccounts))
{
seenAccounts = new HashSet(StringComparer.Ordinal);
serviceSubjects[targetSubject] = seenAccounts;
}
if (!seenAccounts.Add(service.AccountName))
{
errors?.Add(new InvalidOperationException(
$"Duplicate service import subject \"{targetSubject}\", previously used in import for account \"{service.AccountName}\""));
continue;
}
service.Account = account;
serviceImports.Add(service);
}
if (stream != null)
{
stream.Account = account;
streamImports.Add(stream);
}
}
return (streamImports, serviceImports, null);
}
///
/// Parses account descriptor maps used inside import entries.
/// Mirrors parseAccount in opts.go.
///
public static (string AccountName, string Subject, Exception? Error) ParseAccount(
object? value,
ICollection? errors = null)
{
if (!TryGetMap(value, out var map))
{
return (string.Empty, string.Empty, new InvalidOperationException(
$"Expected account descriptor map, got {value?.GetType().Name ?? "null"}"));
}
var accountName = string.Empty;
var subject = string.Empty;
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "account":
accountName = entry as string ?? string.Empty;
break;
case "subject":
subject = entry as string ?? string.Empty;
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return (accountName, subject, null);
}
///
/// Parses a single export entry (stream or service ).
/// Mirrors parseExportStreamOrService in opts.go.
///
public static (AccountExportConfig? Stream, AccountExportConfig? Service, Exception? Error) ParseExportStreamOrService(
object? value,
ICollection? errors = null)
{
if (!TryGetMap(value, out var map))
{
return (null, null, new InvalidOperationException(
$"Export items should be a map with type entry, got {value?.GetType().Name ?? "null"}"));
}
AccountExportConfig? stream = null;
AccountExportConfig? service = null;
var accountNames = new List();
var responseType = ServiceRespType.Singleton;
var responseTypeSeen = false;
var responseThreshold = TimeSpan.Zero;
var thresholdSeen = false;
object? latency = null;
uint accountTokenPosition = 0;
var allowTraceSeen = false;
var allowTrace = false;
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "stream":
{
if (service != null)
{
errors?.Add(new InvalidOperationException(
$"Detected stream \"{entry}\" but already saw a service"));
break;
}
var subject = entry as string;
if (string.IsNullOrEmpty(subject))
{
errors?.Add(new InvalidOperationException(
$"Expected stream name to be string, got {entry?.GetType().Name ?? "null"}"));
break;
}
stream = new AccountExportConfig
{
Subject = subject,
AccountNames = [.. accountNames],
};
break;
}
case "service":
{
if (stream != null)
{
errors?.Add(new InvalidOperationException(
$"Detected service \"{entry}\" but already saw a stream"));
break;
}
var subject = entry as string;
if (string.IsNullOrEmpty(subject))
{
errors?.Add(new InvalidOperationException(
$"Expected service name to be string, got {entry?.GetType().Name ?? "null"}"));
break;
}
service = new AccountExportConfig
{
Subject = subject,
AccountNames = [.. accountNames],
ResponseType = responseType,
Latency = latency,
ResponseThreshold = responseThreshold,
AllowTrace = allowTraceSeen && allowTrace,
};
break;
}
case "response":
case "response_type":
{
if (responseTypeSeen)
{
errors?.Add(new InvalidOperationException("Duplicate response type definition"));
break;
}
responseTypeSeen = true;
var responseString = (entry as string ?? string.Empty).ToLowerInvariant();
var responseTypeParsed = responseString switch
{
"single" or "singleton" => ServiceRespType.Singleton,
"stream" => ServiceRespType.Streamed,
"chunk" or "chunked" => ServiceRespType.Chunked,
_ => (ServiceRespType?)null,
};
if (responseTypeParsed == null)
{
errors?.Add(new InvalidOperationException($"Unknown response type: \"{entry}\""));
break;
}
responseType = responseTypeParsed.Value;
if (stream != null)
errors?.Add(new InvalidOperationException("Detected response directive on non-service"));
if (service != null)
service.ResponseType = responseType;
break;
}
case "threshold":
case "response_threshold":
case "response_max_time":
case "response_time":
{
if (thresholdSeen)
{
errors?.Add(new InvalidOperationException("Duplicate response threshold detected"));
break;
}
thresholdSeen = true;
responseThreshold = ParseDuration(rawKey, entry, errors, warnings: null);
if (stream != null)
errors?.Add(new InvalidOperationException("Detected response directive on non-service"));
if (service != null)
service.ResponseThreshold = responseThreshold;
break;
}
case "accounts":
accountNames = ParseStringList(entry);
if (stream != null)
stream.AccountNames = [.. accountNames];
if (service != null)
service.AccountNames = [.. accountNames];
break;
case "latency":
{
var (parsedLatency, latencyError) = ParseServiceLatency(rawKey, entry);
if (latencyError != null)
{
errors?.Add(latencyError);
break;
}
latency = parsedLatency;
if (stream != null)
{
errors?.Add(new InvalidOperationException("Detected latency directive on non-service"));
break;
}
if (service != null)
service.Latency = latency;
break;
}
case "account_token_position":
if (TryConvertToLong(entry, out var tokenPosition))
accountTokenPosition = checked((uint)tokenPosition);
break;
case "allow_trace":
allowTraceSeen = true;
if (TryConvertToBool(entry, out var allowTraceValue))
allowTrace = allowTraceValue;
if (stream != null)
{
errors?.Add(new InvalidOperationException("Detected allow_trace directive on non-service"));
break;
}
if (service != null)
service.AllowTrace = allowTrace;
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
if (stream != null)
stream.AccountTokenPosition = accountTokenPosition;
if (service != null)
service.AccountTokenPosition = accountTokenPosition;
return (stream, service, null);
}
///
/// Parses service-export latency configuration.
/// Mirrors parseServiceLatency in opts.go.
///
public static (object? Latency, Exception? Error) ParseServiceLatency(string rootField, object? value)
{
if (NormalizeConfigValue(value) is string subject)
{
return (new InternalServiceLatency
{
Subject = subject,
Sampling = ServerConstants.DefaultServiceLatencySampling,
}, null);
}
if (!TryGetMap(value, out var latencyMap))
{
return (null, new InvalidOperationException(
$"Expected latency entry to be a map/struct or string, got {value?.GetType().Name ?? "null"}"));
}
var sampling = ServerConstants.DefaultServiceLatencySampling;
if (latencyMap.TryGetValue("sampling", out var samplingRaw))
{
var samplingValue = NormalizeConfigValue(samplingRaw);
var headerMode = false;
switch (samplingValue)
{
case long longSampling:
sampling = checked((int)longSampling);
break;
case string samplingString:
{
if (samplingString.Trim().Equals("headers", StringComparison.OrdinalIgnoreCase))
{
headerMode = true;
sampling = 0;
break;
}
var normalizedSampling = samplingString.Trim().TrimEnd('%');
if (!int.TryParse(normalizedSampling, out sampling))
{
return (null, new InvalidOperationException(
$"Failed to parse latency sample \"{samplingString}\""));
}
break;
}
default:
return (null, new InvalidOperationException(
$"Expected latency sample to be a string or integer, got {samplingValue?.GetType().Name ?? "null"}"));
}
if (!headerMode && (sampling < 1 || sampling > 100))
return (null, new InvalidOperationException("sampling value should be in range [1..100]"));
}
if (!latencyMap.TryGetValue("subject", out var subjectRaw))
{
return (null, new InvalidOperationException(
$"Latency subject required in \"{rootField}\", but missing"));
}
var latencySubject = NormalizeConfigValue(subjectRaw) as string;
if (string.IsNullOrWhiteSpace(latencySubject))
{
return (null, new InvalidOperationException(
$"Expected latency subject to be a string, got {subjectRaw?.GetType().Name ?? "null"}"));
}
return (new InternalServiceLatency
{
Sampling = sampling,
Subject = latencySubject,
}, null);
}
///
/// Parses a single import entry (stream or service ).
/// Mirrors parseImportStreamOrService in opts.go.
///
public static (AccountImportStreamConfig? Stream, AccountImportServiceConfig? Service, Exception? Error) ParseImportStreamOrService(
object? value,
ICollection? errors = null)
{
if (!TryGetMap(value, out var map))
{
return (null, null, new InvalidOperationException(
$"Import items should be a map with type entry, got {value?.GetType().Name ?? "null"}"));
}
AccountImportStreamConfig? stream = null;
AccountImportServiceConfig? service = null;
var prefix = string.Empty;
var to = string.Empty;
var share = false;
var allowTraceSeen = false;
var allowTrace = false;
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "stream":
{
if (service != null)
{
errors?.Add(new InvalidOperationException("Detected stream but already saw a service"));
break;
}
var (accountName, subject, parseError) = ParseAccount(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
if (string.IsNullOrEmpty(accountName) || string.IsNullOrEmpty(subject))
{
errors?.Add(new InvalidOperationException("Expect an account name and a subject"));
break;
}
stream = new AccountImportStreamConfig
{
AccountName = accountName,
Subject = subject,
Prefix = prefix,
To = to,
AllowTrace = allowTraceSeen && allowTrace,
};
break;
}
case "service":
{
if (stream != null)
{
errors?.Add(new InvalidOperationException("Detected service but already saw a stream"));
break;
}
if (allowTraceSeen)
{
errors?.Add(new InvalidOperationException(
"Detected allow_trace directive on a non-stream"));
break;
}
var (accountName, subject, parseError) = ParseAccount(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
if (string.IsNullOrEmpty(accountName) || string.IsNullOrEmpty(subject))
{
errors?.Add(new InvalidOperationException("Expect an account name and a subject"));
break;
}
service = new AccountImportServiceConfig
{
AccountName = accountName,
Subject = subject,
To = string.IsNullOrEmpty(to) ? subject : to,
Share = share,
};
break;
}
case "prefix":
prefix = entry as string ?? string.Empty;
if (stream != null)
stream.Prefix = prefix;
break;
case "to":
to = entry as string ?? string.Empty;
if (service != null)
service.To = to;
if (stream != null)
{
stream.To = to;
if (!string.IsNullOrEmpty(stream.Prefix))
errors?.Add(new InvalidOperationException(
"Stream import cannot have both 'prefix' and 'to' properties"));
}
break;
case "share":
if (TryConvertToBool(entry, out var shareValue))
share = shareValue;
if (service != null)
service.Share = share;
break;
case "allow_trace":
if (service != null)
{
errors?.Add(new InvalidOperationException(
"Detected allow_trace directive on a non-stream"));
break;
}
allowTraceSeen = true;
if (TryConvertToBool(entry, out var allowTraceValue))
allowTrace = allowTraceValue;
if (stream != null)
stream.AllowTrace = allowTrace;
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return (stream, service, null);
}
///
/// Applies default permissions to users and nkeys that do not have explicit permissions.
/// Mirrors applyDefaultPermissions in opts.go.
///
public static void ApplyDefaultPermissions(
IReadOnlyList? users,
IReadOnlyList? nkeys,
Permissions? defaultPermissions)
{
if (defaultPermissions == null)
return;
if (users != null)
{
foreach (var user in users)
{
user.Permissions ??= defaultPermissions;
}
}
if (nkeys != null)
{
foreach (var user in nkeys)
{
user.Permissions ??= defaultPermissions;
}
}
}
///
/// Parses an authorization block.
/// Mirrors parseAuthorization in opts.go.
///
public static (ParsedAuthorizationBlock? Authorization, Exception? Error) ParseAuthorization(
object? value,
ICollection? errors = null,
ICollection? warnings = null)
{
if (!TryGetMap(value, out var map))
return (null, new InvalidOperationException("authorization should be a map"));
var auth = new ParsedAuthorizationBlock();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "user":
case "username":
auth.User = entry as string ?? string.Empty;
break;
case "pass":
case "password":
auth.Pass = entry as string ?? string.Empty;
break;
case "token":
auth.Token = entry as string ?? string.Empty;
break;
case "timeout":
{
double timeoutSeconds;
switch (entry)
{
case long longTimeout:
timeoutSeconds = longTimeout;
break;
case double doubleTimeout:
timeoutSeconds = doubleTimeout;
break;
case string duration:
timeoutSeconds = ParseDuration("timeout", duration, errors, warnings).TotalSeconds;
break;
default:
return (null, new InvalidOperationException(
"error parsing authorization config, 'timeout' wrong type"));
}
auth.TimeoutSeconds = timeoutSeconds;
if (timeoutSeconds > TimeSpan.FromSeconds(60).TotalSeconds)
{
warnings?.Add(new InvalidOperationException(
$"timeout of {entry} ({timeoutSeconds} seconds) is high, consider keeping it under 60 seconds"));
}
break;
}
case "users":
{
var (nkeys, users, parseError) = ParseUsers(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
auth.Users = users;
auth.Nkeys = nkeys;
break;
}
case "default_permission":
case "default_permissions":
case "permissions":
auth.DefaultPermissions = ParsePermissionsValue(entry, errors);
break;
case "auth_callout":
case "auth_hook":
{
var (callout, parseError) = ParseAuthCallout(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
auth.Callout = callout;
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;
}
}
ApplyDefaultPermissions(auth.Users, auth.Nkeys, auth.DefaultPermissions);
return (auth, null);
}
///
/// Parses users/nkeys block.
/// Mirrors parseUsers in opts.go.
///
public static (List Nkeys, List Users, Exception? Error) ParseUsers(
object? value,
ICollection? errors = null)
{
if (!TryGetArray(value, out var usersArray))
{
return ([], [], new InvalidOperationException(
$"Expected users field to be an array, got {value?.GetType().Name ?? "null"}"));
}
var users = new List();
var nkeys = new List();
foreach (var userRaw in usersArray)
{
if (!TryGetMap(userRaw, out var userMap))
{
errors?.Add(new InvalidOperationException(
$"Expected user entry to be a map/struct, got {userRaw?.GetType().Name ?? "null"}"));
continue;
}
var user = new User();
var nkey = new NkeyUser();
Permissions? permissions = null;
foreach (var (rawKey, rawValue) in userMap)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "nkey":
nkey.Nkey = entry as string ?? string.Empty;
break;
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 "permission":
case "permissions":
case "authorization":
permissions = ParsePermissionsValue(entry, errors);
break;
case "allowed_connection_types":
case "connection_types":
case "clients":
{
var connectionTypes = ParseAllowedConnectionTypes(entry, errors);
nkey.AllowedConnectionTypes = connectionTypes;
user.AllowedConnectionTypes = connectionTypes;
break;
}
case "proxy_required":
if (TryConvertToBool(entry, out var proxyRequired))
{
nkey.ProxyRequired = proxyRequired;
user.ProxyRequired = proxyRequired;
}
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
if (permissions != null)
{
if (!string.IsNullOrEmpty(nkey.Nkey))
nkey.Permissions = permissions;
else
user.Permissions = permissions;
}
if (string.IsNullOrEmpty(nkey.Nkey) && string.IsNullOrEmpty(user.Username))
{
return ([], [], new InvalidOperationException("User entry requires a user"));
}
if (!string.IsNullOrEmpty(nkey.Nkey))
{
if (!IsLikelyPublicNkey(nkey.Nkey, 'U'))
return ([], [], new InvalidOperationException("Not a valid public nkey for a user"));
if (!string.IsNullOrEmpty(user.Username) || !string.IsNullOrEmpty(user.Password))
{
return ([], [], new InvalidOperationException(
"Nkey users do not take usernames or passwords"));
}
nkeys.Add(nkey);
}
else
{
users.Add(user);
}
}
return (nkeys, users, null);
}
///
/// Parses allowed connection types map.
/// Mirrors parseAllowedConnectionTypes in opts.go.
///
public static HashSet? ParseAllowedConnectionTypes(
object? value,
ICollection? errors = null)
{
if (!TryGetArray(value, out var values))
{
errors?.Add(new InvalidOperationException(
$"Expected allowed connection types to be an array, got {value?.GetType().Name ?? "null"}"));
return null;
}
var result = new HashSet(StringComparer.Ordinal);
foreach (var item in values)
{
var connectionType = NormalizeConfigValue(item) as string;
if (string.IsNullOrEmpty(connectionType))
{
errors?.Add(new InvalidOperationException("allowed connection type entries must be strings"));
continue;
}
result.Add(connectionType);
}
var validateError = AuthHandler.ValidateAllowedConnectionTypes(result);
if (validateError != null)
errors?.Add(validateError);
return result;
}
///
/// Parses authorization callout configuration.
/// Mirrors parseAuthCallout in opts.go.
///
public static (AuthCalloutOpts? Callout, Exception? Error) ParseAuthCallout(
object? value,
ICollection? errors = null)
{
if (!TryGetMap(value, out var map))
{
return (null, new InvalidOperationException(
$"Expected authorization callout to be a map/struct, got {value?.GetType().Name ?? "null"}"));
}
var callout = new AuthCalloutOpts();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "issuer":
callout.Issuer = entry as string ?? string.Empty;
if (!IsLikelyPublicNkey(callout.Issuer, 'A'))
{
return (null, new InvalidOperationException(
$"Expected callout user to be a valid public account nkey, got \"{callout.Issuer}\""));
}
break;
case "account":
case "acc":
callout.Account = entry as string ?? string.Empty;
break;
case "auth_users":
case "users":
if (!TryGetArray(entry, out var authUsersArray))
{
return (null, new InvalidOperationException(
$"Expected auth_users field to be an array, got {entry?.GetType().Name ?? "null"}"));
}
foreach (var userValue in authUsersArray)
{
var authUser = NormalizeConfigValue(userValue) as string;
if (!string.IsNullOrEmpty(authUser))
callout.AuthUsers.Add(authUser);
}
break;
case "xkey":
case "key":
callout.XKey = entry as string ?? string.Empty;
if (!string.IsNullOrEmpty(callout.XKey) && !IsLikelyPublicNkey(callout.XKey, 'X'))
{
return (null, new InvalidOperationException(
$"Expected callout xkey to be a valid public xkey, got \"{callout.XKey}\""));
}
break;
case "allowed_accounts":
if (!TryGetArray(entry, out var allowedAccountsArray))
{
return (null, new InvalidOperationException(
$"Expected allowed accounts field to be an array, got {entry?.GetType().Name ?? "null"}"));
}
foreach (var accountValue in allowedAccountsArray)
{
var accountName = NormalizeConfigValue(accountValue) as string;
if (!string.IsNullOrEmpty(accountName))
callout.AllowedAccounts.Add(accountName);
}
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
{
errors?.Add(new InvalidOperationException(
$"Unknown field \"{rawKey}\" parsing authorization callout"));
}
break;
}
}
if (string.IsNullOrEmpty(callout.Account))
callout.Account = ServerConstants.DefaultGlobalAccount;
if (string.IsNullOrEmpty(callout.Issuer))
return (null, new InvalidOperationException("Authorization callouts require an issuer to be specified"));
if (callout.AuthUsers.Count == 0)
return (null, new InvalidOperationException("Authorization callouts require authorized users to be specified"));
return (callout, null);
}
// -------------------------------------------------------------------------
// Batch 6: opts.go package-level parse/config helpers (F4)
// -------------------------------------------------------------------------
///
/// Parses user permission blocks.
/// Mirrors parseUserPermissions in opts.go.
///
public static (Permissions? Permissions, Exception? Error) ParseUserPermissions(
object? value,
ICollection? errors = null)
{
if (!TryGetMap(value, out var map))
{
return (null, new InvalidOperationException(
$"Expected permissions to be a map/struct, got {value?.GetType().Name ?? "null"}"));
}
var permissions = new Permissions();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "pub":
case "publish":
case "import":
{
var (subjectPermissions, parseError) = ParseVariablePermissions(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
permissions.Publish = subjectPermissions;
break;
}
case "sub":
case "subscribe":
case "export":
{
var (subjectPermissions, parseError) = ParseVariablePermissions(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
permissions.Subscribe = subjectPermissions;
break;
}
case "publish_allow_responses":
case "allow_responses":
if (TryConvertToBool(entry, out var responsesEnabled))
{
if (responsesEnabled)
{
permissions.Response = new ResponsePermission
{
MaxMsgs = ServerConstants.DefaultAllowResponseMaxMsgs,
Expires = ServerConstants.DefaultAllowResponseExpiration,
};
}
}
else
{
permissions.Response = ParseAllowResponses(entry, errors);
}
if (permissions.Response != null)
{
permissions.Publish ??= new SubjectPermission();
permissions.Publish.Allow ??= [];
}
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
{
errors?.Add(new InvalidOperationException(
$"Unknown field \"{rawKey}\" parsing permissions"));
}
break;
}
}
AuthHandler.ValidateResponsePermissions(permissions);
return (permissions, null);
}
///
/// Parses variable-style publish/subscribe permission values.
/// Mirrors parseVariablePermissions in opts.go.
///
public static (SubjectPermission? Permissions, Exception? Error) ParseVariablePermissions(
object? value,
ICollection? errors = null)
{
return TryGetMap(value, out _)
? ParseSubjectPermission(value, errors)
: ParseOldPermissionStyle(value, errors);
}
///
/// Parses single or array subject values used in permissions.
/// Mirrors parsePermSubjects in opts.go.
///
public static (List? Subjects, Exception? Error) ParsePermSubjects(
object? value,
ICollection? errors = null)
{
var normalized = NormalizeConfigValue(value);
var subjects = new List();
switch (normalized)
{
case string single:
subjects.Add(single);
break;
case IEnumerable array:
foreach (var entry in array)
{
var subject = NormalizeConfigValue(entry) as string;
if (subject == null)
{
return (null, new InvalidOperationException(
"Subject in permissions array cannot be cast to string"));
}
subjects.Add(subject);
}
break;
default:
return (null, new InvalidOperationException(
$"Expected subject permissions to be a subject, or array of subjects, got {normalized?.GetType().Name ?? "null"}"));
}
var validateError = CheckPermSubjectArray(subjects);
if (validateError != null)
return (null, validateError);
return (subjects, null);
}
///
/// Parses response permissions.
/// Mirrors parseAllowResponses in opts.go.
///
public static ResponsePermission? ParseAllowResponses(
object? value,
ICollection? errors = null)
{
var normalized = NormalizeConfigValue(value);
if (TryConvertToBool(normalized, out var enabled))
{
return enabled
? new ResponsePermission
{
MaxMsgs = ServerConstants.DefaultAllowResponseMaxMsgs,
Expires = ServerConstants.DefaultAllowResponseExpiration,
}
: null;
}
if (!TryGetMap(normalized, out var map))
{
errors?.Add(new InvalidOperationException(
"error parsing response permissions, expected a boolean or a map"));
return null;
}
var responsePermission = new ResponsePermission
{
MaxMsgs = ServerConstants.DefaultAllowResponseMaxMsgs,
Expires = ServerConstants.DefaultAllowResponseExpiration,
};
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "max":
case "max_msgs":
case "max_messages":
case "max_responses":
if (!TryConvertToLong(entry, out var maxMessages))
{
errors?.Add(new InvalidOperationException("error parsing max responses"));
break;
}
if (maxMessages != 0)
responsePermission.MaxMsgs = checked((int)maxMessages);
break;
case "expires":
case "expiration":
case "ttl":
{
var ttl = ParseDuration("expires", entry, errors, warnings: null);
if (ttl != TimeSpan.Zero)
responsePermission.Expires = ttl;
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
{
errors?.Add(new InvalidOperationException(
$"Unknown field \"{rawKey}\" parsing permissions"));
}
break;
}
}
return responsePermission;
}
///
/// Parses old-style allow-only permission syntax.
/// Mirrors parseOldPermissionStyle in opts.go.
///
public static (SubjectPermission? Permissions, Exception? Error) ParseOldPermissionStyle(
object? value,
ICollection? errors = null)
{
var (subjects, parseError) = ParsePermSubjects(value, errors);
if (parseError != null)
return (null, parseError);
return (new SubjectPermission { Allow = subjects }, null);
}
///
/// Parses new-style allow/deny subject permissions.
/// Mirrors parseSubjectPermission in opts.go.
///
public static (SubjectPermission? Permissions, Exception? Error) ParseSubjectPermission(
object? value,
ICollection? errors = null)
{
if (!TryGetMap(value, out var map))
{
return (null, new InvalidOperationException(
$"Expected subject permission map, got {value?.GetType().Name ?? "null"}"));
}
if (map.Count == 0)
return (null, null);
var permission = new SubjectPermission();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "allow":
{
var (subjects, parseError) = ParsePermSubjects(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
permission.Allow = subjects;
break;
}
case "deny":
{
var (subjects, parseError) = ParsePermSubjects(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
permission.Deny = subjects;
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
{
errors?.Add(new InvalidOperationException(
$"Unknown field name \"{rawKey}\" parsing subject permissions, only 'allow' or 'deny' are permitted"));
}
break;
}
}
return (permission, null);
}
///
/// Validates permission subjects.
/// Mirrors checkPermSubjectArray in opts.go.
///
public static Exception? CheckPermSubjectArray(IReadOnlyList subjects)
{
foreach (var subject in subjects)
{
if (SubscriptionIndex.IsValidSubject(subject))
continue;
var parts = subject.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
return new InvalidOperationException($"subject \"{subject}\" is not a valid subject");
if (!SubscriptionIndex.IsValidSubject(parts[0]))
return new InvalidOperationException($"subject \"{parts[0]}\" is not a valid subject");
}
return null;
}
///
/// Prints TLS help text.
/// Mirrors PrintTLSHelpAndDie in opts.go.
///
public static void PrintTLSHelpAndDie()
{
Console.WriteLine("TLS configuration help");
Console.WriteLine("Available cipher suites include:");
foreach (var cipher in CipherSuites.CipherMap.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
Console.WriteLine($" {cipher}");
Console.WriteLine();
Console.WriteLine("Available curve preferences include:");
foreach (var curve in CipherSuites.CurvePreferenceMap.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
Console.WriteLine($" {curve}");
}
///
/// Parses a configured cipher-suite name.
/// Mirrors parseCipher in opts.go.
///
public static (TlsCipherSuite? Cipher, Exception? Error) ParseCipher(string cipherName)
{
if (!CipherSuites.CipherMap.TryGetValue(cipherName, out var cipher))
return (null, new InvalidOperationException($"unrecognized cipher {cipherName}"));
return (cipher, null);
}
///
/// Parses a configured curve-preference name.
/// Mirrors parseCurvePreferences in opts.go.
///
public static (SslApplicationProtocol? Curve, Exception? Error) ParseCurvePreferences(string curveName)
{
if (!CipherSuites.CurvePreferenceMap.TryGetValue(curveName, out var curve))
return (null, new InvalidOperationException($"unrecognized curve preference {curveName}"));
return (curve, null);
}
///
/// Parses minimum TLS version config value.
/// Mirrors parseTLSVersion in opts.go.
///
public static (SslProtocols Version, Exception? Error) ParseTLSVersion(object? value)
{
if (NormalizeConfigValue(value) is not string versionText)
return (SslProtocols.None, new InvalidOperationException($"'min_version' wrong type: {value}"));
SslProtocols minVersion;
try
{
minVersion = TlsVersionJsonConverter.Parse(versionText);
}
catch (Exception ex)
{
return (SslProtocols.None, ex);
}
if (minVersion != SslProtocols.Tls12 && minVersion != SslProtocols.Tls13)
{
return (SslProtocols.None, new InvalidOperationException(
$"unsupported TLS version: {versionText}"));
}
return (minVersion, null);
}
///
/// Parses TLS config options from map values.
/// Mirrors parseTLS in opts.go.
///
public static (TlsConfigOpts? Options, Exception? Error) ParseTLS(object? value, bool isClientCtx)
{
if (!TryGetMap(value, out var map))
return (null, new InvalidOperationException("TLS options should be a map"));
var tlsOptions = new TlsConfigOpts();
var insecureConfigured = new List();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "cert_file":
tlsOptions.CertFile = entry as string ?? string.Empty;
break;
case "key_file":
tlsOptions.KeyFile = entry as string ?? string.Empty;
break;
case "ca_file":
tlsOptions.CaFile = entry as string ?? string.Empty;
break;
case "insecure":
if (!TryConvertToBool(entry, out var insecure))
return (null, new InvalidOperationException("error parsing tls config, expected 'insecure' to be a boolean"));
tlsOptions.Insecure = insecure;
break;
case "verify":
if (!TryConvertToBool(entry, out var verify))
return (null, new InvalidOperationException("error parsing tls config, expected 'verify' to be a boolean"));
tlsOptions.Verify = verify;
break;
case "verify_and_map":
if (!TryConvertToBool(entry, out var verifyAndMap))
return (null, new InvalidOperationException("error parsing tls config, expected 'verify_and_map' to be a boolean"));
if (verifyAndMap)
tlsOptions.Verify = true;
tlsOptions.Map = verifyAndMap;
break;
case "verify_cert_and_check_known_urls":
if (!TryConvertToBool(entry, out var verifyKnownUrls))
{
return (null, new InvalidOperationException(
"error parsing tls config, expected 'verify_cert_and_check_known_urls' to be a boolean"));
}
if (verifyKnownUrls && isClientCtx)
{
return (null, new InvalidOperationException(
"verify_cert_and_check_known_urls not supported in this context"));
}
if (verifyKnownUrls)
tlsOptions.Verify = true;
tlsOptions.TlsCheckKnownUrls = verifyKnownUrls;
break;
case "allow_insecure_cipher_suites":
if (!TryConvertToBool(entry, out var allowInsecureCiphers))
{
return (null, new InvalidOperationException(
"error parsing tls config, expected 'allow_insecure_cipher_suites' to be a boolean"));
}
tlsOptions.AllowInsecureCiphers = allowInsecureCiphers;
break;
case "cipher_suites":
if (!TryGetArray(entry, out var cipherArray) || cipherArray.Count == 0)
{
return (null, new InvalidOperationException(
"error parsing tls config, 'cipher_suites' cannot be empty"));
}
tlsOptions.Ciphers.Clear();
foreach (var cipherEntry in cipherArray)
{
if (NormalizeConfigValue(cipherEntry) is not string cipherName)
return (null, new InvalidOperationException("cipher suite name should be a string"));
var (cipher, parseError) = ParseCipher(cipherName);
if (parseError != null || cipher == null)
return (null, parseError);
tlsOptions.Ciphers.Add(cipher.Value);
if (IsLikelyInsecureCipherSuite(cipher.Value))
insecureConfigured.Add(cipherName);
}
break;
case "curve_preferences":
if (!TryGetArray(entry, out var curveArray) || curveArray.Count == 0)
{
return (null, new InvalidOperationException(
"error parsing tls config, 'curve_preferences' cannot be empty"));
}
tlsOptions.CurvePreferences.Clear();
foreach (var curveEntry in curveArray)
{
if (NormalizeConfigValue(curveEntry) is not string curveName)
return (null, new InvalidOperationException("curve preference should be a string"));
var (curve, parseError) = ParseCurvePreferences(curveName);
if (parseError != null || curve == null)
return (null, parseError);
tlsOptions.CurvePreferences.Add(curve.Value);
}
break;
case "timeout":
switch (entry)
{
case long timeoutLong:
tlsOptions.Timeout = timeoutLong;
break;
case double timeoutDouble:
tlsOptions.Timeout = timeoutDouble;
break;
case string timeoutString:
tlsOptions.Timeout = ParseDuration("tls timeout", timeoutString).TotalSeconds;
break;
default:
return (null, new InvalidOperationException("error parsing tls config, 'timeout' wrong type"));
}
break;
case "connection_rate_limit":
if (!TryConvertToLong(entry, out var rateLimit))
return (null, new InvalidOperationException("error parsing tls config, 'connection_rate_limit' wrong type"));
tlsOptions.RateLimit = rateLimit;
break;
case "pinned_certs":
if (!TryGetArray(entry, out var pinArray))
{
return (null, new InvalidOperationException(
"error parsing tls config, expected 'pinned_certs' to be a list"));
}
if (pinArray.Count > 0)
{
var pinned = new PinnedCertSet();
var pinRegex = new Regex("^[A-Fa-f0-9]{64}$", RegexOptions.Compiled);
foreach (var pinEntry in pinArray)
{
var pin = (NormalizeConfigValue(pinEntry) as string ?? string.Empty).ToLowerInvariant();
if (!pinRegex.IsMatch(pin))
{
return (null, new InvalidOperationException(
$"error parsing tls config, 'pinned_certs' key {pin} does not look like hex-encoded sha256"));
}
pinned.Add(pin);
}
tlsOptions.PinnedCerts = pinned;
}
break;
case "handshake_first":
case "first":
case "immediate":
switch (entry)
{
case bool handshakeFirst:
tlsOptions.HandshakeFirst = handshakeFirst;
break;
case string handshakeValue:
switch (handshakeValue.Trim().ToLowerInvariant())
{
case "true":
case "on":
tlsOptions.HandshakeFirst = true;
break;
case "false":
case "off":
tlsOptions.HandshakeFirst = false;
break;
case "auto":
case "auto_fallback":
tlsOptions.HandshakeFirst = true;
tlsOptions.FallbackDelay = ServerConstants.DefaultTlsHandshakeFirstFallbackDelay;
break;
default:
{
var delay = ParseDuration("handshake_first", handshakeValue);
if (delay == TimeSpan.Zero)
{
return (null, new InvalidOperationException(
$"field \"{rawKey}\" value \"{handshakeValue}\" is invalid"));
}
tlsOptions.HandshakeFirst = true;
tlsOptions.FallbackDelay = delay;
break;
}
}
break;
default:
return (null, new InvalidOperationException(
$"field \"{rawKey}\" should be a boolean or a string, got {entry?.GetType().Name ?? "null"}"));
}
break;
case "certs":
case "certificates":
if (!TryGetArray(entry, out var certsArray))
{
return (null, new InvalidOperationException(
$"error parsing certificates config: unsupported type {entry?.GetType().Name ?? "null"}"));
}
tlsOptions.Certificates.Clear();
foreach (var certEntry in certsArray)
{
if (!TryGetMap(certEntry, out var certMap))
{
return (null, new InvalidOperationException(
$"error parsing certificates config: unsupported type {certEntry?.GetType().Name ?? "null"}"));
}
var pair = new TlsCertPairOpt();
foreach (var (certKey, certValueRaw) in certMap)
{
var certValue = NormalizeConfigValue(certValueRaw) as string;
if (string.IsNullOrEmpty(certValue))
{
return (null, new InvalidOperationException(
$"error parsing certificates config: unsupported type {certValueRaw?.GetType().Name ?? "null"}"));
}
switch (certKey)
{
case "cert_file":
pair.CertFile = certValue;
break;
case "key_file":
pair.KeyFile = certValue;
break;
default:
return (null, new InvalidOperationException(
$"error parsing tls certs config, unknown field \"{certKey}\""));
}
}
if (string.IsNullOrEmpty(pair.CertFile) || string.IsNullOrEmpty(pair.KeyFile))
{
return (null, new InvalidOperationException(
"error parsing certificates config: both 'cert_file' and 'key_file' options are required"));
}
tlsOptions.Certificates.Add(pair);
}
break;
case "min_version":
{
var (minVersion, parseError) = ParseTLSVersion(entry);
if (parseError != null)
return (null, new InvalidOperationException($"error parsing tls config: {parseError.Message}", parseError));
tlsOptions.MinVersion = minVersion;
break;
}
case "cert_match":
tlsOptions.CertMatch = entry as string ?? string.Empty;
break;
case "cert_match_skip_invalid":
if (!TryConvertToBool(entry, out var certMatchSkipInvalid))
{
return (null, new InvalidOperationException(
"error parsing tls config, expected 'cert_match_skip_invalid' to be a boolean"));
}
tlsOptions.CertMatchSkipInvalid = certMatchSkipInvalid;
break;
case "ca_certs_match":
{
var (caCertsMatch, parseError) = ParseStringArray("ca_certs_match", entry, errors: null);
if (parseError != null || caCertsMatch == null)
return (null, parseError);
tlsOptions.CaCertsMatch = caCertsMatch;
break;
}
default:
return (null, new InvalidOperationException(
$"error parsing tls config, unknown field \"{rawKey}\""));
}
}
if (tlsOptions.Certificates.Count > 0 && !string.IsNullOrEmpty(tlsOptions.CertFile))
{
return (null, new InvalidOperationException(
"error parsing tls config, cannot combine 'cert_file' option with 'certs' option"));
}
if (tlsOptions.Ciphers.Count == 0)
tlsOptions.Ciphers.AddRange(CipherSuites.DefaultCipherSuites());
if (tlsOptions.CurvePreferences.Count == 0)
{
foreach (var curveName in CipherSuites.DefaultCurvePreferences())
{
if (CipherSuites.CurvePreferenceMap.TryGetValue(curveName, out var curve))
tlsOptions.CurvePreferences.Add(curve);
}
}
if (!tlsOptions.AllowInsecureCiphers && insecureConfigured.Count > 0)
{
return (null, new InvalidOperationException(
$"insecure cipher suites configured without 'allow_insecure_cipher_suites' option set: {string.Join(", ", insecureConfigured)}"));
}
return (tlsOptions, null);
}
///
/// Parses simple auth objects (user/pass/token/timeout ).
/// Mirrors parseSimpleAuth in opts.go.
///
public static AuthorizationConfig ParseSimpleAuth(
object? value,
ICollection? errors = null)
{
var auth = new AuthorizationConfig();
if (!TryGetMap(value, out var map))
{
errors?.Add(new InvalidOperationException("authorization should be a map"));
return auth;
}
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "user":
case "username":
auth.User = entry as string ?? string.Empty;
break;
case "pass":
case "password":
auth.Pass = entry as string ?? string.Empty;
break;
case "token":
auth.Token = entry as string ?? string.Empty;
break;
case "timeout":
if (TryConvertToDouble(entry, out var timeout))
auth.Timeout = timeout;
else
errors?.Add(new InvalidOperationException("error parsing authorization timeout"));
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return auth;
}
///
/// Parses string or array-of-string fields.
/// Mirrors parseStringArray in opts.go.
///
public static (List? Values, Exception? Error) ParseStringArray(
string fieldName,
object? value,
ICollection? errors = null)
{
switch (NormalizeConfigValue(value))
{
case string text:
return ([text], null);
case IEnumerable array:
{
var values = new List();
foreach (var entry in array)
{
if (NormalizeConfigValue(entry) is string item)
{
values.Add(item);
continue;
}
var parseError = new InvalidOperationException(
$"error parsing {fieldName}: unsupported type in array {entry?.GetType().Name ?? "null"}");
errors?.Add(parseError);
}
return (values, null);
}
default:
{
var parseError = new InvalidOperationException(
$"error parsing {fieldName}: unsupported type {value?.GetType().Name ?? "null"}");
errors?.Add(parseError);
return (null, parseError);
}
}
}
///
/// Parses websocket configuration block.
/// Mirrors parseWebsocket in opts.go.
///
public static Exception? ParseWebsocket(
object? value,
ServerOptions options,
ICollection? errors = null,
ICollection? warnings = null)
{
ArgumentNullException.ThrowIfNull(options);
if (!TryGetMap(value, out var map))
return new InvalidOperationException($"Expected websocket to be a map, 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.Websocket.Host = host;
options.Websocket.Port = port;
}
catch (Exception ex)
{
errors?.Add(ex);
}
break;
case "port":
if (TryConvertToLong(entry, out var websocketPort))
options.Websocket.Port = checked((int)websocketPort);
break;
case "host":
case "net":
options.Websocket.Host = entry as string ?? string.Empty;
break;
case "advertise":
options.Websocket.Advertise = entry as string ?? string.Empty;
break;
case "no_tls":
if (TryConvertToBool(entry, out var noTls))
options.Websocket.NoTls = noTls;
break;
case "tls":
{
var (tlsOptions, parseError) = ParseTLS(entry, isClientCtx: true);
if (parseError != null || tlsOptions == null)
{
errors?.Add(parseError ?? new InvalidOperationException("unable to parse websocket tls options"));
break;
}
var (tlsConfig, genError) = GenTLSConfig(tlsOptions);
if (genError != null)
{
errors?.Add(genError);
break;
}
options.Websocket.TlsConfig = tlsConfig;
options.Websocket.TlsMap = tlsOptions.Map;
options.Websocket.TlsPinnedCerts = tlsOptions.PinnedCerts;
options.Websocket.TlsConfigOpts = tlsOptions;
break;
}
case "same_origin":
if (TryConvertToBool(entry, out var sameOrigin))
options.Websocket.SameOrigin = sameOrigin;
break;
case "allowed_origins":
case "allowed_origin":
case "allow_origins":
case "allow_origin":
case "origins":
case "origin":
{
var (origins, parseError) = ParseStringArray("allowed origins", entry, errors);
if (parseError == null && origins != null)
options.Websocket.AllowedOrigins = origins;
break;
}
case "handshake_timeout":
options.Websocket.HandshakeTimeout = ParseDuration("handshake timeout", entry, errors, warnings);
break;
case "compress":
case "compression":
if (TryConvertToBool(entry, out var websocketCompression))
options.Websocket.Compression = websocketCompression;
break;
case "authorization":
case "authentication":
{
var auth = ParseSimpleAuth(entry, errors);
options.Websocket.Username = auth.User;
options.Websocket.Password = auth.Pass;
options.Websocket.Token = auth.Token;
options.Websocket.AuthTimeout = auth.Timeout;
break;
}
case "jwt_cookie":
options.Websocket.JwtCookie = entry as string ?? string.Empty;
break;
case "user_cookie":
options.Websocket.UsernameCookie = entry as string ?? string.Empty;
break;
case "pass_cookie":
options.Websocket.PasswordCookie = entry as string ?? string.Empty;
break;
case "token_cookie":
options.Websocket.TokenCookie = entry as string ?? string.Empty;
break;
case "no_auth_user":
options.Websocket.NoAuthUser = entry as string ?? string.Empty;
break;
case "headers":
if (!TryGetMap(entry, out var headerMap))
{
errors?.Add(new InvalidOperationException(
$"error parsing headers: unsupported type {entry?.GetType().Name ?? "null"}"));
break;
}
var headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var (headerName, headerValueRaw) in headerMap)
{
if (NormalizeConfigValue(headerValueRaw) is string headerValue)
{
headers[headerName] = headerValue;
}
else
{
errors?.Add(new InvalidOperationException(
$"error parsing header key {headerName}: unsupported type {headerValueRaw?.GetType().Name ?? "null"}"));
}
}
options.Websocket.Headers = headers;
break;
case "ping_interval":
options.Websocket.PingInterval = ParseDuration("ping_interval", entry, errors, warnings);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return null;
}
///
/// Parses MQTT configuration block.
/// Mirrors parseMQTT in opts.go.
///
public static Exception? ParseMQTT(
object? value,
ServerOptions options,
ICollection? errors = null,
ICollection? warnings = null)
{
ArgumentNullException.ThrowIfNull(options);
if (!TryGetMap(value, out var map))
return new InvalidOperationException($"Expected mqtt to be a map, 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.Mqtt.Host = host;
options.Mqtt.Port = port;
}
catch (Exception ex)
{
errors?.Add(ex);
}
break;
case "port":
if (TryConvertToLong(entry, out var mqttPort))
options.Mqtt.Port = checked((int)mqttPort);
break;
case "host":
case "net":
options.Mqtt.Host = entry as string ?? string.Empty;
break;
case "tls":
{
var (tlsOptions, parseError) = ParseTLS(entry, isClientCtx: true);
if (parseError != null || tlsOptions == null)
{
errors?.Add(parseError ?? new InvalidOperationException("unable to parse mqtt tls options"));
break;
}
var (tlsConfig, genError) = GenTLSConfig(tlsOptions);
if (genError != null)
{
errors?.Add(genError);
break;
}
options.Mqtt.TlsConfig = tlsConfig;
options.Mqtt.TlsTimeout = tlsOptions.Timeout;
options.Mqtt.TlsMap = tlsOptions.Map;
options.Mqtt.TlsPinnedCerts = tlsOptions.PinnedCerts;
options.Mqtt.TlsConfigOpts = tlsOptions;
break;
}
case "authorization":
case "authentication":
{
var auth = ParseSimpleAuth(entry, errors);
options.Mqtt.Username = auth.User;
options.Mqtt.Password = auth.Pass;
options.Mqtt.Token = auth.Token;
options.Mqtt.AuthTimeout = auth.Timeout;
break;
}
case "no_auth_user":
options.Mqtt.NoAuthUser = entry as string ?? string.Empty;
break;
case "ack_wait":
case "ackwait":
options.Mqtt.AckWait = ParseDuration("ack_wait", entry, errors, warnings);
break;
case "js_api_timeout":
case "api_timeout":
options.Mqtt.JsApiTimeout = ParseDuration("js_api_timeout", entry, errors, warnings);
break;
case "max_ack_pending":
case "max_pending":
case "max_inflight":
if (!TryConvertToLong(entry, out var maxAckPending))
{
errors?.Add(new InvalidOperationException("invalid max_ack_pending value"));
break;
}
if (maxAckPending is < 0 or > ushort.MaxValue)
{
errors?.Add(new InvalidOperationException(
$"invalid value {maxAckPending}, should be in [0..{ushort.MaxValue}] range"));
break;
}
options.Mqtt.MaxAckPending = (ushort)maxAckPending;
break;
case "js_domain":
options.Mqtt.JsDomain = entry as string ?? string.Empty;
break;
case "stream_replicas":
if (TryConvertToLong(entry, out var streamReplicas))
options.Mqtt.StreamReplicas = checked((int)streamReplicas);
break;
case "consumer_replicas":
warnings?.Add(new InvalidOperationException(
"consumer replicas setting ignored in this server version"));
break;
case "consumer_memory_storage":
if (TryConvertToBool(entry, out var consumerMemoryStorage))
options.Mqtt.ConsumerMemoryStorage = consumerMemoryStorage;
break;
case "consumer_inactive_threshold":
case "consumer_auto_cleanup":
options.Mqtt.ConsumerInactiveThreshold =
ParseDuration("consumer_inactive_threshold", entry, errors, warnings);
break;
case "reject_qos2_publish":
if (TryConvertToBool(entry, out var rejectQoS2Publish))
options.Mqtt.RejectQoS2Pub = rejectQoS2Publish;
break;
case "downgrade_qos2_subscribe":
if (TryConvertToBool(entry, out var downgradeQoS2Subscribe))
options.Mqtt.DowngradeQoS2Sub = downgradeQoS2Subscribe;
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return null;
}
///
/// Parses proxy configuration block.
/// Mirrors parseProxies in opts.go.
///
public static (ProxiesConfig? Proxies, Exception? Error) ParseProxies(
object? value,
ICollection? errors = null)
{
if (!TryGetMap(value, out var map))
{
return (null, new InvalidOperationException(
$"expected proxies to be a map/struct, got {value?.GetType().Name ?? "null"}"));
}
var proxies = new ProxiesConfig();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "trusted":
{
var (trusted, parseError) = ParseProxiesTrusted(entry, errors);
if (parseError != null)
{
errors?.Add(parseError);
break;
}
proxies.Trusted = trusted;
break;
}
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return (proxies, null);
}
///
/// Parses trusted proxy entries.
/// Mirrors parseProxiesTrusted in opts.go.
///
public static (List Trusted, Exception? Error) ParseProxiesTrusted(
object? value,
ICollection? errors = null)
{
if (!TryGetArray(value, out var array))
{
return ([], new InvalidOperationException(
$"expected proxies' trusted field to be an array, got {value?.GetType().Name ?? "null"}"));
}
var trusted = new List();
foreach (var entry in array)
{
if (!TryGetMap(entry, out var proxyMap))
{
errors?.Add(new InvalidOperationException(
$"expected proxies' trusted entry to be a map/struct, got {entry?.GetType().Name ?? "null"}"));
continue;
}
var proxy = new ProxyConfig();
foreach (var (rawKey, rawValue) in proxyMap)
{
var key = rawKey.ToLowerInvariant();
var normalized = NormalizeConfigValue(rawValue);
switch (key)
{
case "key":
case "public_key":
proxy.Key = normalized as string ?? string.Empty;
if (!IsValidNatsPublicKey(proxy.Key))
{
errors?.Add(new InvalidOperationException($"invalid proxy key \"{proxy.Key}\""));
}
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
trusted.Add(proxy);
}
return (trusted, null);
}
///
/// Generates runtime TLS options from parsed TLS config.
/// Mirrors GenTLSConfig in opts.go.
///
public static (SslServerAuthenticationOptions? Config, Exception? Error) GenTLSConfig(TlsConfigOpts options)
{
ArgumentNullException.ThrowIfNull(options);
var config = new SslServerAuthenticationOptions
{
EnabledSslProtocols = options.MinVersion == SslProtocols.Tls13
? SslProtocols.Tls13
: SslProtocols.Tls12 | SslProtocols.Tls13,
CertificateRevocationCheckMode = X509RevocationMode.NoCheck,
ClientCertificateRequired = options.Verify,
};
if (!string.IsNullOrEmpty(options.CertFile) && string.IsNullOrEmpty(options.KeyFile))
return (null, new InvalidOperationException("missing 'key_file' in TLS configuration"));
if (string.IsNullOrEmpty(options.CertFile) && !string.IsNullOrEmpty(options.KeyFile))
return (null, new InvalidOperationException("missing 'cert_file' in TLS configuration"));
try
{
if (!string.IsNullOrEmpty(options.CertFile) && !string.IsNullOrEmpty(options.KeyFile))
{
var certificate = X509Certificate2.CreateFromPemFile(
ExpandPath(options.CertFile),
ExpandPath(options.KeyFile));
config.ServerCertificate = new X509Certificate2(certificate.Export(X509ContentType.Pkcs12));
}
else if (options.Certificates.Count > 0)
{
var pair = options.Certificates[0];
var certificate = X509Certificate2.CreateFromPemFile(
ExpandPath(pair.CertFile),
ExpandPath(pair.KeyFile));
config.ServerCertificate = new X509Certificate2(certificate.Export(X509ContentType.Pkcs12));
}
}
catch (Exception ex)
{
return (null, new InvalidOperationException($"error parsing X509 certificate/key pair: {ex.Message}", ex));
}
if (options.Ciphers.Count > 0)
{
try
{
config.CipherSuitesPolicy = new CipherSuitesPolicy(options.Ciphers);
}
catch (PlatformNotSupportedException)
{
// Some platforms do not allow explicit cipher suite policies.
}
}
if (!string.IsNullOrEmpty(options.CaFile) && !File.Exists(ExpandPath(options.CaFile)))
return (null, new FileNotFoundException("failed to parse root ca certificate", ExpandPath(options.CaFile)));
return (config, null);
}
///
/// Configures options from command-line arguments.
/// Mirrors ConfigureOptions in opts.go.
///
public static (ServerOptions? Options, Exception? Error) ConfigureOptions(
IReadOnlyList args,
Action? printVersion = null,
Action? printHelp = null,
Action? printTLSHelp = null)
{
var flagOptions = new ServerOptions();
var explicitBooleans = new Dictionary(StringComparer.Ordinal);
var nonFlags = new List();
string configFile = string.Empty;
var tlsFlagsSeen = false;
var routesFlagSeen = false;
for (var i = 0; i < args.Count; i++)
{
var rawArg = args[i];
if (!rawArg.StartsWith('-'))
{
nonFlags.Add(rawArg);
continue;
}
var inlineValue = default(string);
var arg = rawArg;
var equalsIndex = rawArg.IndexOf('=');
if (equalsIndex > 0)
{
arg = rawArg[..equalsIndex];
inlineValue = rawArg[(equalsIndex + 1)..];
}
string? ReadValue()
{
if (!string.IsNullOrEmpty(inlineValue))
return inlineValue;
if (i + 1 >= args.Count)
return null;
i++;
return args[i];
}
bool ReadBoolean(bool defaultValue = true)
{
var value = ReadValue();
if (value == null || value.StartsWith('-'))
{
if (value != null)
i--;
return defaultValue;
}
return bool.TryParse(value, out var parsed) ? parsed : defaultValue;
}
switch (arg)
{
case "-h":
case "--help":
printHelp?.Invoke();
return (null, null);
case "-v":
case "--version":
printVersion?.Invoke();
return (null, null);
case "--help_tls":
printTLSHelp?.Invoke();
return (null, null);
case "-p":
case "--port":
if (!int.TryParse(ReadValue(), out var port))
return (null, new InvalidOperationException("Invalid value for port"));
flagOptions.Port = port;
break;
case "-a":
case "--addr":
case "--net":
flagOptions.Host = ReadValue() ?? string.Empty;
break;
case "--client_advertise":
flagOptions.ClientAdvertise = ReadValue() ?? string.Empty;
break;
case "-D":
case "--debug":
flagOptions.Debug = ReadBoolean();
explicitBooleans["Debug"] = flagOptions.Debug;
break;
case "-V":
case "--trace":
flagOptions.Trace = ReadBoolean();
explicitBooleans["Trace"] = flagOptions.Trace;
break;
case "-VV":
flagOptions.Trace = ReadBoolean();
flagOptions.TraceVerbose = flagOptions.Trace;
explicitBooleans["Trace"] = flagOptions.Trace;
explicitBooleans["TraceVerbose"] = flagOptions.TraceVerbose;
break;
case "-DV":
flagOptions.Debug = ReadBoolean();
flagOptions.Trace = flagOptions.Debug;
explicitBooleans["Debug"] = flagOptions.Debug;
explicitBooleans["Trace"] = flagOptions.Trace;
break;
case "-DVV":
flagOptions.Debug = ReadBoolean();
flagOptions.Trace = flagOptions.Debug;
flagOptions.TraceVerbose = flagOptions.Debug;
explicitBooleans["Debug"] = flagOptions.Debug;
explicitBooleans["Trace"] = flagOptions.Trace;
explicitBooleans["TraceVerbose"] = flagOptions.TraceVerbose;
break;
case "-T":
case "--logtime":
flagOptions.Logtime = ReadBoolean(defaultValue: true);
explicitBooleans["Logtime"] = flagOptions.Logtime;
break;
case "--logtime_utc":
flagOptions.LogtimeUtc = ReadBoolean();
break;
case "--user":
flagOptions.Username = ReadValue() ?? string.Empty;
break;
case "--pass":
flagOptions.Password = ReadValue() ?? string.Empty;
break;
case "--auth":
flagOptions.Authorization = ReadValue() ?? string.Empty;
break;
case "-m":
case "--http_port":
if (!int.TryParse(ReadValue(), out var httpPort))
return (null, new InvalidOperationException("Invalid value for http_port"));
flagOptions.HttpPort = httpPort;
break;
case "--ms":
case "--https_port":
if (!int.TryParse(ReadValue(), out var httpsPort))
return (null, new InvalidOperationException("Invalid value for https_port"));
flagOptions.HttpsPort = httpsPort;
break;
case "-c":
case "--config":
configFile = ReadValue() ?? string.Empty;
break;
case "-t":
flagOptions.CheckConfig = ReadBoolean();
break;
case "-P":
case "--pid":
flagOptions.PidFile = ReadValue() ?? string.Empty;
break;
case "--ports_file_dir":
flagOptions.PortsFileDir = ReadValue() ?? string.Empty;
break;
case "-l":
case "--log":
flagOptions.LogFile = ReadValue() ?? string.Empty;
break;
case "--log_size_limit":
if (!long.TryParse(ReadValue(), out var logSizeLimit))
return (null, new InvalidOperationException("Invalid value for log_size_limit"));
flagOptions.LogSizeLimit = logSizeLimit;
break;
case "-s":
case "--syslog":
flagOptions.Syslog = ReadBoolean();
explicitBooleans["Syslog"] = flagOptions.Syslog;
break;
case "-r":
case "--remote_syslog":
flagOptions.RemoteSyslog = ReadValue() ?? string.Empty;
break;
case "--profile":
if (!int.TryParse(ReadValue(), out var profilePort))
return (null, new InvalidOperationException("Invalid value for profile"));
flagOptions.ProfPort = profilePort;
break;
case "--routes":
flagOptions.RoutesStr = ReadValue() ?? string.Empty;
routesFlagSeen = true;
break;
case "--cluster":
case "--cluster_listen":
flagOptions.Cluster.ListenStr = ReadValue() ?? string.Empty;
break;
case "--cluster_advertise":
flagOptions.Cluster.Advertise = ReadValue() ?? string.Empty;
break;
case "--no_advertise":
flagOptions.Cluster.NoAdvertise = ReadBoolean();
explicitBooleans["Cluster.NoAdvertise"] = flagOptions.Cluster.NoAdvertise;
break;
case "--connect_retries":
if (!int.TryParse(ReadValue(), out var connectRetries))
return (null, new InvalidOperationException("Invalid value for connect_retries"));
flagOptions.Cluster.ConnectRetries = connectRetries;
break;
case "--cluster_name":
flagOptions.Cluster.Name = ReadValue() ?? string.Empty;
break;
case "--tls":
flagOptions.Tls = ReadBoolean();
tlsFlagsSeen = true;
break;
case "--tlsverify":
flagOptions.TlsVerify = ReadBoolean();
tlsFlagsSeen = true;
break;
case "--tlscert":
flagOptions.TlsCert = ReadValue() ?? string.Empty;
tlsFlagsSeen = true;
break;
case "--tlskey":
flagOptions.TlsKey = ReadValue() ?? string.Empty;
tlsFlagsSeen = true;
break;
case "--tlscacert":
flagOptions.TlsCaCert = ReadValue() ?? string.Empty;
tlsFlagsSeen = true;
break;
case "--max_traced_msg_len":
if (!int.TryParse(ReadValue(), out var maxTraceLength))
return (null, new InvalidOperationException("Invalid value for max_traced_msg_len"));
flagOptions.MaxTracedMsgLen = maxTraceLength;
break;
case "--js":
case "--jetstream":
flagOptions.JetStream = ReadBoolean();
explicitBooleans["JetStream"] = flagOptions.JetStream;
break;
case "--sd":
case "--store_dir":
flagOptions.StoreDir = ReadValue() ?? string.Empty;
break;
default:
nonFlags.Add(rawArg);
break;
}
}
var (showVersion, showHelp, parseError) = NatsServer.ProcessCommandLineArgs(nonFlags.ToArray());
if (parseError != null)
return (null, parseError);
if (showVersion)
{
printVersion?.Invoke();
return (null, null);
}
if (showHelp)
{
printHelp?.Invoke();
return (null, null);
}
FlagSnapshot = flagOptions.Clone();
foreach (var (name, value) in explicitBooleans)
TrackExplicitVal(FlagSnapshot.InCmdLine, name, value);
ServerOptions resolvedOptions;
if (!string.IsNullOrEmpty(configFile))
{
resolvedOptions = ProcessConfigFile(configFile);
resolvedOptions = MergeOptions(resolvedOptions, flagOptions);
}
else
{
resolvedOptions = flagOptions;
}
if (resolvedOptions.CheckConfig && string.IsNullOrEmpty(configFile))
{
return (null, new InvalidOperationException(
"must specify [-c, --config] option to check configuration file syntax"));
}
foreach (var (name, value) in explicitBooleans)
{
TrackExplicitVal(resolvedOptions.InCmdLine, name, value);
switch (name)
{
case "Debug":
resolvedOptions.Debug = value;
break;
case "Trace":
resolvedOptions.Trace = value;
break;
case "TraceVerbose":
resolvedOptions.TraceVerbose = value;
break;
case "Logtime":
resolvedOptions.Logtime = value;
break;
case "Syslog":
resolvedOptions.Syslog = value;
break;
case "Cluster.NoAdvertise":
resolvedOptions.Cluster.NoAdvertise = value;
break;
case "JetStream":
resolvedOptions.JetStream = value;
break;
}
}
if (!string.IsNullOrEmpty(resolvedOptions.Cluster.ListenStr))
{
var clusterError = resolvedOptions.OverrideCluster();
if (clusterError != null)
return (null, clusterError);
}
if (routesFlagSeen)
{
resolvedOptions.Routes = string.IsNullOrEmpty(resolvedOptions.RoutesStr)
? []
: RoutesFromStr(resolvedOptions.RoutesStr);
}
if (tlsFlagsSeen && resolvedOptions.Tls)
{
var tlsError = resolvedOptions.OverrideTls();
if (tlsError != null)
return (null, tlsError);
}
if (!string.IsNullOrEmpty(resolvedOptions.RoutesStr) &&
string.IsNullOrEmpty(resolvedOptions.Cluster.ListenStr) &&
string.IsNullOrEmpty(resolvedOptions.Cluster.Host) &&
resolvedOptions.Cluster.Port == 0)
{
return (null, new InvalidOperationException(
"solicited routes require cluster capabilities, e.g. --cluster"));
}
return (resolvedOptions, null);
}
// -------------------------------------------------------------------------
// Private helpers
// -------------------------------------------------------------------------
private static Dictionary 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? Users { get; set; }
public string AccountName { get; set; } = string.Empty;
public bool ProxyRequired { get; set; }
}
private static Permissions? ParsePermissionsValue(object? value, ICollection? errors)
{
if (!TryGetMap(value, out var map))
{
errors?.Add(new InvalidOperationException(
$"Expected permissions to be a map/struct, got {value?.GetType().Name ?? "null"}"));
return null;
}
var permissions = new Permissions();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "publish":
case "import":
permissions.Publish = ParseSubjectPermissionValue(entry, errors);
break;
case "subscribe":
case "export":
permissions.Subscribe = ParseSubjectPermissionValue(entry, errors);
break;
case "responses":
case "allow_responses":
permissions.Response = ParseAllowResponsesValue(entry, errors);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
AuthHandler.ValidateResponsePermissions(permissions);
return permissions;
}
private static SubjectPermission? ParseSubjectPermissionValue(object? value, ICollection? errors)
{
var normalized = NormalizeConfigValue(value);
if (TryGetMap(normalized, out var map))
{
var permission = new SubjectPermission();
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "allow":
permission.Allow = ParsePermissionSubjects(entry, errors);
break;
case "deny":
permission.Deny = ParsePermissionSubjects(entry, errors);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return permission;
}
return new SubjectPermission
{
Allow = ParsePermissionSubjects(normalized, errors),
};
}
private static List ParsePermissionSubjects(object? value, ICollection? errors)
{
if (TryGetArray(value, out var array))
{
var subjects = new List(array.Count);
foreach (var entry in array)
{
var subject = NormalizeConfigValue(entry) as string;
if (string.IsNullOrWhiteSpace(subject))
{
errors?.Add(new InvalidOperationException("permission subjects must be non-empty strings"));
continue;
}
if (!SubscriptionIndex.IsValidSubject(subject))
{
errors?.Add(new InvalidOperationException($"invalid subject \"{subject}\" in permissions"));
continue;
}
subjects.Add(subject);
}
return subjects;
}
if (NormalizeConfigValue(value) is string singleSubject)
{
if (!SubscriptionIndex.IsValidSubject(singleSubject))
{
errors?.Add(new InvalidOperationException($"invalid subject \"{singleSubject}\" in permissions"));
return [];
}
return [singleSubject];
}
errors?.Add(new InvalidOperationException(
$"Expected permission subject array/string, got {value?.GetType().Name ?? "null"}"));
return [];
}
private static ResponsePermission? ParseAllowResponsesValue(object? value, ICollection? errors)
{
var normalized = NormalizeConfigValue(value);
if (TryConvertToBool(normalized, out var enabled))
{
return enabled
? new ResponsePermission
{
MaxMsgs = ServerConstants.DefaultAllowResponseMaxMsgs,
Expires = ServerConstants.DefaultAllowResponseExpiration,
}
: null;
}
if (!TryGetMap(normalized, out var map))
{
errors?.Add(new InvalidOperationException(
$"Expected allow_responses to be a boolean or map, got {normalized?.GetType().Name ?? "null"}"));
return null;
}
var response = new ResponsePermission
{
MaxMsgs = ServerConstants.DefaultAllowResponseMaxMsgs,
Expires = ServerConstants.DefaultAllowResponseExpiration,
};
foreach (var (rawKey, rawValue) in map)
{
var key = rawKey.ToLowerInvariant();
var entry = NormalizeConfigValue(rawValue);
switch (key)
{
case "max":
case "max_msgs":
if (TryConvertToLong(entry, out var maxMessages))
response.MaxMsgs = checked((int)maxMessages);
else
errors?.Add(new InvalidOperationException("allow_responses.max should be an integer"));
break;
case "expires":
response.Expires = ParseDuration("allow_responses.expires", entry, errors, warnings: null);
break;
default:
if (!ConfigFlags.AllowUnknownTopLevelField)
errors?.Add(new InvalidOperationException($"unknown field \"{rawKey}\""));
break;
}
}
return response;
}
private static bool IsLikelyPublicNkey(string value, char prefix) =>
!string.IsNullOrWhiteSpace(value) &&
value.Length >= 10 &&
value[0] == prefix;
private static bool IsValidNatsPublicKey(string value)
{
if (string.IsNullOrWhiteSpace(value))
return false;
try
{
_ = NATS.NKeys.KeyPair.FromPublicKey(value.AsSpan());
return true;
}
catch
{
return false;
}
}
private static bool IsLikelyInsecureCipherSuite(TlsCipherSuite cipher)
{
var name = cipher.ToString();
return name.Contains("_CBC_", StringComparison.OrdinalIgnoreCase) ||
name.Contains("RC4", StringComparison.OrdinalIgnoreCase) ||
name.Contains("3DES", StringComparison.OrdinalIgnoreCase) ||
name.Contains("DES", StringComparison.OrdinalIgnoreCase) ||
name.Contains("NULL", StringComparison.OrdinalIgnoreCase);
}
private static WriteTimeoutPolicy ParseWriteDeadlinePolicyFallback(string value, ICollection? 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? errors,
ICollection? 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 map,
IEnumerable 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 map)
{
var normalized = NormalizeConfigValue(value);
if (normalized is IReadOnlyDictionary readonlyMap)
{
map = readonlyMap;
return true;
}
if (normalized is Dictionary dict)
{
map = dict;
return true;
}
map = new Dictionary(StringComparer.OrdinalIgnoreCase);
return false;
}
private static bool TryGetArray(object? value, out IReadOnlyList values)
{
var normalized = NormalizeConfigValue(value);
if (normalized is IReadOnlyList readonlyValues)
{
values = readonlyValues;
return true;
}
if (normalized is List listValues)
{
values = listValues;
return true;
}
values = [];
return false;
}
private static List ParseStringList(object? value)
{
if (TryGetArray(value, out var items))
{
var result = new List(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 string ComputeConfigDigest(string configContent)
{
var bytes = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(configContent));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
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,
};
}