Fix AutoDetect

This commit is contained in:
2025-09-11 22:07:46 +02:00
parent 5366cfbc60
commit 8d0c3aafb3
10 changed files with 228 additions and 55 deletions

View File

@@ -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<ILogger<DiscoveryController>>()
.LogWarning("notifyRequest failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
}
}
catch { /* ignore */ }

View File

@@ -27,6 +27,9 @@
</PackageReference>
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="8.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.9.11" />
<PackageReference Include="StackExchange.Redis.Extensions.Core" Version="10.2.0" />
<PackageReference Include="StackExchange.Redis.Extensions.System.Text.Json" Version="10.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,11 @@
using System.Collections.Concurrent;
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);
bool ValidateToken(string token, out string targetUid);
}

View File

@@ -0,0 +1,74 @@
using System.Collections.Concurrent;
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 TargetUid, DateTimeOffset ExpiresAt)> _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<string> 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;
}
}

View File

@@ -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<RedisPresenceStore> _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<RedisPresenceStore> 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<string> 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<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);
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);
}

View File

@@ -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<DiscoveryPresenceService> _logger;
// hash -> (uid, expiresAt, displayName?)
private readonly ConcurrentDictionary<string, (string Uid, DateTimeOffset ExpiresAt, string? DisplayName)> _presence = new(StringComparer.Ordinal);
// token -> (targetUid, expiresAt)
private readonly ConcurrentDictionary<string, (string TargetUid, DateTimeOffset ExpiresAt)> _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<DiscoveryPresenceService> logger)
public DiscoveryPresenceService(ILogger<DiscoveryPresenceService> 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<string> 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;
return _store.ValidateToken(token, out targetUid);
}
_tokens.TryRemove(token, out _);
}
return false;
public void Dispose()
{
(_store as IDisposable)?.Dispose();
}
}

View File

@@ -85,6 +85,25 @@ public class Startup
// Nearby discovery services (well-known + presence)
services.AddSingleton<DiscoveryWellKnownProvider>();
services.AddHostedService(p => p.GetRequiredService<DiscoveryWellKnownProvider>());
// Presence store selection
var discoveryStore = _configuration.GetValue<string>("NearbyDiscovery:Store") ?? "memory";
TimeSpan presenceTtl = TimeSpan.FromMinutes(_configuration.GetValue<int>("NearbyDiscovery:PresenceTtlMinutes", 5));
TimeSpan tokenTtl = TimeSpan.FromSeconds(_configuration.GetValue<int>("NearbyDiscovery:TokenTtlSeconds", 120));
if (string.Equals(discoveryStore, "redis", StringComparison.OrdinalIgnoreCase))
{
services.AddSingleton<MareSynchronosAuthService.Services.Discovery.IDiscoveryPresenceStore>(sp =>
{
var logger = sp.GetRequiredService<ILogger<MareSynchronosAuthService.Services.Discovery.RedisPresenceStore>>();
var mux = sp.GetRequiredService<IConnectionMultiplexer>();
return new MareSynchronosAuthService.Services.Discovery.RedisPresenceStore(logger, mux, presenceTtl, tokenTtl);
});
}
else
{
services.AddSingleton<MareSynchronosAuthService.Services.Discovery.IDiscoveryPresenceStore>(sp => new MareSynchronosAuthService.Services.Discovery.InMemoryPresenceStore(presenceTtl, tokenTtl));
}
services.AddSingleton<DiscoveryPresenceService>();
services.AddHostedService(p => p.GetRequiredService<DiscoveryPresenceService>());
@@ -210,6 +229,8 @@ public class Startup
};
services.AddStackExchangeRedisExtensions<SystemTextJsonSerializer>(redisConfiguration);
// Also expose raw multiplexer for custom Redis usage (discovery presence)
services.AddSingleton<IConnectionMultiplexer>(_ => ConnectionMultiplexer.Connect(options));
}
private void ConfigureConfigServices(IServiceCollection services)
{

View File

@@ -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
}
}

View File

@@ -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();

View File

@@ -72,7 +72,7 @@ public class Startup
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
if (mareConfig.GetValue<Uri>(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
{