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