//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ /* * 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; } } /// /// Initializes the output cache for an application. /// 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); } } /// /// Disposes of items from the output cache. /// void IHttpModule.Dispose() { } /* * Try to find this request in the cache. If so, return it. Otherwise, * store the cache key for use on Leave. */ /// /// Raises the /// event, which searches the output cache for an item to satisfy the HTTP request. /// 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. */ /// /// Raises the event, which causes any cacheable items to /// be put into the output cache. /// 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"); } } }