Files
jellyfin-ha-src/MediaBrowser.Controller/QuickConnect/InMemoryQuickConnectStore.cs
T
unkin-agent 6c54a02240
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
make quick connect authorize atomic and stop the fallback shadowing redis
2026-09-24 23:38:35 +10:00

111 lines
3.9 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Model.QuickConnect;
namespace MediaBrowser.Controller.QuickConnect;
/// <summary>
/// A process-local <see cref="IQuickConnectStore"/>. It is the single-instance default, and the
/// fallback a shared store degrades to while its backend is unreachable, so quick connect keeps
/// working for clients whose three legs happen to land on one instance.
/// </summary>
public sealed class InMemoryQuickConnectStore : IQuickConnectStore
{
private readonly ConcurrentDictionary<string, Entry<QuickConnectResult>> _requests = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, Entry<AuthenticationResult>> _authorizations = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, DateTime> _authorizationClaims = new(StringComparer.Ordinal);
/// <inheritdoc />
public Task<QuickConnectResult?> GetRequestBySecretAsync(string secret, CancellationToken cancellationToken = default)
{
Expire();
return Task.FromResult(_requests.TryGetValue(secret, out var entry) ? entry.Value : null);
}
/// <inheritdoc />
public Task<QuickConnectResult?> GetRequestByCodeAsync(string code, CancellationToken cancellationToken = default)
{
Expire();
return Task.FromResult(_requests.Values
.Select(entry => entry.Value)
.FirstOrDefault(request => string.Equals(request.Code, code, StringComparison.Ordinal)));
}
/// <inheritdoc />
public Task SetRequestAsync(QuickConnectResult request, DateTime expiresUtc, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
Expire();
_requests[request.Secret] = new Entry<QuickConnectResult>(expiresUtc, request);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<bool> TryClaimAuthorizationAsync(string secret, DateTime expiresUtc, CancellationToken cancellationToken = default)
{
Expire();
if (!_requests.TryGetValue(secret, out var entry) || entry.Value.Authenticated)
{
return Task.FromResult(false);
}
return Task.FromResult(_authorizationClaims.TryAdd(secret, expiresUtc));
}
/// <inheritdoc />
public Task SetAuthorizationAsync(string secret, AuthenticationResult authenticationResult, DateTime expiresUtc, CancellationToken cancellationToken = default)
{
Expire();
_authorizations[secret] = new Entry<AuthenticationResult>(expiresUtc, authenticationResult);
return Task.CompletedTask;
}
/// <inheritdoc />
public Task<AuthenticationResult?> TryConsumeAuthorizationAsync(string secret, CancellationToken cancellationToken = default)
{
Expire();
if (!_authorizations.TryRemove(secret, out var entry) || entry.ExpiresUtc <= DateTime.UtcNow)
{
return Task.FromResult<AuthenticationResult?>(null);
}
return Task.FromResult<AuthenticationResult?>(entry.Value);
}
private void Expire()
{
var now = DateTime.UtcNow;
foreach (var (secret, entry) in _requests)
{
if (entry.ExpiresUtc <= now)
{
_requests.TryRemove(secret, out _);
}
}
foreach (var (secret, entry) in _authorizations)
{
if (entry.ExpiresUtc <= now)
{
_authorizations.TryRemove(secret, out _);
}
}
foreach (var (secret, expiresUtc) in _authorizationClaims)
{
if (expiresUtc <= now)
{
_authorizationClaims.TryRemove(secret, out _);
}
}
}
private sealed record Entry<T>(DateTime ExpiresUtc, T Value);
}