using System; using System.Collections.Concurrent; using System.Collections.Generic; namespace SNote.Server.Security; public class PeerCache { // Mapping: PeerGuid -> PeerDetails (SessionToken, PublicKeyPem, PeerUrl) private readonly ConcurrentDictionary _downstreamPeers = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _recentBroadcastIds = new(StringComparer.OrdinalIgnoreCase); public void RegisterPeer(string peerGuid, string sessionToken, string publicKeyPem, string peerUrl) { if (string.IsNullOrWhiteSpace(peerGuid) || string.IsNullOrWhiteSpace(sessionToken)) return; var cleanUrl = peerUrl.Trim().TrimEnd('/'); var details = new PeerDetails(sessionToken, publicKeyPem.Trim(), cleanUrl); _downstreamPeers[peerGuid] = details; Console.WriteLine($"[PeerCache] Registered downstream peer node: {peerGuid} (URL: {cleanUrl}) with secure session token."); } public void RemovePeer(string peerGuid) { if (string.IsNullOrWhiteSpace(peerGuid)) return; if (_downstreamPeers.TryRemove(peerGuid, out var details)) { Console.WriteLine($"[PeerCache] Evicted offline peer node: {peerGuid} (URL: {details.PeerUrl})"); } } public void RemovePeerByUrl(string peerUrl) { if (string.IsNullOrWhiteSpace(peerUrl)) return; var cleanUrl = peerUrl.Trim().TrimEnd('/'); foreach (var kvp in _downstreamPeers) { if (string.Equals(kvp.Value.PeerUrl, cleanUrl, StringComparison.OrdinalIgnoreCase)) { if (_downstreamPeers.TryRemove(kvp.Key, out _)) { Console.WriteLine($"[PeerCache] Evicted offline peer node by URL: {cleanUrl} (GUID: {kvp.Key})"); } } } } public List GetPeers() { return new List(_downstreamPeers.Keys); } public string? GetToken(string peerGuid) { return _downstreamPeers.TryGetValue(peerGuid, out var details) ? details.SessionToken : null; } public string? GetPublicKey(string peerGuid) { return _downstreamPeers.TryGetValue(peerGuid, out var details) ? details.PublicKeyPem : null; } public string? GetPublicKeyByUrl(string peerUrl) { var cleanUrl = peerUrl.Trim().TrimEnd('/'); foreach (var details in _downstreamPeers.Values) { if (string.Equals(details.PeerUrl, cleanUrl, StringComparison.OrdinalIgnoreCase)) { return details.PublicKeyPem; } } return null; } public bool VerifySessionToken(string peerGuid, string token) { if (string.IsNullOrEmpty(peerGuid) || string.IsNullOrEmpty(token)) return false; if (_downstreamPeers.TryGetValue(peerGuid, out var details)) { return string.Equals(details.SessionToken, token, StringComparison.Ordinal); } return false; } public List<(string Guid, string Url, string Token)> GetActivePeers() { var list = new List<(string Guid, string Url, string Token)>(); foreach (var kvp in _downstreamPeers) { list.Add((kvp.Key, kvp.Value.PeerUrl, kvp.Value.SessionToken)); } return list; } public bool TryProcessBroadcast(string broadcastId) { if (string.IsNullOrWhiteSpace(broadcastId)) return false; // Clean up old entries to prevent infinite memory growth (older than 10 minutes) var cutoff = DateTime.UtcNow.AddMinutes(-10); foreach (var kvp in _recentBroadcastIds) { if (kvp.Value < cutoff) { _recentBroadcastIds.TryRemove(kvp.Key, out _); } } return _recentBroadcastIds.TryAdd(broadcastId, DateTime.UtcNow); } } public record PeerDetails(string SessionToken, string PublicKeyPem, string PeerUrl);