Deploy AutoDetect

This commit is contained in:
2025-09-13 13:40:32 +02:00
parent 8d0c3aafb3
commit 693d3c6af7
7 changed files with 202 additions and 32 deletions

View File

@@ -4,8 +4,9 @@ namespace MareSynchronosAuthService.Services.Discovery;
public interface IDiscoveryPresenceStore : IDisposable
{
void Publish(string uid, IEnumerable<string> hashes, string? displayName = null);
(bool Found, string Token, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash);
void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true);
void Unpublish(string uid);
(bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash);
bool ValidateToken(string token, out string targetUid);
}

View File

@@ -4,7 +4,7 @@ namespace MareSynchronosAuthService.Services.Discovery;
public sealed class InMemoryPresenceStore : IDiscoveryPresenceStore
{
private readonly ConcurrentDictionary<string, (string Uid, DateTimeOffset ExpiresAt, string? DisplayName)> _presence = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, (string Uid, DateTimeOffset ExpiresAt, string? DisplayName, bool AllowRequests)> _presence = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, (string TargetUid, DateTimeOffset ExpiresAt)> _tokens = new(StringComparer.Ordinal);
private readonly TimeSpan _presenceTtl;
private readonly TimeSpan _tokenTtl;
@@ -35,25 +35,43 @@ public sealed class InMemoryPresenceStore : IDiscoveryPresenceStore
}
}
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null)
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true)
{
var exp = DateTimeOffset.UtcNow.Add(_presenceTtl);
foreach (var h in hashes.Distinct(StringComparer.Ordinal))
{
_presence[h] = (uid, exp, displayName);
_presence[h] = (uid, exp, displayName, allowRequests);
}
}
public (bool Found, string Token, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
public void Unpublish(string uid)
{
// Remove all presence hashes owned by this uid
foreach (var kv in _presence.ToArray())
{
if (string.Equals(kv.Value.Uid, uid, StringComparison.Ordinal))
{
_presence.TryRemove(kv.Key, out _);
}
}
}
public (bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
{
if (_presence.TryGetValue(hash, out var entry))
{
if (string.Equals(entry.Uid, requesterUid, StringComparison.Ordinal)) return (false, string.Empty, null);
if (string.Equals(entry.Uid, requesterUid, StringComparison.Ordinal))
return (false, null, string.Empty, null);
// Visible but requests disabled → no token
if (!entry.AllowRequests)
return (true, null, entry.Uid, entry.DisplayName);
var token = Guid.NewGuid().ToString("N");
_tokens[token] = (entry.Uid, DateTimeOffset.UtcNow.Add(_tokenTtl));
return (true, token, entry.DisplayName);
return (true, token, entry.Uid, entry.DisplayName);
}
return (false, string.Empty, null);
return (false, null, string.Empty, null);
}
public bool ValidateToken(string token, out string targetUid)

View File

@@ -24,8 +24,9 @@ public sealed class RedisPresenceStore : IDiscoveryPresenceStore
private static string KeyForHash(string hash) => $"nd:hash:{hash}";
private static string KeyForToken(string token) => $"nd:token:{token}";
private static string KeyForUidSet(string uid) => $"nd:uid:{uid}";
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null)
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true)
{
var entries = hashes.Distinct(StringComparer.Ordinal).ToArray();
if (entries.Length == 0) return;
@@ -33,30 +34,85 @@ public sealed class RedisPresenceStore : IDiscoveryPresenceStore
foreach (var h in entries)
{
var key = KeyForHash(h);
var payload = JsonSerializer.Serialize(new Presence(uid, displayName), _jsonOpts);
var payload = JsonSerializer.Serialize(new Presence(uid, displayName, allowRequests), _jsonOpts);
batch.StringSetAsync(key, payload, _presenceTtl);
// Index this hash under the publisher uid for fast unpublish
batch.SetAddAsync(KeyForUidSet(uid), h);
batch.KeyExpireAsync(KeyForUidSet(uid), _presenceTtl);
}
batch.Execute();
_logger.LogDebug("RedisPresenceStore: published {count} hashes", entries.Length);
}
public (bool Found, string Token, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
public void Unpublish(string uid)
{
try
{
var setKey = KeyForUidSet(uid);
var members = _db.SetMembers(setKey);
if (members is { Length: > 0 })
{
var batch = _db.CreateBatch();
foreach (var m in members)
{
var hash = (string)m;
var key = KeyForHash(hash);
// Defensive: only delete if the hash is still owned by this uid
var val = _db.StringGet(key);
if (val.HasValue)
{
try
{
var p = JsonSerializer.Deserialize<Presence>(val!);
if (p != null && string.Equals(p.Uid, uid, StringComparison.Ordinal))
{
batch.KeyDeleteAsync(key);
}
}
catch { /* ignore corrupted */ }
}
}
// Remove the uid index set itself
batch.KeyDeleteAsync(setKey);
batch.Execute();
}
else
{
// No index set: best-effort, just delete the set key in case it exists
_db.KeyDelete(setKey);
}
_logger.LogDebug("RedisPresenceStore: unpublished all hashes for uid {uid}", uid);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "RedisPresenceStore: Unpublish failed for uid {uid}", uid);
}
}
public (bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
{
var key = KeyForHash(hash);
var val = _db.StringGet(key);
if (!val.HasValue) return (false, string.Empty, null);
if (!val.HasValue) return (false, null, string.Empty, null);
try
{
var p = JsonSerializer.Deserialize<Presence>(val!);
if (p == null || string.IsNullOrEmpty(p.Uid)) return (false, string.Empty, null);
if (string.Equals(p.Uid, requesterUid, StringComparison.Ordinal)) return (false, string.Empty, null);
if (p == null || string.IsNullOrEmpty(p.Uid)) return (false, null, string.Empty, null);
if (string.Equals(p.Uid, requesterUid, StringComparison.Ordinal)) return (false, null, string.Empty, null);
// Visible but requests disabled → return without token
if (!p.AllowRequests)
{
return (true, null, p.Uid, p.DisplayName);
}
var token = Guid.NewGuid().ToString("N");
_db.StringSet(KeyForToken(token), p.Uid, _tokenTtl);
return (true, token, p.DisplayName);
return (true, token, p.Uid, p.DisplayName);
}
catch
{
return (false, string.Empty, null);
return (false, null, string.Empty, null);
}
}
@@ -70,6 +126,5 @@ public sealed class RedisPresenceStore : IDiscoveryPresenceStore
return true;
}
private sealed record Presence(string Uid, string? DisplayName);
private sealed record Presence(string Uid, string? DisplayName, bool AllowRequests);
}

