using System.Runtime.Versioning; using WebhookServer.Core.Auth; using WebhookServer.Core.Models; using WebhookServer.Core.Storage; namespace WebhookServer.Service; /// /// In-memory authoritative copy of the current . Holds parsed /// helpers (allowlists keyed by endpoint id, slug → endpoint map) and notifies subscribers /// when the config is replaced so the listener and dispatcher can react. /// [SupportedOSPlatform("windows")] public sealed class ServiceState { private readonly ConfigStore _store; private readonly object _lock = new(); private ServerConfig _config = new(); private Dictionary _bySlug = new(StringComparer.Ordinal); private Dictionary _allowLists = new(); private IpAllowList _trustedProxies = IpAllowList.Parse(Array.Empty()); public DateTimeOffset StartedAt { get; } = DateTimeOffset.UtcNow; public event EventHandler? ListenerSettingsChanged; public ServiceState(ConfigStore store) { _store = store; } public ServerConfig Snapshot() { lock (_lock) return _config; } public bool TryGetEndpoint(string slug, out EndpointConfig endpoint) { lock (_lock) { return _bySlug.TryGetValue(slug, out endpoint!); } } public IpAllowList GetAllowList(Guid endpointId) { lock (_lock) { return _allowLists.TryGetValue(endpointId, out var l) ? l : IpAllowList.Parse(Array.Empty()); } } public IpAllowList GetTrustedProxies() { lock (_lock) return _trustedProxies; } public async Task LoadAsync(CancellationToken ct = default) { var loaded = await _store.LoadAsync(ct).ConfigureAwait(false); ConfigStore.DecryptSecrets(loaded); Replace(loaded, listenerChanged: true); } public async Task ReplaceAsync(ServerConfig replacement, CancellationToken ct = default) { // Save to disk first; that re-encrypts secrets in place. Then publish in-memory. var listenerChanged = HasListenerSettingsChanged(_config, replacement); await _store.SaveAsync(replacement, ct).ConfigureAwait(false); // SaveAsync filled in Encrypted; ensure Plaintext is populated for runtime use. ConfigStore.DecryptSecrets(replacement); Replace(replacement, listenerChanged); } private void Replace(ServerConfig cfg, bool listenerChanged) { var bySlug = new Dictionary(StringComparer.Ordinal); var allow = new Dictionary(); foreach (var ep in cfg.Endpoints) { if (!string.IsNullOrEmpty(ep.Slug)) bySlug[ep.Slug] = ep; allow[ep.Id] = IpAllowList.Parse(ep.AllowedClients); } var trusted = IpAllowList.Parse(cfg.TrustedProxies); lock (_lock) { _config = cfg; _bySlug = bySlug; _allowLists = allow; _trustedProxies = trusted; } if (listenerChanged) ListenerSettingsChanged?.Invoke(this, EventArgs.Empty); } private static bool HasListenerSettingsChanged(ServerConfig oldCfg, ServerConfig newCfg) { if (oldCfg.HttpPort != newCfg.HttpPort) return true; var a = oldCfg.HttpsBinding; var b = newCfg.HttpsBinding; if ((a is null) != (b is null)) return true; if (a is not null && b is not null) { if (a.Kind != b.Kind || a.Port != b.Port || a.PfxPath != b.PfxPath || a.Thumbprint != b.Thumbprint) return true; } return false; } }