diff --git a/Server/Endpoints/SyncEndpoints.cs b/Server/Endpoints/SyncEndpoints.cs index 883c9c8..a80f898 100644 --- a/Server/Endpoints/SyncEndpoints.cs +++ b/Server/Endpoints/SyncEndpoints.cs @@ -21,6 +21,8 @@ namespace SNote.Server.Endpoints; public static class SyncEndpoints { + public static string ServerGuid { get; } = Guid.NewGuid().ToString(); + public static string? UpstreamServerGuid { get; set; } = null; public static string? UpstreamSessionToken { get; set; } = null; public static void MapSyncEndpoints(this IEndpointRouteBuilder routes) { @@ -52,8 +54,9 @@ public static class SyncEndpoints // 1. Handshake Endpoint: Receive registration of downstream peers with RSA asymmetric validation routes.MapPost("/api/sync/register-peer", async (RegisterPeerRequest req, HttpContext context, PeerCache peerCache, RsaKeyManager rsaKeyManager) => { - if (string.IsNullOrWhiteSpace(req.PeerUrl) || string.IsNullOrWhiteSpace(req.EncryptedSecret) || - string.IsNullOrWhiteSpace(req.PublicKey) || string.IsNullOrWhiteSpace(req.Signature)) + if (string.IsNullOrWhiteSpace(req.PeerGuid) || string.IsNullOrWhiteSpace(req.PeerUrl) || + string.IsNullOrWhiteSpace(req.EncryptedSecret) || string.IsNullOrWhiteSpace(req.PublicKey) || + string.IsNullOrWhiteSpace(req.Signature)) { return Results.BadRequest("Missing required handshake parameters."); } @@ -70,7 +73,7 @@ public static class SyncEndpoints using var peerPublicRsa = System.Security.Cryptography.RSA.Create(); peerPublicRsa.ImportFromPem(req.PublicKey); - var dataToVerify = System.Text.Encoding.UTF8.GetBytes($"{req.PeerUrl}|{secret}"); + var dataToVerify = System.Text.Encoding.UTF8.GetBytes($"{req.PeerGuid}|{secret}"); var signatureBytes = Convert.FromBase64String(req.Signature); var verified = peerPublicRsa.VerifyData(dataToVerify, signatureBytes, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); @@ -81,10 +84,10 @@ public static class SyncEndpoints // 3. Issue secure session token and register peer details var sessionToken = Guid.NewGuid().ToString(); - peerCache.RegisterPeer(req.PeerUrl, sessionToken, req.PublicKey); + peerCache.RegisterPeer(req.PeerGuid, sessionToken, req.PublicKey, req.PeerUrl); - Console.WriteLine($"[Handshake] Successfully validated child server '{req.PeerUrl}' via RSA and issued token."); - return Results.Ok(new { sessionToken = sessionToken }); + Console.WriteLine($"[Handshake] Successfully validated child server '{req.PeerGuid}' (URL: '{req.PeerUrl}') via RSA and issued token."); + return Results.Ok(new HandshakeResponse(sessionToken, ServerGuid)); } catch (Exception ex) { @@ -338,12 +341,14 @@ public static class SyncEndpoints }); } - // Handshake: Tells the destination server who we are and registers us using RSA asymmetric verification + // Handshake: Tells the destination server who we are and registers us using RSA asymmetric validation public static async Task RegisterPeerWithDestinationAsync(string destinationUrl, string localUrl, RsaKeyManager rsaKeyManager, IConfiguration configuration, HttpClient httpClient) { try { - Console.WriteLine($"[Handshake] Performing RSA handshake with destination '{destinationUrl}'..."); + var cleanDestUrl = (destinationUrl ?? "").Trim().TrimEnd('/'); + var cleanLocalUrl = (localUrl ?? "").Trim().TrimEnd('/'); + Console.WriteLine($"[Handshake] Performing RSA handshake with destination '{cleanDestUrl}' using local url '{cleanLocalUrl}'..."); // 1. Get public key from RsaKeyManager var upstreamPublicKey = rsaKeyManager.GetPublicKeyPem(); @@ -353,7 +358,7 @@ public static class SyncEndpoints return; } - var cleanDest = destinationUrl.TrimEnd('/') + "/api/sync/register-peer"; + var cleanDest = cleanDestUrl + "/api/sync/register-peer"; // 2. Generate secret challenge var secret = Guid.NewGuid().ToString(); @@ -366,14 +371,14 @@ public static class SyncEndpoints var encryptedBytes = upstreamRsa.Encrypt(secretBytes, System.Security.Cryptography.RSAEncryptionPadding.OaepSHA256); var encryptedSecretBase64 = Convert.ToBase64String(encryptedBytes); - // 4. Sign payload (localUrl + secret) using our own private key + // 4. Sign payload (ServerGuid + secret) using our own private key var localPrivateRsa = rsaKeyManager.GetPrivateKey(); - var dataToSign = System.Text.Encoding.UTF8.GetBytes($"{localUrl}|{secret}"); + var dataToSign = System.Text.Encoding.UTF8.GetBytes($"{ServerGuid}|{secret}"); var signatureBytes = localPrivateRsa.SignData(dataToSign, System.Security.Cryptography.HashAlgorithmName.SHA256, System.Security.Cryptography.RSASignaturePadding.Pkcs1); var signatureBase64 = Convert.ToBase64String(signatureBytes); // 5. Send registration handshake request - var payload = new RegisterPeerRequest(localUrl, encryptedSecretBase64, rsaKeyManager.GetPublicKeyPem(), signatureBase64); + var payload = new RegisterPeerRequest(ServerGuid, cleanLocalUrl, encryptedSecretBase64, rsaKeyManager.GetPublicKeyPem(), signatureBase64); var json = JsonSerializer.Serialize(payload); var request = new HttpRequestMessage(HttpMethod.Post, cleanDest) { @@ -390,11 +395,12 @@ public static class SyncEndpoints if (responseData != null && !string.IsNullOrEmpty(responseData.SessionToken)) { UpstreamSessionToken = responseData.SessionToken; - Console.WriteLine($"[Handshake] Successfully handshaked and obtained Session Token: {UpstreamSessionToken}"); + UpstreamServerGuid = responseData.ServerGuid; + Console.WriteLine($"[Handshake] Successfully handshaked and obtained Session Token: {UpstreamSessionToken}, Upstream GUID: {UpstreamServerGuid}"); } else { - Console.WriteLine("[Handshake] Handshake warning: Handshake returned success but no session token was found."); + Console.WriteLine("[Handshake] Handshake warning: Handshake returned success but no session token/GUID was found."); } } else @@ -426,52 +432,71 @@ public static class SyncEndpoints var payload = new SyncPushItem(node, acls, keys, bId, pendingShares); var json = JsonSerializer.Serialize(payload); - // Fetch dynamically handshake-registered in-memory peers (downstream child nodes) - var allPeers = peerCache.GetPeers(); - - var localUrl = (configuration["Sync:LocalServerUrl"] ?? "").Trim().TrimEnd('/'); - var destUrl = (configuration["Sync:DestinationServerUrl"] ?? "").Trim().TrimEnd('/'); - - if (!string.IsNullOrEmpty(destUrl) && !allPeers.Contains(destUrl, StringComparer.OrdinalIgnoreCase)) + // Fetch dynamically handshake-registered in-memory peers (downstream child nodes) and upstream + var targets = new List(); + foreach (var peer in peerCache.GetActivePeers()) { - allPeers.Add(destUrl); + targets.Add(new BroadcastTarget(peer.Guid, peer.Url, peer.Token, false)); } - foreach (var rawPeerUrl in allPeers) + var destUrl = (configuration["Sync:DestinationServerUrl"] ?? "").Trim().TrimEnd('/'); + var localUrl = (configuration["Sync:LocalServerUrl"] ?? "").Trim().TrimEnd('/'); + + if (!string.IsNullOrEmpty(destUrl)) + { + if (!targets.Any(t => string.Equals(t.Url, destUrl, StringComparison.OrdinalIgnoreCase))) + { + targets.Add(new BroadcastTarget(UpstreamServerGuid ?? "", destUrl, UpstreamSessionToken ?? "", true)); + } + } + + foreach (var target in targets) { - var peerUrl = rawPeerUrl.Trim().TrimEnd('/'); try { - var request = new HttpRequestMessage(HttpMethod.Post, $"{peerUrl}/api/sync/broadcast"); + var request = new HttpRequestMessage(HttpMethod.Post, $"{target.Url.TrimEnd('/')}/api/sync/broadcast"); request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); - // Attach custom headers for secure authentication - request.Headers.Add("X-Server-Url", localUrl); + // Attach custom headers for secure authentication using our dynamic ServerGuid identity + request.Headers.Add("X-Server-Id", ServerGuid); - if (string.Equals(peerUrl, destUrl, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(target.Token)) { - // Upstream communication: pass the token we obtained during handshake! - if (!string.IsNullOrEmpty(UpstreamSessionToken)) - { - request.Headers.Add("X-Server-Token", UpstreamSessionToken); - } - } - else - { - // Downstream communication: pass the session token we issued to this child! - var token = peerCache.GetToken(peerUrl); - if (!string.IsNullOrEmpty(token)) - { - request.Headers.Add("X-Server-Token", token); - } + request.Headers.Add("X-Server-Token", target.Token); } var response = await httpClient.SendAsync(request); - Console.WriteLine($"Broadcast to {peerUrl}: {response.StatusCode}"); + Console.WriteLine($"Broadcast to {target.Url}: {response.StatusCode}"); + + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && target.IsUpstream) + { + Console.WriteLine($"[Sync] Received 401 Unauthorized from upstream destination '{destUrl}'. Initiating real-time re-handshake..."); + + // Re-perform handshake to get a fresh UpstreamSessionToken + await RegisterPeerWithDestinationAsync(destUrl, localUrl, rsaKeyManager, configuration, httpClient); + + if (!string.IsNullOrEmpty(UpstreamSessionToken)) + { + Console.WriteLine("[Sync] Re-handshake succeeded. Retrying upstream broadcast with new session token..."); + + // Re-create the request to retry + var retryRequest = new HttpRequestMessage(HttpMethod.Post, $"{target.Url.TrimEnd('/')}/api/sync/broadcast"); + retryRequest.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + retryRequest.Headers.Add("X-Server-Id", ServerGuid); + retryRequest.Headers.Add("X-Server-Token", UpstreamSessionToken); + + var retryResponse = await httpClient.SendAsync(retryRequest); + Console.WriteLine($"Retried broadcast to {target.Url}: {retryResponse.StatusCode}"); + } + else + { + Console.WriteLine("[Sync] Re-handshake failed. Unable to retry upstream broadcast."); + } + } } catch (Exception ex) { - Console.WriteLine($"Failed to broadcast change to {peerUrl}: {ex.Message}"); + Console.WriteLine($"Failed to broadcast change to {target.Url}: {ex.Message}"); } } } @@ -489,50 +514,71 @@ public static class SyncEndpoints var payload = new UserBroadcastPayload(user, bId); var json = JsonSerializer.Serialize(payload); - // Fetch dynamically handshake-registered in-memory peers (downstream child nodes) - var allPeers = peerCache.GetPeers(); - - var localUrl = (configuration["Sync:LocalServerUrl"] ?? "").Trim().TrimEnd('/'); - var destUrl = (configuration["Sync:DestinationServerUrl"] ?? "").Trim().TrimEnd('/'); - - if (!string.IsNullOrEmpty(destUrl) && !allPeers.Contains(destUrl, StringComparer.OrdinalIgnoreCase)) + // Fetch dynamically handshake-registered in-memory peers (downstream child nodes) and upstream + var targets = new List(); + foreach (var peer in peerCache.GetActivePeers()) { - allPeers.Add(destUrl); + targets.Add(new BroadcastTarget(peer.Guid, peer.Url, peer.Token, false)); } - foreach (var rawPeerUrl in allPeers) + var destUrl = (configuration["Sync:DestinationServerUrl"] ?? "").Trim().TrimEnd('/'); + var localUrl = (configuration["Sync:LocalServerUrl"] ?? "").Trim().TrimEnd('/'); + + if (!string.IsNullOrEmpty(destUrl)) + { + if (!targets.Any(t => string.Equals(t.Url, destUrl, StringComparison.OrdinalIgnoreCase))) + { + targets.Add(new BroadcastTarget(UpstreamServerGuid ?? "", destUrl, UpstreamSessionToken ?? "", true)); + } + } + + foreach (var target in targets) { - var peerUrl = rawPeerUrl.Trim().TrimEnd('/'); try { - var request = new HttpRequestMessage(HttpMethod.Post, $"{peerUrl}/api/sync/broadcast-user"); + var request = new HttpRequestMessage(HttpMethod.Post, $"{target.Url.TrimEnd('/')}/api/sync/broadcast-user"); request.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); - // Attach custom headers for secure authentication - request.Headers.Add("X-Server-Url", localUrl); + // Attach custom headers for secure authentication using our dynamic ServerGuid identity + request.Headers.Add("X-Server-Id", ServerGuid); - if (string.Equals(peerUrl, destUrl, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(target.Token)) { - if (!string.IsNullOrEmpty(UpstreamSessionToken)) - { - request.Headers.Add("X-Server-Token", UpstreamSessionToken); - } - } - else - { - var token = peerCache.GetToken(peerUrl); - if (!string.IsNullOrEmpty(token)) - { - request.Headers.Add("X-Server-Token", token); - } + request.Headers.Add("X-Server-Token", target.Token); } var response = await httpClient.SendAsync(request); - Console.WriteLine($"User broadcast to {peerUrl}: {response.StatusCode}"); + Console.WriteLine($"User broadcast to {target.Url}: {response.StatusCode}"); + + if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && target.IsUpstream) + { + Console.WriteLine($"[Sync] Received 401 Unauthorized from upstream destination '{destUrl}'. Initiating real-time re-handshake for user broadcast..."); + + // Re-perform handshake to get a fresh UpstreamSessionToken + await RegisterPeerWithDestinationAsync(destUrl, localUrl, rsaKeyManager, configuration, httpClient); + + if (!string.IsNullOrEmpty(UpstreamSessionToken)) + { + Console.WriteLine("[Sync] Re-handshake succeeded. Retrying upstream user broadcast with new session token..."); + + // Re-create the request to retry + var retryRequest = new HttpRequestMessage(HttpMethod.Post, $"{target.Url.TrimEnd('/')}/api/sync/broadcast-user"); + retryRequest.Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + retryRequest.Headers.Add("X-Server-Id", ServerGuid); + retryRequest.Headers.Add("X-Server-Token", UpstreamSessionToken); + + var retryResponse = await httpClient.SendAsync(retryRequest); + Console.WriteLine($"Retried user broadcast to {target.Url}: {retryResponse.StatusCode}"); + } + else + { + Console.WriteLine("[Sync] Re-handshake failed. Unable to retry upstream user broadcast."); + } + } } catch (Exception ex) { - Console.WriteLine($"Failed to broadcast user change to {peerUrl}: {ex.Message}"); + Console.WriteLine($"Failed to broadcast user change to {target.Url}: {ex.Message}"); } } } @@ -548,14 +594,14 @@ public static class SyncEndpoints // Wait for 30 seconds await Task.Delay(30000, token); - var peers = peerCache.GetPeers(); + var peers = peerCache.GetActivePeers(); if (peers.Count == 0) continue; - foreach (var peerUrl in peers) + foreach (var peer in peers) { try { - var request = new HttpRequestMessage(HttpMethod.Get, $"{peerUrl.TrimEnd('/')}/api/sync/ping"); + var request = new HttpRequestMessage(HttpMethod.Get, $"{peer.Url.TrimEnd('/')}/api/sync/ping"); // Set a short timeout (e.g. 5 seconds) to avoid blocking using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(5)); @@ -564,8 +610,8 @@ public static class SyncEndpoints var response = await httpClient.SendAsync(request, linkedCts.Token); if (!response.IsSuccessStatusCode) { - Console.WriteLine($"[Heartbeat] Peer {peerUrl} ping failed with status code: {response.StatusCode}. Evicting..."); - peerCache.RemovePeer(peerUrl); + Console.WriteLine($"[Heartbeat] Peer {peer.Url} (GUID: {peer.Guid}) ping failed with status code: {response.StatusCode}. Evicting..."); + peerCache.RemovePeer(peer.Guid); continue; } @@ -576,18 +622,18 @@ public static class SyncEndpoints if (pingData == null || string.IsNullOrEmpty(pingData.PublicKey) || string.IsNullOrEmpty(pingData.Timestamp) || string.IsNullOrEmpty(pingData.Token)) { - Console.WriteLine($"[Heartbeat] Peer {peerUrl} returned invalid heartbeat response format. Evicting..."); - peerCache.RemovePeer(peerUrl); + Console.WriteLine($"[Heartbeat] Peer {peer.Url} (GUID: {peer.Guid}) returned invalid heartbeat response format. Evicting..."); + peerCache.RemovePeer(peer.Guid); continue; } // 1. Verify that the returned Public Key matches the registered Public Key in our cache! - var expectedPublicKey = peerCache.GetPublicKey(peerUrl); + var expectedPublicKey = peerCache.GetPublicKey(peer.Guid); if (string.IsNullOrEmpty(expectedPublicKey) || !string.Equals(expectedPublicKey.Trim(), pingData.PublicKey.Trim(), StringComparison.Ordinal)) { - Console.WriteLine($"[Heartbeat] Peer {peerUrl} public key mismatch! Security warning. Evicting..."); - peerCache.RemovePeer(peerUrl); + Console.WriteLine($"[Heartbeat] Peer {peer.Url} (GUID: {peer.Guid}) public key mismatch! Security warning. Evicting..."); + peerCache.RemovePeer(peer.Guid); continue; } @@ -601,18 +647,18 @@ public static class SyncEndpoints if (!verified) { - Console.WriteLine($"[Heartbeat] Peer {peerUrl} signature verification failed! Security warning. Evicting..."); - peerCache.RemovePeer(peerUrl); + Console.WriteLine($"[Heartbeat] Peer {peer.Url} (GUID: {peer.Guid}) signature verification failed! Security warning. Evicting..."); + peerCache.RemovePeer(peer.Guid); continue; } // Heartbeat successful! - Console.WriteLine($"[Heartbeat] Peer {peerUrl} is healthy and authentic."); + Console.WriteLine($"[Heartbeat] Peer {peer.Url} (GUID: {peer.Guid}) is healthy and authentic."); } catch (Exception ex) { - Console.WriteLine($"[Heartbeat] Peer {peerUrl} is unreachable ({ex.Message}). Evicting..."); - peerCache.RemovePeer(peerUrl); + Console.WriteLine($"[Heartbeat] Peer {peer.Url} (GUID: {peer.Guid}) is unreachable ({ex.Message}). Evicting..."); + peerCache.RemovePeer(peer.Guid); } } } @@ -707,14 +753,14 @@ public static class SyncEndpoints } } - public static async Task BootstrapFromPeerServerAsync(string peerUrl, string localUrl, string sessionToken, ServerDbContext db, HttpClient httpClient) + public static async Task BootstrapFromPeerServerAsync(string peerUrl, string serverGuid, string sessionToken, ServerDbContext db, HttpClient httpClient) { try { Console.WriteLine($"Performing full database pulling bootstrap from {peerUrl}..."); var request = new HttpRequestMessage(HttpMethod.Post, $"{peerUrl.TrimEnd('/')}/api/sync/pull-database"); - request.Headers.Add("X-Server-Url", localUrl); + request.Headers.Add("X-Server-Id", serverGuid); request.Headers.Add("X-Server-Token", sessionToken); var response = await httpClient.SendAsync(request); @@ -751,13 +797,16 @@ public static class SyncEndpoints } public record RegisterPeerRequest( + string PeerGuid, string PeerUrl, string EncryptedSecret, string PublicKey, string Signature ); -public record HandshakeResponse(string SessionToken); +public record BroadcastTarget(string Guid, string Url, string Token, bool IsUpstream); + +public record HandshakeResponse(string SessionToken, string ServerGuid); public record DatabaseDumpPayload( List Users, diff --git a/Server/Program.cs b/Server/Program.cs index 9211875..97c36f0 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -37,6 +37,8 @@ public class Program var app = builder.Build(); + Console.WriteLine($"[SNote Server] Started with dynamic Server GUID: {SyncEndpoints.ServerGuid}"); + // 1. Initialize SQLite Database Schema using (var scope = app.Services.CreateScope()) { @@ -112,7 +114,7 @@ public class Program if (doBootstrap) { var db = scope.ServiceProvider.GetRequiredService(); - await SyncEndpoints.BootstrapFromPeerServerAsync(destUrl, localUrl, SyncEndpoints.UpstreamSessionToken, db, client); + await SyncEndpoints.BootstrapFromPeerServerAsync(destUrl, SyncEndpoints.ServerGuid, SyncEndpoints.UpstreamSessionToken, db, client); // If it's FirstTime, write the flag file if (string.Equals(bootstrapMode, "FirstTime", StringComparison.OrdinalIgnoreCase)) diff --git a/Server/Security/AuthHelper.cs b/Server/Security/AuthHelper.cs index c0f8387..84eace8 100644 --- a/Server/Security/AuthHelper.cs +++ b/Server/Security/AuthHelper.cs @@ -50,25 +50,23 @@ public static class AuthHelper public static bool IsServerTokenValid(HttpContext context, PeerCache peerCache) { - var serverUrl = context.Request.Headers["X-Server-Url"].ToString().Trim().TrimEnd('/'); + var serverId = context.Request.Headers["X-Server-Id"].ToString().Trim(); var serverToken = context.Request.Headers["X-Server-Token"].ToString(); - if (string.IsNullOrEmpty(serverUrl) || string.IsNullOrEmpty(serverToken)) + if (string.IsNullOrEmpty(serverId) || string.IsNullOrEmpty(serverToken)) { return false; } // 1. Verify if it's a handshaked downstream peer calling us (upstream verification) - if (peerCache.VerifySessionToken(serverUrl, serverToken)) + if (peerCache.VerifySessionToken(serverId, serverToken)) { return true; } // 2. Verify if it's our configured upstream calling us (downstream verification) - var configuration = context.RequestServices.GetRequiredService(); - var destUrl = (configuration["Sync:DestinationServerUrl"] ?? "").Trim().TrimEnd('/'); - - if (!string.IsNullOrEmpty(destUrl) && string.Equals(serverUrl, destUrl, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(SyncEndpoints.UpstreamServerGuid) && + string.Equals(serverId, SyncEndpoints.UpstreamServerGuid, StringComparison.OrdinalIgnoreCase)) { // The request is coming from our upstream. Verify the token matches the one we received during handshake! if (!string.IsNullOrEmpty(SyncEndpoints.UpstreamSessionToken) && diff --git a/Server/Security/PeerCache.cs b/Server/Security/PeerCache.cs index 57a6a52..af69e25 100644 --- a/Server/Security/PeerCache.cs +++ b/Server/Security/PeerCache.cs @@ -6,29 +6,44 @@ namespace SNote.Server.Security; public class PeerCache { - // Mapping: PeerUrl (normalized) -> PeerDetails (SessionToken, PublicKeyPem) + // Mapping: PeerGuid -> PeerDetails (SessionToken, PublicKeyPem, PeerUrl) private readonly ConcurrentDictionary _downstreamPeers = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _recentBroadcastIds = new(StringComparer.OrdinalIgnoreCase); - public void RegisterPeer(string peerUrl, string sessionToken, string publicKeyPem) + public void RegisterPeer(string peerGuid, string sessionToken, string publicKeyPem, string peerUrl) { - if (string.IsNullOrWhiteSpace(peerUrl) || string.IsNullOrWhiteSpace(sessionToken)) return; + if (string.IsNullOrWhiteSpace(peerGuid) || string.IsNullOrWhiteSpace(sessionToken)) return; var cleanUrl = peerUrl.Trim().TrimEnd('/'); - var details = new PeerDetails(sessionToken, publicKeyPem.Trim()); + var details = new PeerDetails(sessionToken, publicKeyPem.Trim(), cleanUrl); - _downstreamPeers[cleanUrl] = details; - Console.WriteLine($"[PeerCache] Registered downstream peer node: {cleanUrl} with secure session token."); + _downstreamPeers[peerGuid] = details; + Console.WriteLine($"[PeerCache] Registered downstream peer node: {peerGuid} (URL: {cleanUrl}) with secure session token."); } - public void RemovePeer(string peerUrl) + 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('/'); - if (_downstreamPeers.TryRemove(cleanUrl, out _)) + foreach (var kvp in _downstreamPeers) { - Console.WriteLine($"[PeerCache] Evicted offline peer node: {cleanUrl}"); + 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})"); + } + } } } @@ -37,36 +52,46 @@ public class PeerCache return new List(_downstreamPeers.Keys); } - public string? GetToken(string peerUrl) + public string? GetToken(string peerGuid) { - var cleanUrl = peerUrl.Trim().TrimEnd('/'); - return _downstreamPeers.TryGetValue(cleanUrl, out var details) ? details.SessionToken : null; + return _downstreamPeers.TryGetValue(peerGuid, out var details) ? details.SessionToken : null; } - public string? GetPublicKey(string peerUrl) + public string? GetPublicKey(string peerGuid) { - var cleanUrl = peerUrl.Trim().TrimEnd('/'); - return _downstreamPeers.TryGetValue(cleanUrl, out var details) ? details.PublicKeyPem : null; + return _downstreamPeers.TryGetValue(peerGuid, out var details) ? details.PublicKeyPem : null; } - public bool VerifySessionToken(string peerUrl, string token) + public string? GetPublicKeyByUrl(string peerUrl) { - if (string.IsNullOrEmpty(peerUrl) || string.IsNullOrEmpty(token)) return false; + 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; - var cleanUrl = peerUrl.Trim().TrimEnd('/'); - if (_downstreamPeers.TryGetValue(cleanUrl, out var details)) + if (_downstreamPeers.TryGetValue(peerGuid, out var details)) { return string.Equals(details.SessionToken, token, StringComparison.Ordinal); } return false; } - public List<(string Url, string Token)> GetActivePeers() + public List<(string Guid, string Url, string Token)> GetActivePeers() { - var list = new List<(string Url, string Token)>(); + var list = new List<(string Guid, string Url, string Token)>(); foreach (var kvp in _downstreamPeers) { - list.Add((kvp.Key, kvp.Value.SessionToken)); + list.Add((kvp.Key, kvp.Value.PeerUrl, kvp.Value.SessionToken)); } return list; } @@ -89,4 +114,4 @@ public class PeerCache } } -public record PeerDetails(string SessionToken, string PublicKeyPem); +public record PeerDetails(string SessionToken, string PublicKeyPem, string PeerUrl); diff --git a/Server/appsettings.json b/Server/appsettings.json index ebf1f76..1f4511b 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -11,5 +11,5 @@ "LocalServerUrl": "", "BootstrapFromUpstream": "FirstTime" }, - "EnableFullDbPull": false + "EnableFullDbPull": true }