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;
|
||||
if (tokenContent == null)
|
||||
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
|
||||
{
|
||||
Token = tokenContent.Content,
|
||||
WellKnown = _configuration.GetValueOrDefault(nameof(AuthServiceConfiguration.WellKnown), string.Empty),
|
||||
WellKnown = wk,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,4 +193,3 @@ public class JwtController : Controller
|
||||
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>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<EnableDefaultContentItems>false</EnableDefaultContentItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
<Content Remove="appsettings.Development.json" />
|
||||
<Content Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.Development.json" />
|
||||
<None Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Include="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</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 Microsoft.EntityFrameworkCore;
|
||||
using Prometheus;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using MareSynchronosShared.Utils.Configuration;
|
||||
|
||||
namespace MareSynchronosAuthService;
|
||||
@@ -35,6 +36,12 @@ public class Startup
|
||||
{
|
||||
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.UseHttpMetrics();
|
||||
@@ -65,8 +72,8 @@ public class Startup
|
||||
|
||||
ConfigureRedis(services, mareConfig);
|
||||
|
||||
services.AddSingleton<SecretKeyAuthenticatorService>();
|
||||
services.AddSingleton<AccountRegistrationService>();
|
||||
services.AddScoped<SecretKeyAuthenticatorService>();
|
||||
services.AddScoped<AccountRegistrationService>();
|
||||
services.AddSingleton<GeoIPService>();
|
||||
|
||||
services.AddHostedService(provider => provider.GetRequiredService<GeoIPService>());
|
||||
@@ -75,6 +82,11 @@ public class Startup
|
||||
services.Configure<MareConfigurationBase>(_configuration.GetRequiredSection("MareSynchronos"));
|
||||
|
||||
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);
|
||||
|
||||
@@ -88,8 +100,11 @@ public class Startup
|
||||
services.AddControllers().ConfigureApplicationPartManager(a =>
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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.IdentityModel.Tokens;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using StackExchange.Redis;
|
||||
using StackExchange.Redis.Extensions.Core.Configuration;
|
||||
using System.Net;
|
||||
@@ -198,6 +199,21 @@ public class Startup
|
||||
ValidateIssuerSigningKey = true,
|
||||
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 =>
|
||||
@@ -307,6 +323,12 @@ public class Startup
|
||||
|
||||
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.UseRouting();
|
||||
|
||||
Reference in New Issue
Block a user