using PrivaPub.Infrastructure.Geo; using System.Collections.Concurrent; namespace PrivaPub.Infrastructure.Http { public interface IEdgeHints { void Remember(string host, CdnProvider cdn); CdnProvider Of(string host); } // The CDN each remote host's responses came through, as their headers tell it (CdnCatalog fingerprints), for the // weekly description of the server. Like ConnectedAddresses, memory only: what a host answers is never stored per // request. public class EdgeHints : IEdgeHints { const int MaxHosts = 50_000; readonly ConcurrentDictionary _hints = new(StringComparer.OrdinalIgnoreCase); public void Remember(string host, CdnProvider cdn) { if (string.IsNullOrEmpty(host)) return; if (cdn == default) { _hints.TryRemove(host, out _); return; } if (_hints.Count >= MaxHosts) _hints.Clear(); _hints[host] = cdn; } public CdnProvider Of(string host) => host != default && _hints.TryGetValue(host, out var cdn) ? cdn : default; } // On the federation client: reads each response's CDN fingerprint for its host. A response without one forgets the // host's hint, so a server that leaves its CDN is seen as itself again. public class EdgeHintsHandler : DelegatingHandler { readonly IEdgeHints _hints; public EdgeHintsHandler(IEdgeHints hints) => _hints = hints; protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var response = await base.SendAsync(request, cancellationToken); if (request.RequestUri is { } uri && (int)response.StatusCode is >= 200 and < 500) _hints.Remember(uri.Host, CdnCatalog.FromResponse(response)); return response; } } }