From 8d0c3aafb33571767688a444a495694da6f26420 Mon Sep 17 00:00:00 2001 From: SirConstance Date: Thu, 11 Sep 2025 22:07:46 +0200 Subject: [PATCH] Fix AutoDetect --- .../Controllers/DiscoveryController.cs | 13 +++- .../MareSynchronosAuthService.csproj | 3 + .../Discovery/IDiscoveryPresenceStore.cs | 11 +++ .../Discovery/InMemoryPresenceStore.cs | 74 ++++++++++++++++++ .../Services/Discovery/RedisPresenceStore.cs | 75 +++++++++++++++++++ .../Services/DiscoveryPresenceService.cs | 64 ++++------------ .../MareSynchronosAuthService/Startup.cs | 21 ++++++ .../appsettings.Development.json | 18 +++++ .../Controllers/DiscoveryNotifyController.cs | 2 +- .../MareSynchronosServer/Startup.cs | 2 +- 10 files changed, 228 insertions(+), 55 deletions(-) create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/IDiscoveryPresenceStore.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/InMemoryPresenceStore.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/RedisPresenceStore.cs create mode 100644 MareSynchronosServer/MareSynchronosAuthService/appsettings.Development.json diff --git a/MareSynchronosServer/MareSynchronosAuthService/Controllers/DiscoveryController.cs b/MareSynchronosServer/MareSynchronosAuthService/Controllers/DiscoveryController.cs index 8fb1fbf..17b95ae 100644 --- a/MareSynchronosServer/MareSynchronosAuthService/Controllers/DiscoveryController.cs +++ b/MareSynchronosServer/MareSynchronosAuthService/Controllers/DiscoveryController.cs @@ -2,6 +2,7 @@ using System.Text.Json.Serialization; using MareSynchronosAuthService.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; namespace MareSynchronosAuthService.Controllers; @@ -59,6 +60,7 @@ public class DiscoveryController : Controller public sealed class RequestDto { [JsonPropertyName("token")] public string Token { get; set; } = string.Empty; + [JsonPropertyName("displayName")] public string? DisplayName { get; set; } } [HttpPost("request")] @@ -71,7 +73,9 @@ public class DiscoveryController : Controller try { var fromUid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty; - var fromAlias = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Alias)?.Value ?? string.Empty; + var fromAlias = string.IsNullOrEmpty(req.DisplayName) + ? (User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Alias)?.Value ?? string.Empty) + : req.DisplayName; using var http = new HttpClient(); // Use same host as public (goes through nginx) @@ -83,7 +87,12 @@ public class DiscoveryController : Controller http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", serverToken); var payload = System.Text.Json.JsonSerializer.Serialize(new { targetUid, fromUid, fromAlias }); var resp = await http.PostAsync(url, new StringContent(payload, System.Text.Encoding.UTF8, "application/json")); - // ignore response content, just return Accepted to caller + if (!resp.IsSuccessStatusCode) + { + var txt = await resp.Content.ReadAsStringAsync(); + HttpContext.RequestServices.GetRequiredService>() + .LogWarning("notifyRequest failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt); + } } catch { /* ignore */ } diff --git a/MareSynchronosServer/MareSynchronosAuthService/MareSynchronosAuthService.csproj b/MareSynchronosServer/MareSynchronosAuthService/MareSynchronosAuthService.csproj index 15e6abd..0bea862 100644 --- a/MareSynchronosServer/MareSynchronosAuthService/MareSynchronosAuthService.csproj +++ b/MareSynchronosServer/MareSynchronosAuthService/MareSynchronosAuthService.csproj @@ -27,6 +27,9 @@ + + + diff --git a/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/IDiscoveryPresenceStore.cs b/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/IDiscoveryPresenceStore.cs new file mode 100644 index 0000000..710377b --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/IDiscoveryPresenceStore.cs @@ -0,0 +1,11 @@ +using System.Collections.Concurrent; + +namespace MareSynchronosAuthService.Services.Discovery; + +public interface IDiscoveryPresenceStore : IDisposable +{ + void Publish(string uid, IEnumerable hashes, string? displayName = null); + (bool Found, string Token, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash); + bool ValidateToken(string token, out string targetUid); +} + diff --git a/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/InMemoryPresenceStore.cs b/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/InMemoryPresenceStore.cs new file mode 100644 index 0000000..7ff0e43 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/InMemoryPresenceStore.cs @@ -0,0 +1,74 @@ +using System.Collections.Concurrent; + +namespace MareSynchronosAuthService.Services.Discovery; + +public sealed class InMemoryPresenceStore : IDiscoveryPresenceStore +{ + private readonly ConcurrentDictionary _presence = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary _tokens = new(StringComparer.Ordinal); + private readonly TimeSpan _presenceTtl; + private readonly TimeSpan _tokenTtl; + private readonly Timer _cleanupTimer; + + public InMemoryPresenceStore(TimeSpan presenceTtl, TimeSpan tokenTtl) + { + _presenceTtl = presenceTtl; + _tokenTtl = tokenTtl; + _cleanupTimer = new Timer(_ => Cleanup(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); + } + + public void Dispose() + { + _cleanupTimer.Dispose(); + } + + private void Cleanup() + { + var now = DateTimeOffset.UtcNow; + foreach (var kv in _presence.ToArray()) + { + if (kv.Value.ExpiresAt <= now) _presence.TryRemove(kv.Key, out _); + } + foreach (var kv in _tokens.ToArray()) + { + if (kv.Value.ExpiresAt <= now) _tokens.TryRemove(kv.Key, out _); + } + } + + public void Publish(string uid, IEnumerable hashes, string? displayName = null) + { + var exp = DateTimeOffset.UtcNow.Add(_presenceTtl); + foreach (var h in hashes.Distinct(StringComparer.Ordinal)) + { + _presence[h] = (uid, exp, displayName); + } + } + + public (bool Found, string Token, 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); + var token = Guid.NewGuid().ToString("N"); + _tokens[token] = (entry.Uid, DateTimeOffset.UtcNow.Add(_tokenTtl)); + return (true, token, entry.DisplayName); + } + return (false, string.Empty, null); + } + + public bool ValidateToken(string token, out string targetUid) + { + targetUid = string.Empty; + if (_tokens.TryGetValue(token, out var info)) + { + if (info.ExpiresAt > DateTimeOffset.UtcNow) + { + targetUid = info.TargetUid; + return true; + } + _tokens.TryRemove(token, out _); + } + return false; + } +} + diff --git a/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/RedisPresenceStore.cs b/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/RedisPresenceStore.cs new file mode 100644 index 0000000..51fe70e --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/Services/Discovery/RedisPresenceStore.cs @@ -0,0 +1,75 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; + +namespace MareSynchronosAuthService.Services.Discovery; + +public sealed class RedisPresenceStore : IDiscoveryPresenceStore +{ + private readonly ILogger _logger; + private readonly IDatabase _db; + private readonly TimeSpan _presenceTtl; + private readonly TimeSpan _tokenTtl; + private readonly JsonSerializerOptions _jsonOpts = new() { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }; + + public RedisPresenceStore(ILogger logger, IConnectionMultiplexer mux, TimeSpan presenceTtl, TimeSpan tokenTtl) + { + _logger = logger; + _db = mux.GetDatabase(); + _presenceTtl = presenceTtl; + _tokenTtl = tokenTtl; + } + + public void Dispose() { } + + private static string KeyForHash(string hash) => $"nd:hash:{hash}"; + private static string KeyForToken(string token) => $"nd:token:{token}"; + + public void Publish(string uid, IEnumerable hashes, string? displayName = null) + { + var entries = hashes.Distinct(StringComparer.Ordinal).ToArray(); + if (entries.Length == 0) return; + var batch = _db.CreateBatch(); + foreach (var h in entries) + { + var key = KeyForHash(h); + var payload = JsonSerializer.Serialize(new Presence(uid, displayName), _jsonOpts); + batch.StringSetAsync(key, payload, _presenceTtl); + } + batch.Execute(); + _logger.LogDebug("RedisPresenceStore: published {count} hashes", entries.Length); + } + + public (bool Found, string Token, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash) + { + var key = KeyForHash(hash); + var val = _db.StringGet(key); + if (!val.HasValue) return (false, string.Empty, null); + try + { + var p = JsonSerializer.Deserialize(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); + var token = Guid.NewGuid().ToString("N"); + _db.StringSet(KeyForToken(token), p.Uid, _tokenTtl); + return (true, token, p.DisplayName); + } + catch + { + return (false, string.Empty, null); + } + } + + public bool ValidateToken(string token, out string targetUid) + { + targetUid = string.Empty; + var key = KeyForToken(token); + var val = _db.StringGet(key); + if (!val.HasValue) return false; + targetUid = val!; + return true; + } + + private sealed record Presence(string Uid, string? DisplayName); +} + diff --git a/MareSynchronosServer/MareSynchronosAuthService/Services/DiscoveryPresenceService.cs b/MareSynchronosServer/MareSynchronosAuthService/Services/DiscoveryPresenceService.cs index 116a44e..776725b 100644 --- a/MareSynchronosServer/MareSynchronosAuthService/Services/DiscoveryPresenceService.cs +++ b/MareSynchronosServer/MareSynchronosAuthService/Services/DiscoveryPresenceService.cs @@ -1,88 +1,50 @@ -using System.Collections.Concurrent; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using MareSynchronosAuthService.Services.Discovery; namespace MareSynchronosAuthService.Services; -public class DiscoveryPresenceService : IHostedService +public class DiscoveryPresenceService : IHostedService, IDisposable { private readonly ILogger _logger; - - // hash -> (uid, expiresAt, displayName?) - private readonly ConcurrentDictionary _presence = new(StringComparer.Ordinal); - - // token -> (targetUid, expiresAt) - private readonly ConcurrentDictionary _tokens = new(StringComparer.Ordinal); - - private Timer? _cleanupTimer; + private readonly IDiscoveryPresenceStore _store; private readonly TimeSpan _presenceTtl = TimeSpan.FromMinutes(5); private readonly TimeSpan _tokenTtl = TimeSpan.FromMinutes(2); - public DiscoveryPresenceService(ILogger logger) + public DiscoveryPresenceService(ILogger logger, IDiscoveryPresenceStore store) { _logger = logger; + _store = store; } public Task StartAsync(CancellationToken cancellationToken) { - _cleanupTimer = new Timer(_ => Cleanup(), null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { - _cleanupTimer?.Dispose(); return Task.CompletedTask; } - private void Cleanup() - { - var now = DateTimeOffset.UtcNow; - foreach (var kv in _presence.ToArray()) - { - if (kv.Value.ExpiresAt <= now) _presence.TryRemove(kv.Key, out _); - } - foreach (var kv in _tokens.ToArray()) - { - if (kv.Value.ExpiresAt <= now) _tokens.TryRemove(kv.Key, out _); - } - } - public void Publish(string uid, IEnumerable hashes, string? displayName = null) { - var exp = DateTimeOffset.UtcNow.Add(_presenceTtl); - foreach (var h in hashes.Distinct(StringComparer.Ordinal)) - { - _presence[h] = (uid, exp, displayName); - } + _store.Publish(uid, hashes, displayName); _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) { - if (_presence.TryGetValue(hash, out var entry)) - { - if (string.Equals(entry.Uid, requesterUid, StringComparison.Ordinal)) return (false, string.Empty, null); - var token = Guid.NewGuid().ToString("N"); - _tokens[token] = (entry.Uid, DateTimeOffset.UtcNow.Add(_tokenTtl)); - return (true, token, entry.DisplayName); - } - return (false, string.Empty, null); + return _store.TryMatchAndIssueToken(requesterUid, hash); } public bool ValidateToken(string token, out string targetUid) { - targetUid = string.Empty; - if (_tokens.TryGetValue(token, out var info)) - { - if (info.ExpiresAt > DateTimeOffset.UtcNow) - { - targetUid = info.TargetUid; - return true; - } - _tokens.TryRemove(token, out _); - } - return false; + return _store.ValidateToken(token, out targetUid); + } + + public void Dispose() + { + (_store as IDisposable)?.Dispose(); } } - diff --git a/MareSynchronosServer/MareSynchronosAuthService/Startup.cs b/MareSynchronosServer/MareSynchronosAuthService/Startup.cs index 6d1c8c2..4d82c08 100644 --- a/MareSynchronosServer/MareSynchronosAuthService/Startup.cs +++ b/MareSynchronosServer/MareSynchronosAuthService/Startup.cs @@ -85,6 +85,25 @@ public class Startup // Nearby discovery services (well-known + presence) services.AddSingleton(); services.AddHostedService(p => p.GetRequiredService()); + + // Presence store selection + var discoveryStore = _configuration.GetValue("NearbyDiscovery:Store") ?? "memory"; + TimeSpan presenceTtl = TimeSpan.FromMinutes(_configuration.GetValue("NearbyDiscovery:PresenceTtlMinutes", 5)); + TimeSpan tokenTtl = TimeSpan.FromSeconds(_configuration.GetValue("NearbyDiscovery:TokenTtlSeconds", 120)); + if (string.Equals(discoveryStore, "redis", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + var mux = sp.GetRequiredService(); + return new MareSynchronosAuthService.Services.Discovery.RedisPresenceStore(logger, mux, presenceTtl, tokenTtl); + }); + } + else + { + services.AddSingleton(sp => new MareSynchronosAuthService.Services.Discovery.InMemoryPresenceStore(presenceTtl, tokenTtl)); + } + services.AddSingleton(); services.AddHostedService(p => p.GetRequiredService()); @@ -210,6 +229,8 @@ public class Startup }; services.AddStackExchangeRedisExtensions(redisConfiguration); + // Also expose raw multiplexer for custom Redis usage (discovery presence) + services.AddSingleton(_ => ConnectionMultiplexer.Connect(options)); } private void ConfigureConfigServices(IServiceCollection services) { diff --git a/MareSynchronosServer/MareSynchronosAuthService/appsettings.Development.json b/MareSynchronosServer/MareSynchronosAuthService/appsettings.Development.json new file mode 100644 index 0000000..226edb3 --- /dev/null +++ b/MareSynchronosServer/MareSynchronosAuthService/appsettings.Development.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=umbra_dev" + }, + "MareSynchronos": { + "Jwt": "dev-secret-umbra-abcdefghijklmnopqrstuvwxyz123456", + "RedisConnectionString": "localhost:6379,connectTimeout=5000,syncTimeout=5000", + "MetricsPort": 4985 + } +} + diff --git a/MareSynchronosServer/MareSynchronosServer/Controllers/DiscoveryNotifyController.cs b/MareSynchronosServer/MareSynchronosServer/Controllers/DiscoveryNotifyController.cs index 863ec16..3786b30 100644 --- a/MareSynchronosServer/MareSynchronosServer/Controllers/DiscoveryNotifyController.cs +++ b/MareSynchronosServer/MareSynchronosServer/Controllers/DiscoveryNotifyController.cs @@ -32,7 +32,7 @@ public class DiscoveryNotifyController : Controller { if (string.IsNullOrEmpty(dto.TargetUid)) return BadRequest(); var name = string.IsNullOrEmpty(dto.FromAlias) ? dto.FromUid : dto.FromAlias; - var msg = $"{name} wants to pair with you (Nearby)"; + var msg = $"Nearby Request: {name} [{dto.FromUid}]"; _logger.LogInformation("Discovery notify request to {target} from {from}", dto.TargetUid, name); await _hub.Clients.User(dto.TargetUid).Client_ReceiveServerMessage(MareSynchronos.API.Data.Enum.MessageSeverity.Information, msg); return Accepted(); diff --git a/MareSynchronosServer/MareSynchronosServer/Startup.cs b/MareSynchronosServer/MareSynchronosServer/Startup.cs index 20c837f..63cd403 100644 --- a/MareSynchronosServer/MareSynchronosServer/Startup.cs +++ b/MareSynchronosServer/MareSynchronosServer/Startup.cs @@ -72,7 +72,7 @@ public class Startup a.FeatureProviders.Remove(a.FeatureProviders.OfType().First()); if (mareConfig.GetValue(nameof(ServerConfiguration.MainServerAddress), defaultValue: null) == null) { - a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(MareServerConfigurationController), typeof(MareBaseConfigurationController), typeof(ClientMessageController), typeof(MainController))); + a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(MareServerConfigurationController), typeof(MareBaseConfigurationController), typeof(ClientMessageController), typeof(MainController), typeof(DiscoveryNotifyController))); } else {