feat(batch7): implement f1 config receiver and reload helpers
This commit is contained in:
@@ -18,6 +18,7 @@ using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Net.Security;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
@@ -439,6 +440,368 @@ public sealed partial class ServerOptions
|
||||
public static ServerOptions ProcessConfigFile(string configFile) =>
|
||||
ServerOptionsConfiguration.ProcessConfigFile(configFile);
|
||||
|
||||
/// <summary>
|
||||
/// Receiver-style config loader that updates this instance with values from
|
||||
/// <paramref name="configFile"/>.
|
||||
/// Mirrors Go <c>Options.ProcessConfigFile</c>.
|
||||
/// </summary>
|
||||
public Exception? ProcessConfigFileOverload2510(string configFile)
|
||||
{
|
||||
ConfigFile = configFile;
|
||||
if (string.IsNullOrEmpty(configFile))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var data = File.ReadAllText(configFile);
|
||||
ConfigDigestValue = ComputeConfigDigest(data);
|
||||
return ProcessConfigString(data);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receiver-style config loader from in-memory content.
|
||||
/// Mirrors Go <c>Options.ProcessConfigString</c>.
|
||||
/// </summary>
|
||||
public Exception? ProcessConfigString(string data)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(
|
||||
data,
|
||||
new JsonDocumentOptions
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
CommentHandling = JsonCommentHandling.Skip,
|
||||
});
|
||||
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return new InvalidOperationException("configuration root must be an object");
|
||||
|
||||
var normalized = NormalizeConfigValue(doc.RootElement);
|
||||
var configMap = normalized as IReadOnlyDictionary<string, object?>
|
||||
?? normalized as Dictionary<string, object?>;
|
||||
if (configMap == null)
|
||||
return new InvalidOperationException("configuration root must be a key/value object");
|
||||
|
||||
return ProcessConfigFileInternal(string.Empty, configMap);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal receiver config pipeline that processes each top-level config key.
|
||||
/// Mirrors Go <c>Options.processConfigFile</c>.
|
||||
/// </summary>
|
||||
public Exception? ProcessConfigFileInternal(string configFile, IReadOnlyDictionary<string, object?> config)
|
||||
{
|
||||
var errors = new List<Exception>();
|
||||
var warnings = new List<Exception>();
|
||||
|
||||
if (config.Count == 0)
|
||||
warnings.Add(new InvalidOperationException($"{configFile}: config has no values or is empty"));
|
||||
|
||||
var sysErr = ConfigureSystemAccount(this, config);
|
||||
if (sysErr != null)
|
||||
errors.Add(sysErr);
|
||||
|
||||
foreach (var (key, value) in config)
|
||||
ProcessConfigFileLine(key, value, errors, warnings);
|
||||
|
||||
if (AuthCallout?.AllowedAccounts is { Count: > 0 })
|
||||
{
|
||||
var configuredAccounts = new HashSet<string>(
|
||||
Accounts.Select(a => a.Name),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
foreach (var account in AuthCallout.AllowedAccounts)
|
||||
{
|
||||
if (!configuredAccounts.Contains(account))
|
||||
{
|
||||
errors.Add(new InvalidOperationException(
|
||||
$"auth_callout allowed account \"{account}\" not found in configured accounts"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count == 0 && warnings.Count == 0)
|
||||
return null;
|
||||
|
||||
return new ProcessConfigException(errors, warnings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a single top-level config key.
|
||||
/// Mirrors Go <c>Options.processConfigFileLine</c>.
|
||||
/// </summary>
|
||||
public void ProcessConfigFileLine(
|
||||
string key,
|
||||
object? value,
|
||||
ICollection<Exception> errors,
|
||||
ICollection<Exception> warnings)
|
||||
{
|
||||
try
|
||||
{
|
||||
var normalized = NormalizeConfigValue(value);
|
||||
switch (key.ToLowerInvariant())
|
||||
{
|
||||
case "listen":
|
||||
{
|
||||
var (host, port) = ParseListen(normalized);
|
||||
Host = host;
|
||||
Port = port;
|
||||
break;
|
||||
}
|
||||
case "client_advertise":
|
||||
if (normalized is string ca)
|
||||
ClientAdvertise = ca;
|
||||
else
|
||||
errors.Add(new InvalidOperationException("client_advertise must be a string"));
|
||||
break;
|
||||
case "port":
|
||||
if (TryConvertToLong(normalized, out var p))
|
||||
Port = checked((int)p);
|
||||
else
|
||||
errors.Add(new InvalidOperationException("port must be an integer"));
|
||||
break;
|
||||
case "server_name":
|
||||
if (normalized is not string sn)
|
||||
{
|
||||
errors.Add(new InvalidOperationException("server_name must be a string"));
|
||||
}
|
||||
else if (sn.Contains(' '))
|
||||
{
|
||||
errors.Add(ServerErrors.ErrServerNameHasSpaces);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerName = sn;
|
||||
}
|
||||
break;
|
||||
case "host":
|
||||
case "net":
|
||||
if (normalized is string configuredHost)
|
||||
Host = configuredHost;
|
||||
else
|
||||
errors.Add(new InvalidOperationException($"{key} must be a string"));
|
||||
break;
|
||||
case "debug":
|
||||
if (TryConvertToBool(normalized, out var debug))
|
||||
{
|
||||
Debug = debug;
|
||||
TrackExplicitVal(InConfig, nameof(Debug), Debug);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new InvalidOperationException("debug must be a boolean"));
|
||||
}
|
||||
break;
|
||||
case "trace":
|
||||
if (TryConvertToBool(normalized, out var trace))
|
||||
{
|
||||
Trace = trace;
|
||||
TrackExplicitVal(InConfig, nameof(Trace), Trace);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new InvalidOperationException("trace must be a boolean"));
|
||||
}
|
||||
break;
|
||||
case "trace_verbose":
|
||||
if (TryConvertToBool(normalized, out var traceVerbose))
|
||||
{
|
||||
TraceVerbose = traceVerbose;
|
||||
Trace = traceVerbose;
|
||||
TrackExplicitVal(InConfig, nameof(TraceVerbose), TraceVerbose);
|
||||
TrackExplicitVal(InConfig, nameof(Trace), Trace);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new InvalidOperationException("trace_verbose must be a boolean"));
|
||||
}
|
||||
break;
|
||||
case "trace_headers":
|
||||
if (TryConvertToBool(normalized, out var traceHeaders))
|
||||
{
|
||||
TraceHeaders = traceHeaders;
|
||||
Trace = traceHeaders;
|
||||
TrackExplicitVal(InConfig, nameof(TraceHeaders), TraceHeaders);
|
||||
TrackExplicitVal(InConfig, nameof(Trace), Trace);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new InvalidOperationException("trace_headers must be a boolean"));
|
||||
}
|
||||
break;
|
||||
case "logtime":
|
||||
if (TryConvertToBool(normalized, out var logtime))
|
||||
{
|
||||
Logtime = logtime;
|
||||
TrackExplicitVal(InConfig, nameof(Logtime), Logtime);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new InvalidOperationException("logtime must be a boolean"));
|
||||
}
|
||||
break;
|
||||
case "logtime_utc":
|
||||
if (TryConvertToBool(normalized, out var logtimeUtc))
|
||||
{
|
||||
LogtimeUtc = logtimeUtc;
|
||||
TrackExplicitVal(InConfig, nameof(LogtimeUtc), LogtimeUtc);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new InvalidOperationException("logtime_utc must be a boolean"));
|
||||
}
|
||||
break;
|
||||
case "disable_sublist_cache":
|
||||
case "no_sublist_cache":
|
||||
if (TryConvertToBool(normalized, out var noSublistCache))
|
||||
NoSublistCache = noSublistCache;
|
||||
else
|
||||
errors.Add(new InvalidOperationException($"{key} must be a boolean"));
|
||||
break;
|
||||
case "accounts":
|
||||
{
|
||||
var err = ParseAccounts(normalized, this, errors, warnings);
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
case "default_sentinel":
|
||||
if (normalized is string sentinel)
|
||||
DefaultSentinel = sentinel;
|
||||
else
|
||||
errors.Add(new InvalidOperationException("default_sentinel must be a string"));
|
||||
break;
|
||||
case "authorization":
|
||||
{
|
||||
var (auth, err) = ParseAuthorization(normalized, errors, warnings);
|
||||
if (err != null)
|
||||
{
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
|
||||
if (auth == null)
|
||||
break;
|
||||
|
||||
AuthBlockDefined = true;
|
||||
Username = auth.User;
|
||||
Password = auth.Pass;
|
||||
ProxyRequired = auth.ProxyRequired;
|
||||
Authorization = auth.Token;
|
||||
AuthTimeout = auth.TimeoutSeconds;
|
||||
AuthCallout = auth.Callout;
|
||||
|
||||
if ((!string.IsNullOrEmpty(auth.User) || !string.IsNullOrEmpty(auth.Pass)) &&
|
||||
!string.IsNullOrEmpty(auth.Token))
|
||||
{
|
||||
errors.Add(new InvalidOperationException("Cannot have a user/pass and token"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "cluster":
|
||||
{
|
||||
var err = ParseCluster(normalized, this, errors, warnings);
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
case "gateway":
|
||||
{
|
||||
var err = ParseGateway(normalized, this, errors, warnings);
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
case "leafnodes":
|
||||
{
|
||||
var err = ParseLeafNodes(normalized, this, errors, warnings);
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
case "routes":
|
||||
if (normalized is string routesString)
|
||||
{
|
||||
RoutesStr = routesString;
|
||||
Routes = RoutesFromStr(routesString);
|
||||
break;
|
||||
}
|
||||
|
||||
if (TryGetArray(normalized, out var routes))
|
||||
{
|
||||
Routes = ParseURLs(routes, "route", warnings, errors);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(new InvalidOperationException("routes must be a string or array"));
|
||||
}
|
||||
break;
|
||||
case "jetstream":
|
||||
{
|
||||
var err = ParseJetStream(normalized, this, errors, warnings);
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
case "websocket":
|
||||
{
|
||||
var err = ParseWebsocket(normalized, this, errors, warnings);
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
case "mqtt":
|
||||
{
|
||||
var err = ParseMQTT(normalized, this, errors, warnings);
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
case "proxies":
|
||||
{
|
||||
var (proxies, err) = ParseProxies(normalized);
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
else
|
||||
Proxies = proxies;
|
||||
break;
|
||||
}
|
||||
case "system_account":
|
||||
case "system":
|
||||
{
|
||||
var err = ConfigureSystemAccount(
|
||||
this,
|
||||
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[key] = normalized,
|
||||
});
|
||||
if (err != null)
|
||||
errors.Add(err);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (!ConfigFlags.AllowUnknownTopLevelField)
|
||||
errors.Add(new InvalidOperationException($"unknown field \"{key}\""));
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes token-like values to plain CLR values.
|
||||
/// Mirrors <c>unwrapValue</c> intent from opts.go.
|
||||
@@ -5914,6 +6277,12 @@ public sealed partial class ServerOptions
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeConfigDigest(string configContent)
|
||||
{
|
||||
var bytes = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(configContent));
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static void MergeRoutes(ServerOptions opts, ServerOptions flagOpts)
|
||||
{
|
||||
var routeUrls = RoutesFromStr(flagOpts.RoutesStr);
|
||||
|
||||
Reference in New Issue
Block a user