// Copyright 2012-2026 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/server.go in the NATS server Go source (lines 85–2579).
// Session 09: server initialization, validation, compression helpers.
using System.Net;
using System.Text.RegularExpressions;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
namespace ZB.MOM.NatsNet.Server;
public sealed partial class NatsServer
{
// =========================================================================
// Server protocol version helpers (features 2974–2975)
// =========================================================================
///
/// Returns the latest server-to-server protocol version, unless the test
/// override option is set.
/// Mirrors Go Server.getServerProto().
///
public int GetServerProto()
{
var opts = GetOpts();
var proto = ServerProtocol.MsgTraceProto;
if (opts.OverrideProto < 0)
{
// overrideProto was set by SetServerProtoForTest as (wantedProto+1)*-1
proto = (opts.OverrideProto * -1) - 1;
}
return proto;
}
///
/// Converts a wanted protocol level into the override value stored in options.
/// Mirrors Go setServerProtoForTest (test helper).
///
public static int SetServerProtoForTest(int wantedProto) => (wantedProto + 1) * -1;
// =========================================================================
// Compression helpers (features 2976–2982)
// =========================================================================
///
/// Validates and normalises the field.
/// Mirrors Go validateAndNormalizeCompressionOption.
///
public static void ValidateAndNormalizeCompressionOption(CompressionOpts? c, string chosenModeForOn)
{
if (c is null) return;
var cmtl = c.Mode.ToLowerInvariant();
// Resolve "on" to the caller-chosen default.
switch (cmtl)
{
case "on":
case "enabled":
case "true":
cmtl = chosenModeForOn;
break;
}
switch (cmtl)
{
case "not supported":
case "not_supported":
c.Mode = CompressionMode.NotSupported;
break;
case "disabled":
case "off":
case "false":
c.Mode = CompressionMode.Off;
break;
case "accept":
c.Mode = CompressionMode.Accept;
break;
case "auto":
case "s2_auto":
{
List rtts;
if (c.RttThresholds.Count == 0)
{
rtts = [.. DefaultCompressionS2AutoRttThresholds];
}
else
{
rtts = [];
foreach (var n in c.RttThresholds)
{
var t = n < TimeSpan.Zero ? TimeSpan.Zero : n;
if (rtts.Count > 0 && t != TimeSpan.Zero)
{
foreach (var v in rtts)
{
if (t < v)
throw new InvalidOperationException(
$"RTT threshold values {c.RttThresholds} should be in ascending order");
}
}
rtts.Add(t);
}
// Trim trailing zeros.
int stop = -1;
for (int i = rtts.Count - 1; i >= 0; i--)
{
if (rtts[i] != TimeSpan.Zero) { stop = i; break; }
}
rtts = rtts[..(stop + 1)];
if (rtts.Count > 4)
throw new InvalidOperationException(
$"Compression mode \"{c.Mode}\" should have no more than 4 RTT thresholds");
if (rtts.Count == 0)
throw new InvalidOperationException(
$"Compression mode \"{c.Mode}\" requires at least one RTT threshold");
}
c.Mode = CompressionMode.S2Auto;
c.RttThresholds = rtts;
break;
}
case "fast":
case "s2_fast":
c.Mode = CompressionMode.S2Fast;
break;
case "better":
case "s2_better":
c.Mode = CompressionMode.S2Better;
break;
case "best":
case "s2_best":
c.Mode = CompressionMode.S2Best;
break;
default:
throw new InvalidOperationException($"Unsupported compression mode \"{c.Mode}\"");
}
}
///
/// Default RTT thresholds for mode.
/// Mirrors Go defaultCompressionS2AutoRTTThresholds.
///
public static readonly TimeSpan[] DefaultCompressionS2AutoRttThresholds =
[
TimeSpan.FromMilliseconds(10),
TimeSpan.FromMilliseconds(50),
TimeSpan.FromMilliseconds(100),
];
///
/// Returns true if the compression mode requires negotiation with the remote.
/// Mirrors Go needsCompression.
///
public static bool NeedsCompression(string m) =>
m != string.Empty && m != CompressionMode.Off && m != CompressionMode.NotSupported;
///
/// Selects the effective compression mode given our local ()
/// and remote () compression modes.
/// Mirrors Go selectCompressionMode.
///
public static string SelectCompressionMode(string scm, string rcm)
{
if (rcm == CompressionMode.NotSupported || rcm == string.Empty)
return CompressionMode.NotSupported;
switch (rcm)
{
case CompressionMode.Off:
return CompressionMode.Off;
case CompressionMode.Accept:
return scm == CompressionMode.Accept ? CompressionMode.Off : scm;
case CompressionMode.S2Auto:
case CompressionMode.S2Uncompressed:
case CompressionMode.S2Fast:
case CompressionMode.S2Better:
case CompressionMode.S2Best:
if (scm == CompressionMode.Accept)
return rcm == CompressionMode.S2Auto ? CompressionMode.S2Fast : rcm;
return scm;
default:
throw new InvalidOperationException($"Unsupported route compression mode \"{rcm}\"");
}
}
///
/// Returns S2Auto if the configured mode is S2Auto; otherwise returns .
/// Mirrors Go compressionModeForInfoProtocol.
///
public static string CompressionModeForInfoProtocol(CompressionOpts co, string cm) =>
co.Mode == CompressionMode.S2Auto ? CompressionMode.S2Auto : cm;
///
/// Given an RTT and the configured thresholds, returns the appropriate S2 level.
/// Mirrors Go selectS2AutoModeBasedOnRTT.
///
public static string SelectS2AutoModeBasedOnRtt(TimeSpan rtt, IReadOnlyList rttThresholds)
{
int idx = -1;
for (int i = 0; i < rttThresholds.Count; i++)
{
if (rtt <= rttThresholds[i]) { idx = i; break; }
}
if (idx < 0)
{
// Not found — use "best" when ≥3 levels, otherwise last index.
idx = rttThresholds.Count >= 3 ? 3 : rttThresholds.Count - 1;
}
return idx switch
{
0 => CompressionMode.S2Uncompressed,
1 => CompressionMode.S2Fast,
2 => CompressionMode.S2Better,
_ => CompressionMode.S2Best,
};
}
///
/// Returns true if the two are logically equal.
/// Mirrors Go compressOptsEqual.
///
public static bool CompressOptsEqual(CompressionOpts? c1, CompressionOpts? c2)
{
if (ReferenceEquals(c1, c2)) return true;
if (c1 is null || c2 is null) return false;
if (c1.Mode != c2.Mode) return false;
if (c1.Mode == CompressionMode.S2Auto)
{
var c1Rtts = c1.RttThresholds.Count == 0
? (IReadOnlyList)DefaultCompressionS2AutoRttThresholds
: c1.RttThresholds;
var c2Rtts = c2.RttThresholds.Count == 0
? (IReadOnlyList)DefaultCompressionS2AutoRttThresholds
: c2.RttThresholds;
if (c1Rtts.Count != c2Rtts.Count) return false;
for (int i = 0; i < c1Rtts.Count; i++)
if (c1Rtts[i] != c2Rtts[i]) return false;
}
return true;
}
///
/// Returns S2 writer options for the selected route compression mode.
/// Mirrors Go s2WriterOptions.
///
internal static string[]? S2WriterOptions(string cm)
{
var opts = new List { "writer_concurrency=1" };
return cm switch
{
CompressionMode.S2Uncompressed => [.. opts, "writer_uncompressed"],
CompressionMode.S2Best => [.. opts, "writer_best_compression"],
CompressionMode.S2Better => [.. opts, "writer_better_compression"],
_ => null,
};
}
// =========================================================================
// Factory methods (features 2983–2985)
// =========================================================================
///
/// Deprecated factory. Use instead.
/// Mirrors Go New.
///
public static NatsServer? New(ServerOptions opts)
{
var (s, _) = NewServer(opts);
return s;
}
///
/// Creates a server from an options file path if is set.
/// Mirrors Go NewServerFromConfig.
///
public static (NatsServer? Server, Exception? Error) NewServerFromConfig(ServerOptions opts)
{
if (!string.IsNullOrEmpty(opts.ConfigFile) && string.IsNullOrEmpty(opts.ConfigDigest()))
{
// opts.ProcessConfigFile(opts.ConfigFile) — full config file parsing deferred to session 03.
// For now, skip re-processing since Phase 6 tests supply options directly.
}
return NewServer(opts);
}
///
/// Creates and fully initializes a new NATS server.
/// Mirrors Go NewServer.
///
public static (NatsServer? Server, Exception? Error) NewServer(ServerOptions opts)
{
opts.SetBaselineOptions();
// Generate server NKey identity.
// In Go: nkeys.CreateServer() — simplified here with Guid-based id.
var pub = Guid.NewGuid().ToString("N"); // mirrors kp.PublicKey()
// xkp (curve keys for encryption) — stub; full implementation in session 09 crypto.
var xpub = string.Empty;
var serverName = !string.IsNullOrEmpty(opts.ServerName) ? opts.ServerName : pub;
var httpBasePath = ServerOptions.NormalizeBasePath(opts.HttpBasePath);
// Validate options.
var valErr = ValidateOptions(opts);
if (valErr != null) return (null, valErr);
var now = DateTime.UtcNow;
var tlsReq = opts.TlsConfig != null;
var verify = tlsReq && opts.TlsVerify;
var info = new ServerInfo
{
Id = pub,
XKey = xpub,
Version = ServerConstants.Version,
Proto = ServerConstants.Proto,
GitCommit = ServerConstants.GitCommit,
GoVersion = System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription,
Name = serverName,
Host = opts.Host,
Port = opts.Port,
AuthRequired = false,
TlsRequired = tlsReq && !opts.AllowNonTls,
TlsVerify = verify,
MaxPayload = opts.MaxPayload,
JetStream = opts.JetStream,
Headers = !opts.NoHeaderSupport,
Cluster = opts.Cluster.Name,
Domain = opts.JetStreamDomain,
JsApiLevel = 1, // stub — session 19
};
if (tlsReq && !info.TlsRequired)
info.TlsAvailable = true;
var s = new NatsServer(opts)
{
_info = info,
_configFile = opts.ConfigFile,
_start = now,
_configTime = now,
_pub = pub,
_xpub = xpub,
_httpBasePath = httpBasePath,
};
// Fill sync semaphore.
// (Already at capacity since SemaphoreSlim starts at MaxConcurrentSyncRequests)
if (opts.TlsRateLimit > 0)
s._connRateCounter = RateCounterFactory.Create(opts.TlsRateLimit);
// Process trusted operator keys.
if (!s.ProcessTrustedKeys())
return (null, new InvalidOperationException("Error processing trusted operator keys"));
// Handle leaf-node-only scenario (no cluster, needs stable cluster name).
if (opts.LeafNode.Remotes.Count > 0 && opts.Cluster.Port == 0 && string.IsNullOrEmpty(opts.Cluster.Name))
{
s._leafNoCluster = true;
opts.Cluster.Name = opts.ServerName;
}
if (!string.IsNullOrEmpty(opts.Cluster.Name))
{
s._cnMu.EnterWriteLock();
try { s._cn = opts.Cluster.Name; }
finally { s._cnMu.ExitWriteLock(); }
}
s._mu.EnterWriteLock();
try
{
// Process proxies trusted public keys (stub — session 09 proxy).
s.ProcessProxiesTrustedKeys();
// JetStream node info.
if (opts.JetStream)
{
var ourNode = GetHash(serverName);
s._nodeToInfo.TryAdd(ourNode, new NodeInfo
{
Name = serverName,
Version = ServerConstants.Version,
Cluster = opts.Cluster.Name,
Domain = opts.JetStreamDomain,
Id = info.Id,
Tags = [.. opts.Tags],
Js = true,
BinarySnapshots = true,
AccountNrg = true,
});
}
// Route resolver.
s._routeResolver = null; // Default system DNS — session 14
// URL maps.
// (Initialized via new() in field declarations)
// Assign leaf options.
s._leafNodeEnabled = opts.LeafNode.Port != 0 || opts.LeafNode.Remotes.Count > 0;
var ocspError = s.EnableOCSP();
if (ocspError != null)
{
s._mu.ExitWriteLock();
return (null, ocspError);
}
s.InitOCSPResponseCache();
// Gateway (stub — session 16).
// s.NewGateway(opts) — deferred
// Cluster name.
if (opts.Cluster.Port != 0 && string.IsNullOrEmpty(opts.Cluster.Name))
s._info.Cluster = Guid.NewGuid().ToString("N");
else if (!string.IsNullOrEmpty(opts.Cluster.Name))
s._info.Cluster = opts.Cluster.Name;
// INFO host/port (stub — needs listener port, session 10).
s.SetInfoHostPort();
// Client tracking.
// (Initialized in field declarations)
// Closed-clients ring buffer.
s._closed = new ClosedRingBuffer(opts.MaxClosedClients);
// Route structures.
s.InitRouteStructures(opts);
// Quit channel already via CancellationTokenSource.
// Check account resolver.
var resolverErr = s.ConfigureResolver();
if (resolverErr != null)
{
s._mu.ExitWriteLock();
return (null, resolverErr);
}
// URL account resolver health check (stub — session 11 for URLAccResolver).
// Operator mode bootstrap: inject temporary system account if needed.
// (stub — requires full Account implementation from session 11)
// Configure accounts.
var (_, accErr) = s.ConfigureAccounts(false);
if (accErr != null)
{
s._mu.ExitWriteLock();
return (null, accErr);
}
// Configure authorization (stub — session 09 auth).
s.ConfigureAuthorization();
// Signal handler (stub — session 04 already handled signals separately).
s.HandleSignals();
}
finally
{
if (s._mu.IsWriteLockHeld)
s._mu.ExitWriteLock();
}
return (s, null);
}
// =========================================================================
// Route structures (feature 2986)
// =========================================================================
///
/// Initializes route tracking structures based on pool size and pinned accounts.
/// Mirrors Go Server.initRouteStructures.
/// Server lock must be held on entry.
///
public void InitRouteStructures(ServerOptions opts)
{
_routes = [];
_routesPoolSize = opts.Cluster.PoolSize > 0 ? opts.Cluster.PoolSize : 1;
if (opts.Cluster.PinnedAccounts.Count > 0)
{
_accRoutes = [];
foreach (var acc in opts.Cluster.PinnedAccounts)
_accRoutes[acc] = [];
}
}
// =========================================================================
// logRejectedTLSConns background loop (feature 2987)
// =========================================================================
///
/// Background loop that logs TLS rate-limited connection rejections every second.
/// Mirrors Go Server.logRejectedTLSConns.
///
internal Task LogRejectedTlsConnsAsync(CancellationToken ct) =>
LogRejectedTLSConns(ct);
///
/// Background loop that logs TLS rate-limited connection rejections every second.
/// Mirrors Go Server.logRejectedTLSConns.
///
internal async Task LogRejectedTLSConns(CancellationToken ct, TimeSpan? interval = null)
{
using var timer = new PeriodicTimer(interval ?? TimeSpan.FromSeconds(1));
while (!ct.IsCancellationRequested)
{
if (_connRateCounter is not null)
{
var blocked = _connRateCounter.CountBlocked();
if (blocked > 0)
Warnf("Rejected {0} connections due to TLS rate limiting", blocked);
}
try { await timer.WaitForNextTickAsync(ct); }
catch (OperationCanceledException) { break; }
}
}
// =========================================================================
// Cluster name (features 2988–2991)
// =========================================================================
/// Returns the cluster name from the server info.
public string ClusterName()
{
_mu.EnterReadLock();
try { return _info.Cluster ?? string.Empty; }
finally { _mu.ExitReadLock(); }
}
/// Returns cluster name via the dedicated cluster-name lock (faster).
public string CachedClusterName()
{
_cnMu.EnterReadLock();
try { return _cn; }
finally { _cnMu.ExitReadLock(); }
}
///
/// Updates the cluster name and notifies affected leaf nodes.
/// Mirrors Go Server.setClusterName.
///
public void SetClusterName(string name)
{
_mu.EnterWriteLock();
_info.Cluster = name;
_routeInfo.Cluster = name;
_mu.ExitWriteLock();
_cnMu.EnterWriteLock();
try { _cn = name; }
finally { _cnMu.ExitWriteLock(); }
Noticef("Cluster name updated to {0}", name);
}
/// Returns true if the cluster name was not set in config (dynamic assignment).
public bool IsClusterNameDynamic()
{
_optsMu.EnterReadLock();
try { return string.IsNullOrEmpty(_opts.Cluster.Name); }
finally { _optsMu.ExitReadLock(); }
}
// =========================================================================
// Server name / URLs (features 2992–2994)
// =========================================================================
/// Returns the configured server name.
public string ServerName() => GetOpts().ServerName;
///
/// Returns the URL used to connect clients.
/// Mirrors Go Server.ClientURL.
///
public string ClientUrl()
{
var opts = GetOpts();
var scheme = opts.TlsConfig != null ? "tls" : "nats";
return $"{scheme}://{opts.Host}:{opts.Port}";
}
///
/// Returns the URL used to connect WebSocket clients.
/// Mirrors Go Server.WebsocketURL.
///
public string WebsocketUrl()
{
var opts = GetOpts();
var scheme = opts.Websocket.TlsConfig != null ? "wss" : "ws";
return $"{scheme}://{opts.Websocket.Host}:{opts.Websocket.Port}";
}
// =========================================================================
// Validation (features 2995–2997)
// =========================================================================
///
/// Validates cluster configuration options.
/// Mirrors Go validateCluster.
///
public static Exception? ValidateCluster(ServerOptions o)
{
if (!string.IsNullOrEmpty(o.Cluster.Name) && o.Cluster.Name.Contains(' '))
return ServerErrors.ErrClusterNameHasSpaces;
if (!string.IsNullOrEmpty(o.Cluster.Compression.Mode))
{
try { ValidateAndNormalizeCompressionOption(o.Cluster.Compression, CompressionMode.S2Fast); }
catch (Exception ex) { return ex; }
}
var pinnedErr = ValidatePinnedCerts(o.Cluster.TlsPinnedCerts);
if (pinnedErr != null)
return new InvalidOperationException($"cluster: {pinnedErr.Message}", pinnedErr);
// Sync gateway name with cluster name.
if (!string.IsNullOrEmpty(o.Gateway.Name) && o.Gateway.Name != o.Cluster.Name)
{
if (!string.IsNullOrEmpty(o.Cluster.Name))
return ServerErrors.ErrClusterNameConfigConflict;
o.Cluster.Name = o.Gateway.Name;
}
if (o.Cluster.PinnedAccounts.Count > 0)
{
if (o.Cluster.PoolSize < 0)
return new InvalidOperationException("pool_size cannot be negative if pinned accounts are specified");
var seen = new HashSet(StringComparer.Ordinal);
foreach (var a in o.Cluster.PinnedAccounts)
{
if (!seen.Add(a))
return new InvalidOperationException(
$"found duplicate account name \"{a}\" in pinned accounts list");
}
}
return null;
}
///
/// Validates pinned certificate SHA-256 fingerprints.
/// Mirrors Go validatePinnedCerts.
///
public static Exception? ValidatePinnedCerts(PinnedCertSet? pinned)
{
if (pinned is null) return null;
var re = new Regex("^[a-f0-9]{64}$", RegexOptions.Compiled);
foreach (var certId in pinned)
{
if (!re.IsMatch(certId.ToLowerInvariant()))
return new InvalidOperationException(
$"error parsing 'pinned_certs' key {certId} does not look like lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo");
}
return null;
}
///
/// Validates all server options.
/// Mirrors Go validateOptions.
///
public static Exception? ValidateOptions(ServerOptions o)
{
if (o.LameDuckDuration > TimeSpan.Zero && o.LameDuckGracePeriod >= o.LameDuckDuration)
return new InvalidOperationException(
$"lame duck grace period ({o.LameDuckGracePeriod}) should be strictly lower than lame duck duration ({o.LameDuckDuration})");
if ((long)o.MaxPayload > o.MaxPending)
return new InvalidOperationException(
$"max_payload ({o.MaxPayload}) cannot be higher than max_pending ({o.MaxPending})");
if (!string.IsNullOrEmpty(o.ServerName) && o.ServerName.Contains(' '))
return new InvalidOperationException("server name cannot contain spaces");
// Trusted operators, leafnode, auth, proxies, gateway, cluster, MQTT, websocket
// — validation stubs delegating to not-yet-ported subsystems.
var jsErr = JetStreamEngine.ValidateJetStreamOptions(o);
if (jsErr != null)
return jsErr;
var err = ValidateCluster(o);
return err;
}
// =========================================================================
// Options accessors (features 2998–2999)
// =========================================================================
/// Returns a thread-safe snapshot of the current options.
public ServerOptions GetOpts()
{
_optsMu.EnterReadLock();
try { return _opts; }
finally { _optsMu.ExitReadLock(); }
}
/// Replaces the options atomically (used during config reload).
public void SetOpts(ServerOptions opts)
{
_optsMu.EnterWriteLock();
try { _opts = opts; }
finally { _optsMu.ExitWriteLock(); }
}
// =========================================================================
// Global account (feature 3000)
// =========================================================================
///
/// Returns the global account (internal, lock-protected).
/// Mirrors Go Server.globalAccount.
///
internal Account? GlobalAccountInternal()
{
_mu.EnterReadLock();
try { return _gacc; }
finally { _mu.ExitReadLock(); }
}
// =========================================================================
// Trusted keys (features 3008–3011)
// =========================================================================
///
/// Returns true if the given issuer key is trusted.
/// Mirrors Go Server.isTrustedIssuer.
///
public bool IsTrustedIssuer(string issuer)
{
_mu.EnterReadLock();
try
{
if (_trustedKeys is null && string.IsNullOrEmpty(issuer)) return true;
return _trustedKeys?.Contains(issuer) == true;
}
finally { _mu.ExitReadLock(); }
}
///
/// Processes binary-stamped and options-based trusted NKeys.
/// Mirrors Go Server.processTrustedKeys.
///
public bool ProcessTrustedKeys()
{
_strictSigningKeyUsage = [];
var opts = GetOpts();
if (!string.IsNullOrEmpty(StampedTrustedKeys) && !InitStampedTrustedKeys())
return false;
if (opts.TrustedKeys is { Count: > 0 })
{
foreach (var key in opts.TrustedKeys)
{
if (!IsValidPublicOperatorKey(key))
return false;
}
_trustedKeys = [.. opts.TrustedKeys];
foreach (var claim in opts.TrustedOperators)
{
// stub: claim.StrictSigningKeyUsage / claim.SigningKeys — session 06
// Will be populated in auth session when TrustedOperator is fully typed.
}
}
return true;
}
///
/// Parses a space-separated list of public operator NKeys.
/// Mirrors Go checkTrustedKeyString.
///
public static List? CheckTrustedKeyString(string keys)
{
var tks = keys.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (tks.Length == 0) return null;
foreach (var key in tks)
{
if (!IsValidPublicOperatorKey(key)) return null;
}
return [.. tks];
}
///
/// Initialises trusted keys from the binary-stamped field.
/// Mirrors Go Server.initStampedTrustedKeys.
///
public bool InitStampedTrustedKeys()
{
if (GetOpts().TrustedKeys is { Count: > 0 }) return false;
var tks = CheckTrustedKeyString(StampedTrustedKeys);
if (tks is null) return false;
_trustedKeys = tks;
return true;
}
// =========================================================================
// CLI helpers (features 3012–3014)
// =========================================================================
/// Prints to stderr and exits with code 1.
public static void PrintAndDie(string msg)
{
Console.Error.WriteLine(msg);
Environment.Exit(1);
}
/// Prints the server version string and exits with code 0.
public static void PrintServerAndExit()
{
Console.WriteLine($"nats-server: v{ServerConstants.Version}");
Environment.Exit(0);
}
///
/// Processes subcommands in the CLI args array.
/// Mirrors Go ProcessCommandLineArgs.
/// Returns (showVersion, showHelp, error).
///
public static (bool ShowVersion, bool ShowHelp, Exception? Error)
ProcessCommandLineArgs(string[] args)
{
foreach (var arg in args)
{
switch (arg.ToLowerInvariant())
{
case "version": return (true, false, null);
case "help": return (false, true, null);
default:
return (false, false,
new InvalidOperationException($"Unknown argument: {arg}"));
}
}
return (false, false, null);
}
// =========================================================================
// Running state (features 3015–3016)
// =========================================================================
/// Returns true if the server is running.
public bool Running() => IsRunning();
/// Protected check on running state.
private bool IsRunning() => Interlocked.CompareExchange(ref _running, 0, 0) != 0;
/// Returns true if the server is shutting down.
public bool IsShuttingDown() => Interlocked.CompareExchange(ref _shutdown, 0, 0) != 0;
// =========================================================================
// PID file (feature 3017)
// =========================================================================
///
/// Writes the process PID to the configured PID file.
/// Mirrors Go Server.logPid.
///
public Exception? LogPid()
{
var pidFile = GetOpts().PidFile;
if (string.IsNullOrEmpty(pidFile)) return null;
try
{
File.WriteAllText(pidFile, Environment.ProcessId.ToString());
return null;
}
catch (Exception ex) { return ex; }
}
// =========================================================================
// Active account counters (features 3018–3021)
// =========================================================================
/// Returns the number of reserved accounts (currently always 1).
public int NumReservedAccounts() => 1;
/// Reports the number of active accounts on this server.
public int NumActiveAccounts() => Interlocked.CompareExchange(ref _activeAccounts, 0, 0);
// =========================================================================
// Misc helpers
// =========================================================================
///
/// Sets the INFO host/port from either ClientAdvertise or the bind options.
/// Mirrors Go Server.setInfoHostPort().
/// Returns non-null on parse error.
///
internal Exception? SetInfoHostPort()
{
var opts = GetOpts();
if (!string.IsNullOrEmpty(opts.ClientAdvertise))
{
var (h, p, err) = Internal.ServerUtilities.ParseHostPort(opts.ClientAdvertise, opts.Port);
if (err != null) return err;
_info.Host = h;
_info.Port = p;
}
else
{
_info.Host = opts.Host;
_info.Port = opts.Port;
}
return null;
}
// ConfigureAuthorization, HandleSignals, ProcessProxiesTrustedKeys
// are implemented in NatsServer.Auth.cs and NatsServer.Signals.cs.
///
/// Computes a stable short hash from a string (used for JetStream node names).
/// Mirrors Go getHash.
///
internal static string GetHash(string s)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(s);
var hash = System.Security.Cryptography.SHA256.HashData(bytes);
return Convert.ToBase64String(hash)[..8].Replace('+', '-').Replace('/', '_');
}
///
/// Validates that a string is a valid public operator NKey.
/// Mirrors Go nkeys.IsValidPublicOperatorKey.
/// Simplified: checks length and prefix 'O' for operator keys.
///
internal static bool IsValidPublicOperatorKey(string key) =>
!string.IsNullOrEmpty(key) && key.Length == 56 && key[0] == 'O';
// =========================================================================
// Start (feature 3049)
// =========================================================================
///
/// Starts the server (non-blocking). Writes startup log lines and begins accept loops.
/// Full implementation requires sessions 10-23 (gateway, websocket, leafnode, routes, etc.).
/// This stub handles the bootstrap sequence up to the subsystems not yet ported.
/// Mirrors Go Server.Start.
///
public void Start()
{
Noticef("Starting nats-server");
var gc = string.IsNullOrEmpty(ServerConstants.GitCommit) ? "not set" : ServerConstants.GitCommit;
var opts = GetOpts();
_mu.EnterReadLock();
var leafNoCluster = _leafNoCluster;
_mu.ExitReadLock();
var clusterName = leafNoCluster ? string.Empty : ClusterName();
Noticef(" Version: {0}", ServerConstants.Version);
Noticef(" Git: [{0}]", gc);
if (!string.IsNullOrEmpty(clusterName))
Noticef(" Cluster: {0}", clusterName);
Noticef(" Name: {0}", _info.Name);
Noticef(" ID: {0}", _info.Id);
// Avoid RACE between Start() and Shutdown().
Interlocked.Exchange(ref _running, 1);
_mu.EnterWriteLock();
_leafNodeEnabled = opts.LeafNode.Port != 0 || opts.LeafNode.Remotes.Count > 0;
_mu.ExitWriteLock();
lock (_grMu) { _grRunning = true; }
// Log PID.
if (!string.IsNullOrEmpty(opts.PidFile))
{
var pidErr = LogPid();
if (pidErr != null)
{
Fatalf("Could not write pidfile: {0}", pidErr);
return;
}
}
// System account setup.
if (!string.IsNullOrEmpty(opts.SystemAccount))
{
var saErr = SetSystemAccount(opts.SystemAccount);
if (saErr != null)
{
Fatalf("Can't set system account: {0}", saErr);
return;
}
}
else if (!opts.NoSystemAccount)
{
SetDefaultSystemAccount();
}
StartOCSPResponseCache();
// Signal startup complete.
_startupComplete.TrySetResult();
Noticef("Server is ready");
}
// =========================================================================
// Account resolver (feature 3002)
// =========================================================================
///
/// Wires up the account resolver from opts and preloads any JWT claims.
/// Mirrors Go Server.configureResolver.
/// Server lock should be held on entry; released/reacquired internally for preloads.
///
public Exception? ConfigureResolver()
{
var opts = GetOpts();
_accResolver = opts.AccountResolver;
if (opts.AccountResolver is not null && opts.ResolverPreloads.Count > 0)
{
var ar = _accResolver!;
if (ar.IsReadOnly())
return new InvalidOperationException(
"resolver preloads only available for writeable resolver types MEM/DIR/CACHE_DIR");
foreach (var (k, v) in opts.ResolverPreloads)
{
// Validate JWT format (stub — session 06 has JWT decoder).
// jwt.DecodeAccountClaims(v) — skip here, checked again in CheckResolvePreloads.
ar.StoreAsync(k, v).GetAwaiter().GetResult();
}
}
return null;
}
///
/// Validates preloaded resolver JWT claims and logs warnings.
/// Mirrors Go Server.checkResolvePreloads.
///
public void CheckResolvePreloads()
{
var opts = GetOpts();
foreach (var (k, _) in opts.ResolverPreloads)
{
// Full JWT validation deferred to session 06 JWT integration.
Debugf("Checking preloaded account [{0}]", k);
}
}
/// Returns the configured account resolver.
public IAccountResolver? AccountResolver() => _accResolver;
}