Merge pull request #2250 from esdrubal/master
[mono.git] / mcs / class / referencesource / System.Web / OutputCacheModule.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="OutputCacheModule.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>                                                                
5 //------------------------------------------------------------------------------
6
7 /*
8  * Output cache module
9  */
10 namespace System.Web.Caching {
11     using System.Text;
12     using System.IO;
13     using System.Threading;
14     using System.Collections;
15     using System.Globalization;
16     using System.Security.Cryptography;
17     using System.Web;
18     using System.Web.Caching;
19     using System.Web.Util;
20     using System.Collections.Specialized;
21     using System.Web.Configuration;
22     using System.Web.Management;
23     using System.Web.Hosting;
24     using System.Web.Security.Cryptography;
25
26     /*
27      * Holds header and param names that this cached item varies by.
28      */
29     [Serializable]
30     internal class CachedVary {
31         // _id is used by OutputCacheProviders
32         private           Guid      _cachedVaryId;
33         internal readonly string[]  _contentEncodings;
34         internal readonly string[]  _headers;
35         internal readonly string[]  _params;
36         internal readonly string    _varyByCustom;
37         internal readonly bool      _varyByAllParams;
38
39         internal Guid CachedVaryId { get { return _cachedVaryId; } }
40
41         internal CachedVary(string[] contentEncodings, string[] headers, string[] parameters, bool varyByAllParams, string varyByCustom) {
42             _contentEncodings = contentEncodings;
43             _headers = headers;
44             _params = parameters;
45             _varyByAllParams = varyByAllParams;
46             _varyByCustom = varyByCustom;
47             _cachedVaryId = Guid.NewGuid();
48         }
49
50         public override bool Equals(Object obj) {
51             CachedVary cv = obj as CachedVary;
52             if (cv == null) {
53                 return false;
54             }
55
56             return _varyByAllParams == cv._varyByAllParams
57                 && _varyByCustom == cv._varyByCustom
58                 && StringUtil.StringArrayEquals(_contentEncodings, cv._contentEncodings)
59                 && StringUtil.StringArrayEquals(_headers, cv._headers)
60                 && StringUtil.StringArrayEquals(_params, cv._params);
61         }
62
63         public override int GetHashCode() {
64             HashCodeCombiner hashCodeCombiner = new HashCodeCombiner();
65             hashCodeCombiner.AddObject(_varyByAllParams);
66             
67             // Cast _varyByCustom to an object, since the HashCodeCombiner.AddObject(string)
68             // overload uses StringUtil.GetStringHashCode().  We want to use String.GetHashCode()
69             // in this method, since we do not require a stable hash code across architectures.
70             hashCodeCombiner.AddObject((object)_varyByCustom);
71             
72             hashCodeCombiner.AddArray(_contentEncodings);
73             hashCodeCombiner.AddArray(_headers);
74             hashCodeCombiner.AddArray(_params);
75             return hashCodeCombiner.CombinedHash32;
76         }
77     }
78
79     /*
80      * Holds the cached response.
81      */
82     internal class CachedRawResponse {
83         /*
84          * Fields to store an actual response.
85          */
86         internal          Guid                      _cachedVaryId;
87         internal readonly HttpRawResponse           _rawResponse;
88         internal readonly HttpCachePolicySettings   _settings;
89         internal readonly String                    _kernelCacheUrl;
90
91         internal CachedRawResponse(
92                   HttpRawResponse         rawResponse,
93                   HttpCachePolicySettings settings,
94                   String                  kernelCacheUrl,
95                   Guid                    cachedVaryId) {
96             _rawResponse = rawResponse;
97             _settings = settings;
98             _kernelCacheUrl = kernelCacheUrl;
99             _cachedVaryId = cachedVaryId;
100         }
101     }
102
103     // 
104     //  OutputCacheModule real implementation for premium SKUs
105     //
106
107     sealed class  OutputCacheModule : IHttpModule {
108         const int                   MAX_POST_KEY_LENGTH = 15000;
109         const string                NULL_VARYBY_VALUE = "+n+";
110         const string                ERROR_VARYBY_VALUE = "+e+";
111         internal const string       TAG_OUTPUTCACHE = "OutputCache";
112         const string                OUTPUTCACHE_KEYPREFIX_POST = CacheInternal.PrefixOutputCache + "1";
113         const string                OUTPUTCACHE_KEYPREFIX_GET = CacheInternal.PrefixOutputCache + "2";
114         const string                IDENTITY = "identity";
115         const string                ASTERISK = "*";
116
117         static internal readonly char[] s_fieldSeparators;
118
119         string              _key;
120         bool                _recordedCacheMiss;
121
122         static OutputCacheModule() {
123             s_fieldSeparators = new char[] {',', ' '};
124         }
125
126         internal OutputCacheModule() {
127         }
128
129         internal static string CreateOutputCachedItemKey(
130                 string              path, 
131                 HttpVerb            verb,
132                 HttpContext         context,
133                 CachedVary          cachedVary) {
134
135             StringBuilder       sb;
136             int                 i, j, n;
137             string              name, value;
138             string[]            a;
139             byte[]              buf;
140             HttpRequest         request;
141             NameValueCollection col;
142             int                 contentLength;
143             bool                getAllParams;
144
145             if (verb == HttpVerb.POST) {
146                 sb = new StringBuilder(OUTPUTCACHE_KEYPREFIX_POST, path.Length + OUTPUTCACHE_KEYPREFIX_POST.Length);
147             }
148             else {
149                 sb = new StringBuilder(OUTPUTCACHE_KEYPREFIX_GET, path.Length + OUTPUTCACHE_KEYPREFIX_GET.Length);
150             }
151
152             sb.Append(CultureInfo.InvariantCulture.TextInfo.ToLower(path));
153
154             /* key for cached vary item has additional information */
155             if (cachedVary != null) {
156                 request = context.Request;
157
158                 /* params part */
159                 for (j = 0; j <= 2; j++) {
160                     a = null;
161                     col = null;
162                     getAllParams = false;
163
164                     switch (j) {
165                         case 0:
166                             sb.Append("H");
167                             a = cachedVary._headers;
168                             if (a != null) {
169                                 col = request.GetServerVarsWithoutDemand();
170                             }
171
172                             break;
173
174                         case 1:
175                             Debug.Assert(cachedVary._params == null || !cachedVary._varyByAllParams, "cachedVary._params == null || !cachedVary._varyByAllParams");
176
177                             sb.Append("Q");
178                             a = cachedVary._params;
179                             if (request.HasQueryString && (a != null || cachedVary._varyByAllParams)) {
180                                 col = request.QueryString;
181                                 getAllParams = cachedVary._varyByAllParams;
182                             }
183
184                             break;
185
186                         case 2:
187                         default:
188                             Debug.Assert(cachedVary._params == null || !cachedVary._varyByAllParams, "cachedVary._params == null || !cachedVary._varyByAllParams");
189
190                             sb.Append("F");
191                             if (verb == HttpVerb.POST) {
192                                 a = cachedVary._params;
193                                 if (request.HasForm && (a != null || cachedVary._varyByAllParams)) {
194                                     col = request.Form;
195                                     getAllParams = cachedVary._varyByAllParams;
196                                 }
197                             }
198
199                             break;
200                     }
201
202                     Debug.Assert(a == null || !getAllParams, "a == null || !getAllParams");
203
204                     /* handle all params case (VaryByParams[*] = true) */
205                     if (getAllParams && col.Count > 0) {
206                         a = col.AllKeys;
207                         for (i = a.Length - 1; i >= 0; i--) {
208                             if (a[i] != null)
209                                 a[i] = CultureInfo.InvariantCulture.TextInfo.ToLower(a[i]);
210                         }
211
212                         Array.Sort(a, InvariantComparer.Default);
213                     }
214
215                     if (a != null) {
216                         for (i = 0, n = a.Length; i < n; i++) {
217                             name = a[i];
218                             if (col == null) {
219                                 value = NULL_VARYBY_VALUE;
220                             }
221                             else {
222                                 value = col[name];
223                                 if (value == null) {
224                                     value = NULL_VARYBY_VALUE;
225                                 }
226                             }
227
228                             sb.Append("N");
229                             sb.Append(name);
230                             sb.Append("V");
231                             sb.Append(value);
232                         }
233                     }
234                 }
235
236                 /* custom string part */
237                 sb.Append("C");
238                 if (cachedVary._varyByCustom != null) {
239                     sb.Append("N");
240                     sb.Append(cachedVary._varyByCustom);
241                     sb.Append("V");
242
243                     try {
244                         value = context.ApplicationInstance.GetVaryByCustomString(
245                                 context, cachedVary._varyByCustom);
246                         if (value == null) {
247                             value = NULL_VARYBY_VALUE;
248                         }
249                     }
250                     catch (Exception e) {
251                         value = ERROR_VARYBY_VALUE;
252                         HttpApplicationFactory.RaiseError(e);
253                     }
254
255                     sb.Append(value);
256                 }
257
258                 /* 
259                  * if VaryByParms=*, and method is not a form, then 
260                  * use a cryptographically strong hash of the data as
261                  * part of the key.
262                  */
263                 sb.Append("D");
264                 if (    verb == HttpVerb.POST && 
265                         cachedVary._varyByAllParams && 
266                         request.Form.Count == 0) {
267
268                     contentLength = request.ContentLength;
269                     if (contentLength > MAX_POST_KEY_LENGTH || contentLength < 0) {
270                         return null;
271                     }
272
273                     if (contentLength > 0) {
274                         buf = ((HttpInputStream)request.InputStream).GetAsByteArray();
275                         if (buf == null) {
276                             return null;
277                         }
278
279                         // Use SHA256 to generate a collision-free hash of the input data
280                         value = Convert.ToBase64String(CryptoUtil.ComputeSHA256Hash(buf));
281                         sb.Append(value);
282                     }
283                 }
284
285                 /*
286                  * VaryByContentEncoding
287                  */
288                 sb.Append("E");
289                 string[] contentEncodings = cachedVary._contentEncodings;
290                 if (contentEncodings != null) {
291                     string coding = context.Response.GetHttpHeaderContentEncoding();
292                     if (coding != null) {
293                         for (int k = 0; k < contentEncodings.Length; k++) {
294                             if (contentEncodings[k] == coding) {
295                                 sb.Append(coding);
296                                 break;
297                             }
298                         }
299                     }
300                 }
301
302                 // The key must end in "E", or the VaryByContentEncoding feature will break. Unfortunately, 
303                 // there was no good way to encapsulate the logic within this routine.  See the code in
304                 // OnEnter where we append the result of GetAcceptableEncoding to the key.
305             }
306
307             return sb.ToString();
308         }
309
310         /*
311          * Return a key to lookup a cached response. The key contains 
312          * the path and optionally, vary parameters, vary headers, custom strings,
313          * and form posted data.
314          */
315         string CreateOutputCachedItemKey(HttpContext context, CachedVary cachedVary) {
316             return CreateOutputCachedItemKey(context.Request.Path, context.Request.HttpVerb, context, cachedVary);
317         }
318
319         /*
320          * GetAcceptableEncoding finds an acceptable coding for the given
321          * Accept-Encoding header (see RFC 2616)
322          * returns either i) an acceptable index in contentEncodings, ii) -1 if the identity is acceptable, or iii) -2 if nothing is acceptable
323          */
324         static int GetAcceptableEncoding(string[] contentEncodings, int startIndex, string acceptEncoding) {
325             // The format of Accept-Encoding is ( 1#( codings [ ";" "q" "=" qvalue ] ) | "*" )
326             if (String.IsNullOrEmpty(acceptEncoding)) {
327                 return -1; // use "identity"
328             }
329
330             // is there only one token?
331             int tokenEnd = acceptEncoding.IndexOf(',');
332             if (tokenEnd == -1) {
333                 string acceptEncodingWithoutWeight = acceptEncoding;
334                 // WOS 1984913: is there a weight?
335                 tokenEnd = acceptEncoding.IndexOf(';');
336                 if (tokenEnd > -1) {
337                     // remove weight
338                     int space = acceptEncoding.IndexOf(' ');
339                     if (space > -1 && space < tokenEnd) {
340                         tokenEnd = space;
341                     }
342                     acceptEncodingWithoutWeight = acceptEncoding.Substring(0, tokenEnd);                    
343                     if (ParseWeight(acceptEncoding, tokenEnd) == 0) {
344                         // WOS 1985352 & WOS 1985353: weight is 0, use "identity" only if it is acceptable
345                         bool identityIsAcceptable = acceptEncodingWithoutWeight != IDENTITY && acceptEncodingWithoutWeight != ASTERISK;
346                         return (identityIsAcceptable) ? -1 : -2;
347                     }
348                 }
349                 // WOS 1985353: is this the special "*" symbol?
350                 if (acceptEncodingWithoutWeight == ASTERISK) {
351                     // just return the index of the first entry in the list, since it is acceptable
352                     return 0;
353                 }
354                 for (int i = startIndex; i < contentEncodings.Length; i++) {
355                     if (StringUtil.EqualsIgnoreCase(contentEncodings[i], acceptEncodingWithoutWeight)) {
356                         return i; // found
357                     }
358                 }
359                 return -1; // not found, use "identity"
360             }
361             
362             // there are multiple tokens
363             int bestCodingIndex = -1;
364             double bestCodingWeight = 0;
365             for (int i = startIndex; i < contentEncodings.Length; i++) {
366                 string coding = contentEncodings[i];
367                 // get weight of current coding
368                 double weight = GetAcceptableEncodingHelper(coding, acceptEncoding);
369                 // if it is 1, use it
370                 if (weight == 1) {
371                     return i;
372                 }
373                 // if it is the best so far, remember it
374                 if (weight > bestCodingWeight) {
375                     bestCodingIndex = i;
376                     bestCodingWeight = weight;
377                 }
378             }
379             // WOS 1985352: use "identity" only if it is acceptable
380             if (bestCodingIndex == -1 && !IsIdentityAcceptable(acceptEncoding)) {
381                     bestCodingIndex = -2;
382             }
383             return bestCodingIndex; // coding index with highest weight, possibly -1 or -2
384         }
385
386         // Get the weight of the specified coding from the Accept-Encoding header.
387         // 1 means use this coding.  0 means don't use this coding.  A number between
388         // 1 and 0 must be compared with other codings.  -1 means the coding was not found
389         static double GetAcceptableEncodingHelper(string coding, string acceptEncoding) {
390             double weight = -1;
391             int startSearchIndex = 0;
392             int codingLength = coding.Length;
393             int acceptEncodingLength = acceptEncoding.Length;
394             int maxSearchIndex = acceptEncodingLength - codingLength;
395             while (startSearchIndex < maxSearchIndex) {
396                 int indexStart = acceptEncoding.IndexOf(coding, startSearchIndex, StringComparison.OrdinalIgnoreCase);
397                 
398                 if (indexStart == -1) {
399                     break; // not found
400                 }
401                 
402                 // if index is in middle of string, previous char should be ' ' or ','
403                 if (indexStart != 0) {
404                     char previousChar = acceptEncoding[indexStart-1];
405                     if (previousChar != ' ' && previousChar != ',') {
406                         startSearchIndex = indexStart + 1;
407                         continue; // move index forward and continue searching
408                     }
409                 }
410                 
411                 // the match starts on a token boundary, but it must also end
412                 // on a token boundary ...
413                 
414                 int indexNextChar = indexStart + codingLength;
415                 char nextChar = '\0';
416                 if (indexNextChar < acceptEncodingLength) {
417                     nextChar = acceptEncoding[indexNextChar];
418                     while (nextChar == ' ' && ++indexNextChar < acceptEncodingLength) {
419                         nextChar = acceptEncoding[indexNextChar];
420                     }
421                     if (nextChar != ' ' && nextChar != ',' && nextChar != ';') {
422                         startSearchIndex = indexStart + 1;
423                         continue; // move index forward and continue searching
424                     }
425                 }
426                 weight = (nextChar == ';') ? ParseWeight(acceptEncoding, indexNextChar) : 1;
427                 break; // found
428             }
429             return weight;
430         }
431
432         // Gets the weight of the encoding beginning at startIndex.
433         // If Accept-Encoding header is formatted incorrectly, return 1 to short-circuit search.
434         static double ParseWeight(string acceptEncoding, int startIndex) {
435             double weight = 1;
436             int tokenEnd = acceptEncoding.IndexOf(',', startIndex);
437             if (tokenEnd == -1) {
438                 tokenEnd = acceptEncoding.Length;
439             }
440             int qIndex = acceptEncoding.IndexOf('q', startIndex);
441             if (qIndex > -1 && qIndex < tokenEnd) {
442                 int equalsIndex = acceptEncoding.IndexOf('=', qIndex);
443                 if (equalsIndex > -1 && equalsIndex < tokenEnd) {
444                     string s = acceptEncoding.Substring(equalsIndex+1, tokenEnd - (equalsIndex + 1));
445                     double d;
446                     if (Double.TryParse(s, NumberStyles.Float & ~NumberStyles.AllowLeadingSign & ~NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out d)) {
447                         weight = (d >= 0 && d <= 1) ? d : 1; // if format is invalid, short-circut search by returning weight of 1
448                     }
449                 }
450             }
451             return weight;
452         }
453
454         static bool IsIdentityAcceptable(string acceptEncoding) {
455             bool result = true;
456             double identityWeight = GetAcceptableEncodingHelper(IDENTITY, acceptEncoding);
457             if (identityWeight == 0
458                 || (identityWeight <= 0 && GetAcceptableEncodingHelper(ASTERISK, acceptEncoding) == 0)) {
459                 result = false;
460             }
461             return result;
462         }
463
464         static bool IsAcceptableEncoding(string contentEncoding, string acceptEncoding) {
465             if (String.IsNullOrEmpty(contentEncoding)) {
466                 // if Content-Encoding is not set treat it as the identity
467                 contentEncoding = IDENTITY;
468             }
469             if (String.IsNullOrEmpty(acceptEncoding)) {
470                 // only the identity is acceptable if Accept-Encoding is not set
471                 return (contentEncoding == IDENTITY);
472             }
473             double weight = GetAcceptableEncodingHelper(contentEncoding, acceptEncoding);
474             if (weight == 0
475                 || (weight <= 0 && GetAcceptableEncodingHelper(ASTERISK, acceptEncoding) == 0)) {
476                 return false;
477             }
478             return true;
479         }
480
481         /*
482          * Record a cache miss to the perf counters.
483          */
484         void RecordCacheMiss() {
485             if (!_recordedCacheMiss) {
486                 PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_RATIO_BASE);
487                 PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_MISSES);
488                 _recordedCacheMiss = true;
489             }
490         }
491
492
493         /// <devdoc>
494         ///    <para>Initializes the output cache for an application.</para>
495         /// </devdoc>
496         void IHttpModule.Init(HttpApplication app) {
497             OutputCacheSection cacheConfig = RuntimeConfig.GetAppConfig().OutputCache;
498             if (cacheConfig.EnableOutputCache) {
499                 app.ResolveRequestCache += new EventHandler(this.OnEnter);
500                 app.UpdateRequestCache += new EventHandler(this.OnLeave);
501             }
502         }
503
504
505         /// <devdoc>
506         ///    <para>Disposes of items from the output cache.</para>
507         /// </devdoc>
508         void IHttpModule.Dispose() {
509         }
510
511         /*
512          * Try to find this request in the cache. If so, return it. Otherwise,
513          * store the cache key for use on Leave.
514          */
515
516         /// <devdoc>
517         /// <para>Raises the <see langword='Enter'/> 
518         /// event, which searches the output cache for an item to satisfy the HTTP request. </para>
519         /// </devdoc>
520         internal void OnEnter(Object source, EventArgs eventArgs) {
521             Debug.Trace("OutputCacheModuleEnter", "Beginning OutputCacheModule::Enter");
522             _key = null;
523             _recordedCacheMiss = false;
524
525             if (!OutputCache.InUse) {
526                 Debug.Trace("OutputCacheModuleEnter", "Miss, no entries in output Cache" + 
527                             "\nReturning from OutputCacheModule::Enter");
528                 return;
529             }
530
531             HttpApplication             app;
532             HttpContext                 context;
533             string                      key;
534             HttpRequest                 request; 
535             HttpResponse                response; 
536             Object                      item;
537             CachedRawResponse           cachedRawResponse;            
538             HttpCachePolicySettings     settings;
539             int                         i, n;                      
540             bool                        sendBody;                  
541             HttpValidationStatus        validationStatus, validationStatusFinal;                     
542             ValidationCallbackInfo      callbackInfo;
543             string                      ifModifiedSinceHeader;
544             DateTime                    utcIfModifiedSince;
545             string                      etag;
546             string[]                    etags;
547             int                         send304;
548             string                      cacheControl;
549             string[]                    cacheDirectives = null;
550             string                      pragma;
551             string[]                    pragmaDirectives = null;
552             string                      directive;
553             int                         maxage;
554             int                         minfresh;
555             int                         age;
556             int                         fresh;
557             bool                        hasValidationPolicy;    
558             CachedVary                  cachedVary;
559             HttpRawResponse             rawResponse;
560             CachedPathData              cachedPathData;
561
562             app = (HttpApplication)source;
563             context = app.Context;
564             cachedPathData = context.GetFilePathData();
565             request = context.Request;
566             response = context.Response;
567             
568             /*
569              * Check if the request can be resolved for this method.
570              */
571             switch (request.HttpVerb) {
572                 case HttpVerb.HEAD:
573                 case HttpVerb.GET:
574                 case HttpVerb.POST:
575                     break;
576
577                 default:
578                     Debug.Trace("OutputCacheModuleEnter", "Miss, Http method not GET, POST, or HEAD" +
579                                 "\nReturning from OutputCacheModule::Enter");
580     
581                     return;
582             }
583
584             /*
585              * Create a lookup key. Remember the key for use inside Leave()
586              */
587             _key = key = CreateOutputCachedItemKey(context, null);
588             Debug.Assert(_key != null, "_key != null");
589
590             /*
591              *  Lookup the cache vary for this key.
592              */
593             item = OutputCache.Get(key);
594             if (item == null) {
595                 Debug.Trace("OutputCacheModuleEnter", "Miss, item not found.\n\tkey=" + key +
596                             "\nReturning from OutputCacheModule::Enter");
597                 return;
598             }
599
600             // 'item' may be one of the following:
601             //  - a CachedVary object (if the object varies by something)
602             //  - a "no vary" CachedRawResponse object (i.e. it doesn't vary on anything)
603             
604             // Let's assume it's a CacheVary and see what happens.
605             cachedVary = item as CachedVary;
606
607             // If we have one, create a new cache key for it (this is a must)
608             if (cachedVary != null) {
609                 /*
610                  * This cached output has a Vary policy. Create a new key based 
611                  * on the vary headers in cachedRawResponse and try again.
612                  *
613                  * Skip this step if it's a VaryByNone vary policy.
614                  */
615
616                 
617                 key = CreateOutputCachedItemKey(context, cachedVary);
618                 if (key == null) {
619                     Debug.Trace("OutputCacheModuleEnter", "Miss, key could not be created for vary-by item." + 
620                                 "\nReturning from OutputCacheModule::Enter");
621                     
622                     return;
623                 }
624
625                 if (cachedVary._contentEncodings == null) {
626                     // With the new key, look up the in-memory key.
627                     // At this point, we've exhausted the lookups in memory for this item.
628                     item = OutputCache.Get(key);
629                 }
630                 else {
631 #if DBG
632                     Debug.Assert(key[key.Length-1] == 'E', "key[key.Length-1] == 'E'");
633 #endif
634                     item = null;
635                     bool identityIsAcceptable = true;
636                     string acceptEncoding = context.WorkerRequest.GetKnownRequestHeader(HttpWorkerRequest.HeaderAcceptEncoding);
637                     if (acceptEncoding != null) {
638                         string[] contentEncodings = cachedVary._contentEncodings;
639                         int startIndex = 0;
640                         bool done = false;
641                         while (!done) {
642                             done = true;
643                             int index = GetAcceptableEncoding(contentEncodings, startIndex, acceptEncoding);
644                             if (index > -1) {
645 #if DBG                               
646                                 Debug.Trace("OutputCacheModuleEnter", "VaryByContentEncoding key=" + key + contentEncodings[index]);
647 #endif
648                                 identityIsAcceptable = false; // the client Accept-Encoding header contains an encoding that's in the VaryByContentEncoding list
649                                 item = OutputCache.Get(key + contentEncodings[index]);
650                                 if (item == null) {
651                                     startIndex = index+1;
652                                     if (startIndex < contentEncodings.Length) {
653                                         done = false;
654                                     }
655                                 }
656                             }
657                             else if (index == -2) {
658                                 // the identity has a weight of 0 and is not acceptable
659                                 identityIsAcceptable = false;
660                             }
661                         }
662                     }
663                     
664                     // the identity should not be used if the client Accept-Encoding contains an entry in the VaryByContentEncoding list or "identity" is not acceptable
665                     if (item == null && identityIsAcceptable) {
666 #if DBG                               
667                         Debug.Trace("OutputCacheModuleEnter", "VaryByContentEncoding key=" + key);
668 #endif
669                         item = OutputCache.Get(key);   
670                     }
671                 }
672
673                 Debug.Assert(item == null || item is CachedRawResponse, "item == null || item is CachedRawResponse");
674                 if (item == null || ((CachedRawResponse)item)._cachedVaryId != cachedVary.CachedVaryId) {
675 #if DBG
676                     if (item == null) {
677                         Debug.Trace("OutputCacheModuleEnter", "Miss, cVary found, cRawResponse not found.\n\t\tkey=" + key +
678                                     "\nReturning from OutputCacheModule::Enter");
679                     }
680                     else {
681                         string msg = "Miss, _cachedVaryId=" + ((CachedRawResponse)item)._cachedVaryId.ToString() + ", cVary.CachedVaryId=" + cachedVary.CachedVaryId.ToString();
682                         Debug.Trace("OutputCacheModuleEnter", msg + key +
683                                     "\nReturning from OutputCacheModule::Enter");
684                     }
685 #endif
686                     if (item != null) {
687                         // explicitly remove entry because _cachedVaryId does not match
688                         OutputCache.Remove(key, context);
689                     }
690                     return;
691                 }
692             }
693
694             // From this point on, we have an entry to work with.
695
696             Debug.Assert(item is CachedRawResponse, "item is CachedRawResponse");
697             cachedRawResponse = (CachedRawResponse) item;
698             settings = cachedRawResponse._settings;
699             if (cachedVary == null && !settings.IgnoreParams) {
700                 /*
701                  * This cached output has no vary policy, so make sure it doesn't have a query string or form post.
702                  */
703                 if (request.HttpVerb == HttpVerb.POST) {
704                     Debug.Trace("OutputCacheModuleEnter", "Output cache item found but method is POST and no VaryByParam specified." +
705                                 "\n\tkey=" + key +
706                                 "\nReturning from OutputCacheModule::Enter");
707                     RecordCacheMiss();
708                     return;
709                 }
710                 
711                 if (request.HasQueryString) {
712                     Debug.Trace("OutputCacheModuleEnter", "Output cache item found but contains a querystring and no VaryByParam specified." +
713                                 "\n\tkey=" + key +
714                                 "\nReturning from OutputCacheModule::Enter");
715                     RecordCacheMiss();
716                     return;
717                 }
718             }
719
720             if (settings.IgnoreRangeRequests) {
721                 string rangeHeader = request.Headers["Range"];
722                 if (StringUtil.StringStartsWithIgnoreCase(rangeHeader, "bytes")) {
723                     Debug.Trace("OutputCacheModuleEnter", "Output cache item found but this is a Range request and IgnoreRangeRequests is true." +
724                                 "\n\tkey=" + key +
725                                 "\nReturning from OutputCacheModule::Enter");
726                     // Don't record this as a cache miss. The response for a range request is not cached, and so
727                     // we don't want to pollute the cache hit/miss ratio.
728                     return;
729                 }
730             }
731
732             hasValidationPolicy = settings.HasValidationPolicy();
733
734             /*
735              * Determine whether the client can accept a cached copy, and
736              * get values of other cache control directives.
737              * 
738              * We do this after lookup so we don't have to break down the headers
739              * if the item is not found. Cracking the headers is expensive.
740              */
741             if (!hasValidationPolicy) {
742                 cacheControl = request.Headers["Cache-Control"];
743                 if (cacheControl != null) {
744                     cacheDirectives = cacheControl.Split(s_fieldSeparators);
745                     for (i = 0; i < cacheDirectives.Length; i++) {
746                         directive = cacheDirectives[i];
747                         if (directive == "no-cache" || directive == "no-store") {
748                             Debug.Trace("OutputCacheModuleEnter", 
749                                         "Skipping lookup because of Cache-Control: no-cache or no-store directive." + 
750                                         "\nReturning from OutputCacheModule::Enter");
751
752                             RecordCacheMiss();
753                             return;
754                         }
755
756                         if (StringUtil.StringStartsWith(directive, "max-age=")) {
757                             try {
758                                 maxage = Convert.ToInt32(directive.Substring(8), CultureInfo.InvariantCulture);
759                             }
760                             catch {
761                                 maxage = -1;
762                             }
763
764                             if (maxage >= 0) {
765                                 age = (int) ((context.UtcTimestamp.Ticks - settings.UtcTimestampCreated.Ticks) / TimeSpan.TicksPerSecond);
766                                 if (age >= maxage) {
767                                     Debug.Trace("OutputCacheModuleEnter", 
768                                                 "Not returning found item due to Cache-Control: max-age directive." + 
769                                                 "\nReturning from OutputCacheModule::Enter");
770
771                                     RecordCacheMiss();
772                                     return;
773                                 }
774                             }
775                         }
776                         else if (StringUtil.StringStartsWith(directive, "min-fresh=")) {
777                             try {
778                                 minfresh = Convert.ToInt32(directive.Substring(10), CultureInfo.InvariantCulture);
779                             }
780                             catch {
781                                 minfresh = -1;
782                             }
783
784                             if (minfresh >= 0 && settings.IsExpiresSet && !settings.SlidingExpiration) {
785                                 fresh = (int) ((settings.UtcExpires.Ticks - context.UtcTimestamp.Ticks) / TimeSpan.TicksPerSecond);
786                                 if (fresh < minfresh) {
787                                     Debug.Trace("OutputCacheModuleEnter", 
788                                                 "Not returning found item due to Cache-Control: min-fresh directive." + 
789                                                 "\nReturning from OutputCacheModule::Enter");
790
791                                     RecordCacheMiss();
792                                     return;
793                                 }
794                             }
795                         }
796                     }
797                 }
798
799                 pragma = request.Headers["Pragma"];
800                 if (pragma != null) {
801                     pragmaDirectives = pragma.Split(s_fieldSeparators);
802                     for (i = 0; i < pragmaDirectives.Length; i++) {
803                         if (pragmaDirectives[i] == "no-cache") {
804                             Debug.Trace("OutputCacheModuleEnter", 
805                                         "Skipping lookup because of Pragma: no-cache directive." + 
806                                         "\nReturning from OutputCacheModule::Enter");
807
808                             RecordCacheMiss();
809                             return;
810                         }
811                     }
812                 }
813             }
814             else if (settings.ValidationCallbackInfo != null) {
815                 /*
816                  * Check if the item is still valid.
817                  */
818                 validationStatus = HttpValidationStatus.Valid;
819                 validationStatusFinal = validationStatus;
820                 for (i = 0, n = settings.ValidationCallbackInfo.Length; i < n; i++) {
821                     callbackInfo = settings.ValidationCallbackInfo[i];
822                     try {
823                         callbackInfo.handler(context, callbackInfo.data, ref validationStatus);
824                     }
825                     catch (Exception e) {
826                         validationStatus = HttpValidationStatus.Invalid;
827                         HttpApplicationFactory.RaiseError(e);
828                     }
829
830                     switch (validationStatus) {
831                         case HttpValidationStatus.Invalid:
832                             Debug.Trace("OutputCacheModuleEnter", "Output cache item found but callback invalidated it." +
833                                         "\n\tkey=" + key +
834                                         "\nReturning from OutputCacheModule::Enter");
835
836                             OutputCache.Remove(key, context);
837                             RecordCacheMiss();
838                             return;
839
840                         case HttpValidationStatus.IgnoreThisRequest:
841                             validationStatusFinal = HttpValidationStatus.IgnoreThisRequest;
842                             break;
843
844                         case HttpValidationStatus.Valid:
845                             break;
846
847                         default:
848                             Debug.Trace("OutputCacheModuleEnter", "Invalid validation status, ignoring it, status=" + validationStatus + 
849                                         "\n\tkey=" + key);
850
851                             validationStatus = validationStatusFinal;
852                             break;
853                     }
854
855                 }
856
857                 if (validationStatusFinal == HttpValidationStatus.IgnoreThisRequest) {
858                     Debug.Trace("OutputCacheModuleEnter", "Output cache item found but callback status is IgnoreThisRequest." +
859                                 "\n\tkey=" + key +
860                                 "\nReturning from OutputCacheModule::Enter");
861
862
863                     RecordCacheMiss();
864                     return;
865                 }
866
867                 Debug.Assert(validationStatusFinal == HttpValidationStatus.Valid, 
868                              "validationStatusFinal == HttpValidationStatus.Valid");
869             }
870
871             rawResponse = cachedRawResponse._rawResponse;
872
873             // WOS 1985154 ensure Content-Encoding is acceptable
874             if (cachedVary == null || cachedVary._contentEncodings == null) {
875                 string acceptEncoding = request.Headers["Accept-Encoding"];                
876                 string contentEncoding = null;
877                 ArrayList headers = rawResponse.Headers;
878                 if (headers != null) {
879                     foreach (HttpResponseHeader h in headers) {
880                         if (h.Name == "Content-Encoding") {
881                             contentEncoding = h.Value;
882                             break;
883                         }
884                     }
885                 }
886                 if (!IsAcceptableEncoding(contentEncoding, acceptEncoding)) {
887                     RecordCacheMiss();
888                     return;
889                 }
890             }
891
892
893             /*
894              * Try to satisfy a conditional request. The cached response
895              * must satisfy all conditions that are present.
896              * 
897              * We can only satisfy a conditional request if the response
898              * is buffered and has no substitution blocks.
899              * 
900              * N.B. RFC 2616 says conditional requests only occur 
901              * with the GET method, but we try to satisfy other
902              * verbs (HEAD, POST) as well.
903              */
904             send304 = -1;
905
906             if (!rawResponse.HasSubstBlocks) {
907                 /* Check "If-Modified-Since" header */
908                 ifModifiedSinceHeader = request.IfModifiedSince;
909                 if (ifModifiedSinceHeader != null) {
910                     send304 = 0;
911                     try {
912                         utcIfModifiedSince = HttpDate.UtcParse(ifModifiedSinceHeader);
913                         if (    settings.IsLastModifiedSet && 
914                                 settings.UtcLastModified <= utcIfModifiedSince &&
915                                 utcIfModifiedSince <= context.UtcTimestamp) {
916                             
917                             send304 = 1;
918                         }
919                     }
920                     catch {
921                         Debug.Trace("OutputCacheModuleEnter", "Ignore If-Modified-Since header, invalid format: " + ifModifiedSinceHeader);
922                     }
923                 }
924                 
925                 /* Check "If-None-Match" header */
926                 if (send304 != 0) {
927                     etag = request.IfNoneMatch;
928                     if (etag != null) {
929                         send304 = 0;
930                         etags = etag.Split(s_fieldSeparators);
931                         for (i = 0, n = etags.Length; i < n; i++) {
932                             if (i == 0 && etags[i].Equals(ASTERISK)) {
933                                 send304 = 1;
934                                 break;
935                             }
936
937                             if (etags[i].Equals(settings.ETag)) {
938                                 send304 = 1;
939                                 break;
940                             }
941                         }
942                     }
943                 }
944             }
945
946             if (send304 == 1) {
947                 /*
948                  * Send 304 Not Modified
949                  */
950                 Debug.Trace("OutputCacheModuleEnter", "Hit, conditional request satisfied, status=304." + 
951                             "\n\tkey=" + key + 
952                             "\nReturning from OutputCacheModule::Enter");
953
954                 response.ClearAll();
955                 response.StatusCode = 304;
956             }
957             else {
958                 /*
959                  * Send the full response.
960                  */
961 #if DBG
962                 if (send304 == -1) {
963                     Debug.Trace("OutputCacheModuleEnter", "Hit.\n\tkey=" + key +
964                                 "\nReturning from OutputCacheModule::Enter");
965
966                 }
967                 else {
968                     Debug.Trace("OutputCacheModuleEnter", "Hit, but conditional request not satisfied.\n\tkey=" + key +
969                                 "\nReturning from OutputCacheModule::Enter");
970                 }
971 #endif
972
973                 sendBody = (request.HttpVerb != HttpVerb.HEAD);
974
975                 // Check and see if the cachedRawResponse is from the disk
976                 // If so, we must clone the HttpRawResponse before sending it
977
978                 // UseSnapshot calls ClearAll
979                 response.UseSnapshot(rawResponse, sendBody);
980             }
981             
982             response.Cache.ResetFromHttpCachePolicySettings(settings, context.UtcTimestamp);
983
984             // re-insert entry in kernel cache if necessary
985             string originalCacheUrl = cachedRawResponse._kernelCacheUrl;
986             if (originalCacheUrl != null) {
987                 response.SetupKernelCaching(originalCacheUrl);
988             }
989
990             PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_RATIO_BASE);
991             PerfCounters.IncrementCounter(AppPerfCounter.OUTPUT_CACHE_HITS);
992             _key = null;
993             _recordedCacheMiss = false;
994             
995             app.CompleteRequest();
996         }
997
998
999         /*
1000          * If the item is cacheable, add it to the cache.
1001          */
1002
1003         /// <devdoc>
1004         /// <para>Raises the <see langword='Leave'/> event, which causes any cacheable items to 
1005         ///    be put into the output cache.</para>
1006         /// </devdoc>
1007         internal /*public*/ void OnLeave(Object source, EventArgs eventArgs) {
1008             HttpApplication         app;
1009             HttpContext             context;
1010             bool                    cacheable;
1011             CachedVary              cachedVary;
1012             HttpCachePolicy         cache;
1013             HttpCachePolicySettings settings;
1014             string                  keyRawResponse;                
1015             string[]                varyByContentEncodings;
1016             string[]                varyByHeaders;
1017             string[]                varyByParams;
1018             bool                    varyByAllParams;
1019             HttpRequest             request;                        
1020             HttpResponse            response;                       
1021             int                     i, n;
1022             bool                    cacheAuthorizedPage;
1023
1024             Debug.Trace("OutputCacheModuleLeave", "Beginning OutputCacheModule::Leave");
1025
1026             app = (HttpApplication)source;
1027             context = app.Context;
1028             request = context.Request;
1029             response = context.Response;
1030             cache = null;
1031
1032 #if DBG
1033             string  reason = null;
1034 #endif
1035             /*
1036              * Determine whether the response is cacheable.
1037              */
1038             cacheable = false;
1039             do {
1040                 if (!response.HasCachePolicy) {
1041 #if DBG
1042                     reason = "CachePolicy not created, not modified from non-caching default.";
1043 #endif
1044                     break;
1045                 }
1046
1047                 cache = response.Cache;
1048                 if (!cache.IsModified()) {
1049 #if DBG
1050                     reason = "CachePolicy created, but not modified from non-caching default.";
1051 #endif
1052                     break;
1053                 }
1054
1055                 if (response.StatusCode != 200) {
1056 #if DBG
1057                     reason = "response.StatusCode != 200.";
1058 #endif
1059                     break;
1060                 }
1061
1062                 if (request.HttpVerb != HttpVerb.GET && request.HttpVerb != HttpVerb.POST) {
1063 #if DBG
1064                     reason = "the cache can only cache responses to GET and POST.";
1065 #endif
1066                     break;
1067                 }
1068
1069                 if (!response.IsBuffered()) {
1070 #if DBG
1071                     reason = "the response is not buffered.";
1072 #endif
1073                     break;
1074                 }
1075
1076                 /*
1077                  * Change a response with HttpCacheability.Public to HttpCacheability.Private
1078                  * if it requires authorization, and allow it to be cached.
1079                  *
1080                  * Note that setting Cacheability to ServerAndPrivate would accomplish
1081                  * the same thing without needing the "cacheAuthorizedPage" variable,
1082                  * but in RTM we did not have ServerAndPrivate, and setting that value
1083                  * would change the behavior.
1084                  */
1085                 cacheAuthorizedPage = false;
1086                 if (    cache.GetCacheability() == HttpCacheability.Public &&
1087                         context.RequestRequiresAuthorization()) {
1088
1089                     cache.SetCacheability(HttpCacheability.Private);
1090                     cacheAuthorizedPage = true;
1091                 }
1092
1093                 if (    cache.GetCacheability() != HttpCacheability.Public &&
1094                         cache.GetCacheability() != HttpCacheability.ServerAndPrivate && 
1095                         cache.GetCacheability() != HttpCacheability.ServerAndNoCache && 
1096                         !cacheAuthorizedPage) {
1097 #if DBG
1098                     reason = "CachePolicy.Cacheability is not Public, ServerAndPrivate, or ServerAndNoCache.";
1099 #endif
1100                     break;
1101                 }
1102
1103                 if (cache.GetNoServerCaching()) {
1104 #if DBG
1105                     reason = "CachePolicy.NoServerCaching is set.";
1106 #endif
1107                     break;
1108                 }
1109
1110                 // MSRC 11855 (DevDiv 297240 / 362405) - We should suppress output caching for responses which contain non-shareable cookies.
1111                 // We already disable the HTTP.SYS and IIS user mode cache when *any* response cookie is present (see IIS7WorkerRequest.SendUnknownResponseHeader)
1112                 if (response.ContainsNonShareableCookies()) {
1113 #if DBG
1114                     reason = "Non-shareable response cookies were present.";
1115 #endif
1116                     break;
1117                 }
1118
1119                 if (!cache.HasExpirationPolicy() && !cache.HasValidationPolicy()) {
1120 #if DBG
1121                     reason = "CachePolicy has no expiration policy or validation policy.";
1122 #endif
1123                     break;
1124                 }
1125
1126                 if (cache.VaryByHeaders.GetVaryByUnspecifiedParameters()) {
1127 #if DBG
1128                     reason = "CachePolicy.Vary.VaryByUnspecifiedParameters was called.";
1129 #endif
1130                     break;
1131                 }
1132
1133                 if (!cache.VaryByParams.AcceptsParams() && (request.HttpVerb == HttpVerb.POST || request.HasQueryString)) {
1134 #if DBG
1135                     reason = "the cache cannot cache responses to POSTs or GETs with query strings unless Cache.VaryByParams is modified.";
1136 #endif
1137                     break;
1138                 }
1139
1140                 if (cache.VaryByContentEncodings.IsModified() && !cache.VaryByContentEncodings.IsCacheableEncoding(context.Response.GetHttpHeaderContentEncoding())) {
1141 #if DBG
1142                     reason = "the cache cannot cache encoded responses that are not listed in the VaryByContentEncodings collection.";
1143 #endif
1144                     break;
1145                 }
1146
1147                 cacheable = true;
1148             } while (false);
1149             
1150             /*
1151              * Add response to cache.
1152              */
1153             if (!cacheable) {
1154 #if DBG
1155                 Debug.Assert(reason != null, "reason != null");
1156                 Debug.Trace("OutputCacheModuleLeave", "Item is not output cacheable because " + reason + 
1157                             "\n\tUrl=" + request.Path + 
1158                             "\nReturning from OutputCacheModule::Leave");
1159 #endif
1160
1161                 return;
1162             }
1163
1164             RecordCacheMiss();
1165
1166             settings = cache.GetCurrentSettings(response);
1167
1168             varyByContentEncodings = settings.VaryByContentEncodings;
1169
1170             varyByHeaders = settings.VaryByHeaders;
1171             if (settings.IgnoreParams) {
1172                 varyByParams = null;
1173             }
1174             else {
1175                 varyByParams = settings.VaryByParams;
1176             }
1177
1178
1179             /* Create the key if it was not created in OnEnter */
1180             if (_key == null) {
1181                 _key = CreateOutputCachedItemKey(context, null);
1182                 Debug.Assert(_key != null, "_key != null");
1183             }
1184
1185
1186             if (varyByContentEncodings == null && varyByHeaders == null && varyByParams == null && settings.VaryByCustom == null) {
1187                 /*
1188                  * This is not a varyBy item.
1189                  */
1190                 keyRawResponse = _key;
1191                 cachedVary = null;
1192             }
1193             else {
1194                 /*
1195                  * There is a vary in the cache policy. We handle this
1196                  * by adding another item to the cache which contains
1197                  * a list of the vary headers. A request for the item
1198                  * without the vary headers in the key will return this 
1199                  * item. From the headers another key can be constructed
1200                  * to lookup the item with the raw response.
1201                  */
1202                 if (varyByHeaders != null) {
1203                     for (i = 0, n = varyByHeaders.Length; i < n; i++) {
1204                         varyByHeaders[i] = "HTTP_" + CultureInfo.InvariantCulture.TextInfo.ToUpper(
1205                                 varyByHeaders[i].Replace('-', '_'));
1206                     }
1207                 }
1208
1209                 varyByAllParams = false;
1210                 if (varyByParams != null) {
1211                     varyByAllParams = (varyByParams.Length == 1 && varyByParams[0] == ASTERISK);
1212                     if (varyByAllParams) {
1213                         varyByParams = null;
1214                     }
1215                     else {
1216                         for (i = 0, n = varyByParams.Length; i < n; i++) {
1217                             varyByParams[i] = CultureInfo.InvariantCulture.TextInfo.ToLower(varyByParams[i]);
1218                         }
1219                     }
1220                 }
1221
1222                 cachedVary = new CachedVary(varyByContentEncodings, varyByHeaders, varyByParams, varyByAllParams, settings.VaryByCustom);
1223                 keyRawResponse = CreateOutputCachedItemKey(context, cachedVary);
1224                 if (keyRawResponse == null) {
1225                     Debug.Trace("OutputCacheModuleLeave", "Couldn't add non-cacheable post.\n\tkey=" + _key);
1226                     return;
1227                 }
1228
1229                 // it is possible that the user code calculating custom vary-by
1230                 // string would Flush making the response non-cacheable. Check fo it here.
1231                 if (!response.IsBuffered()) {
1232                     Debug.Trace("OutputCacheModuleLeave", "Response.Flush() inside GetVaryByCustomString\n\tkey=" + _key);
1233                     return;
1234                 }
1235             }
1236
1237             DateTime utcExpires = Cache.NoAbsoluteExpiration;
1238             TimeSpan slidingDelta = Cache.NoSlidingExpiration;
1239
1240             if (settings.SlidingExpiration) {
1241                 slidingDelta = settings.SlidingDelta;
1242             }
1243             else if (settings.IsMaxAgeSet) {
1244                 DateTime utcTimestamp = (settings.UtcTimestampCreated != DateTime.MinValue) ? settings.UtcTimestampCreated : context.UtcTimestamp;
1245                 utcExpires = utcTimestamp + settings.MaxAge;
1246             }
1247             else if (settings.IsExpiresSet) {
1248                 utcExpires = settings.UtcExpires;
1249             }
1250             
1251             // Check and ensure that item hasn't expired:
1252             if (utcExpires > DateTime.UtcNow) {
1253
1254                 // Create the response object to be sent on cache hits.
1255                 HttpRawResponse httpRawResponse = response.GetSnapshot();
1256                 string kernelCacheUrl = response.SetupKernelCaching(null);
1257                 Guid cachedVaryId = (cachedVary != null) ? cachedVary.CachedVaryId : Guid.Empty;
1258                 CachedRawResponse cachedRawResponse = new CachedRawResponse(httpRawResponse, settings, kernelCacheUrl, cachedVaryId);
1259
1260                 Debug.Trace("OutputCacheModuleLeave", "Adding response to cache.\n\tkey=" + keyRawResponse);
1261
1262                 CacheDependency dep = response.CreateCacheDependencyForResponse();
1263                 try {
1264                     OutputCache.InsertResponse(_key, cachedVary,
1265                                                keyRawResponse, cachedRawResponse,
1266                                                dep,
1267                                                utcExpires, slidingDelta);
1268                 }
1269                 catch {
1270                     if (dep != null) {
1271                         dep.Dispose();
1272                     }
1273                     throw;
1274                 }
1275             }
1276
1277             _key = null;
1278             
1279             Debug.Trace("OutputCacheModuleLeave", "Returning from OutputCacheModule::Leave");
1280         }
1281     }
1282 }