Support AutoDetect
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using MareSynchronosAuthService.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace MareSynchronosAuthService.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[ApiController]
|
||||||
|
[Route("discovery")]
|
||||||
|
public class DiscoveryController : Controller
|
||||||
|
{
|
||||||
|
private readonly DiscoveryWellKnownProvider _provider;
|
||||||
|
private readonly DiscoveryPresenceService _presence;
|
||||||
|
|
||||||
|
public DiscoveryController(DiscoveryWellKnownProvider provider, DiscoveryPresenceService presence)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
_presence = presence;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class QueryRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("hashes")] public string[] Hashes { get; set; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class QueryResponseEntry
|
||||||
|
{
|
||||||
|
[JsonPropertyName("hash")] public string Hash { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("token")] public string Token { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("query")]
|
||||||
|
public IActionResult Query([FromBody] QueryRequest req)
|
||||||
|
{
|
||||||
|
if (_provider.IsExpired())
|
||||||
|
{
|
||||||
|
return BadRequest(new { code = "DISCOVERY_SALT_EXPIRED" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var uid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(uid) || req?.Hashes == null || req.Hashes.Length == 0)
|
||||||
|
return Json(Array.Empty<QueryResponseEntry>());
|
||||||
|
|
||||||
|
List<QueryResponseEntry> matches = new();
|
||||||
|
foreach (var h in req.Hashes.Distinct(StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
var (found, token, displayName) = _presence.TryMatchAndIssueToken(uid, h);
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
matches.Add(new QueryResponseEntry { Hash = h, Token = token, DisplayName = displayName });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class RequestDto
|
||||||
|
{
|
||||||
|
[JsonPropertyName("token")] public string Token { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("request")]
|
||||||
|
public async Task<IActionResult> RequestPair([FromBody] RequestDto req)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(req.Token)) return BadRequest();
|
||||||
|
if (_presence.ValidateToken(req.Token, out var targetUid))
|
||||||
|
{
|
||||||
|
// Phase 3 (minimal): notify target via mare-server internal 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;
|
||||||
|
|
||||||
|
using var http = new HttpClient();
|
||||||
|
// Use same host as public (goes through nginx)
|
||||||
|
var baseUrl = $"{Request.Scheme}://{Request.Host.Value}";
|
||||||
|
var url = new Uri(new Uri(baseUrl), "/main/discovery/notifyRequest");
|
||||||
|
|
||||||
|
// Generate internal JWT
|
||||||
|
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, 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
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
return BadRequest(new { code = "INVALID_TOKEN" });
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PublishRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("hashes")] public string[] Hashes { get; set; } = Array.Empty<string>();
|
||||||
|
[JsonPropertyName("displayName")] public string? DisplayName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("publish")]
|
||||||
|
public IActionResult Publish([FromBody] PublishRequest req)
|
||||||
|
{
|
||||||
|
if (_provider.IsExpired())
|
||||||
|
{
|
||||||
|
return BadRequest(new { code = "DISCOVERY_SALT_EXPIRED" });
|
||||||
|
}
|
||||||
|
var uid = User?.Claims?.FirstOrDefault(c => c.Type == MareSynchronosShared.Utils.MareClaimTypes.Uid)?.Value ?? string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(uid) || req?.Hashes == null || req.Hashes.Length == 0)
|
||||||
|
return Accepted();
|
||||||
|
|
||||||
|
_presence.Publish(uid, req.Hashes, req.DisplayName);
|
||||||
|
return Accepted();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -134,10 +134,13 @@ public class JwtController : Controller
|
|||||||
var tokenContent = tokenResponse as ContentResult;
|
var tokenContent = tokenResponse as ContentResult;
|
||||||
if (tokenContent == null)
|
if (tokenContent == null)
|
||||||
return tokenResponse;
|
return tokenResponse;
|
||||||
|
var provider = HttpContext.RequestServices.GetService<DiscoveryWellKnownProvider>();
|
||||||
|
var wk = provider?.GetWellKnownJson(Request.Scheme, Request.Host.Value)
|
||||||
|
?? _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty);
|
||||||
return Json(new AuthReplyDto
|
return Json(new AuthReplyDto
|
||||||
{
|
{
|
||||||
Token = tokenContent.Content,
|
Token = tokenContent.Content,
|
||||||
WellKnown = _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty),
|
WellKnown = wk,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,5 +192,4 @@ public class JwtController : Controller
|
|||||||
var handler = new JwtSecurityTokenHandler();
|
var handler = new JwtSecurityTokenHandler();
|
||||||
return handler.CreateJwtSecurityToken(token);
|
return handler.CreateJwtSecurityToken(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MareSynchronosAuthService.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace MareSynchronosAuthService.Controllers;
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ApiController]
|
||||||
|
public class WellKnownController : Controller
|
||||||
|
{
|
||||||
|
private readonly DiscoveryWellKnownProvider _provider;
|
||||||
|
|
||||||
|
public WellKnownController(DiscoveryWellKnownProvider provider)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("/.well-known/Umbra/client")]
|
||||||
|
public IActionResult Get()
|
||||||
|
{
|
||||||
|
var json = _provider.GetWellKnownJson(Request.Scheme, Request.Host.Value);
|
||||||
|
return Content(json, "application/json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -4,30 +4,16 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Remove="appsettings.Development.json" />
|
<Content Include="appsettings.Development.json">
|
||||||
<Content Remove="appsettings.json" />
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</ItemGroup>
|
</Content>
|
||||||
|
<Content Include="appsettings.json">
|
||||||
<ItemGroup>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
<None Include="appsettings.Development.json" />
|
</Content>
|
||||||
<None Include="appsettings.json">
|
|
||||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Remove="appsettings.Development.json" />
|
|
||||||
<Content Remove="appsettings.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="appsettings.Development.json" />
|
|
||||||
<None Include="appsettings.json">
|
|
||||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronosAuthService.Services;
|
||||||
|
|
||||||
|
public class DiscoveryPresenceService : IHostedService
|
||||||
|
{
|
||||||
|
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 TimeSpan _presenceTtl = TimeSpan.FromMinutes(5);
|
||||||
|
private readonly TimeSpan _tokenTtl = TimeSpan.FromMinutes(2);
|
||||||
|
|
||||||
|
public DiscoveryPresenceService(ILogger<DiscoveryPresenceService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace MareSynchronosAuthService.Services;
|
||||||
|
|
||||||
|
public class DiscoveryWellKnownProvider : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ILogger<DiscoveryWellKnownProvider> _logger;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private byte[] _currentSalt = Array.Empty<byte>();
|
||||||
|
private DateTimeOffset _currentSaltExpiresAt;
|
||||||
|
private Timer? _rotationTimer;
|
||||||
|
private readonly TimeSpan _saltTtl = TimeSpan.FromDays(30 * 6);
|
||||||
|
private readonly int _refreshSec = 86400; // 24h
|
||||||
|
|
||||||
|
public DiscoveryWellKnownProvider(ILogger<DiscoveryWellKnownProvider> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
RotateSalt();
|
||||||
|
var period = _saltTtl;
|
||||||
|
if (period.TotalMilliseconds > uint.MaxValue - 1)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("DiscoveryWellKnownProvider: salt TTL {ttl} exceeds timer limit, skipping rotation timer in beta", period);
|
||||||
|
_rotationTimer = new Timer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_rotationTimer = new Timer(_ => RotateSalt(), null, period, period);
|
||||||
|
}
|
||||||
|
_logger.LogInformation("DiscoveryWellKnownProvider started. Salt expires at {exp}", _currentSaltExpiresAt);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_rotationTimer?.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RotateSalt()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_currentSalt = RandomNumberGenerator.GetBytes(32);
|
||||||
|
_currentSaltExpiresAt = DateTimeOffset.UtcNow.Add(_saltTtl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsExpired()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return DateTimeOffset.UtcNow > _currentSaltExpiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetWellKnownJson(string scheme, string host)
|
||||||
|
{
|
||||||
|
var isHttps = string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var wsScheme = isHttps ? "wss" : "ws";
|
||||||
|
var httpScheme = isHttps ? "https" : "http";
|
||||||
|
|
||||||
|
byte[] salt;
|
||||||
|
DateTimeOffset exp;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
salt = _currentSalt.ToArray();
|
||||||
|
exp = _currentSaltExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = new WellKnownRoot
|
||||||
|
{
|
||||||
|
ApiUrl = $"{wsScheme}://{host}",
|
||||||
|
HubUrl = $"{wsScheme}://{host}/mare",
|
||||||
|
Features = new() { NearbyDiscovery = true },
|
||||||
|
NearbyDiscovery = new()
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
HashAlgo = "sha256",
|
||||||
|
SaltB64 = Convert.ToBase64String(salt),
|
||||||
|
SaltExpiresAt = exp,
|
||||||
|
RefreshSec = _refreshSec,
|
||||||
|
Endpoints = new()
|
||||||
|
{
|
||||||
|
Publish = $"{httpScheme}://{host}/discovery/publish",
|
||||||
|
Query = $"{httpScheme}://{host}/discovery/query",
|
||||||
|
Request = $"{httpScheme}://{host}/discovery/request"
|
||||||
|
},
|
||||||
|
Policies = new()
|
||||||
|
{
|
||||||
|
MaxQueryBatch = 100,
|
||||||
|
MinQueryIntervalMs = 2000,
|
||||||
|
RateLimitPerMin = 30,
|
||||||
|
TokenTtlSec = 120
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(root, new JsonSerializerOptions { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class WellKnownRoot
|
||||||
|
{
|
||||||
|
[JsonPropertyName("api_url")] public string ApiUrl { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("hub_url")] public string HubUrl { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("skip_negotiation")] public bool SkipNegotiation { get; set; } = true;
|
||||||
|
[JsonPropertyName("transports")] public string[] Transports { get; set; } = new[] { "websockets" };
|
||||||
|
[JsonPropertyName("features")] public Features Features { get; set; } = new();
|
||||||
|
[JsonPropertyName("nearby_discovery")] public Nearby NearbyDiscovery { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Features
|
||||||
|
{
|
||||||
|
[JsonPropertyName("nearby_discovery")] public bool NearbyDiscovery { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Nearby
|
||||||
|
{
|
||||||
|
[JsonPropertyName("enabled")] public bool Enabled { get; set; }
|
||||||
|
[JsonPropertyName("hash_algo")] public string HashAlgo { get; set; } = "sha256";
|
||||||
|
[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("endpoints")] public Endpoints Endpoints { get; set; } = new();
|
||||||
|
[JsonPropertyName("policies")] public Policies Policies { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Endpoints
|
||||||
|
{
|
||||||
|
[JsonPropertyName("publish")] public string? Publish { get; set; }
|
||||||
|
[JsonPropertyName("query")] public string? Query { get; set; }
|
||||||
|
[JsonPropertyName("request")] public string? Request { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Policies
|
||||||
|
{
|
||||||
|
[JsonPropertyName("max_query_batch")] public int MaxQueryBatch { get; set; }
|
||||||
|
[JsonPropertyName("min_query_interval_ms")] public int MinQueryIntervalMs { get; set; }
|
||||||
|
[JsonPropertyName("rate_limit_per_min")] public int RateLimitPerMin { get; set; }
|
||||||
|
[JsonPropertyName("token_ttl_sec")] public int TokenTtlSec { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ using System.Text;
|
|||||||
using MareSynchronosShared.Data;
|
using MareSynchronosShared.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using MareSynchronosShared.Utils.Configuration;
|
using MareSynchronosShared.Utils.Configuration;
|
||||||
|
|
||||||
namespace MareSynchronosAuthService;
|
namespace MareSynchronosAuthService;
|
||||||
@@ -35,6 +36,12 @@ public class Startup
|
|||||||
{
|
{
|
||||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
|
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
|
||||||
|
|
||||||
|
// Respect X-Forwarded-* headers from the reverse proxy so generated links use the public scheme/host
|
||||||
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor
|
||||||
|
});
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
app.UseHttpMetrics();
|
app.UseHttpMetrics();
|
||||||
@@ -65,8 +72,8 @@ public class Startup
|
|||||||
|
|
||||||
ConfigureRedis(services, mareConfig);
|
ConfigureRedis(services, mareConfig);
|
||||||
|
|
||||||
services.AddSingleton<SecretKeyAuthenticatorService>();
|
services.AddScoped<SecretKeyAuthenticatorService>();
|
||||||
services.AddSingleton<AccountRegistrationService>();
|
services.AddScoped<AccountRegistrationService>();
|
||||||
services.AddSingleton<GeoIPService>();
|
services.AddSingleton<GeoIPService>();
|
||||||
|
|
||||||
services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>());
|
services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>());
|
||||||
@@ -75,6 +82,11 @@ public class Startup
|
|||||||
services.Configure<MareConfigurationBase>(_configuration.GetRequiredSection("MareSynchronos"));
|
services.Configure<MareConfigurationBase>(_configuration.GetRequiredSection("MareSynchronos"));
|
||||||
|
|
||||||
services.AddSingleton<ServerTokenGenerator>();
|
services.AddSingleton<ServerTokenGenerator>();
|
||||||
|
// Nearby discovery services (well-known + presence)
|
||||||
|
services.AddSingleton<DiscoveryWellKnownProvider>();
|
||||||
|
services.AddHostedService(p => p.GetRequiredService<DiscoveryWellKnownProvider>());
|
||||||
|
services.AddSingleton<DiscoveryPresenceService>();
|
||||||
|
services.AddHostedService(p => p.GetRequiredService<DiscoveryPresenceService>());
|
||||||
|
|
||||||
ConfigureAuthorization(services);
|
ConfigureAuthorization(services);
|
||||||
|
|
||||||
@@ -88,8 +100,11 @@ public class Startup
|
|||||||
services.AddControllers().ConfigureApplicationPartManager(a =>
|
services.AddControllers().ConfigureApplicationPartManager(a =>
|
||||||
{
|
{
|
||||||
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
|
a.FeatureProviders.Remove(a.FeatureProviders.OfType<ControllerFeatureProvider>().First());
|
||||||
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController)));
|
a.FeatureProviders.Add(new AllowedControllersFeatureProvider(typeof(JwtController), typeof(WellKnownController), typeof(DiscoveryController)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddSingleton<DiscoveryWellKnownProvider>();
|
||||||
|
services.AddHostedService(p => p.GetRequiredService<DiscoveryWellKnownProvider>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureAuthorization(IServiceCollection services)
|
private static void ConfigureAuthorization(IServiceCollection services)
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using MareSynchronos.API.SignalR;
|
||||||
|
using MareSynchronosShared.Utils;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace MareSynchronosServer.Controllers;
|
||||||
|
|
||||||
|
[Route("/main/discovery")]
|
||||||
|
[Authorize(Policy = "Internal")]
|
||||||
|
public class DiscoveryNotifyController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<DiscoveryNotifyController> _logger;
|
||||||
|
private readonly IHubContext<Hubs.MareHub, IMareHub> _hub;
|
||||||
|
|
||||||
|
public DiscoveryNotifyController(ILogger<DiscoveryNotifyController> logger, IHubContext<Hubs.MareHub, IMareHub> hub)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_hub = hub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NotifyRequestDto
|
||||||
|
{
|
||||||
|
[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("notifyRequest")]
|
||||||
|
public async Task<IActionResult> NotifyRequest([FromBody] NotifyRequestDto dto)
|
||||||
|
{
|
||||||
|
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)";
|
||||||
|
_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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ using Prometheus;
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using StackExchange.Redis.Extensions.Core.Configuration;
|
using StackExchange.Redis.Extensions.Core.Configuration;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -198,6 +199,21 @@ public class Startup
|
|||||||
ValidateIssuerSigningKey = true,
|
ValidateIssuerSigningKey = true,
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(MareConfigurationBase.Jwt)))),
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.GetValue<string>(nameof(MareConfigurationBase.Jwt)))),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Allow SignalR WebSocket connections to authenticate via access_token query on the hub path
|
||||||
|
options.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var accessToken = context.Request.Query["access_token"].ToString();
|
||||||
|
var path = context.HttpContext.Request.Path;
|
||||||
|
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments(IMareHub.Path))
|
||||||
|
{
|
||||||
|
context.Token = accessToken;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddAuthentication(o =>
|
services.AddAuthentication(o =>
|
||||||
@@ -307,6 +323,12 @@ public class Startup
|
|||||||
|
|
||||||
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
|
var config = app.ApplicationServices.GetRequiredService<IConfigurationService<MareConfigurationBase>>();
|
||||||
|
|
||||||
|
// Respect X-Forwarded-* headers from reverse proxies (required for correct scheme/host)
|
||||||
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
|
{
|
||||||
|
ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedFor
|
||||||
|
});
|
||||||
|
|
||||||
app.UseIpRateLimiting();
|
app.UseIpRateLimiting();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|||||||
Reference in New Issue
Block a user