Try to fix boardcasting with Antigravity.
This commit is contained in:
@@ -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
@@ -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))
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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('/');
|
||||
if (_downstreamPeers.TryGetValue(cleanUrl, out var details))
|
||||
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 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);
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"LocalServerUrl": "",
|
||||
"BootstrapFromUpstream": "FirstTime"
|
||||
},
|
||||
"EnableFullDbPull": false
|
||||
"EnableFullDbPull": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user