feat(batch6-task2): implement F1 opts parsing and verify features
This commit is contained in:
@@ -14,7 +14,9 @@
|
||||
// Adapted from server/opts.go in the NATS server Go source.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using ZB.MOM.NatsNet.Server.Config;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server;
|
||||
|
||||
@@ -36,6 +38,12 @@ internal static class ConfigFlags
|
||||
|
||||
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.
|
||||
@@ -399,10 +407,804 @@ public sealed partial class ServerOptions
|
||||
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;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
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 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);
|
||||
|
||||
Reference in New Issue
Block a user