diff --git a/Backend/Remora.Discord.Caching.Redis/Services/RedisCacheProvider.cs b/Backend/Remora.Discord.Caching.Redis/Services/RedisCacheProvider.cs index fd1fdb10cf..c602d5aa29 100644 --- a/Backend/Remora.Discord.Caching.Redis/Services/RedisCacheProvider.cs +++ b/Backend/Remora.Discord.Caching.Redis/Services/RedisCacheProvider.cs @@ -21,6 +21,8 @@ // using System; +using System.Security.Cryptography; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -29,6 +31,7 @@ using Microsoft.Extensions.Options; using Remora.Discord.Caching.Abstractions; using Remora.Discord.Caching.Abstractions.Services; +using Remora.Discord.Rest; using Remora.Results; namespace Remora.Discord.Caching.Redis.Services; @@ -41,16 +44,40 @@ public class RedisCacheProvider : ICacheProvider { private readonly IDistributedCache _cache; private readonly JsonSerializerOptions _jsonOptions; + private readonly string? _tokenHash; /// /// Initializes a new instance of the class. /// /// The redis cache. /// The JSON options. - public RedisCacheProvider(IDistributedCache cache, IOptionsMonitor jsonOptions) + /// The token store, if one is available. + public RedisCacheProvider + ( + IDistributedCache cache, + IOptionsMonitor jsonOptions, + ITokenStore? tokenStore = null + ) { _cache = cache; _jsonOptions = jsonOptions.Get("Discord"); + + if (tokenStore is null) + { + _tokenHash = null; + return; + } + + using var hasher = SHA256.Create(); + var hashBuilder = new StringBuilder(64); + var hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(tokenStore.Token)); + + foreach (var value in hash) + { + hashBuilder.Append(value.ToString("x2")); + } + + _tokenHash = hashBuilder.ToString(); } /// @@ -77,7 +104,7 @@ public virtual async ValueTask CacheAsync var serialized = JsonSerializer.SerializeToUtf8Bytes(instance, _jsonOptions); - await _cache.SetAsync(key.ToCanonicalString(), serialized, options, ct); + await _cache.SetAsync(CreateTokenScopedKey(key), serialized, options, ct); } /// @@ -95,7 +122,7 @@ public virtual async ValueTask> RetrieveAsync ) where TInstance : class { - var keyString = key.ToCanonicalString(); + var keyString = CreateTokenScopedKey(key); var value = await _cache.GetAsync(keyString, ct); @@ -114,7 +141,7 @@ public virtual async ValueTask> RetrieveAsync /// public async ValueTask EvictAsync(CacheKey key, CancellationToken ct = default) { - var keyString = key.ToCanonicalString(); + var keyString = CreateTokenScopedKey(key); var existingValue = await _cache.GetAsync(keyString, ct); @@ -143,7 +170,7 @@ public virtual async ValueTask> EvictAsync ) where TInstance : class { - var keyString = key.ToCanonicalString(); + var keyString = CreateTokenScopedKey(key); var existingValue = await _cache.GetAsync(keyString, ct); @@ -158,4 +185,13 @@ public virtual async ValueTask> EvictAsync return deserialized; } + + /// + /// Creates a cache key scoped to a specific token. + /// + /// The key. + /// The scoped key. + private string CreateTokenScopedKey(CacheKey key) => _tokenHash is not null + ? $"{_tokenHash}:{key.ToCanonicalString()}" + : key.ToCanonicalString(); } diff --git a/Backend/Remora.Discord.Caching/Services/CacheService.cs b/Backend/Remora.Discord.Caching/Services/CacheService.cs index 6253cc5e0b..e872ef2066 100644 --- a/Backend/Remora.Discord.Caching/Services/CacheService.cs +++ b/Backend/Remora.Discord.Caching/Services/CacheService.cs @@ -45,7 +45,11 @@ public class CacheService /// /// The cache provider. /// The cache settings. - public CacheService(ICacheProvider cacheProvider, ImmutableCacheSettings cacheSettings) + public CacheService + ( + ICacheProvider cacheProvider, + ImmutableCacheSettings cacheSettings + ) { _cacheProvider = cacheProvider; _cacheSettings = cacheSettings; diff --git a/Backend/Remora.Discord.Rest/Caching/MemoryCacheProvider.cs b/Backend/Remora.Discord.Rest/Caching/MemoryCacheProvider.cs index 8d84d0acf5..665c3454b1 100644 --- a/Backend/Remora.Discord.Rest/Caching/MemoryCacheProvider.cs +++ b/Backend/Remora.Discord.Rest/Caching/MemoryCacheProvider.cs @@ -20,6 +20,8 @@ // along with this program. If not, see . // +using System.Security.Cryptography; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -37,14 +39,33 @@ namespace Remora.Discord.Rest.Caching; public class MemoryCacheProvider : ICacheProvider { private readonly IMemoryCache _memoryCache; + private readonly string? _tokenHash; /// /// Initializes a new instance of the class. /// /// The memory cache. - public MemoryCacheProvider(IMemoryCache memoryCache) + /// The token store, if one is available. + public MemoryCacheProvider(IMemoryCache memoryCache, ITokenStore? tokenStore = null) { _memoryCache = memoryCache; + + if (tokenStore is null) + { + _tokenHash = null; + return; + } + + using var hasher = SHA256.Create(); + var hashBuilder = new StringBuilder(64); + var hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(tokenStore.Token)); + + foreach (var value in hash) + { + hashBuilder.Append(value.ToString("x2")); + } + + _tokenHash = hashBuilder.ToString(); } /// @@ -57,7 +78,7 @@ public ValueTask CacheAsync ) where TInstance : class { - _memoryCache.Set(key, instance, options); + _memoryCache.Set(CreateTokenScopedKey(key), instance, options); return default; } @@ -66,7 +87,7 @@ public ValueTask CacheAsync public ValueTask> RetrieveAsync(CacheKey key, CancellationToken ct = default) where TInstance : class { - if (_memoryCache.TryGetValue(key, out var instance)) + if (_memoryCache.TryGetValue(CreateTokenScopedKey(key), out var instance)) { return new(instance); } @@ -77,12 +98,12 @@ public ValueTask> RetrieveAsync(CacheKey key, Cance /// public ValueTask EvictAsync(CacheKey key, CancellationToken ct = default) { - if (!_memoryCache.TryGetValue(key, out _)) + if (!_memoryCache.TryGetValue(CreateTokenScopedKey(key), out _)) { return new(new NotFoundError($"The key \"{key}\" did not contain a value in cache.")); } - _memoryCache.Remove(key); + _memoryCache.Remove(CreateTokenScopedKey(key)); return new(Result.FromSuccess()); } @@ -90,12 +111,19 @@ public ValueTask EvictAsync(CacheKey key, CancellationToken ct = default public ValueTask> EvictAsync(CacheKey key, CancellationToken ct = default) where TInstance : class { - if (!_memoryCache.TryGetValue(key, out TInstance? existingValue)) + if (!_memoryCache.TryGetValue(CreateTokenScopedKey(key), out TInstance? existingValue)) { return new(new NotFoundError($"The key \"{key}\" did not contain a value in cache.")); } - _memoryCache.Remove(key); + _memoryCache.Remove(CreateTokenScopedKey(key)); return new(existingValue); } + + /// + /// Creates a cache key scoped to a specific token. + /// + /// The key. + /// The scoped key. + private object CreateTokenScopedKey(CacheKey key) => (_tokenHash, key); }