// 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; 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(); foreach (var (key, value) in config) { try { ProcessKey(key, value, opts, errors); } catch (Exception ex) { errors.Add($"Error processing '{key}': {ex.Message}"); } } if (errors.Count > 0) { throw new ConfigProcessorException("Configuration errors", errors); } } private static void ProcessKey(string key, object? value, NatsOptions opts, List errors) { // 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; // 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; // Unknown keys silently ignored (cluster, jetstream, gateway, leafnode, etc.) default: 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 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}'"), }; } // ─── Authorization parsing ───────────────────────────────────── private static void ParseAuthorization(Dictionary dict, NatsOptions opts, List errors) { foreach (var (key, value) in dict) { switch (key.ToLowerInvariant()) { case "user" or "username": opts.Username = ToString(value); break; case "pass" or "password": opts.Password = ToString(value); break; case "token": opts.Authorization = ToString(value); break; case "timeout": opts.AuthTimeout = value switch { long l => TimeSpan.FromSeconds(l), double d => TimeSpan.FromSeconds(d), string s => ParseDuration(s), _ => throw new FormatException($"Invalid auth timeout type: {value?.GetType().Name}"), }; break; case "users": if (value is List userList) opts.Users = ParseUsers(userList, errors); break; default: // Unknown auth keys silently ignored break; } } } private static List ParseUsers(List list, List errors) { var users = new List(); foreach (var item in list) { if (item is not Dictionary userDict) { errors.Add("Expected user entry to be a map"); continue; } string? username = null; string? password = null; string? account = null; Permissions? permissions = null; foreach (var (key, value) in userDict) { switch (key.ToLowerInvariant()) { case "user" or "username": username = ToString(value); break; case "pass" or "password": password = ToString(value); break; case "account": account = ToString(value); break; case "permissions" or "permission": if (value is Dictionary permDict) permissions = ParsePermissions(permDict, errors); break; } } if (username is null) { errors.Add("User entry missing 'user' field"); continue; } users.Add(new User { Username = username, Password = password ?? string.Empty, Account = account, Permissions = permissions, }); } return users; } private static Permissions ParsePermissions(Dictionary dict, List errors) { SubjectPermission? publish = null; SubjectPermission? subscribe = null; ResponsePermission? response = null; foreach (var (key, value) in dict) { switch (key.ToLowerInvariant()) { case "publish" or "pub": publish = ParseSubjectPermission(value, errors); break; case "subscribe" or "sub": subscribe = ParseSubjectPermission(value, errors); break; case "resp" or "response": if (value is Dictionary respDict) response = ParseResponsePermission(respDict); break; } } return new Permissions { Publish = publish, Subscribe = subscribe, Response = response, }; } private static SubjectPermission? ParseSubjectPermission(object? value, List errors) { // Can be a simple list of strings (treated as allow) or a dict with allow/deny if (value is Dictionary dict) { IReadOnlyList? allow = null; IReadOnlyList? deny = null; foreach (var (key, v) in dict) { switch (key.ToLowerInvariant()) { case "allow": allow = ToStringList(v); break; case "deny": deny = ToStringList(v); break; } } return new SubjectPermission { Allow = allow, Deny = deny }; } if (value is List list) { return new SubjectPermission { Allow = ToStringList(list) }; } if (value is string s) { return new SubjectPermission { Allow = [s] }; } return null; } private static ResponsePermission ParseResponsePermission(Dictionary dict) { var maxMsgs = 0; var expires = TimeSpan.Zero; foreach (var (key, value) in dict) { switch (key.ToLowerInvariant()) { case "max_msgs" or "max": maxMsgs = ToInt(value); break; case "expires" or "ttl": expires = ParseDuration(value); break; } } return new ResponsePermission { MaxMsgs = maxMsgs, Expires = expires }; } // ─── TLS parsing ─────────────────────────────────────────────── private static void ParseTls(Dictionary dict, NatsOptions opts, List errors) { foreach (var (key, value) in dict) { switch (key.ToLowerInvariant()) { case "cert_file": opts.TlsCert = ToString(value); break; case "key_file": opts.TlsKey = ToString(value); break; case "ca_file": opts.TlsCaCert = ToString(value); break; case "verify": opts.TlsVerify = ToBool(value); break; case "verify_and_map": var map = ToBool(value); opts.TlsMap = map; if (map) opts.TlsVerify = true; break; case "timeout": opts.TlsTimeout = value switch { long l => TimeSpan.FromSeconds(l), double d => TimeSpan.FromSeconds(d), string s => ParseDuration(s), _ => throw new FormatException($"Invalid TLS timeout type: {value?.GetType().Name}"), }; break; case "connection_rate_limit": opts.TlsRateLimit = ToLong(value); break; case "pinned_certs": if (value is List pinnedList) { var certs = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in pinnedList) { if (item is string s) certs.Add(s.ToLowerInvariant()); } opts.TlsPinnedCerts = certs; } break; case "handshake_first" or "first" or "immediate": opts.TlsHandshakeFirst = ToBool(value); break; case "handshake_first_fallback": opts.TlsHandshakeFirstFallback = ParseDuration(value); break; default: // Unknown TLS keys silently ignored break; } } } // ─── Tags parsing ────────────────────────────────────────────── private static void ParseTags(Dictionary dict, NatsOptions opts) { var tags = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var (key, value) in dict) { tags[key] = ToString(value); } opts.Tags = tags; } // ─── Type conversion helpers ─────────────────────────────────── private static int ToInt(object? value) => value switch { long l => (int)l, int i => i, double d => (int)d, string s when int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) => i, _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to int"), }; private static long ToLong(object? value) => value switch { long l => l, int i => i, double d => (long)d, string s when long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var l) => l, _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to long"), }; private static bool ToBool(object? value) => value switch { bool b => b, _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to bool"), }; private static string ToString(object? value) => value switch { string s => s, long l => l.ToString(CultureInfo.InvariantCulture), _ => throw new FormatException($"Cannot convert {value?.GetType().Name ?? "null"} to string"), }; private static IReadOnlyList ToStringList(object? value) { if (value is List list) { var result = new List(list.Count); foreach (var item in list) { if (item is string s) result.Add(s); } return result; } if (value is string str) return [str]; return []; } } /// /// Thrown when one or more configuration validation errors are detected. /// All errors are collected rather than failing on the first one. /// public sealed class ConfigProcessorException(string message, List errors) : Exception(message) { public IReadOnlyList Errors => errors; }