Try to fix boardcasting with Antigravity.

This commit is contained in:
Creeper Lv
2026-06-01 17:11:09 +10:00
parent 78a99dc279
commit 9decde570f
5 changed files with 197 additions and 123 deletions
+139 -90
View File
@@ -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<BroadcastTarget>();
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<BroadcastTarget>();
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<User> Users,
+3 -1
View File
@@ -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<ServerDbContext>();
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))
+5 -7
View File
@@ -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<IConfiguration>();
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) &&
+49 -24
View File
@@ -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<string, PeerDetails> _downstreamPeers = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, DateTime> _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<string>(_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);
+1 -1
View File
@@ -11,5 +11,5 @@
"LocalServerUrl": "",
"BootstrapFromUpstream": "FirstTime"
},
"EnableFullDbPull": false
"EnableFullDbPull": true
}