| File: OutputCacheKeyProvider.cs | Web Access |
| Project: src\aspnetcore\src\Middleware\OutputCaching\src\Microsoft.AspNetCore.OutputCaching.csproj (Microsoft.AspNetCore.OutputCaching) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using System.Linq; using System.Text; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.OutputCaching; internal sealed class OutputCacheKeyProvider : IOutputCacheKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private const char KeyDelimiter = '\x1e'; // Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions private const char KeySubDelimiter = '\x1f'; private readonly ObjectPool<StringBuilder> _builderPool; private readonly OutputCacheOptions _options; internal OutputCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions<OutputCacheOptions> options) { ArgumentNullException.ThrowIfNull(poolProvider); ArgumentNullException.ThrowIfNull(options); _builderPool = poolProvider.CreateStringBuilderPool(); _options = options.Value; } // <VaryByKeyPrefix><delimiter> // GET<delimiter>SCHEME<delimiter>HOST:PORT/PATHBASE/PATH<delimiter> // H<delimiter>HeaderName=HeaderValue1<subdelimiter>HeaderValue2<delimiter> // Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2<delimiter> // R<delimiter>RouteName1=RouteValue1<delimiter>RouteName2=RouteValue2 // V<delimiter>ValueName1=Value1<delimiter>ValueName2=Value2 public string CreateStorageKey(OutputCacheContext context) { ArgumentNullException.ThrowIfNull(_builderPool); var builder = _builderPool.Get(); try { if (!TryAppendKeyPrefix(context, builder)) { return string.Empty; } if (!TryAppendBaseKey(context, builder)) { return string.Empty; } if (!TryAppendVaryByKey(context, builder)) { return string.Empty; } return builder.ToString(); } finally { _builderPool.Return(builder); } } public static bool ContainsDelimiters(string? value) { return !string.IsNullOrEmpty(value) && value.ContainsAny(KeyDelimiter, KeySubDelimiter); } public static bool TryAppendKeyPrefix(OutputCacheContext context, StringBuilder builder) { var cacheKeyPrefix = context.CacheVaryByRules.CacheKeyPrefix; if (!string.IsNullOrEmpty(cacheKeyPrefix)) { if (ContainsDelimiters(cacheKeyPrefix)) { return false; } builder .Append(context.CacheVaryByRules.CacheKeyPrefix) .Append(KeyDelimiter); } return true; } // GET<delimiter>SCHEME<delimiter>HOST:PORT/PATHBASE/PATH public bool TryAppendBaseKey(OutputCacheContext context, StringBuilder builder) { var request = context.HttpContext.Request; if (ContainsDelimiters(request.PathBase.Value) || ContainsDelimiters(request.Path.Value)) { return false; } builder .AppendUpperInvariant(request.Method) .Append(KeyDelimiter) .AppendUpperInvariant(request.Scheme) .Append(KeyDelimiter); if (context.CacheVaryByRules.VaryByHost) { builder.AppendUpperInvariant(request.Host.Value); } else { // Use a fake HOST header to prevent substitutions builder.AppendUpperInvariant("*:*"); } if (_options.UseCaseSensitivePaths) { builder .Append(request.PathBase.Value) .Append(request.Path.Value); } else { builder .AppendUpperInvariant(request.PathBase.Value) .AppendUpperInvariant(request.Path.Value); } return true; } public bool TryAppendVaryByKey(OutputCacheContext context, StringBuilder builder) { var varyByRules = context.CacheVaryByRules; if (varyByRules == null) { throw new InvalidOperationException($"{nameof(OutputCacheContext.CacheVaryByRules)} must not be null on the {nameof(OutputCacheContext)}"); } var varyHeaderNames = context.CacheVaryByRules.HeaderNames; var varyRouteValueNames = context.CacheVaryByRules.RouteValueNames; var varyQueryKeys = context.CacheVaryByRules.QueryKeys; var varyByValues = context.CacheVaryByRules.HasVaryByValues ? context.CacheVaryByRules.VaryByValues : null; // Vary by header names var headersCount = varyByRules.HeaderNames.Count; if (headersCount > 0) { // Append a group separator for the header segment of the cache key builder .Append(KeyDelimiter) .Append('H'); var requestHeaders = context.HttpContext.Request.Headers; for (var i = 0; i < headersCount; i++) { var header = varyByRules.HeaderNames[i] ?? string.Empty; var headerValues = requestHeaders[header]; // Delimiters are not checked in the keys since they are taken // from configuration builder .Append(KeyDelimiter) .Append(header) .Append('='); var headerValuesArray = headerValues.ToArray(); Array.Sort(headerValuesArray, StringComparer.Ordinal); for (var j = 0; j < headerValuesArray.Length; j++) { if (j > 0) { builder.Append(KeySubDelimiter); } if (ContainsDelimiters(headerValuesArray[j])) { return false; } builder.Append(headerValuesArray[j]); } } } // Vary by query keys if (varyQueryKeys.Count > 0) { // Append a group separator for the query key segment of the cache key builder .Append(KeyDelimiter) .Append('Q'); if (varyQueryKeys.Count == 1 && string.Equals(varyQueryKeys[0], "*", StringComparison.Ordinal) && context.HttpContext.Request.Query.Count > 0) { // Vary by all available query keys var queryArray = context.HttpContext.Request.Query.ToArray(); // Query keys are aggregated case-insensitively whereas the query values are compared ordinally. Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase); for (var i = 0; i < queryArray.Length; i++) { if (ContainsDelimiters(queryArray[i].Key)) { return false; } builder .Append(KeyDelimiter) .AppendUpperInvariant(queryArray[i].Key) .Append('='); var queryValueArray = queryArray[i].Value.ToArray(); Array.Sort(queryValueArray, StringComparer.Ordinal); for (var j = 0; j < queryValueArray.Length; j++) { if (j > 0) { builder.Append(KeySubDelimiter); } if (ContainsDelimiters(queryValueArray[j])) { return false; } builder.Append(queryValueArray[j]); } } } else { for (var i = 0; i < varyByRules.QueryKeys.Count; i++) { var queryKey = varyByRules.QueryKeys[i] ?? string.Empty; var queryKeyValues = context.HttpContext.Request.Query[queryKey]; // Delimiters are not checked in the keys since they are taken // from configuration builder .Append(KeyDelimiter) .Append(queryKey) .Append('='); var queryValueArray = queryKeyValues.ToArray(); Array.Sort(queryValueArray, StringComparer.Ordinal); for (var j = 0; j < queryValueArray.Length; j++) { if (j > 0) { builder.Append(KeySubDelimiter); } if (ContainsDelimiters(queryValueArray[j])) { return false; } builder.Append(queryValueArray[j]); } } } } // Vary by route value names var routeValueNamesCount = varyByRules.RouteValueNames.Count; if (routeValueNamesCount > 0) { // Append a group separator for the route values segment of the cache key builder .Append(KeyDelimiter) .Append('R'); for (var i = 0; i < routeValueNamesCount; i++) { // The lookup key can't be null var routeValueName = varyByRules.RouteValueNames[i] ?? string.Empty; // RouteValueNames returns null if the key doesn't exist var routeValueValue = context.HttpContext.Request.RouteValues[routeValueName]; var stringRouteValue = Convert.ToString(routeValueValue, CultureInfo.InvariantCulture); // Delimiters are not checked in the keys since they are taken // from configuration if (ContainsDelimiters(stringRouteValue)) { return false; } builder.Append(KeyDelimiter) .Append(routeValueName) .Append('=') .Append(stringRouteValue); } } // Vary by values // Order keys to have a deterministic key var orderedKeys = GetOrderDictionaryKeys(varyByValues); var valueNamesCount = orderedKeys.Length; if (valueNamesCount > 0) { // Append a group separator for the values segment of the cache key builder .Append(KeyDelimiter) .Append('V'); for (var i = 0; i < valueNamesCount; i++) { // The lookup key can't be null var key = orderedKeys[i] ?? string.Empty; var value = varyByRules.VaryByValues[key]; if (ContainsDelimiters(key) || ContainsDelimiters(value)) { return false; } builder.Append(KeyDelimiter) .Append(key) .Append('=') .Append(value); } } return true; } internal static string[] GetOrderDictionaryKeys(IDictionary<string, string>? dictionary) { if (dictionary == null || dictionary.Count == 0) { return Array.Empty<string>(); } var newArray = dictionary.Keys.ToArray(); Array.Sort(newArray, StringComparer.OrdinalIgnoreCase); return newArray; } private sealed class QueryKeyComparer : IComparer<KeyValuePair<string, StringValues>> { private readonly StringComparer _stringComparer; public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase); public QueryKeyComparer(StringComparer stringComparer) { _stringComparer = stringComparer; } public int Compare(KeyValuePair<string, StringValues> x, KeyValuePair<string, StringValues> y) => _stringComparer.Compare(x.Key, y.Key); } }