e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
1283 lines
55 KiB
C#
1283 lines
55 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="OutputCacheModule.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//------------------------------------------------------------------------------
|
|
|
|
/*
|
|
* Output cache module
|
|
*/
|
|
namespace System.Web.Caching {
|
|
using System.Text;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Collections;
|
|
using System.Globalization;
|
|
using System.Security.Cryptography;
|
|
using System.Web;
|
|
using System.Web.Caching;
|
|
using System.Web.Util;
|
|
using System.Collections.Specialized;
|
|
using System.Web.Configuration;
|
|
using System.Web.Management;
|
|
using System.Web.Hosting;
|
|
using System.Web.Security.Cryptography;
|
|
|
|
/*
|
|
* Holds header and param names that this cached item varies by.
|
|
*/
|
|
[Serializable]
|
|
internal class CachedVary {
|
|
// _id is used by OutputCacheProviders
|
|
private Guid _cachedVaryId;
|
|
internal readonly string[] _contentEncodings;
|
|
internal readonly string[] _headers;
|
|
internal readonly string[] _params;
|
|
internal readonly string _varyByCustom;
|
|
internal readonly bool _varyByAllParams;
|
|
|
|
internal Guid CachedVaryId { get { return _cachedVaryId; } }
|
|
|
|
internal CachedVary(string[] contentEncodings, string[] headers, string[] parameters, bool varyByAllParams, string varyByCustom) {
|
|
_contentEncodings = contentEncodings;
|
|
_headers = headers;
|
|
_params = parameters;
|
|
_varyByAllParams = varyByAllParams;
|
|
_varyByCustom = varyByCustom;
|
|
_cachedVaryId = Guid.NewGuid();
|
|
}
|
|
|
|
public override bool Equals(Object obj) {
|
|
CachedVary cv = obj as CachedVary;
|
|
if (cv == null) {
|
|
return false;
|
|
}
|
|
|
|
return _varyByAllParams == cv._varyByAllParams
|
|
&& _varyByCustom == cv._varyByCustom
|
|
&& StringUtil.StringArrayEquals(_contentEncodings, cv._contentEncodings)
|
|
&& StringUtil.StringArrayEquals(_headers, cv._headers)
|
|
&& StringUtil.StringArrayEquals(_params, cv._params);
|
|
}
|
|
|
|
public override int GetHashCode() {
|
|
HashCodeCombiner hashCodeCombiner = new HashCodeCombiner();
|
|
hashCodeCombiner.AddObject(_varyByAllParams);
|
|
|
|
// Cast _varyByCustom to an object, since the HashCodeCombiner.AddObject(string)
|
|
// overload uses StringUtil.GetStringHashCode(). We want to use String.GetHashCode()
|
|
// in this method, since we do not require a stable hash code across architectures.
|
|
hashCodeCombiner.AddObject((object)_varyByCustom);
|
|
|
|
hashCodeCombiner.AddArray(_contentEncodings);
|
|
hashCodeCombiner.AddArray(_headers);
|
|
hashCodeCombiner.AddArray(_params);
|
|
return hashCodeCombiner.CombinedHash32;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Holds the cached response.
|
|
*/
|
|
internal class CachedRawResponse {
|
|
/*
|
|
* Fields to store an actual response.
|
|
*/
|
|
internal Guid _cachedVaryId;
|
|
internal readonly HttpRawResponse _rawResponse;
|
|
internal readonly HttpCachePolicySettings _settings;
|
|
internal readonly String _kernelCacheUrl;
|
|
|
|
internal CachedRawResponse(
|
|
HttpRawResponse rawResponse,
|
|
HttpCachePolicySettings settings,
|
|
String kernelCacheUrl,
|
|
Guid cachedVaryId) {
|
|
_rawResponse = rawResponse;
|
|
_settings = settings;
|
|
_kernelCacheUrl = kernelCacheUrl;
|
|
_cachedVaryId = cachedVaryId;
|
|
}
|
|
}
|
|
|
|
//
|
|
// OutputCacheModule real implementation for premium SKUs
|
|
//
|
|
|
|
sealed class OutputCacheModule : IHttpModule {
|
|
const int MAX_POST_KEY_LENGTH = 15000;
|
|
const string NULL_VARYBY_VALUE = "+n+";
|
|
const string ERROR_VARYBY_VALUE = "+e+";
|
|
internal const string TAG_OUTPUTCACHE = "OutputCache";
|
|
const string OUTPUTCACHE_KEYPREFIX_POST = CacheInternal.PrefixOutputCache + "1";
|
|
const string OUTPUTCACHE_KEYPREFIX_GET = CacheInternal.PrefixOutputCache + "2";
|
|
const string IDENTITY = "identity";
|
|
const string ASTERISK = "*";
|
|
|
|
static internal readonly char[] s_fieldSeparators;
|
|
|
|
string _key;
|
|
bool _recordedCacheMiss;
|
|
|
|
static OutputCacheModule() {
|
|
s_fieldSeparators = new char[] {',', ' '};
|
|
}
|
|
|
|
internal OutputCacheModule() {
|
|
}
|
|
|
|
internal static string CreateOutputCachedItemKey(
|
|
string path,
|
|
HttpVerb verb,
|
|
HttpContext context,
|
|
CachedVary cachedVary) {
|
|
|
|
StringBuilder sb;
|
|
int i, j, n;
|
|
string name, value;
|
|
string[] a;
|
|
byte[] buf;
|
|
HttpRequest request;
|
|
NameValueCollection col;
|
|
int contentLength;
|
|
bool getAllParams;
|
|
|
|
if (verb == HttpVerb.POST) {
|
|
sb = new StringBuilder(OUTPUTCACHE_KEYPREFIX_POST, path.Length + OUTPUTCACHE_KEYPREFIX_POST.Length);
|
|
}
|
|
else {
|
|
sb = new StringBuilder(OUTPUTCACHE_KEYPREFIX_GET, path.Length + OUTPUTCACHE_KEYPREFIX_GET.Length);
|
|
}
|
|
|
|
sb.Append(CultureInfo.InvariantCulture.TextInfo.ToLower(path));
|
|
|
|
/* key for cached vary item has additional information */
|
|
if (cachedVary != null) {
|
|
request = context.Request;
|
|
|
|
/* params part */
|
|
for (j = 0; j <= 2; j++) {
|
|
a = null;
|
|
col = null;
|
|
getAllParams = false;
|
|
|
|
switch (j) {
|
|
case 0:
|
|
sb.Append("H");
|
|
a = cachedVary._headers;
|
|
if (a != null) {
|
|
col = request.GetServerVarsWithoutDemand();
|
|
}
|
|
|
|
break;
|
|
|
|
case 1:
|
|
Debug.Assert(cachedVary._params == null || !cachedVary._varyByAllParams, "cachedVary._params == null || !cachedVary._varyByAllParams");
|
|
|
|
sb.Append("Q");
|
|
a = cachedVary._params;
|
|
if (request.HasQueryString && (a != null || cachedVary._varyByAllParams)) {
|
|
col = request.QueryString;
|
|
getAllParams = cachedVary._varyByAllParams;
|
|
}
|
|
|
|
break;
|
|
|
|
case 2:
|
|
default:
|
|
Debug.Assert(cachedVary._params == null || !cachedVary._varyByAllParams, "cachedVary._params == null || !cachedVary._varyByAllParams");
|
|
|
|
sb.Append("F");
|
|
if (verb == HttpVerb.POST) {
|
|
a = cachedVary._params;
|
|
if (request.HasForm && (a != null || cachedVary._varyByAllParams)) {
|
|
col = request.Form;
|
|
getAllParams = cachedVary._varyByAllParams;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
Debug.Assert(a == null || !getAllParams, "a == null || !getAllParams");
|
|
|
|
/* handle all params case (VaryByParams[*] = true) */
|
|
if (getAllParams && col.Count > 0) {
|
|
a = col.AllKeys;
|
|
for (i = a.Length - 1; i >= 0; i--) {
|
|
if (a[i] != null)
|
|
a[i] = CultureInfo.InvariantCulture.TextInfo.ToLower(a[i]);
|
|
}
|
|
|
|
Array.Sort(a, InvariantComparer.Default);
|
|
}
|
|
|
|
if (a != null) {
|
|
for (i = 0, n = a.Length; i < n; i++) {
|
|
name = a[i];
|
|
if (col == null) {
|
|
value = NULL_VARYBY_VALUE;
|
|
}
|
|
else {
|
|
value = col[name];
|
|
if (value == null) {
|
|
value = NULL_VARYBY_VALUE;
|
|
}
|
|
}
|
|
|
|
sb.Append("N");
|
|
sb.Append(name);
|
|
sb.Append("V");
|
|
sb.Append(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* custom string part */
|
|
sb.Append("C");
|
|
if (cachedVary._varyByCustom != null) {
|
|
sb.Append("N");
|
|
sb.Append(cachedVary._varyByCustom);
|
|
sb.Append("V");
|
|
|
|
try {
|
|
value = context.ApplicationInstance.GetVaryByCustomString(
|
|
context, cachedVary._varyByCustom);
|
|
if (value == null) {
|
|
value = NULL_VARYBY_VALUE;
|
|
}
|
|
}
|
|
catch (Exception e) {
|
|
value = ERROR_VARYBY_VALUE;
|
|
HttpApplicationFactory.RaiseError(e);
|
|
}
|
|
|
|
sb.Append(value);
|
|
}
|
|
|
|
/*
|
|
* if VaryByParms=*, and method is not a form, then
|
|
* use a cryptographically strong hash of the data as
|
|
* part of the key.
|
|
*/
|
|
sb.Append("D");
|
|
if ( verb == HttpVerb.POST &&
|
|
cachedVary._varyByAllParams &&
|
|
request.Form.Count == 0) {
|
|
|
|
contentLength = request.ContentLength;
|
|
if (contentLength > MAX_POST_KEY_LENGTH || contentLength < 0) {
|
|
return null;
|
|
}
|
|
|
|
if (contentLength > 0) {
|
|
buf = ((HttpInputStream)request.InputStream).GetAsByteArray();
|
|
if (buf == null) {
|
|
return null;
|
|
}
|
|
|
|
// Use SHA256 to generate a collision-free hash of the input data
|
|
value = Convert.ToBase64String(CryptoUtil.ComputeSHA256Hash(buf));
|
|
sb.Append(value);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* VaryByContentEncoding
|
|
*/
|
|
sb.Append("E");
|
|
string[] contentEncodings = cachedVary._contentEncodings;
|
|
if (contentEncodings != null) {
|
|
string coding = context.Response.GetHttpHeaderContentEncoding();
|
|
if (coding != null) {
|
|
for (int k = 0; k < contentEncodings.Length; k++) {
|
|
if (contentEncodings[k] == coding) {
|
|
sb.Append(coding);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The key must end in "E", or the VaryByContentEncoding feature will break. Unfortunately,
|
|
// there was no good way to encapsulate the logic within this routine. See the code in
|
|
// OnEnter where we append the result of GetAcceptableEncoding to the key.
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
/*
|
|
* Return a key to lookup a cached response. The key contains
|
|
* the path and optionally, vary parameters, vary headers, custom strings,
|
|
* and form posted data.
|
|
*/
|
|
string CreateOutputCachedItemKey(HttpContext context, CachedVary cachedVary) {
|
|
return CreateOutputCachedItemKey(context.Request.Path, context.Request.HttpVerb, context, cachedVary);
|
|
}
|
|
|
|
/*
|
|
* GetAcceptableEncoding finds an acceptable coding for the given
|
|
* Accept-Encoding header (see RFC 2616)
|
|
* returns either i) an acceptable index in contentEncodings, ii) -1 if the identity is acceptable, or iii) -2 if nothing is acceptable
|
|
*/
|
|
static int GetAcceptableEncoding(string[] contentEncodings, int startIndex, string acceptEncoding) {
|
|
// The format of Accept-Encoding is ( 1#( codings [ ";" "q" "=" qvalue ] ) | "*" )
|
|
if (String.IsNullOrEmpty(acceptEncoding)) {
|
|
return -1; // use "identity"
|
|
}
|
|
|
|
// is there only one token?
|
|
int tokenEnd = acceptEncoding.IndexOf(',');
|
|
if (tokenEnd == -1) {
|
|
string acceptEncodingWithoutWeight = acceptEncoding;
|
|
// WOS 1984913: is there a weight?
|
|
tokenEnd = acceptEncoding.IndexOf(';');
|
|
if (tokenEnd > -1) {
|
|
// remove weight
|
|
int space = acceptEncoding.IndexOf(' ');
|
|
if (space > -1 && space < tokenEnd) {
|
|
tokenEnd = space;
|
|
}
|
|
acceptEncodingWithoutWeight = acceptEncoding.Substring(0, tokenEnd);
|
|
if (ParseWeight(acceptEncoding, tokenEnd) == 0) {
|
|
// WOS 1985352 & WOS 1985353: weight is 0, use "identity" only if it is acceptable
|
|
bool identityIsAcceptable = acceptEncodingWithoutWeight != IDENTITY && acceptEncodingWithoutWeight != ASTERISK;
|
|
return (identityIsAcceptable) ? -1 : -2;
|
|
}
|
|
}
|
|
// WOS 1985353: is this the special "*" symbol?
|
|
if (acceptEncodingWithoutWeight == ASTERISK) {
|
|
// just return the index of the first entry in the list, since it is acceptable
|
|
return 0;
|
|
}
|
|
for (int i = startIndex; i < contentEncodings.Length; i++) {
|
|
if (StringUtil.EqualsIgnoreCase(contentEncodings[i], acceptEncodingWithoutWeight)) {
|
|
return i; // found
|
|
}
|
|
}
|
|
return -1; // not found, use "identity"
|
|
}
|
|
|
|
// there are multiple tokens
|
|
int bestCodingIndex = -1;
|
|
double bestCodingWeight = 0;
|
|
for (int i = startIndex; i < contentEncodings.Length; i++) {
|
|
string coding = contentEncodings[i];
|
|
// get weight of current coding
|
|
double weight = GetAcceptableEncodingHelper(coding, acceptEncoding);
|
|
// if it is 1, use it
|
|
if (weight == 1) {
|
|
return i;
|
|
}
|
|
// if it is the best so far, remember it
|
|
if (weight > bestCodingWeight) {
|
|
bestCodingIndex = i;
|
|
bestCodingWeight = weight;
|
|
}
|
|
}
|
|
// WOS 1985352: use "identity" only if it is acceptable
|
|
if (bestCodingIndex == -1 && !IsIdentityAcceptable(acceptEncoding)) {
|
|
bestCodingIndex = -2;
|
|
}
|
|
return bestCodingIndex; // coding index with highest weight, possibly -1 or -2
|
|
}
|
|
|
|
// Get the weight of the specified coding from the Accept-Encoding header.
|
|
// 1 means use this coding. 0 means don't use this coding. A number between
|
|
// 1 and 0 must be compared with other codings. -1 means the coding was not found
|
|
static double GetAcceptableEncodingHelper(string coding, string acceptEncoding) {
|
|
double weight = -1;
|
|
int startSearchIndex = 0;
|
|
int codingLength = coding.Length;
|
|
int acceptEncodingLength = acceptEncoding.Length;
|
|
int maxSearchIndex = acceptEncodingLength - codingLength;
|
|
while (startSearchIndex < maxSearchIndex) {
|
|
int indexStart = acceptEncoding.IndexOf(coding, startSearchIndex, StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (indexStart == -1) {
|
|
break; // not found
|
|
}
|
|
|
|
// if index is in middle of string, previous char should be ' ' or ','
|
|
if (indexStart != 0) {
|
|
char previousChar = acceptEncoding[indexStart-1];
|
|
if (previousChar != ' ' && previousChar != ',') {
|
|
startSearchIndex = indexStart + 1;
|
|
continue; // move index forward and continue searching
|
|
}
|
|
}
|
|
|
|
// the match starts on a token boundary, but it must also end
|
|
// on a token boundary ...
|
|
|
|
int indexNextChar = indexStart + codingLength;
|
|
char nextChar = '\0';
|
|
if (indexNextChar < acceptEncodingLength) {
|
|
nextChar = acceptEncoding[indexNextChar];
|
|
while (nextChar == ' ' && ++indexNextChar < acceptEncodingLength) {
|
|
nextChar = acceptEncoding[indexNextChar];
|
|
}
|
|
if (nextChar != ' ' && nextChar != ',' && nextChar != ';') {
|
|
startSearchIndex = indexStart + 1;
|
|
continue; // move index forward and continue searching
|
|
}
|
|
}
|
|
weight = (nextChar == ';') ? ParseWeight(acceptEncoding, indexNextChar) : 1;
|
|
break; // found
|
|
}
|
|
return weight;
|
|
}
|
|
|
|
// Gets the weight of the encoding beginning at startIndex.
|
|
// If Accept-Encoding header is formatted incorrectly, return 1 to short-circuit search.
|
|
static double ParseWeight(string acceptEncoding, int startIndex) {
|
|
double weight = 1;
|
|
int tokenEnd = acceptEncoding.IndexOf(',', startIndex);
|
|
if (tokenEnd == -1) {
|
|
tokenEnd = acceptEncoding.Length;
|
|
}
|
|
int qIndex = acceptEncoding.IndexOf('q', startIndex);
|
|
if (qIndex > -1 && qIndex < tokenEnd) {
|
|
int equalsIndex = acceptEncoding.IndexOf('=', qIndex);
|
|
if (equalsIndex > -1 && equalsIndex < tokenEnd) {
|
|
string s = acceptEncoding.Substring(equalsIndex+1, tokenEnd - (equalsIndex + 1));
|
|
double d;
|
|
if (Double.TryParse(s, NumberStyles.Float & ~NumberStyles.AllowLeadingSign & ~NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out d)) {
|
|
weight = (d >= 0 && d <= 1) ? d : 1; // if format is invalid, short-circut search by returning weight of 1
|
|
}
|
|
}
|
|
}
|
|
return weight;
|
|
}
|
|
|
|
static bool IsIdentityAcceptable(string acceptEncoding) {
|
|
bool result = true;
|
|
double identityWeight = GetAcceptableEncodingHelper(IDENTITY, acceptEncoding);
|
|
if (identityWeight == 0
|
|
|| (identityWeight <= 0 && GetAcceptableEncodingHelper(ASTERISK, acceptEncoding) == 0)) {
|
|
result = false;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static bool IsAcceptableEncoding(string contentEncoding, string acceptEncoding) {
|
|
if (String.IsNullOrEmpty(contentEncoding)) {
|
|
// if Content-Encoding is not set treat it as the identity
|
|
contentEncoding = IDENTITY;
|
|
}
|
|
if (String.IsNullOrEmpty(acceptEncoding)) {
|
|
// only the identity is acceptable if Accept-Encoding is not set
|
|
return (contentEncoding == IDENTITY);
|
|
}
|
|
double weight = GetAcceptableEncodingHelper(contentEncoding, acceptEncoding);
|
|
if (weight == 0
|
|
|| (weight <= 0 && GetAcceptableEncodingHelper(ASTERISK, acceptEncoding) == 0)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Record a cache miss to the perf counters.
|
|
*/
|
|
void RecordCacheMiss() {
|
|
if (!_recordedCacheMiss) {
|
|
PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_RATIO_BASE);
|
|
PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_MISSES);
|
|
_recordedCacheMiss = true;
|
|
}
|
|
}
|
|
|
|
|
|
/// <devdoc>
|
|
/// <para>Initializes the output cache for an application.</para>
|
|
/// </devdoc>
|
|
void IHttpModule.Init(HttpApplication app) {
|
|
OutputCacheSection cacheConfig = RuntimeConfig.GetAppConfig().OutputCache;
|
|
if (cacheConfig.EnableOutputCache) {
|
|
app.ResolveRequestCache += new EventHandler(this.OnEnter);
|
|
app.UpdateRequestCache += new EventHandler(this.OnLeave);
|
|
}
|
|
}
|
|
|
|
|
|
/// <devdoc>
|
|
/// <para>Disposes of items from the output cache.</para>
|
|
/// </devdoc>
|
|
void IHttpModule.Dispose() {
|
|
}
|
|
|
|
/*
|
|
* Try to find this request in the cache. If so, return it. Otherwise,
|
|
* store the cache key for use on Leave.
|
|
*/
|
|
|
|
/// <devdoc>
|
|
/// <para>Raises the <see langword='Enter'/>
|
|
/// event, which searches the output cache for an item to satisfy the HTTP request. </para>
|
|
/// </devdoc>
|
|
internal void OnEnter(Object source, EventArgs eventArgs) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Beginning OutputCacheModule::Enter");
|
|
_key = null;
|
|
_recordedCacheMiss = false;
|
|
|
|
if (!OutputCache.InUse) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Miss, no entries in output Cache" +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
return;
|
|
}
|
|
|
|
HttpApplication app;
|
|
HttpContext context;
|
|
string key;
|
|
HttpRequest request;
|
|
HttpResponse response;
|
|
Object item;
|
|
CachedRawResponse cachedRawResponse;
|
|
HttpCachePolicySettings settings;
|
|
int i, n;
|
|
bool sendBody;
|
|
HttpValidationStatus validationStatus, validationStatusFinal;
|
|
ValidationCallbackInfo callbackInfo;
|
|
string ifModifiedSinceHeader;
|
|
DateTime utcIfModifiedSince;
|
|
string etag;
|
|
string[] etags;
|
|
int send304;
|
|
string cacheControl;
|
|
string[] cacheDirectives = null;
|
|
string pragma;
|
|
string[] pragmaDirectives = null;
|
|
string directive;
|
|
int maxage;
|
|
int minfresh;
|
|
int age;
|
|
int fresh;
|
|
bool hasValidationPolicy;
|
|
CachedVary cachedVary;
|
|
HttpRawResponse rawResponse;
|
|
CachedPathData cachedPathData;
|
|
|
|
app = (HttpApplication)source;
|
|
context = app.Context;
|
|
cachedPathData = context.GetFilePathData();
|
|
request = context.Request;
|
|
response = context.Response;
|
|
|
|
/*
|
|
* Check if the request can be resolved for this method.
|
|
*/
|
|
switch (request.HttpVerb) {
|
|
case HttpVerb.HEAD:
|
|
case HttpVerb.GET:
|
|
case HttpVerb.POST:
|
|
break;
|
|
|
|
default:
|
|
Debug.Trace("OutputCacheModuleEnter", "Miss, Http method not GET, POST, or HEAD" +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* Create a lookup key. Remember the key for use inside Leave()
|
|
*/
|
|
_key = key = CreateOutputCachedItemKey(context, null);
|
|
Debug.Assert(_key != null, "_key != null");
|
|
|
|
/*
|
|
* Lookup the cache vary for this key.
|
|
*/
|
|
item = OutputCache.Get(key);
|
|
if (item == null) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Miss, item not found.\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
return;
|
|
}
|
|
|
|
// 'item' may be one of the following:
|
|
// - a CachedVary object (if the object varies by something)
|
|
// - a "no vary" CachedRawResponse object (i.e. it doesn't vary on anything)
|
|
|
|
// Let's assume it's a CacheVary and see what happens.
|
|
cachedVary = item as CachedVary;
|
|
|
|
// If we have one, create a new cache key for it (this is a must)
|
|
if (cachedVary != null) {
|
|
/*
|
|
* This cached output has a Vary policy. Create a new key based
|
|
* on the vary headers in cachedRawResponse and try again.
|
|
*
|
|
* Skip this step if it's a VaryByNone vary policy.
|
|
*/
|
|
|
|
|
|
key = CreateOutputCachedItemKey(context, cachedVary);
|
|
if (key == null) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Miss, key could not be created for vary-by item." +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
return;
|
|
}
|
|
|
|
if (cachedVary._contentEncodings == null) {
|
|
// With the new key, look up the in-memory key.
|
|
// At this point, we've exhausted the lookups in memory for this item.
|
|
item = OutputCache.Get(key);
|
|
}
|
|
else {
|
|
#if DBG
|
|
Debug.Assert(key[key.Length-1] == 'E', "key[key.Length-1] == 'E'");
|
|
#endif
|
|
item = null;
|
|
bool identityIsAcceptable = true;
|
|
string acceptEncoding = context.WorkerRequest.GetKnownRequestHeader(HttpWorkerRequest.HeaderAcceptEncoding);
|
|
if (acceptEncoding != null) {
|
|
string[] contentEncodings = cachedVary._contentEncodings;
|
|
int startIndex = 0;
|
|
bool done = false;
|
|
while (!done) {
|
|
done = true;
|
|
int index = GetAcceptableEncoding(contentEncodings, startIndex, acceptEncoding);
|
|
if (index > -1) {
|
|
#if DBG
|
|
Debug.Trace("OutputCacheModuleEnter", "VaryByContentEncoding key=" + key + contentEncodings[index]);
|
|
#endif
|
|
identityIsAcceptable = false; // the client Accept-Encoding header contains an encoding that's in the VaryByContentEncoding list
|
|
item = OutputCache.Get(key + contentEncodings[index]);
|
|
if (item == null) {
|
|
startIndex = index+1;
|
|
if (startIndex < contentEncodings.Length) {
|
|
done = false;
|
|
}
|
|
}
|
|
}
|
|
else if (index == -2) {
|
|
// the identity has a weight of 0 and is not acceptable
|
|
identityIsAcceptable = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// the identity should not be used if the client Accept-Encoding contains an entry in the VaryByContentEncoding list or "identity" is not acceptable
|
|
if (item == null && identityIsAcceptable) {
|
|
#if DBG
|
|
Debug.Trace("OutputCacheModuleEnter", "VaryByContentEncoding key=" + key);
|
|
#endif
|
|
item = OutputCache.Get(key);
|
|
}
|
|
}
|
|
|
|
Debug.Assert(item == null || item is CachedRawResponse, "item == null || item is CachedRawResponse");
|
|
if (item == null || ((CachedRawResponse)item)._cachedVaryId != cachedVary.CachedVaryId) {
|
|
#if DBG
|
|
if (item == null) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Miss, cVary found, cRawResponse not found.\n\t\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
}
|
|
else {
|
|
string msg = "Miss, _cachedVaryId=" + ((CachedRawResponse)item)._cachedVaryId.ToString() + ", cVary.CachedVaryId=" + cachedVary.CachedVaryId.ToString();
|
|
Debug.Trace("OutputCacheModuleEnter", msg + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
}
|
|
#endif
|
|
if (item != null) {
|
|
// explicitly remove entry because _cachedVaryId does not match
|
|
OutputCache.Remove(key, context);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// From this point on, we have an entry to work with.
|
|
|
|
Debug.Assert(item is CachedRawResponse, "item is CachedRawResponse");
|
|
cachedRawResponse = (CachedRawResponse) item;
|
|
settings = cachedRawResponse._settings;
|
|
if (cachedVary == null && !settings.IgnoreParams) {
|
|
/*
|
|
* This cached output has no vary policy, so make sure it doesn't have a query string or form post.
|
|
*/
|
|
if (request.HttpVerb == HttpVerb.POST) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Output cache item found but method is POST and no VaryByParam specified." +
|
|
"\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
RecordCacheMiss();
|
|
return;
|
|
}
|
|
|
|
if (request.HasQueryString) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Output cache item found but contains a querystring and no VaryByParam specified." +
|
|
"\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
RecordCacheMiss();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (settings.IgnoreRangeRequests) {
|
|
string rangeHeader = request.Headers["Range"];
|
|
if (StringUtil.StringStartsWithIgnoreCase(rangeHeader, "bytes")) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Output cache item found but this is a Range request and IgnoreRangeRequests is true." +
|
|
"\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
// Don't record this as a cache miss. The response for a range request is not cached, and so
|
|
// we don't want to pollute the cache hit/miss ratio.
|
|
return;
|
|
}
|
|
}
|
|
|
|
hasValidationPolicy = settings.HasValidationPolicy();
|
|
|
|
/*
|
|
* Determine whether the client can accept a cached copy, and
|
|
* get values of other cache control directives.
|
|
*
|
|
* We do this after lookup so we don't have to break down the headers
|
|
* if the item is not found. Cracking the headers is expensive.
|
|
*/
|
|
if (!hasValidationPolicy) {
|
|
cacheControl = request.Headers["Cache-Control"];
|
|
if (cacheControl != null) {
|
|
cacheDirectives = cacheControl.Split(s_fieldSeparators);
|
|
for (i = 0; i < cacheDirectives.Length; i++) {
|
|
directive = cacheDirectives[i];
|
|
if (directive == "no-cache" || directive == "no-store") {
|
|
Debug.Trace("OutputCacheModuleEnter",
|
|
"Skipping lookup because of Cache-Control: no-cache or no-store directive." +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
RecordCacheMiss();
|
|
return;
|
|
}
|
|
|
|
if (StringUtil.StringStartsWith(directive, "max-age=")) {
|
|
try {
|
|
maxage = Convert.ToInt32(directive.Substring(8), CultureInfo.InvariantCulture);
|
|
}
|
|
catch {
|
|
maxage = -1;
|
|
}
|
|
|
|
if (maxage >= 0) {
|
|
age = (int) ((context.UtcTimestamp.Ticks - settings.UtcTimestampCreated.Ticks) / TimeSpan.TicksPerSecond);
|
|
if (age >= maxage) {
|
|
Debug.Trace("OutputCacheModuleEnter",
|
|
"Not returning found item due to Cache-Control: max-age directive." +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
RecordCacheMiss();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if (StringUtil.StringStartsWith(directive, "min-fresh=")) {
|
|
try {
|
|
minfresh = Convert.ToInt32(directive.Substring(10), CultureInfo.InvariantCulture);
|
|
}
|
|
catch {
|
|
minfresh = -1;
|
|
}
|
|
|
|
if (minfresh >= 0 && settings.IsExpiresSet && !settings.SlidingExpiration) {
|
|
fresh = (int) ((settings.UtcExpires.Ticks - context.UtcTimestamp.Ticks) / TimeSpan.TicksPerSecond);
|
|
if (fresh < minfresh) {
|
|
Debug.Trace("OutputCacheModuleEnter",
|
|
"Not returning found item due to Cache-Control: min-fresh directive." +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
RecordCacheMiss();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pragma = request.Headers["Pragma"];
|
|
if (pragma != null) {
|
|
pragmaDirectives = pragma.Split(s_fieldSeparators);
|
|
for (i = 0; i < pragmaDirectives.Length; i++) {
|
|
if (pragmaDirectives[i] == "no-cache") {
|
|
Debug.Trace("OutputCacheModuleEnter",
|
|
"Skipping lookup because of Pragma: no-cache directive." +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
RecordCacheMiss();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (settings.ValidationCallbackInfo != null) {
|
|
/*
|
|
* Check if the item is still valid.
|
|
*/
|
|
validationStatus = HttpValidationStatus.Valid;
|
|
validationStatusFinal = validationStatus;
|
|
for (i = 0, n = settings.ValidationCallbackInfo.Length; i < n; i++) {
|
|
callbackInfo = settings.ValidationCallbackInfo[i];
|
|
try {
|
|
callbackInfo.handler(context, callbackInfo.data, ref validationStatus);
|
|
}
|
|
catch (Exception e) {
|
|
validationStatus = HttpValidationStatus.Invalid;
|
|
HttpApplicationFactory.RaiseError(e);
|
|
}
|
|
|
|
switch (validationStatus) {
|
|
case HttpValidationStatus.Invalid:
|
|
Debug.Trace("OutputCacheModuleEnter", "Output cache item found but callback invalidated it." +
|
|
"\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
OutputCache.Remove(key, context);
|
|
RecordCacheMiss();
|
|
return;
|
|
|
|
case HttpValidationStatus.IgnoreThisRequest:
|
|
validationStatusFinal = HttpValidationStatus.IgnoreThisRequest;
|
|
break;
|
|
|
|
case HttpValidationStatus.Valid:
|
|
break;
|
|
|
|
default:
|
|
Debug.Trace("OutputCacheModuleEnter", "Invalid validation status, ignoring it, status=" + validationStatus +
|
|
"\n\tkey=" + key);
|
|
|
|
validationStatus = validationStatusFinal;
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if (validationStatusFinal == HttpValidationStatus.IgnoreThisRequest) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Output cache item found but callback status is IgnoreThisRequest." +
|
|
"\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
|
|
RecordCacheMiss();
|
|
return;
|
|
}
|
|
|
|
Debug.Assert(validationStatusFinal == HttpValidationStatus.Valid,
|
|
"validationStatusFinal == HttpValidationStatus.Valid");
|
|
}
|
|
|
|
rawResponse = cachedRawResponse._rawResponse;
|
|
|
|
// WOS 1985154 ensure Content-Encoding is acceptable
|
|
if (cachedVary == null || cachedVary._contentEncodings == null) {
|
|
string acceptEncoding = request.Headers["Accept-Encoding"];
|
|
string contentEncoding = null;
|
|
ArrayList headers = rawResponse.Headers;
|
|
if (headers != null) {
|
|
foreach (HttpResponseHeader h in headers) {
|
|
if (h.Name == "Content-Encoding") {
|
|
contentEncoding = h.Value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!IsAcceptableEncoding(contentEncoding, acceptEncoding)) {
|
|
RecordCacheMiss();
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Try to satisfy a conditional request. The cached response
|
|
* must satisfy all conditions that are present.
|
|
*
|
|
* We can only satisfy a conditional request if the response
|
|
* is buffered and has no substitution blocks.
|
|
*
|
|
* N.B. RFC 2616 says conditional requests only occur
|
|
* with the GET method, but we try to satisfy other
|
|
* verbs (HEAD, POST) as well.
|
|
*/
|
|
send304 = -1;
|
|
|
|
if (!rawResponse.HasSubstBlocks) {
|
|
/* Check "If-Modified-Since" header */
|
|
ifModifiedSinceHeader = request.IfModifiedSince;
|
|
if (ifModifiedSinceHeader != null) {
|
|
send304 = 0;
|
|
try {
|
|
utcIfModifiedSince = HttpDate.UtcParse(ifModifiedSinceHeader);
|
|
if ( settings.IsLastModifiedSet &&
|
|
settings.UtcLastModified <= utcIfModifiedSince &&
|
|
utcIfModifiedSince <= context.UtcTimestamp) {
|
|
|
|
send304 = 1;
|
|
}
|
|
}
|
|
catch {
|
|
Debug.Trace("OutputCacheModuleEnter", "Ignore If-Modified-Since header, invalid format: " + ifModifiedSinceHeader);
|
|
}
|
|
}
|
|
|
|
/* Check "If-None-Match" header */
|
|
if (send304 != 0) {
|
|
etag = request.IfNoneMatch;
|
|
if (etag != null) {
|
|
send304 = 0;
|
|
etags = etag.Split(s_fieldSeparators);
|
|
for (i = 0, n = etags.Length; i < n; i++) {
|
|
if (i == 0 && etags[i].Equals(ASTERISK)) {
|
|
send304 = 1;
|
|
break;
|
|
}
|
|
|
|
if (etags[i].Equals(settings.ETag)) {
|
|
send304 = 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (send304 == 1) {
|
|
/*
|
|
* Send 304 Not Modified
|
|
*/
|
|
Debug.Trace("OutputCacheModuleEnter", "Hit, conditional request satisfied, status=304." +
|
|
"\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
response.ClearAll();
|
|
response.StatusCode = 304;
|
|
}
|
|
else {
|
|
/*
|
|
* Send the full response.
|
|
*/
|
|
#if DBG
|
|
if (send304 == -1) {
|
|
Debug.Trace("OutputCacheModuleEnter", "Hit.\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
|
|
}
|
|
else {
|
|
Debug.Trace("OutputCacheModuleEnter", "Hit, but conditional request not satisfied.\n\tkey=" + key +
|
|
"\nReturning from OutputCacheModule::Enter");
|
|
}
|
|
#endif
|
|
|
|
sendBody = (request.HttpVerb != HttpVerb.HEAD);
|
|
|
|
// Check and see if the cachedRawResponse is from the disk
|
|
// If so, we must clone the HttpRawResponse before sending it
|
|
|
|
// UseSnapshot calls ClearAll
|
|
response.UseSnapshot(rawResponse, sendBody);
|
|
}
|
|
|
|
response.Cache.ResetFromHttpCachePolicySettings(settings, context.UtcTimestamp);
|
|
|
|
// re-insert entry in kernel cache if necessary
|
|
string originalCacheUrl = cachedRawResponse._kernelCacheUrl;
|
|
if (originalCacheUrl != null) {
|
|
response.SetupKernelCaching(originalCacheUrl);
|
|
}
|
|
|
|
PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_RATIO_BASE);
|
|
PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_HITS);
|
|
_key = null;
|
|
_recordedCacheMiss = false;
|
|
|
|
app.CompleteRequest();
|
|
}
|
|
|
|
|
|
/*
|
|
* If the item is cacheable, add it to the cache.
|
|
*/
|
|
|
|
/// <devdoc>
|
|
/// <para>Raises the <see langword='Leave'/> event, which causes any cacheable items to
|
|
/// be put into the output cache.</para>
|
|
/// </devdoc>
|
|
internal /*public*/ void OnLeave(Object source, EventArgs eventArgs) {
|
|
HttpApplication app;
|
|
HttpContext context;
|
|
bool cacheable;
|
|
CachedVary cachedVary;
|
|
HttpCachePolicy cache;
|
|
HttpCachePolicySettings settings;
|
|
string keyRawResponse;
|
|
string[] varyByContentEncodings;
|
|
string[] varyByHeaders;
|
|
string[] varyByParams;
|
|
bool varyByAllParams;
|
|
HttpRequest request;
|
|
HttpResponse response;
|
|
int i, n;
|
|
bool cacheAuthorizedPage;
|
|
|
|
Debug.Trace("OutputCacheModuleLeave", "Beginning OutputCacheModule::Leave");
|
|
|
|
app = (HttpApplication)source;
|
|
context = app.Context;
|
|
request = context.Request;
|
|
response = context.Response;
|
|
cache = null;
|
|
|
|
#if DBG
|
|
string reason = null;
|
|
#endif
|
|
/*
|
|
* Determine whether the response is cacheable.
|
|
*/
|
|
cacheable = false;
|
|
do {
|
|
if (!response.HasCachePolicy) {
|
|
#if DBG
|
|
reason = "CachePolicy not created, not modified from non-caching default.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
cache = response.Cache;
|
|
if (!cache.IsModified()) {
|
|
#if DBG
|
|
reason = "CachePolicy created, but not modified from non-caching default.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if (response.StatusCode != 200) {
|
|
#if DBG
|
|
reason = "response.StatusCode != 200.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if (request.HttpVerb != HttpVerb.GET && request.HttpVerb != HttpVerb.POST) {
|
|
#if DBG
|
|
reason = "the cache can only cache responses to GET and POST.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if (!response.IsBuffered()) {
|
|
#if DBG
|
|
reason = "the response is not buffered.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Change a response with HttpCacheability.Public to HttpCacheability.Private
|
|
* if it requires authorization, and allow it to be cached.
|
|
*
|
|
* Note that setting Cacheability to ServerAndPrivate would accomplish
|
|
* the same thing without needing the "cacheAuthorizedPage" variable,
|
|
* but in RTM we did not have ServerAndPrivate, and setting that value
|
|
* would change the behavior.
|
|
*/
|
|
cacheAuthorizedPage = false;
|
|
if ( cache.GetCacheability() == HttpCacheability.Public &&
|
|
context.RequestRequiresAuthorization()) {
|
|
|
|
cache.SetCacheability(HttpCacheability.Private);
|
|
cacheAuthorizedPage = true;
|
|
}
|
|
|
|
if ( cache.GetCacheability() != HttpCacheability.Public &&
|
|
cache.GetCacheability() != HttpCacheability.ServerAndPrivate &&
|
|
cache.GetCacheability() != HttpCacheability.ServerAndNoCache &&
|
|
!cacheAuthorizedPage) {
|
|
#if DBG
|
|
reason = "CachePolicy.Cacheability is not Public, ServerAndPrivate, or ServerAndNoCache.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if (cache.GetNoServerCaching()) {
|
|
#if DBG
|
|
reason = "CachePolicy.NoServerCaching is set.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
// MSRC 11855 (DevDiv 297240 / 362405) - We should suppress output caching for responses which contain non-shareable cookies.
|
|
// We already disable the HTTP.SYS and IIS user mode cache when *any* response cookie is present (see IIS7WorkerRequest.SendUnknownResponseHeader)
|
|
if (response.ContainsNonShareableCookies()) {
|
|
#if DBG
|
|
reason = "Non-shareable response cookies were present.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if (!cache.HasExpirationPolicy() && !cache.HasValidationPolicy()) {
|
|
#if DBG
|
|
reason = "CachePolicy has no expiration policy or validation policy.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if (cache.VaryByHeaders.GetVaryByUnspecifiedParameters()) {
|
|
#if DBG
|
|
reason = "CachePolicy.Vary.VaryByUnspecifiedParameters was called.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if (!cache.VaryByParams.AcceptsParams() && (request.HttpVerb == HttpVerb.POST || request.HasQueryString)) {
|
|
#if DBG
|
|
reason = "the cache cannot cache responses to POSTs or GETs with query strings unless Cache.VaryByParams is modified.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if (cache.VaryByContentEncodings.IsModified() && !cache.VaryByContentEncodings.IsCacheableEncoding(context.Response.GetHttpHeaderContentEncoding())) {
|
|
#if DBG
|
|
reason = "the cache cannot cache encoded responses that are not listed in the VaryByContentEncodings collection.";
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
cacheable = true;
|
|
} while (false);
|
|
|
|
/*
|
|
* Add response to cache.
|
|
*/
|
|
if (!cacheable) {
|
|
#if DBG
|
|
Debug.Assert(reason != null, "reason != null");
|
|
Debug.Trace("OutputCacheModuleLeave", "Item is not output cacheable because " + reason +
|
|
"\n\tUrl=" + request.Path +
|
|
"\nReturning from OutputCacheModule::Leave");
|
|
#endif
|
|
|
|
return;
|
|
}
|
|
|
|
RecordCacheMiss();
|
|
|
|
settings = cache.GetCurrentSettings(response);
|
|
|
|
varyByContentEncodings = settings.VaryByContentEncodings;
|
|
|
|
varyByHeaders = settings.VaryByHeaders;
|
|
if (settings.IgnoreParams) {
|
|
varyByParams = null;
|
|
}
|
|
else {
|
|
varyByParams = settings.VaryByParams;
|
|
}
|
|
|
|
|
|
/* Create the key if it was not created in OnEnter */
|
|
if (_key == null) {
|
|
_key = CreateOutputCachedItemKey(context, null);
|
|
Debug.Assert(_key != null, "_key != null");
|
|
}
|
|
|
|
|
|
if (varyByContentEncodings == null && varyByHeaders == null && varyByParams == null && settings.VaryByCustom == null) {
|
|
/*
|
|
* This is not a varyBy item.
|
|
*/
|
|
keyRawResponse = _key;
|
|
cachedVary = null;
|
|
}
|
|
else {
|
|
/*
|
|
* There is a vary in the cache policy. We handle this
|
|
* by adding another item to the cache which contains
|
|
* a list of the vary headers. A request for the item
|
|
* without the vary headers in the key will return this
|
|
* item. From the headers another key can be constructed
|
|
* to lookup the item with the raw response.
|
|
*/
|
|
if (varyByHeaders != null) {
|
|
for (i = 0, n = varyByHeaders.Length; i < n; i++) {
|
|
varyByHeaders[i] = "HTTP_" + CultureInfo.InvariantCulture.TextInfo.ToUpper(
|
|
varyByHeaders[i].Replace('-', '_'));
|
|
}
|
|
}
|
|
|
|
varyByAllParams = false;
|
|
if (varyByParams != null) {
|
|
varyByAllParams = (varyByParams.Length == 1 && varyByParams[0] == ASTERISK);
|
|
if (varyByAllParams) {
|
|
varyByParams = null;
|
|
}
|
|
else {
|
|
for (i = 0, n = varyByParams.Length; i < n; i++) {
|
|
varyByParams[i] = CultureInfo.InvariantCulture.TextInfo.ToLower(varyByParams[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
cachedVary = new CachedVary(varyByContentEncodings, varyByHeaders, varyByParams, varyByAllParams, settings.VaryByCustom);
|
|
keyRawResponse = CreateOutputCachedItemKey(context, cachedVary);
|
|
if (keyRawResponse == null) {
|
|
Debug.Trace("OutputCacheModuleLeave", "Couldn't add non-cacheable post.\n\tkey=" + _key);
|
|
return;
|
|
}
|
|
|
|
// it is possible that the user code calculating custom vary-by
|
|
// string would Flush making the response non-cacheable. Check fo it here.
|
|
if (!response.IsBuffered()) {
|
|
Debug.Trace("OutputCacheModuleLeave", "Response.Flush() inside GetVaryByCustomString\n\tkey=" + _key);
|
|
return;
|
|
}
|
|
}
|
|
|
|
DateTime utcExpires = Cache.NoAbsoluteExpiration;
|
|
TimeSpan slidingDelta = Cache.NoSlidingExpiration;
|
|
|
|
if (settings.SlidingExpiration) {
|
|
slidingDelta = settings.SlidingDelta;
|
|
}
|
|
else if (settings.IsMaxAgeSet) {
|
|
DateTime utcTimestamp = (settings.UtcTimestampCreated != DateTime.MinValue) ? settings.UtcTimestampCreated : context.UtcTimestamp;
|
|
utcExpires = utcTimestamp + settings.MaxAge;
|
|
}
|
|
else if (settings.IsExpiresSet) {
|
|
utcExpires = settings.UtcExpires;
|
|
}
|
|
|
|
// Check and ensure that item hasn't expired:
|
|
if (utcExpires > DateTime.UtcNow) {
|
|
|
|
// Create the response object to be sent on cache hits.
|
|
HttpRawResponse httpRawResponse = response.GetSnapshot();
|
|
string kernelCacheUrl = response.SetupKernelCaching(null);
|
|
Guid cachedVaryId = (cachedVary != null) ? cachedVary.CachedVaryId : Guid.Empty;
|
|
CachedRawResponse cachedRawResponse = new CachedRawResponse(httpRawResponse, settings, kernelCacheUrl, cachedVaryId);
|
|
|
|
Debug.Trace("OutputCacheModuleLeave", "Adding response to cache.\n\tkey=" + keyRawResponse);
|
|
|
|
CacheDependency dep = response.CreateCacheDependencyForResponse();
|
|
try {
|
|
OutputCache.InsertResponse(_key, cachedVary,
|
|
keyRawResponse, cachedRawResponse,
|
|
dep,
|
|
utcExpires, slidingDelta);
|
|
}
|
|
catch {
|
|
if (dep != null) {
|
|
dep.Dispose();
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
_key = null;
|
|
|
|
Debug.Trace("OutputCacheModuleLeave", "Returning from OutputCacheModule::Leave");
|
|
}
|
|
}
|
|
}
|