Deploy AutoDetect
This commit is contained in:
@@ -23,19 +23,21 @@ public class DiscoveryController : Controller
|
|||||||
public sealed class QueryRequest
|
public sealed class QueryRequest
|
||||||
{
|
{
|
||||||
[JsonPropertyName("hashes")] public string[] Hashes { get; set; } = Array.Empty<string>();
|
[JsonPropertyName("hashes")] public string[] Hashes { get; set; } = Array.Empty<string>();
|
||||||
|
[JsonPropertyName("salt")] public string SaltB64 { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class QueryResponseEntry
|
public sealed class QueryResponseEntry
|
||||||
{
|
{
|
||||||
[JsonPropertyName("hash")] public string Hash { get; set; } = string.Empty;
|
[JsonPropertyName("hash")] public string Hash { get; set; } = string.Empty;
|
||||||
[JsonPropertyName("token")] public string Token { get; set; } = string.Empty;
|
[JsonPropertyName("token")] public string? Token { get; set; }
|
||||||
|
[JsonPropertyName("uid")] public string Uid { get; set; } = string.Empty;
|
||||||
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("query")]
|
[HttpPost("query")]
|
||||||
public IActionResult Query([FromBody] QueryRequest req)
|
public IActionResult Query([FromBody] QueryRequest req)
|
||||||
{
|
{
|
||||||
if (_provider.IsExpired())
|
if (_provider.IsExpired(req.SaltB64))
|
||||||
{
|
{
|
||||||
return BadRequest(new { code = "DISCOVERY_SALT_EXPIRED" });
|
return BadRequest(new { code = "DISCOVERY_SALT_EXPIRED" });
|
||||||
}
|
}
|
||||||
@@ -47,10 +49,10 @@ public class DiscoveryController : Controller
|
|||||||
List<QueryResponseEntry> matches = new();
|
List<QueryResponseEntry> matches = new();
|
||||||
foreach (var h in req.Hashes.Distinct(StringComparer.Ordinal))
|
foreach (var h in req.Hashes.Distinct(StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
var (found, token, displayName) = _presence.TryMatchAndIssueToken(uid, h);
|
var (found, token, targetUid, displayName) = _presence.TryMatchAndIssueToken(uid, h);
|
||||||
if (found)
|
if (found)
|
||||||
{
|
{
|
||||||
matches.Add(new QueryResponseEntry { Hash = h, Token = token, DisplayName = displayName });
|
matches.Add(new QueryResponseEntry { Hash = h, Token = token, Uid = targetUid, DisplayName = displayName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,16 +103,64 @@ public class DiscoveryController : Controller
|
|||||||
return BadRequest(new { code = "INVALID_TOKEN" });
|
return BadRequest(new { code = "INVALID_TOKEN" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class AcceptNotifyDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("targetUid")] public string TargetUid { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept notification relay (sender -> auth -> main)
|
||||||
|
[HttpPost("acceptNotify")]
|
||||||
|
public async Task<IActionResult> AcceptNotify([FromBody] AcceptNotifyDto req)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(req.TargetUid)) return BadRequest();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fromUid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.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();
|
||||||
|
var baseUrl = $"{Request.Scheme}://{Request.Host.Value}";
|
||||||
|
var url = new Uri(new Uri(baseUrl), "/main/discovery/notifyAccept");
|
||||||
|
var serverToken = HttpContext.RequestServices.GetRequiredService<MareSynchronosShared.Utils.ServerTokenGenerator>().Token;
|
||||||
|
http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", serverToken);
|
||||||
|
var payload = System.Text.Json.JsonSerializer.Serialize(new { targetUid = req.TargetUid, fromUid, fromAlias });
|
||||||
|
var resp = await http.PostAsync(url, new StringContent(payload, System.Text.Encoding.UTF8, "application/json"));
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var txt = await resp.Content.ReadAsStringAsync();
|
||||||
|
HttpContext.RequestServices.GetRequiredService<ILogger<DiscoveryController>>()
|
||||||
|
.LogWarning("notifyAccept failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class PublishRequest
|
public sealed class PublishRequest
|
||||||
{
|
{
|
||||||
[JsonPropertyName("hashes")] public string[] Hashes { get; set; } = Array.Empty<string>();
|
[JsonPropertyName("hashes")] public string[] Hashes { get; set; } = Array.Empty<string>();
|
||||||
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
||||||
|
[JsonPropertyName("salt")] public string SaltB64 { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("allowRequests")] public bool AllowRequests { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("disable")]
|
||||||
|
public IActionResult Disable()
|
||||||
|
{
|
||||||
|
var uid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(uid)) return Accepted();
|
||||||
|
_presence.Unpublish(uid);
|
||||||
|
return Accepted();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("publish")]
|
[HttpPost("publish")]
|
||||||
public IActionResult Publish([FromBody] PublishRequest req)
|
public IActionResult Publish([FromBody] PublishRequest req)
|
||||||
{
|
{
|
||||||
if (_provider.IsExpired())
|
if (_provider.IsExpired(req.SaltB64))
|
||||||
{
|
{
|
||||||
return BadRequest(new { code = "DISCOVERY_SALT_EXPIRED" });
|
return BadRequest(new { code = "DISCOVERY_SALT_EXPIRED" });
|
||||||
}
|
}
|
||||||
@@ -118,7 +168,7 @@ public class DiscoveryController : Controller
|
|||||||
if (string.IsNullOrEmpty(uid) || req?.Hashes == null || req.Hashes.Length == 0)
|
if (string.IsNullOrEmpty(uid) || req?.Hashes == null || req.Hashes.Length == 0)
|
||||||
return Accepted();
|
return Accepted();
|
||||||
|
|
||||||
_presence.Publish(uid, req.Hashes, req.DisplayName);
|
_presence.Publish(uid, req.Hashes, req.DisplayName, req.AllowRequests);
|
||||||
return Accepted();
|
return Accepted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ namespace MareSynchronosAuthService.Services.Discovery;
|
|||||||
|
|
||||||
public interface IDiscoveryPresenceStore : IDisposable
|
public interface IDiscoveryPresenceStore : IDisposable
|
||||||
{
|
{
|
||||||
void Publish(string uid, IEnumerable<string> hashes, string? displayName = null);
|
void Publish(string uid, IEnumerable<string> hashes, string? displayName = null, bool allowRequests = true);
|
||||||
(bool Found, string Token, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash);
|
void Unpublish(string uid);
|
||||||
|
(bool Found, string? Token, string TargetUid, string? DisplayName) TryMatchAndIssueToken(string requesterUid, string hash);
|
||||||
bool ValidateToken(string token, out string targetUid);
|
bool ValidateToken(string token, out string targetUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace MareSynchronosAuthService.Services.Discovery;
|
|||||||
|
|
||||||
public sealed class InMemoryPresenceStore : IDiscoveryPresenceStore
|
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 ConcurrentDictionary<string, (string TargetUid, DateTimeOffset ExpiresAt)> _tokens = new(StringComparer.Ordinal);
|
||||||
private readonly TimeSpan _presenceTtl;
|
private readonly TimeSpan _presenceTtl;
|
||||||
private readonly TimeSpan _tokenTtl;
|
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);
|
var exp = DateTimeOffset.UtcNow.Add(_presenceTtl);
|
||||||
foreach (var h in hashes.Distinct(StringComparer.Ordinal))
|
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 (_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");
|
var token = Guid.NewGuid().ToString("N");
|
||||||
_tokens[token] = (entry.Uid, DateTimeOffset.UtcNow.Add(_tokenTtl));
|
_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)
|
public bool ValidateToken(string token, out string targetUid)
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ public sealed class RedisPresenceStore : IDiscoveryPresenceStore
|
|||||||
|
|
||||||
private static string KeyForHash(string hash) => $"nd:hash:{hash}";
|
private static string KeyForHash(string hash) => $"nd:hash:{hash}";
|
||||||
private static string KeyForToken(string token) => $"nd:token:{token}";
|
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();
|
var entries = hashes.Distinct(StringComparer.Ordinal).ToArray();
|
||||||
if (entries.Length == 0) return;
|
if (entries.Length == 0) return;
|
||||||
@@ -33,30 +34,85 @@ public sealed class RedisPresenceStore : IDiscoveryPresenceStore
|
|||||||
foreach (var h in entries)
|
foreach (var h in entries)
|
||||||
{
|
{
|
||||||
var key = KeyForHash(h);
|
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);
|
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();
|
batch.Execute();
|
||||||
_logger.LogDebug("RedisPresenceStore: published {count} hashes", entries.Length);
|
_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);
|
var key = KeyForHash(hash);
|
||||||
|
// Defensive: only delete if the hash is still owned by this uid
|
||||||
var val = _db.StringGet(key);
|
var val = _db.StringGet(key);
|
||||||
if (!val.HasValue) return (false, string.Empty, null);
|
if (val.HasValue)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var p = JsonSerializer.Deserialize<Presence>(val!);
|
var p = JsonSerializer.Deserialize<Presence>(val!);
|
||||||
if (p == null || string.IsNullOrEmpty(p.Uid)) return (false, string.Empty, null);
|
if (p != null && string.Equals(p.Uid, uid, StringComparison.Ordinal))
|
||||||
if (string.Equals(p.Uid, requesterUid, StringComparison.Ordinal)) return (false, string.Empty, null);
|
{
|
||||||
|
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, null, string.Empty, null);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var p = JsonSerializer.Deserialize<Presence>(val!);
|
||||||
|
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");
|
var token = Guid.NewGuid().ToString("N");
|
||||||
_db.StringSet(KeyForToken(token), p.Uid, _tokenTtl);
|
_db.StringSet(KeyForToken(token), p.Uid, _tokenTtl);
|
||||||
return (true, token, p.DisplayName);
|
return (true, token, p.Uid, p.DisplayName);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return (false, string.Empty, null);
|
return (false, null, string.Empty, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +126,5 @@ public sealed class RedisPresenceStore : IDiscoveryPresenceStore
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record Presence(string Uid, string? DisplayName);
|
private sealed record Presence(string Uid, string? DisplayName, bool AllowRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,15 +27,22 @@ public class DiscoveryPresenceService : IHostedService, IDisposable
|
|||||||
return Task.CompletedTask;
|
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());
|
_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)
|
public bool ValidateToken(string token, out string targetUid)
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ public class DiscoveryWellKnownProvider : IHostedService
|
|||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
private byte[] _currentSalt = Array.Empty<byte>();
|
private byte[] _currentSalt = Array.Empty<byte>();
|
||||||
private DateTimeOffset _currentSaltExpiresAt;
|
private DateTimeOffset _currentSaltExpiresAt;
|
||||||
|
private byte[] _previousSalt = Array.Empty<byte>();
|
||||||
|
private DateTimeOffset _previousSaltExpiresAt;
|
||||||
|
private readonly TimeSpan _gracePeriod = TimeSpan.FromMinutes(5);
|
||||||
private Timer? _rotationTimer;
|
private Timer? _rotationTimer;
|
||||||
private readonly TimeSpan _saltTtl = TimeSpan.FromDays(30 * 6);
|
private readonly TimeSpan _saltTtl = TimeSpan.FromDays(30 * 6);
|
||||||
private readonly int _refreshSec = 86400; // 24h
|
private readonly int _refreshSec = 86400; // 24h
|
||||||
@@ -48,16 +51,30 @@ public class DiscoveryWellKnownProvider : IHostedService
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
if (_currentSalt.Length > 0)
|
||||||
|
{
|
||||||
|
_previousSalt = _currentSalt;
|
||||||
|
_previousSaltExpiresAt = DateTimeOffset.UtcNow.Add(_gracePeriod);
|
||||||
|
}
|
||||||
_currentSalt = RandomNumberGenerator.GetBytes(32);
|
_currentSalt = RandomNumberGenerator.GetBytes(32);
|
||||||
_currentSaltExpiresAt = DateTimeOffset.UtcNow.Add(_saltTtl);
|
_currentSaltExpiresAt = DateTimeOffset.UtcNow.Add(_saltTtl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsExpired()
|
public bool IsExpired(string providedSaltB64)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
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),
|
SaltB64 = Convert.ToBase64String(salt),
|
||||||
SaltExpiresAt = exp,
|
SaltExpiresAt = exp,
|
||||||
RefreshSec = _refreshSec,
|
RefreshSec = _refreshSec,
|
||||||
|
GraceSec = (int)_gracePeriod.TotalSeconds,
|
||||||
Endpoints = new()
|
Endpoints = new()
|
||||||
{
|
{
|
||||||
Publish = $"{httpScheme}://{host}/discovery/publish",
|
Publish = $"{httpScheme}://{host}/discovery/publish",
|
||||||
Query = $"{httpScheme}://{host}/discovery/query",
|
Query = $"{httpScheme}://{host}/discovery/query",
|
||||||
Request = $"{httpScheme}://{host}/discovery/request"
|
Request = $"{httpScheme}://{host}/discovery/request",
|
||||||
|
Accept = $"{httpScheme}://{host}/discovery/acceptNotify"
|
||||||
},
|
},
|
||||||
Policies = new()
|
Policies = new()
|
||||||
{
|
{
|
||||||
@@ -128,6 +147,7 @@ public class DiscoveryWellKnownProvider : IHostedService
|
|||||||
[JsonPropertyName("salt_b64")] public string SaltB64 { get; set; } = string.Empty;
|
[JsonPropertyName("salt_b64")] public string SaltB64 { get; set; } = string.Empty;
|
||||||
[JsonPropertyName("salt_expires_at")] public DateTimeOffset SaltExpiresAt { get; set; }
|
[JsonPropertyName("salt_expires_at")] public DateTimeOffset SaltExpiresAt { get; set; }
|
||||||
[JsonPropertyName("refresh_sec")] public int RefreshSec { 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("endpoints")] public Endpoints Endpoints { get; set; } = new();
|
||||||
[JsonPropertyName("policies")] public Policies Policies { 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("publish")] public string? Publish { get; set; }
|
||||||
[JsonPropertyName("query")] public string? Query { get; set; }
|
[JsonPropertyName("query")] public string? Query { get; set; }
|
||||||
[JsonPropertyName("request")] public string? Request { get; set; }
|
[JsonPropertyName("request")] public string? Request { get; set; }
|
||||||
|
[JsonPropertyName("accept")] public string? Accept { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class Policies
|
private sealed class Policies
|
||||||
|
|||||||
@@ -37,4 +37,22 @@ public class DiscoveryNotifyController : Controller
|
|||||||
await _hub.Clients.User(dto.TargetUid).Client_ReceiveServerMessage(MareSynchronos.API.Data.Enum.MessageSeverity.Information, msg);
|
await _hub.Clients.User(dto.TargetUid).Client_ReceiveServerMessage(MareSynchronos.API.Data.Enum.MessageSeverity.Information, msg);
|
||||||
return Accepted();
|
return Accepted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class NotifyAcceptDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("targetUid")] public string TargetUid { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("fromUid")] public string FromUid { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("fromAlias")] public string? FromAlias { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("notifyAccept")]
|
||||||
|
public async Task<IActionResult> NotifyAccept([FromBody] NotifyAcceptDto dto)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(dto.TargetUid)) return BadRequest();
|
||||||
|
var name = string.IsNullOrEmpty(dto.FromAlias) ? dto.FromUid : dto.FromAlias;
|
||||||
|
var msg = $"Nearby Accept: {name} [{dto.FromUid}]";
|
||||||
|
_logger.LogInformation("Discovery notify accept to {target} from {from}", dto.TargetUid, name);
|
||||||
|
await _hub.Clients.User(dto.TargetUid).Client_ReceiveServerMessage(MareSynchronos.API.Data.Enum.MessageSeverity.Information, msg);
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user