aspnetcore/src/Microsoft.AspNetCore.Respon.../CacheabilityValidator.cs

209 lines
7.5 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 Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching
{
public class CacheabilityValidator : ICacheabilityValidator
{
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
public virtual bool RequestIsCacheable(HttpContext httpContext)
{
var state = httpContext.GetResponseCachingState();
// Verify the method
// TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit.
var request = httpContext.Request;
if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) &&
!string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Verify existence of authorization headers
// TODO: The server may indicate that the response to these request are cacheable
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]))
{
return false;
}
// Verify request cache-control parameters
// TODO: no-cache requests can be retrieved upon validation with origin
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl]))
{
if (state.RequestCacheControl.NoCache)
{
return false;
}
}
else
{
// Support for legacy HTTP 1.0 cache directive
var pragmaHeaderValues = request.Headers[HeaderNames.Pragma];
foreach (var directive in pragmaHeaderValues)
{
if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
// TODO: Verify global middleware settings? Explicit ignore list, range requests, etc.
return true;
}
public virtual bool ResponseIsCacheable(HttpContext httpContext)
{
var state = httpContext.GetResponseCachingState();
// Only cache pages explicitly marked with public
// TODO: Consider caching responses that are not marked as public but otherwise cacheable?
if (!state.ResponseCacheControl.Public)
{
return false;
}
// Check no-store
if (state.RequestCacheControl.NoStore || state.ResponseCacheControl.NoStore)
{
return false;
}
// Check no-cache
// TODO: Handle no-cache with headers
if (state.ResponseCacheControl.NoCache)
{
return false;
}
var response = httpContext.Response;
// Do not cache responses with Set-Cookie headers
if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie]))
{
return false;
}
// Do not cache responses varying by *
var varyHeader = response.Headers[HeaderNames.Vary];
if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// TODO: public MAY override the cacheability checks for private and status codes
// Check private
if (state.ResponseCacheControl.Private)
{
return false;
}
// Check response code
// TODO: RFC also lists 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 as cacheable by default
if (response.StatusCode != StatusCodes.Status200OK)
{
return false;
}
// Check response freshness
// TODO: apparent age vs corrected age value
if (state.ResponseHeaders.Date == null)
{
if (state.ResponseCacheControl.SharedMaxAge == null &&
state.ResponseCacheControl.MaxAge == null &&
state.ResponseTime > state.ResponseHeaders.Expires)
{
return false;
}
}
else
{
var age = state.ResponseTime - state.ResponseHeaders.Date.Value;
// Validate shared max age
if (age > state.ResponseCacheControl.SharedMaxAge)
{
return false;
}
else if (state.ResponseCacheControl.SharedMaxAge == null)
{
// Validate max age
if (age > state.ResponseCacheControl.MaxAge)
{
return false;
}
else if (state.ResponseCacheControl.MaxAge == null)
{
// Validate expiration
if (state.ResponseTime > state.ResponseHeaders.Expires)
{
return false;
}
}
}
}
return true;
}
public virtual bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders)
{
var state = httpContext.GetResponseCachingState();
var age = state.CachedEntryAge;
var cachedControlHeaders = cachedResponseHeaders.CacheControl ?? EmptyCacheControl;
// Add min-fresh requirements
if (state.RequestCacheControl.MinFresh != null)
{
age += state.RequestCacheControl.MinFresh.Value;
}
// Validate shared max age, this overrides any max age settings for shared caches
if (age > cachedControlHeaders.SharedMaxAge)
{
// shared max age implies must revalidate
return false;
}
else if (cachedControlHeaders.SharedMaxAge == null)
{
// Validate max age
if (age > cachedControlHeaders.MaxAge || age > state.RequestCacheControl.MaxAge)
{
// Must revalidate
if (cachedControlHeaders.MustRevalidate)
{
return false;
}
// Request allows stale values
if (age < state.RequestCacheControl.MaxStaleLimit)
{
// TODO: Add warning header indicating the response is stale
return true;
}
return false;
}
else if (cachedControlHeaders.MaxAge == null && state.RequestCacheControl.MaxAge == null)
{
// Validate expiration
if (state.ResponseTime > cachedResponseHeaders.Expires)
{
return false;
}
}
}
return true;
}
}
}