// Port of Go server/opts.go processConfigFileLine — maps parsed config dictionaries
// to NatsOptions. Reference: golang/nats-server/server/opts.go lines 1050-1400.
using System.Globalization;
using System.Text.RegularExpressions;
using NATS.Server.Auth;
using NATS.Server.JetStream;
using NATS.Server.Tls;
namespace NATS.Server.Configuration;
///
/// Maps a parsed NATS configuration dictionary (produced by )
/// into a fully populated instance. Collects all validation
/// errors rather than failing on the first one.
///
public static class ConfigProcessor
{
///
/// Parses a configuration file and returns the populated options.
///
public static NatsOptions ProcessConfigFile(string filePath)
{
var config = NatsConfParser.ParseFile(filePath);
var opts = new NatsOptions { ConfigFile = filePath };
ApplyConfig(config, opts);
return opts;
}
///
/// Parses configuration text (not from a file) and returns the populated options.
///
public static NatsOptions ProcessConfig(string configText)
{
var config = NatsConfParser.Parse(configText);
var opts = new NatsOptions();
ApplyConfig(config, opts);
return opts;
}
///
/// Applies a parsed configuration dictionary to existing options.
/// Throws if any validation errors are collected.
///
public static void ApplyConfig(Dictionary config, NatsOptions opts)
{
var errors = new List();
var warnings = new List();
foreach (var (key, value) in config)
{
try
{
ProcessKey(key, value, opts, errors, warnings);
}
catch (Exception ex)
{
errors.Add($"Error processing '{key}': {ex.Message}");
}
}
if (errors.Count > 0)
{
throw new ConfigProcessorException("Configuration errors", errors, warnings);
}
}
private static void ProcessKey(
string key,
object? value,
NatsOptions opts,
List errors,
List warnings)
{
// Keys are already case-insensitive from the parser (OrdinalIgnoreCase dictionaries),
// but we normalize here for the switch statement.
switch (key.ToLowerInvariant())
{
case "listen":
ParseListen(value, opts);
break;
case "port":
opts.Port = ToInt(value);
break;
case "host" or "net":
opts.Host = ToString(value);
break;
case "server_name":
var name = ToString(value);
if (name.Contains(' '))
errors.Add("server_name cannot contain spaces");
else
opts.ServerName = name;
break;
case "client_advertise":
opts.ClientAdvertise = ToString(value);
break;
// Logging
case "debug":
opts.Debug = ToBool(value);
break;
case "trace":
opts.Trace = ToBool(value);
break;
case "trace_verbose":
opts.TraceVerbose = ToBool(value);
if (opts.TraceVerbose)
opts.Trace = true;
break;
case "logtime":
opts.Logtime = ToBool(value);
break;
case "logtime_utc":
opts.LogtimeUTC = ToBool(value);
break;
case "logfile" or "log_file":
opts.LogFile = ToString(value);
break;
case "log_size_limit":
opts.LogSizeLimit = ToLong(value);
break;
case "log_max_num":
opts.LogMaxFiles = ToInt(value);
break;
case "syslog":
opts.Syslog = ToBool(value);
break;
case "remote_syslog":
opts.RemoteSyslog = ToString(value);
break;
// Limits
case "max_payload":
opts.MaxPayload = ToInt(value);
break;
case "max_control_line":
opts.MaxControlLine = ToInt(value);
break;
case "max_connections" or "max_conn":
opts.MaxConnections = ToInt(value);
break;
case "max_pending":
opts.MaxPending = ToLong(value);
break;
case "max_subs" or "max_subscriptions":
opts.MaxSubs = ToInt(value);
break;
case "max_sub_tokens" or "max_subscription_tokens":
var tokens = ToInt(value);
if (tokens > 256)
errors.Add("max_sub_tokens cannot exceed 256");
else
opts.MaxSubTokens = tokens;
break;
case "max_traced_msg_len":
opts.MaxTracedMsgLen = ToInt(value);
break;
case "max_closed_clients":
opts.MaxClosedClients = ToInt(value);
break;
case "disable_sublist_cache" or "no_sublist_cache":
opts.DisableSublistCache = ToBool(value);
break;
case "write_deadline":
opts.WriteDeadline = ParseDuration(value);
break;
// Ping
case "ping_interval":
opts.PingInterval = ParseDuration(value);
break;
case "ping_max" or "ping_max_out":
opts.MaxPingsOut = ToInt(value);
break;
// Monitoring
case "http_port" or "monitor_port":
opts.MonitorPort = ToInt(value);
break;
case "https_port":
opts.MonitorHttpsPort = ToInt(value);
break;
case "http":
ParseMonitorListen(value, opts, isHttps: false);
break;
case "https":
ParseMonitorListen(value, opts, isHttps: true);
break;
case "http_base_path":
opts.MonitorBasePath = ToString(value);
break;
// Lifecycle
case "lame_duck_duration":
opts.LameDuckDuration = ParseDuration(value);
break;
case "lame_duck_grace_period":
opts.LameDuckGracePeriod = ParseDuration(value);
break;
// Files
case "pidfile" or "pid_file":
opts.PidFile = ToString(value);
break;
case "ports_file_dir":
opts.PortsFileDir = ToString(value);
break;
// Auth
case "authorization":
if (value is Dictionary authDict)
ParseAuthorization(authDict, opts, errors);
break;
case "no_auth_user":
opts.NoAuthUser = ToString(value);
break;
// TLS
case "tls":
if (value is Dictionary tlsDict)
ParseTls(tlsDict, opts, errors);
break;
case "allow_non_tls":
opts.AllowNonTls = ToBool(value);
break;
// Cluster / inter-server / JetStream
case "cluster":
if (value is Dictionary clusterDict)
opts.Cluster = ParseCluster(clusterDict, errors);
break;
case "gateway":
if (value is Dictionary gatewayDict)
opts.Gateway = ParseGateway(gatewayDict, errors);
break;
case "leaf":
case "leafnode":
case "leafnodes":
if (value is Dictionary leafDict)
opts.LeafNode = ParseLeafNode(leafDict, errors);
break;
case "jetstream":
if (value is Dictionary jsDict)
opts.JetStream = ParseJetStream(jsDict, errors);
break;
// Tags
case "server_tags":
if (value is Dictionary tagsDict)
ParseTags(tagsDict, opts);
break;
// Profiling
case "prof_port":
opts.ProfPort = ToInt(value);
break;
// System account
case "system_account":
opts.SystemAccount = ToString(value);
break;
case "no_system_account":
opts.NoSystemAccount = ToBool(value);
break;
case "no_header_support":
opts.NoHeaderSupport = ToBool(value);
break;
case "connect_error_reports":
opts.ConnectErrorReports = ToInt(value);
break;
case "reconnect_error_reports":
opts.ReconnectErrorReports = ToInt(value);
break;
// MQTT
case "mqtt":
if (value is Dictionary mqttDict)
ParseMqtt(mqttDict, opts, errors);
break;
// WebSocket
case "websocket" or "ws":
if (value is Dictionary wsDict)
ParseWebSocket(wsDict, opts, errors);
break;
// Accounts block — each key is an account name containing users/limits
case "accounts":
if (value is Dictionary accountsDict)
ParseAccounts(accountsDict, opts, errors);
break;
// Server-level subject mappings: mappings { src: dest }
// Go reference: server/opts.go — "mappings" case
case "mappings" or "maps":
if (value is Dictionary mappingsDict)
{
opts.SubjectMappings ??= new Dictionary();
foreach (var (src, dest) in mappingsDict)
{
if (dest is string destStr)
opts.SubjectMappings[src] = destStr;
}
}
break;
// JWT operator mode — trusted operator public NKeys
// Go reference: server/opts.go — "trusted_keys" / "trusted" case
case "trusted_keys" or "trusted":
opts.TrustedKeys = ParseStringArray(value);
break;
// JWT resolver type and preload
// Go reference: server/opts.go — "resolver" case
case "resolver" or "account_resolver" or "accounts_resolver":
if (value is string resolverStr && resolverStr.Equals("MEMORY", StringComparison.OrdinalIgnoreCase))
opts.AccountResolver = new Auth.Jwt.MemAccountResolver();
break;
// Pre-load account JWTs into the resolver
// Go reference: server/opts.go — "resolver_preload" case
case "resolver_preload":
if (value is Dictionary preloadDict && opts.AccountResolver != null)
{
foreach (var (accNkey, jwtObj) in preloadDict)
{
if (jwtObj is string jwt)
opts.AccountResolver.StoreAsync(accNkey, jwt).GetAwaiter().GetResult();
}
}
break;
// Operator key (can derive trusted_keys from operator JWT — for now just accept NKeys directly)
case "operator" or "operators" or "root" or "roots" or "root_operators" or "root_operator":
// For simple mode: treat as trusted_keys alias if string array
opts.TrustedKeys ??= ParseStringArray(value);
break;
// Unknown keys silently ignored
default:
warnings.Add(new UnknownConfigFieldWarning(key).Message);
break;
}
}
// ─── Listen parsing ────────────────────────────────────────────
///
/// Parses a "listen" value that can be:
///
/// ":4222" — port only
/// "0.0.0.0:4222" — host + port
/// "4222" — bare number (port only)
/// 4222 — integer (port only)
///
///
private static void ParseListen(object? value, NatsOptions opts)
{
var (host, port) = ParseHostPort(value);
if (host is not null)
opts.Host = host;
if (port is not null)
opts.Port = port.Value;
}
///
/// Parses a monitor listen value. For "http" the port goes to MonitorPort;
/// for "https" the port goes to MonitorHttpsPort.
///
private static void ParseMonitorListen(object? value, NatsOptions opts, bool isHttps)
{
var (host, port) = ParseHostPort(value);
if (host is not null)
opts.MonitorHost = host;
if (port is not null)
{
if (isHttps)
opts.MonitorHttpsPort = port.Value;
else
opts.MonitorPort = port.Value;
}
}
///
/// Shared host:port parsing logic.
///
private static (string? Host, int? Port) ParseHostPort(object? value)
{
if (value is long l)
return (null, (int)l);
var str = ToString(value);
// Try bare integer
if (int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var barePort))
return (null, barePort);
// Check for host:port
var colonIdx = str.LastIndexOf(':');
if (colonIdx >= 0)
{
var hostPart = str[..colonIdx];
var portPart = str[(colonIdx + 1)..];
if (int.TryParse(portPart, NumberStyles.Integer, CultureInfo.InvariantCulture, out var p))
{
var host = hostPart.Length > 0 ? hostPart : null;
return (host, p);
}
}
throw new FormatException($"Cannot parse listen value: '{str}'");
}
// ─── Duration parsing ──────────────────────────────────────────
///
/// Parses a duration value. Accepts:
///
/// A string with unit suffix: "30s", "2m", "1h", "500ms"
/// A number (long/double) treated as seconds
///
///
internal static TimeSpan ParseDuration(object? value)
{
return value switch
{
long seconds => TimeSpan.FromSeconds(seconds),
double seconds => TimeSpan.FromSeconds(seconds),
string s => ParseDurationString(s),
_ => throw new FormatException($"Cannot parse duration from {value?.GetType().Name ?? "null"}"),
};
}
private static readonly Regex DurationPattern = new(
@"^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h)$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex ByteSizePattern = new(
@"^(\d+)\s*(b|kb|mb|gb|tb)?$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static TimeSpan ParseDurationString(string s)
{
var match = DurationPattern.Match(s);
if (!match.Success)
throw new FormatException($"Cannot parse duration: '{s}'");
var amount = double.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);
var unit = match.Groups[2].Value.ToLowerInvariant();
return unit switch
{
"ms" => TimeSpan.FromMilliseconds(amount),
"s" => TimeSpan.FromSeconds(amount),
"m" => TimeSpan.FromMinutes(amount),
"h" => TimeSpan.FromHours(amount),
_ => throw new FormatException($"Unknown duration unit: '{unit}'"),
};
}
// ─── Cluster / gateway / leafnode / JetStream parsing ────────
private static ClusterOptions ParseCluster(Dictionary dict, List errors)
{
var options = new ClusterOptions();
foreach (var (key, value) in dict)
{
switch (key.ToLowerInvariant())
{
case "name":
options.Name = ToString(value);
break;
case "listen":
try
{
var (host, port) = ParseHostPort(value);
if (host is not null)
options.Host = host;
if (port is not null)
options.Port = port.Value;
}
catch (Exception ex)
{
errors.Add($"Invalid cluster.listen: {ex.Message}");
}
break;
case "write_deadline":
try
{
options.WriteDeadline = ParseDuration(value);
}
catch (Exception ex)
{
errors.Add($"Invalid cluster.write_deadline: {ex.Message}");
}
break;
case "routes":
if (value is List