View File

@@ -27,15 +27,22 @@ public class DiscoveryPresenceService : IHostedService, IDisposable
return Task.CompletedTask;
}
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null)
public void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true)
{
_store.Publish(uid, hashes, displayName);
_store.Publish(uid, hashes, displayName, allowRequests);
_logger.LogDebug("Discovery presence published for {uid} with {n} hashes", uid, hashes.Count());
}
public (bool Found, string Token, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
public void Unpublish(string uid)
{
return _store.TryMatchAndIssueToken(requesterUid, hash);
_store.Unpublish(uid);
_logger.LogDebug("Discovery presence unpublished for {uid}", uid);
}
public (bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash)
{
var res = _store.TryMatchAndIssueToken(requesterUid, hash);
return (res.Found, res.Token, res.TargetUid, res.DisplayName);
}
public bool ValidateToken(string token, out string targetUid)

View File

@@ -12,6 +12,9 @@ public class DiscoveryWellKnownProvider : IHostedService
private readonly object _lock = new();
private byte[] _currentSalt = Array.Empty<byte>();
private DateTimeOffset _currentSaltExpiresAt;
private byte[] _previousSalt = Array.Empty<byte>();
private DateTimeOffset _previousSaltExpiresAt;
private readonly TimeSpan _gracePeriod = TimeSpan.FromMinutes(5);
private Timer? _rotationTimer;
private readonly TimeSpan _saltTtl = TimeSpan.FromDays(30 * 6);
private readonly int _refreshSec = 86400; // 24h
@@ -48,16 +51,30 @@ public class DiscoveryWellKnownProvider : IHostedService
{
lock (_lock)
{
if (_currentSalt.Length > 0)
{
_previousSalt = _currentSalt;
_previousSaltExpiresAt = DateTimeOffset.UtcNow.Add(_gracePeriod);
}
_currentSalt = RandomNumberGenerator.GetBytes(32);
_currentSaltExpiresAt = DateTimeOffset.UtcNow.Add(_saltTtl);
}
}
public bool IsExpired()
public bool IsExpired(string providedSaltB64)
{
lock (_lock)
{
return DateTimeOffset.UtcNow > _currentSaltExpiresAt;
var now = DateTimeOffset.UtcNow;
var provided = Convert.FromBase64String(providedSaltB64);
if (_currentSalt.SequenceEqual(provided) && now <= _currentSaltExpiresAt)
return false;
if (_previousSalt.Length > 0 && _previousSalt.SequenceEqual(provided) && now <= _previousSaltExpiresAt)
return false;
return true;
}
}
@@ -87,11 +104,13 @@ public class DiscoveryWellKnownProvider : IHostedService
SaltB64 = Convert.ToBase64String(salt),
SaltExpiresAt = exp,
RefreshSec = _refreshSec,
GraceSec = (int)_gracePeriod.TotalSeconds,
Endpoints = new()
{
Publish = $"{httpScheme}://{host}/discovery/publish",
Query = $"{httpScheme}://{host}/discovery/query",
Request = $"{httpScheme}://{host}/discovery/request"
Request = $"{httpScheme}://{host}/discovery/request",
Accept = $"{httpScheme}://{host}/discovery/acceptNotify"
},
Policies = new()
{
@@ -128,6 +147,7 @@ public class DiscoveryWellKnownProvider : IHostedService
[JsonPropertyName("salt_b64")] public string SaltB64 { get; set; } = string.Empty;
[JsonPropertyName("salt_expires_at")] public DateTimeOffset SaltExpiresAt { get; set; }
[JsonPropertyName("refresh_sec")] public int RefreshSec { get; set; }
[JsonPropertyName("grace_sec")] public int GraceSec { get; set; }
[JsonPropertyName("endpoints")] public Endpoints Endpoints { get; set; } = new();
[JsonPropertyName("policies")] public Policies Policies { get; set; } = new();
}
@@ -137,6 +157,7 @@ public class DiscoveryWellKnownProvider : IHostedService
[JsonPropertyName("publish")] public string? Publish { get; set; }
[JsonPropertyName("query")] public string? Query { get; set; }
[JsonPropertyName("request")] public string? Request { get; set; }
[JsonPropertyName("accept")] public string? Accept { get; set; }
}
private sealed class Policies