339 lines
13 KiB
C#
339 lines
13 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Globalization;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Http.Features;
|
|
using Microsoft.AspNetCore.Http.Headers;
|
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
|
using Microsoft.Extensions.Primitives;
|
|
using Microsoft.Net.Http.Headers;
|
|
|
|
namespace Microsoft.AspNetCore.ResponseCaching
|
|
{
|
|
internal class ResponseCachingContext
|
|
{
|
|
private readonly HttpContext _httpContext;
|
|
private readonly IResponseCache _cache;
|
|
private readonly ResponseCachingOptions _options;
|
|
private readonly ICacheabilityValidator _cacheabilityValidator;
|
|
private readonly IKeyProvider _keyProvider;
|
|
|
|
private ResponseCachingState _state;
|
|
|
|
internal ResponseCachingContext(
|
|
HttpContext httpContext,
|
|
IResponseCache cache,
|
|
ResponseCachingOptions options,
|
|
ICacheabilityValidator cacheabilityValidator,
|
|
IKeyProvider keyProvider)
|
|
{
|
|
_httpContext = httpContext;
|
|
_cache = cache;
|
|
_options = options;
|
|
_cacheabilityValidator = cacheabilityValidator;
|
|
_keyProvider = keyProvider;
|
|
}
|
|
|
|
internal ResponseCachingState State
|
|
{
|
|
get
|
|
{
|
|
if (_state == null)
|
|
{
|
|
_state = _httpContext.GetResponseCachingState();
|
|
}
|
|
return _state;
|
|
}
|
|
}
|
|
|
|
internal bool ResponseStarted { get; set; }
|
|
|
|
private Stream OriginalResponseStream { get; set; }
|
|
|
|
private ResponseCacheStream ResponseCacheStream { get; set; }
|
|
|
|
private IHttpSendFileFeature OriginalSendFileFeature { get; set; }
|
|
|
|
internal async Task<bool> TryServeFromCacheAsync()
|
|
{
|
|
State.BaseKey = _keyProvider.CreateBaseKey(_httpContext);
|
|
var cacheEntry = _cache.Get(State.BaseKey);
|
|
var responseServed = false;
|
|
|
|
if (cacheEntry is CachedVaryRules)
|
|
{
|
|
// Request contains vary rules, recompute key and try again
|
|
State.CachedVaryRules = cacheEntry as CachedVaryRules;
|
|
var varyKey = _keyProvider.CreateVaryKey(_httpContext, ((CachedVaryRules)cacheEntry).VaryRules);
|
|
cacheEntry = _cache.Get(varyKey);
|
|
}
|
|
|
|
if (cacheEntry is CachedResponse)
|
|
{
|
|
State.CachedResponse = cacheEntry as CachedResponse;
|
|
var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers);
|
|
|
|
State.ResponseTime = _options.SystemClock.UtcNow;
|
|
var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created;
|
|
State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
|
|
|
|
if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders))
|
|
{
|
|
responseServed = true;
|
|
|
|
// Check conditional request rules
|
|
if (ConditionalRequestSatisfied(cachedResponseHeaders))
|
|
{
|
|
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
|
|
}
|
|
else
|
|
{
|
|
var response = _httpContext.Response;
|
|
// Copy the cached status code and response headers
|
|
response.StatusCode = State.CachedResponse.StatusCode;
|
|
foreach (var header in State.CachedResponse.Headers)
|
|
{
|
|
response.Headers.Add(header);
|
|
}
|
|
|
|
response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
|
|
|
|
var body = State.CachedResponse.Body ??
|
|
((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body;
|
|
|
|
// If the body is not found, something went wrong.
|
|
if (body == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Copy the cached response body
|
|
if (body.Length > 0)
|
|
{
|
|
// Add a content-length if required
|
|
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
|
|
{
|
|
response.ContentLength = body.Length;
|
|
}
|
|
await response.Body.WriteAsync(body, 0, body.Length);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// TODO: Validate with endpoint instead
|
|
}
|
|
}
|
|
|
|
if (!responseServed && State.RequestCacheControl.OnlyIfCached)
|
|
{
|
|
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
|
|
responseServed = true;
|
|
}
|
|
|
|
return responseServed;
|
|
}
|
|
|
|
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
|
|
{
|
|
var ifNoneMatchHeader = State.RequestHeaders.IfNoneMatch;
|
|
|
|
if (ifNoneMatchHeader != null)
|
|
{
|
|
if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (cachedResponseHeaders.ETag != null)
|
|
{
|
|
foreach (var tag in ifNoneMatchHeader)
|
|
{
|
|
if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= State.RequestHeaders.IfUnmodifiedSince)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal void FinalizeCachingHeaders()
|
|
{
|
|
if (_cacheabilityValidator.ResponseIsCacheable(_httpContext))
|
|
{
|
|
State.ShouldCacheResponse = true;
|
|
|
|
// Create the cache entry now
|
|
var response = _httpContext.Response;
|
|
var varyHeaderValue = response.Headers[HeaderNames.Vary];
|
|
var varyParamsValue = _httpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty;
|
|
State.CachedResponseValidFor = State.ResponseCacheControl.SharedMaxAge
|
|
?? State.ResponseCacheControl.MaxAge
|
|
?? (State.ResponseHeaders.Expires - State.ResponseTime)
|
|
// TODO: Heuristics for expiration?
|
|
?? TimeSpan.FromSeconds(10);
|
|
|
|
// Check if any vary rules exist
|
|
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
|
|
{
|
|
// Normalize order and casing of vary by rules
|
|
var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue);
|
|
var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue);
|
|
|
|
// Update vary rules if they are different
|
|
if (State.CachedVaryRules == null ||
|
|
!StringValues.Equals(State.CachedVaryRules.VaryRules.Params, normalizedVaryParamsValue) ||
|
|
!StringValues.Equals(State.CachedVaryRules.VaryRules.Headers, normalizedVaryHeaderValue))
|
|
{
|
|
var cachedVaryRules = new CachedVaryRules
|
|
{
|
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
|
VaryRules = new VaryRules()
|
|
{
|
|
// TODO: Vary Encoding
|
|
Headers = normalizedVaryHeaderValue,
|
|
Params = normalizedVaryParamsValue
|
|
}
|
|
};
|
|
|
|
State.CachedVaryRules = cachedVaryRules;
|
|
_cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor);
|
|
}
|
|
|
|
State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, State.CachedVaryRules.VaryRules);
|
|
}
|
|
|
|
// Ensure date header is set
|
|
if (State.ResponseHeaders.Date == null)
|
|
{
|
|
State.ResponseHeaders.Date = State.ResponseTime;
|
|
}
|
|
|
|
// Store the response on the state
|
|
State.CachedResponse = new CachedResponse
|
|
{
|
|
BodyKeyPrefix = FastGuid.NewGuid().IdString,
|
|
Created = State.ResponseHeaders.Date.Value,
|
|
StatusCode = _httpContext.Response.StatusCode
|
|
};
|
|
|
|
foreach (var header in State.ResponseHeaders.Headers)
|
|
{
|
|
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
State.CachedResponse.Headers.Add(header);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ResponseCacheStream.DisableBuffering();
|
|
}
|
|
}
|
|
|
|
internal void FinalizeCachingBody()
|
|
{
|
|
if (State.ShouldCacheResponse && ResponseCacheStream.BufferingEnabled)
|
|
{
|
|
if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize)
|
|
{
|
|
// Store response and response body separately
|
|
_cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor);
|
|
|
|
var cachedResponseBody = new CachedResponseBody()
|
|
{
|
|
Body = ResponseCacheStream.BufferedStream.ToArray()
|
|
};
|
|
|
|
_cache.Set(State.CachedResponse.BodyKeyPrefix, cachedResponseBody, State.CachedResponseValidFor);
|
|
}
|
|
else
|
|
{
|
|
// Store response and response body together
|
|
State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
|
|
_cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void OnResponseStarting()
|
|
{
|
|
if (!ResponseStarted)
|
|
{
|
|
ResponseStarted = true;
|
|
State.ResponseTime = _options.SystemClock.UtcNow;
|
|
|
|
FinalizeCachingHeaders();
|
|
}
|
|
}
|
|
|
|
internal void ShimResponseStream()
|
|
{
|
|
// TODO: Consider caching large responses on disk and serving them from there.
|
|
|
|
// Shim response stream
|
|
OriginalResponseStream = _httpContext.Response.Body;
|
|
ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream, _options.MaximumCachedBodySize);
|
|
_httpContext.Response.Body = ResponseCacheStream;
|
|
|
|
// Shim IHttpSendFileFeature
|
|
OriginalSendFileFeature = _httpContext.Features.Get<IHttpSendFileFeature>();
|
|
if (OriginalSendFileFeature != null)
|
|
{
|
|
_httpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream));
|
|
}
|
|
|
|
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
|
_httpContext.AddResponseCachingFeature();
|
|
}
|
|
|
|
internal void UnshimResponseStream()
|
|
{
|
|
// Unshim response stream
|
|
_httpContext.Response.Body = OriginalResponseStream;
|
|
|
|
// Unshim IHttpSendFileFeature
|
|
_httpContext.Features.Set(OriginalSendFileFeature);
|
|
|
|
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
|
_httpContext.RemoveResponseCachingFeature();
|
|
}
|
|
|
|
// Normalize order and casing
|
|
internal static StringValues GetNormalizedStringValues(StringValues stringVales)
|
|
{
|
|
if (stringVales.Count == 1)
|
|
{
|
|
return new StringValues(stringVales.ToString().ToUpperInvariant());
|
|
}
|
|
else
|
|
{
|
|
var originalArray = stringVales.ToArray();
|
|
var newArray = new string[originalArray.Length];
|
|
|
|
for (int i = 0; i < originalArray.Length; i++)
|
|
{
|
|
newArray[i] = originalArray[i].ToUpperInvariant();
|
|
}
|
|
|
|
// Since the casing has already been normalized, use Ordinal comparison
|
|
Array.Sort(newArray, StringComparer.Ordinal);
|
|
|
|
return new StringValues(newArray);
|
|
}
|
|
}
|
|
}
|
|
}
|