From 3f45f1865b3a90b32af99e2ab2931bc1c76525ce Mon Sep 17 00:00:00 2001 From: Loporrit <141286461+loporrit@users.noreply.github.com> Date: Sat, 28 Jun 2025 14:28:38 +0000 Subject: [PATCH] .well-known stapling via createWithIdentV2 --- MareAPI | 2 +- .../WebAPI/SignalR/ApiController.cs | 1 + MareSynchronos/WebAPI/SignalR/HubFactory.cs | 109 ++++++++++-------- .../WebAPI/SignalR/TokenProvider.cs | 27 +++-- 4 files changed, 82 insertions(+), 57 deletions(-) diff --git a/MareAPI b/MareAPI index fef2365..4e4b2da 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit fef23652805ec4cd5a62e5a5597bc3b61002b0ae +Subproject commit 4e4b2dab1774cb5fb7d7e05435eab3dd83112620 diff --git a/MareSynchronos/WebAPI/SignalR/ApiController.cs b/MareSynchronos/WebAPI/SignalR/ApiController.cs index d851bc8..366ad37 100644 --- a/MareSynchronos/WebAPI/SignalR/ApiController.cs +++ b/MareSynchronos/WebAPI/SignalR/ApiController.cs @@ -2,6 +2,7 @@ using MareSynchronos.API.Data; using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Dto; +using MareSynchronos.API.Dto.CharaData; using MareSynchronos.API.Dto.User; using MareSynchronos.API.SignalR; using MareSynchronos.MareConfiguration; diff --git a/MareSynchronos/WebAPI/SignalR/HubFactory.cs b/MareSynchronos/WebAPI/SignalR/HubFactory.cs index 733578a..37463af 100644 --- a/MareSynchronos/WebAPI/SignalR/HubFactory.cs +++ b/MareSynchronos/WebAPI/SignalR/HubFactory.cs @@ -63,29 +63,11 @@ public class HubFactory : MediatorSubscriberBase return BuildHubConnection(_cachedConfig, ct); } - public async Task ResolveHubConfig() + private async Task ResolveHubConfig() { - var uri = new Uri(_serverConfigurationManager.CurrentApiUrl); + var stapledWellKnown = _tokenProvider.GetStapledWellKnown(_serverConfigurationManager.CurrentApiUrl); - var httpScheme = uri.Scheme.ToLowerInvariant() switch - { - "ws" => "http", - "wss" => "https", - _ => uri.Scheme - }; - - var wellKnownUrl = $"{httpScheme}://{uri.Host}/.well-known/loporrit/client"; - - using var httpClient = new HttpClient( - new HttpClientHandler - { - AllowAutoRedirect = true, - MaxAutomaticRedirections = 5 - } - ); - - var ver = Assembly.GetExecutingAssembly().GetName().Version; - httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + var apiUrl = new Uri(_serverConfigurationManager.CurrentApiUrl); HubConnectionConfig defaultConfig; @@ -93,38 +75,72 @@ public class HubFactory : MediatorSubscriberBase { defaultConfig = _cachedConfig; } - else if (_serverConfigurationManager.CurrentApiUrl == ApiController.LoporritServiceUri) - { - defaultConfig = new HubConnectionConfig - { - HubUrl = ApiController.LoporritServiceHubUri, - SkipNegotiation = true, - Transports = ["websockets"] - }; - } else { defaultConfig = new HubConnectionConfig { - HubUrl = uri.AbsoluteUri.TrimEnd('/') + IMareHub.Path, + HubUrl = _serverConfigurationManager.CurrentApiUrl.TrimEnd('/') + IMareHub.Path, Transports = [] }; } + if (_serverConfigurationManager.CurrentApiUrl == ApiController.LoporritServiceUri) + defaultConfig.HubUrl = ApiController.LoporritServiceHubUri; + + string jsonResponse; + + if (stapledWellKnown != null) + { + jsonResponse = stapledWellKnown; + Logger.LogTrace("Using stapled hub config for {url}", _serverConfigurationManager.CurrentApiUrl); + } + else + { + try + { + var httpScheme = apiUrl.Scheme.ToLowerInvariant() switch + { + "ws" => "http", + "wss" => "https", + _ => apiUrl.Scheme + }; + + var wellKnownUrl = $"{httpScheme}://{apiUrl.Host}/.well-known/loporrit/client"; + Logger.LogTrace("Fetching hub config for {uri} via {wk}", _serverConfigurationManager.CurrentApiUrl, wellKnownUrl); + + using var httpClient = new HttpClient( + new HttpClientHandler + { + AllowAutoRedirect = true, + MaxAutomaticRedirections = 5 + } + ); + + var ver = Assembly.GetExecutingAssembly().GetName().Version; + httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); + + // Make a GET request to the loporrit endpoint + var response = await httpClient.GetAsync(wellKnownUrl).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + return defaultConfig; + + var contentType = response.Content.Headers.ContentType?.MediaType; + + if (contentType == null || contentType != "application/json") + return defaultConfig; + + jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + catch (HttpRequestException ex) + { + Logger.LogWarning(ex, "HTTP request failed for .well-known"); + return defaultConfig; + } + } + try { - // Make a GET request to the loporrit endpoint - var response = await httpClient.GetAsync(wellKnownUrl).ConfigureAwait(false); - - if (!response.IsSuccessStatusCode) - return defaultConfig; - - var contentType = response.Content.Headers.ContentType?.MediaType; - - if (contentType == null || contentType != "application/json") - return defaultConfig; - - var jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var config = JsonSerializer.Deserialize( jsonResponse, new JsonSerializerOptions @@ -137,17 +153,12 @@ public class HubFactory : MediatorSubscriberBase return defaultConfig; if (string.IsNullOrEmpty(config.HubUrl)) - config.HubUrl ??= defaultConfig.HubUrl; + config.HubUrl = defaultConfig.HubUrl; config.Transports ??= []; return config; } - catch (HttpRequestException ex) - { - Logger.LogWarning(ex, "HTTP request failed for .well-known"); - return defaultConfig; - } catch (JsonException ex) { Logger.LogWarning(ex, "Invalid JSON in .well-known response"); diff --git a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs index 632ad74..c3341db 100644 --- a/MareSynchronos/WebAPI/SignalR/TokenProvider.cs +++ b/MareSynchronos/WebAPI/SignalR/TokenProvider.cs @@ -4,10 +4,12 @@ using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Utils; +using MareSynchronos.API.Dto; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Reflection; namespace MareSynchronos.WebAPI.SignalR; @@ -19,6 +21,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber private readonly ILogger _logger; private readonly ServerConfigurationManager _serverManager; private readonly ConcurrentDictionary _tokenCache = new(); + private readonly ConcurrentDictionary _wellKnownCache = new(); public TokenProvider(ILogger logger, ServerConfigurationManager serverManager, DalamudUtilService dalamudUtil, MareMediator mareMediator) { @@ -32,11 +35,13 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber { _lastJwtIdentifier = null; _tokenCache.Clear(); + _wellKnownCache.Clear(); }); Mediator.Subscribe(this, (_) => { _lastJwtIdentifier = null; _tokenCache.Clear(); + _wellKnownCache.Clear(); }); _httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build)); } @@ -54,14 +59,13 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber public async Task GetNewToken(JwtIdentifier identifier, CancellationToken token) { Uri tokenUri; - string response = string.Empty; HttpResponseMessage result; try { _logger.LogDebug("GetNewToken: Requesting"); - tokenUri = MareAuth.AuthFullPath(new Uri(_serverManager.CurrentApiUrl + tokenUri = MareAuth.AuthV2FullPath(new Uri(_serverManager.CurrentApiUrl .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); var secretKey = _serverManager.GetSecretKey(out _)!; @@ -72,13 +76,16 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber new KeyValuePair("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)), }), token).ConfigureAwait(false); - response = await result.Content.ReadAsStringAsync().ConfigureAwait(false); + var response = await result.Content.ReadFromJsonAsync().ConfigureAwait(false) ?? new(); result.EnsureSuccessStatusCode(); - _tokenCache[identifier] = response; + _tokenCache[identifier] = response.Token; + _wellKnownCache[_serverManager.CurrentApiUrl] = response.WellKnown; + return response.Token; } catch (HttpRequestException ex) { _tokenCache.TryRemove(identifier, out _); + _wellKnownCache.TryRemove(_serverManager.CurrentApiUrl, out _); _logger.LogError(ex, "GetNewToken: Failure to get token"); @@ -86,13 +93,10 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber { Mediator.Publish(new NotificationMessage("Error refreshing token", "Your authentication token could not be renewed. Try reconnecting manually.", NotificationType.Error)); Mediator.Publish(new DisconnectedMessage()); - throw new MareAuthFailureException(response); } throw; } - - return response; } private async Task GetIdentifier() @@ -153,4 +157,13 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber _logger.LogTrace("GetOrUpdate: Getting new token"); return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false); } + + public string? GetStapledWellKnown(string apiUrl) + { + _wellKnownCache.TryGetValue(apiUrl, out var wellKnown); + // Treat an empty string as null -- it won't decode as JSON anyway + if (string.IsNullOrEmpty(wellKnown)) + return null; + return wellKnown; + } } \ No newline at end of file