209 lines
7.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|