1 /*++
2 Copyright (c) Microsoft Corporation
3 
4 Module Name:
5 
6     _Rfc2616CacheValidators.cs
7 
8 Abstract:
9     The class implements set of HTTP validators as per RFC2616
10 
11 Author:
12 
13     Alexei Vopilov    21-Dec-2002
14 
15 Revision History:
16 
17 --*/
18 namespace System.Net.Cache {
19 using System;
20 using System.Net;
21 using System.IO;
22 using System.Globalization;
23 using System.Collections;
24 using System.Collections.Specialized;
25 
26 
27     //
28     // Caching RFC
29     //
30     internal class Rfc2616 {
31 
Rfc2616()32         private Rfc2616() {
33         }
34 
35         internal enum TriState {
36             Unknown,
37             Valid,
38             Invalid
39         }
40 
41         /*----------*/
42         // Continue           = Proceed to the next protocol stage.
43         // DoNotTakeFromCache = Don't used caches value for this request
44         // DoNotUseCache      = Cache is not used for this request and response is not cached.
OnValidateRequest(HttpRequestCacheValidator ctx)45         public static CacheValidationStatus OnValidateRequest(HttpRequestCacheValidator ctx)
46         {
47 
48             CacheValidationStatus  result = Common.OnValidateRequest(ctx);
49 
50             if (result == CacheValidationStatus.DoNotUseCache)
51             {
52                 return result;
53             }
54 
55             /*
56                HTTP/1.1 caches SHOULD treat "Pragma: no-cache" as if the client had
57                sent "Cache-Control: no-cache". No new Pragma directives will be
58                defined in HTTP.
59 
60                we use above information to remove pragma header (we control it itself)
61             */
62             ctx.Request.Headers.RemoveInternal(HttpKnownHeaderNames.Pragma);
63 
64             /*
65                 we want to control cache-control header as well, any specifi extensions should be done
66                 using a derived validator class and custom policy
67             */
68             ctx.Request.Headers.RemoveInternal(HttpKnownHeaderNames.CacheControl);
69 
70             if (ctx.Policy.Level == HttpRequestCacheLevel.NoCacheNoStore)
71             {
72                 //adjust request headers since retrieval validators will be suppressed upon return.
73                 ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "no-store");
74                 ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "no-cache");
75                 ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.Pragma, "no-cache");
76                 result = CacheValidationStatus.DoNotTakeFromCache;
77             }
78             else if (result == CacheValidationStatus.Continue)
79             {
80                 if (ctx.Policy.Level == HttpRequestCacheLevel.Reload || ctx.Policy.Level == HttpRequestCacheLevel.NoCacheNoStore)
81                 {
82                 //adjust request headers since retrieval validators will be suppressed upon return.
83                 ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "no-cache");
84                 ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.Pragma, "no-cache");
85                 result = CacheValidationStatus.DoNotTakeFromCache;
86                 }
87                 else if (ctx.Policy.Level == HttpRequestCacheLevel.Refresh)
88                 {
89                     //adjust request headers since retrieval validators will be suppressed upon return.
90                     ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "max-age=0");
91                     ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.Pragma, "no-cache");
92                     result = CacheValidationStatus.DoNotTakeFromCache;
93                 }
94                 else if (ctx.Policy.Level == HttpRequestCacheLevel.Default)
95                 {
96                     //Transfer Policy into CacheControl directives
97                     if (ctx.Policy.MinFresh > TimeSpan.Zero) {
98                         ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "min-fresh=" + (int)ctx.Policy.MinFresh.TotalSeconds);
99                     }
100                     if (ctx.Policy.MaxAge != TimeSpan.MaxValue) {
101                         ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "max-age=" + (int)ctx.Policy.MaxAge.TotalSeconds);
102                     }
103                     if (ctx.Policy.MaxStale > TimeSpan.Zero) {
104                         ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "max-stale=" + (int)ctx.Policy.MaxStale.TotalSeconds);
105                     }
106                 }
107                 else if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly || ctx.Policy.Level == HttpRequestCacheLevel.CacheOrNextCacheOnly)
108                 {
109                     // In case other validators will not be called
110                     ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "only-if-cached");
111                 }
112             }
113             return result;
114         }
115         /*----------*/
OnValidateFreshness(HttpRequestCacheValidator ctx)116         public static CacheFreshnessStatus OnValidateFreshness(HttpRequestCacheValidator  ctx)
117         {
118             // This will figure out ctx.CacheAge and ctx.CacheMaxAge memebers
119             CacheFreshnessStatus result = Common.ComputeFreshness(ctx);
120 
121             /*
122                We note one exception to this rule: since some applications have
123                traditionally used GETs and HEADs with query URLs (those containing a
124                "?" in the rel_path part) to perform operations with significant side
125                effects, caches MUST NOT treat responses to such URIs as fresh unless
126                the server provides an explicit expiration time. This specifically
127                means that responses from HTTP/1.0 servers for such URIs SHOULD NOT
128                be taken from a cache. See section 9.1.1 for related information.
129             */
130             if (ctx.Uri.Query.Length != 0) {
131                 if (ctx.CacheHeaders.Expires == null && (ctx.CacheEntry.IsPrivateEntry?ctx.CacheCacheControl.MaxAge == -1:ctx.CacheCacheControl.SMaxAge == -1)) {
132                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_uri_with_query_has_no_expiration));
133                     return CacheFreshnessStatus.Stale;
134                 }
135                 if (ctx.CacheHttpVersion.Major <= 1 && ctx.CacheHttpVersion.Minor < 1) {
136                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_uri_with_query_and_cached_resp_from_http_10));
137                     return CacheFreshnessStatus.Stale;
138                 }
139             }
140 
141             return result;
142 
143         }
144 
145         /*----------*/
146         // ReturnCachedResponse        =  Return cached response to the application
147         // DoNotTakeFromCache          =  Don't used caches value for this request
148         // Continue                    =  Proceed to the next protocol stage.
OnValidateCache(HttpRequestCacheValidator ctx)149         public static CacheValidationStatus OnValidateCache(HttpRequestCacheValidator ctx)
150         {
151 
152             if (Common.ValidateCacheByVaryHeader(ctx) == TriState.Invalid) {
153                 // RFC 2616 is tricky on this. In theory we could make a conditional request.
154                 // However we rather will not.
155                 // And the reason can be deducted from the RFC definitoin of the response Vary Header.
156                 return CacheValidationStatus.DoNotTakeFromCache;
157             }
158 
159 
160             // For Revalidate option we perform a wire request anyway
161             if (ctx.Policy.Level == HttpRequestCacheLevel.Revalidate) {
162                 return Common.TryConditionalRequest(ctx);
163             }
164 
165             if (Common.ValidateCacheBySpecialCases(ctx) == TriState.Invalid)
166             {
167                 // This takes over the cache policy since the cache content may be sematically incorrect
168                 if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly) {
169                     // Cannot do a wire request
170                     return CacheValidationStatus.DoNotTakeFromCache;
171                 }
172                 return Common.TryConditionalRequest(ctx);
173             }
174 
175             // So now we have either fresh or stale entry that might be used in place of the response
176             // At this point it's safe to consider cache freshness and effective Policy as the core decision rules
177             // Reminder: This method should not be executed with Level >= CacheLevel.Refresh
178 
179             bool enoughFresh = Common.ValidateCacheByClientPolicy(ctx);
180 
181             if (enoughFresh || ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly || ctx.Policy.Level == HttpRequestCacheLevel.CacheIfAvailable || ctx.Policy.Level == HttpRequestCacheLevel.CacheOrNextCacheOnly)
182             {
183                 // The freshness does not matter, check does user requested Range fits into cached entry
184                 CacheValidationStatus result = Common.TryResponseFromCache(ctx);
185 
186                 if (result != CacheValidationStatus.ReturnCachedResponse) {
187                     if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly) {
188                         // Cannot do a wire request
189                         return CacheValidationStatus.DoNotTakeFromCache;
190                     }
191                     return result;
192                 }
193 
194                 if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_valid_as_fresh_or_because_policy, ctx.Policy.ToString()));
195                 return CacheValidationStatus.ReturnCachedResponse;
196             }
197             // This will return either Continue=conditional request or DoNotTakeFromCache==Unconditional request
198             return Common.TryConditionalRequest(ctx);
199         }
200 
201         /*----------*/
202         // Returns
203         // RetryResponseFromServer     =  Retry this request as the result of invalid response received
204         // Continue                    =  The response can be accepted
OnValidateResponse(HttpRequestCacheValidator ctx)205         public static CacheValidationStatus OnValidateResponse(HttpRequestCacheValidator  ctx)
206         {
207             //
208             // At this point we assume that policy >= CacheOrNextCacheOnly && policy < Refresh
209             //
210 
211 
212             // If there was a retry already, it should go with cache disabled so by default we won't retry it again
213             if (ctx.ResponseCount > 1) {
214                 if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_accept_based_on_retry_count, ctx.ResponseCount));
215                 return CacheValidationStatus.Continue;
216             }
217 
218             // We don't convert user-range request to a conditional one
219             if (ctx.RequestRangeUser) {
220                 // was a user range request, we did not touch it.
221                 return CacheValidationStatus.Continue;
222             }
223 
224             //If a live response has older Date, then request should be retried
225             if (ctx.CacheDate != DateTime.MinValue &&
226                 ctx.ResponseDate != DateTime.MinValue &&
227                 ctx.CacheDate > ctx.ResponseDate) {
228                 if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_date_header_older_than_cache_entry));
229                 Common.ConstructUnconditionalRefreshRequest(ctx);
230                 return CacheValidationStatus.RetryResponseFromServer;
231             }
232 
233             HttpWebResponse resp = ctx.Response as HttpWebResponse;
234             if (ctx.RequestRangeCache && resp.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) {
235 
236                 if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_server_didnt_satisfy_range, ctx.Request.Headers[HttpKnownHeaderNames.Range]));
237                 Common.ConstructUnconditionalRefreshRequest(ctx);
238                 return CacheValidationStatus.RetryResponseFromServer;
239             }
240 
241 
242             if (resp.StatusCode == HttpStatusCode.NotModified)
243             {
244                 if (ctx.RequestIfHeader1 == null)
245                 {
246                     // something is really broken on the wire
247                     if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_304_received_on_unconditional_request));
248                     Common.ConstructUnconditionalRefreshRequest(ctx);
249                     return CacheValidationStatus.RetryResponseFromServer;
250                 }
251                 else if (ctx.RequestRangeCache)
252                 {
253                     // The way _we_ create range requests shoyuld never result in 304
254                     if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_304_received_on_unconditional_request_expected_200_206));
255                     Common.ConstructUnconditionalRefreshRequest(ctx);
256                     return CacheValidationStatus.RetryResponseFromServer;
257                 }
258             }
259 
260             if (ctx.CacheHttpVersion.Major <= 1 && resp.ProtocolVersion.Major <=1 &&
261                 ctx.CacheHttpVersion.Minor < 1  && resp.ProtocolVersion.Minor <1 &&
262                 ctx.CacheLastModified > ctx.ResponseLastModified)
263             {
264                 if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_last_modified_header_older_than_cache_entry));
265                 // On http <= 1.0 cache LastModified > resp LastModified
266                 Common.ConstructUnconditionalRefreshRequest(ctx);
267                 return CacheValidationStatus.RetryResponseFromServer;
268             }
269 
270             if (ctx.Policy.Level == HttpRequestCacheLevel.Default && ctx.ResponseAge != TimeSpan.MinValue) {
271                 // If the client has requested MaxAge/MinFresh/MaxStale
272                 // check does the response meet the requirements
273                 if ( (ctx.ResponseAge > ctx.Policy.MaxAge) ||
274                      (ctx.ResponseExpires != DateTime.MinValue &&
275                      (ctx.Policy.MinFresh > TimeSpan.Zero &&  (ctx.ResponseExpires - DateTime.UtcNow) <  ctx.Policy.MinFresh) ||
276                      (ctx.Policy.MaxStale > TimeSpan.Zero &&  (DateTime.UtcNow - ctx.ResponseExpires) >  ctx.Policy.MaxStale)))
277                 {
278                     if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_freshness_outside_policy_limits));
279                     Common.ConstructUnconditionalRefreshRequest(ctx);
280                     return CacheValidationStatus.RetryResponseFromServer;
281                 }
282             }
283 
284             //Cleanup what we've done to this request since protcol can resubmit for auth or redirect.
285             if (ctx.RequestIfHeader1 != null) {
286                 ctx.Request.Headers.RemoveInternal(ctx.RequestIfHeader1);
287                 ctx.RequestIfHeader1 = null;
288             }
289             if (ctx.RequestIfHeader2 != null) {
290                 ctx.Request.Headers.RemoveInternal(ctx.RequestIfHeader2);
291                 ctx.RequestIfHeader2 = null;
292             }
293             if (ctx.RequestRangeCache) {
294                 ctx.Request.Headers.RemoveInternal(HttpKnownHeaderNames.Range);
295                 ctx.RequestRangeCache = false;
296             }
297             return CacheValidationStatus.Continue;
298         }
299 
300         /*----------*/
301         // Returns:
302         // CacheResponse               = Replace cache entry with received live response
303         // UpdateResponseInformation   = Update Metadata of cache entry using live response headers
304         // RemoveFromCache             = Remove cache entry referenced to by a cache key.
305         // Continue                    = Simply do not update cache.
306         //
OnUpdateCache(HttpRequestCacheValidator ctx)307         public static CacheValidationStatus OnUpdateCache(HttpRequestCacheValidator ctx) {
308 
309             // Below condition is to get rid of a broken cache entry, we cannot update cache in that case
310             if (ctx.CacheStatusCode == HttpStatusCode.NotModified) {
311                 if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_need_to_remove_invalid_cache_entry_304));
312                 return CacheValidationStatus.RemoveFromCache;
313             }
314 
315             HttpWebResponse resp = ctx.Response as HttpWebResponse;
316             if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_status, resp.StatusCode));
317 
318 
319             /*********
320                 Vs Whidbey#127214
321                 It was decided not to play with ResponseContentLocation in our implementation.
322                 A derived class may still want to play.
323 
324             // Compute new Cache Update Key if Content-Location is present on the response
325             if (ctx.ResponseContentLocation != null) {
326                 if (!Uri.TryParse(ctx.ResponseContentLocation, true, true, out cacheUri)) {
327                     if(Logging.On)Logging.PrintError(Logging.RequestCache, "Cannot parse Uri from Response Content-Location: " + ctx.ResponseContentLocation);
328                     return CacheValidationStatus.RemoveFromCache;
329                 }
330                 if (!cacheUri.IsAbsoluteUri) {
331                     try {
332                         ctx.CacheKey = new Uri(ctx.RequestUri, cacheUri);
333                     }
334                     catch {
335                         return CacheValidationStatus.RemoveFromCache;
336                     }
337                 }
338             }
339             *********/
340 
341             if (ctx.ValidationStatus == CacheValidationStatus.RemoveFromCache) {
342                 return CacheValidationStatus.RemoveFromCache;
343             }
344 
345             CacheValidationStatus noUpdateResult =
346                             (ctx.RequestMethod >= HttpMethod.Post && ctx.RequestMethod <= HttpMethod.Delete || ctx.RequestMethod == HttpMethod.Other)
347                                 ?CacheValidationStatus.RemoveFromCache
348                                 :CacheValidationStatus.DoNotUpdateCache;
349 
350             if (Common.OnUpdateCache(ctx, resp) != TriState.Valid) {
351                 return noUpdateResult;
352             }
353 
354             CacheValidationStatus result = CacheValidationStatus.CacheResponse;
355             ctx.CacheEntry.IsPartialEntry = false;
356 
357             if (resp.StatusCode == HttpStatusCode.NotModified || ctx.RequestMethod == HttpMethod.Head)
358             {
359                 result = CacheValidationStatus.UpdateResponseInformation;
360 
361                 // This may take a shorter path when updating the entry
362                 if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_304_or_request_head));
363                 if (ctx.CacheDontUpdateHeaders) {
364                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_dont_update_cached_headers));
365                     ctx.CacheHeaders = null;
366                     ctx.CacheEntry.ExpiresUtc = ctx.ResponseExpires;
367                     ctx.CacheEntry.LastModifiedUtc = ctx.ResponseLastModified;
368                     if (ctx.Policy.Level == HttpRequestCacheLevel.Default) {
369                         ctx.CacheEntry.MaxStale = ctx.Policy.MaxStale;
370                     }
371                     else {
372                         ctx.CacheEntry.MaxStale = TimeSpan.MinValue;
373                     }
374                     ctx.CacheEntry.LastSynchronizedUtc = DateTime.UtcNow;
375                 }
376                 else {
377                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_update_cached_headers));
378                 }
379             }
380             else if (resp.StatusCode == HttpStatusCode.PartialContent)
381             {
382                 // Check on whether the user requested range can be appended to the cache entry
383                 // We only support combining of non-overlapped increasing bytes ranges
384                 if (ctx.CacheEntry.StreamSize != ctx.ResponseRangeStart && ctx.ResponseRangeStart != 0)
385                 {
386                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_partial_resp_not_combined_with_existing_entry, ctx.CacheEntry.StreamSize, ctx.ResponseRangeStart));
387                     return noUpdateResult;
388                 }
389 
390                 // We might be appending a live stream to cache BUT user has asked for a specific range.
391                 // Hence don't reset CacheStreamOffset here so the protocol will create a cache forwarding stream that will hide first bytes from the user
392                 if (!ctx.RequestRangeUser) {
393                     ctx.CacheStreamOffset = 0;
394                 }
395 
396                 // Below code assumes that a combined response has been given to the user,
397 
398                 Common.ReplaceOrUpdateCacheHeaders(ctx, resp);
399 
400                 ctx.CacheHttpVersion  = resp.ProtocolVersion;
401                 ctx.CacheEntityLength = ctx.ResponseEntityLength;
402                 ctx.CacheStreamLength = ctx.CacheEntry.StreamSize = ctx.ResponseRangeEnd+1;
403                 if (ctx.CacheEntityLength > 0 && ctx.CacheEntityLength == ctx.CacheEntry.StreamSize)
404                 {
405                     //eventually cache is about to store a complete response
406                     Common.Construct200ok(ctx);
407                 }
408                 else
409                     Common.Construct206PartialContent(ctx, 0);
410             }
411             else
412             {
413                 Common.ReplaceOrUpdateCacheHeaders(ctx, resp);
414 
415                 ctx.CacheHttpVersion        = resp.ProtocolVersion;
416                 ctx.CacheStatusCode         = resp.StatusCode;
417                 ctx.CacheStatusDescription  = resp.StatusDescription;
418                 ctx.CacheEntry.StreamSize   = resp.ContentLength;
419             }
420 
421             return result;
422         }
423 
424 
425         //
426         // Implements various cache validation helper methods
427         //
428         internal static class Common {
429             public const string PartialContentDescription = "Partial Content";
430             public const string OkDescription = "OK";
431             //
432             // Implements logic as of the Request caching suitability.
433             //
434             // Returns:
435             // Continue           = Proceed to the next protocol stage.
436             // DoNotTakeFromCache = Don't use cached response for this request
437             // DoNotUseCache      = Cache is not used for this request and response is not cached.
OnValidateRequest(HttpRequestCacheValidator ctx)438             public static CacheValidationStatus OnValidateRequest(HttpRequestCacheValidator ctx) {
439 
440                 /*
441                    Some HTTP methods MUST cause a cache to invalidate an entity. This is
442                    either the entity referred to by the Request-URI, or by the Location
443                    or Content-Location headers (if present). These methods are:
444                    PUT, DELETE, POST.
445 
446                    A cache that passes through requests for methods it does not
447                    understand SHOULD invalidate any entities referred to by the
448                    Request-URI
449                 */
450                 if (ctx.RequestMethod >= HttpMethod.Post && ctx.RequestMethod <= HttpMethod.Delete)
451                 {
452                     if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly)
453                     {
454                         // Throw because the request must hit the wire and it's cache-only policy
455                         ctx.FailRequest(WebExceptionStatus.RequestProhibitedByCachePolicy);
456                     }
457                     // here we could return a hint on removing existing entry, but UpdateCache should handle this case correctly
458                     return CacheValidationStatus.DoNotTakeFromCache;
459                 }
460                 //
461                 // Additionally to said above we can only cache GET or HEAD, for any other methods we request bypassing cache.
462                 //
463                 if (ctx.RequestMethod < HttpMethod.Head || ctx.RequestMethod > HttpMethod.Get )
464                 {
465                     if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly)
466                     {
467                         // Throw because the request must hit the wire and it's cache-only policy
468                         ctx.FailRequest(WebExceptionStatus.RequestProhibitedByCachePolicy);
469                     }
470                     return CacheValidationStatus.DoNotUseCache;
471                 }
472 
473 
474                 if (ctx.Request.Headers[HttpKnownHeaderNames.IfModifiedSince]   != null ||
475                     ctx.Request.Headers[HttpKnownHeaderNames.IfNoneMatch]       != null ||
476                     ctx.Request.Headers[HttpKnownHeaderNames.IfRange]           != null ||
477                     ctx.Request.Headers[HttpKnownHeaderNames.IfMatch]           != null ||
478                     ctx.Request.Headers[HttpKnownHeaderNames.IfUnmodifiedSince] != null )
479                 {
480                     // The _user_ request contains conditonal cache directives
481                     // Those will conflict with the caching engine => do not lookup a cached item.
482                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_request_contains_conditional_header));
483 
484                     if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly)
485                     {
486                         // Throw because the request must hit the wire and it's cache-only policy
487                         ctx.FailRequest(WebExceptionStatus.RequestProhibitedByCachePolicy);
488                     }
489 
490                     return CacheValidationStatus.DoNotTakeFromCache;
491 
492                 }
493                 return CacheValidationStatus.Continue;
494             }
495             //
496             // Implements logic as to compute cache freshness.
497             // Client Policy is not considered
498             //
ComputeFreshness(HttpRequestCacheValidator ctx)499             public static CacheFreshnessStatus ComputeFreshness(HttpRequestCacheValidator ctx) {
500 
501                 if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_now_time, DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture)));
502 
503                 /*
504                      apparent_age = max(0, response_time - date_value);
505                 */
506 
507                 DateTime nowDate = DateTime.UtcNow;
508 
509                 TimeSpan age  = TimeSpan.MaxValue;
510                 DateTime date = ctx.CacheDate;
511 
512                 if (date != DateTime.MinValue) {
513                     age = (nowDate - date);
514                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_age1_date_header, ((int)age.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo), ctx.CacheDate.ToString("r", CultureInfo.InvariantCulture)));
515                 }
516                 else if (ctx.CacheEntry.LastSynchronizedUtc != DateTime.MinValue) {
517                     /*
518                         Another way to compute cache age but only if Date header is absent.
519                     */
520                     age = nowDate - ctx.CacheEntry.LastSynchronizedUtc;
521                     if (ctx.CacheAge != TimeSpan.MinValue) {
522                         age += ctx.CacheAge;
523                     }
524                     if(Logging.On) {
525                         if (ctx.CacheAge != TimeSpan.MinValue)
526                             Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_age1_last_synchronized_age_header, ((int)age.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo), ctx.CacheEntry.LastSynchronizedUtc.ToString("r", CultureInfo.InvariantCulture), ((int)ctx.CacheAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)));
527                         else
528                             Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_age1_last_synchronized, ((int)age.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo), ctx.CacheEntry.LastSynchronizedUtc.ToString("r", CultureInfo.InvariantCulture)));
529                     }
530                 }
531 
532                 /*
533                     corrected_received_age = max(apparent_age, age_value);
534                 */
535                 if (ctx.CacheAge != TimeSpan.MinValue) {
536                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_age2, ((int)ctx.CacheAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)));
537                     if (ctx.CacheAge > age || age == TimeSpan.MaxValue) {
538                         age = ctx.CacheAge;
539                     }
540                 }
541 
542                 // Updating CacheAge ...
543                 // Note we don't account on response "transit" delay
544                 // Also undefined cache entry Age is reported as TimeSpan.MaxValue (which is impossble to get from HTTP)
545                 // Also a negative age is reset to 0 as per RFC
546                 ctx.CacheAge = (age < TimeSpan.Zero? TimeSpan.Zero: age);
547 
548                 // Now we start checking the server specified requirements
549 
550                 /*
551                 The calculation to determine if a response has expired is quite simple:
552                 response_is_fresh = (freshness_lifetime > current_age)
553                 */
554 
555                 // If we managed to compute the Cache Age
556                 if (ctx.CacheAge != TimeSpan.MinValue) {
557 
558                     /*
559                         s-maxage
560                         If a response includes an s-maxage directive, then for a shared
561                         cache (but not for a private cache), the maximum age specified by
562                         this directive overrides the maximum age specified by either the
563                         max-age directive or the Expires header.
564                     */
565                     if (!ctx.CacheEntry.IsPrivateEntry && ctx.CacheCacheControl.SMaxAge != -1) {
566                         ctx.CacheMaxAge = TimeSpan.FromSeconds(ctx.CacheCacheControl.SMaxAge);
567                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_max_age_cache_s_max_age, ((int)ctx.CacheMaxAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)));
568                         if (ctx.CacheAge < ctx.CacheMaxAge) {
569                             return CacheFreshnessStatus.Fresh;
570                         }
571                         return CacheFreshnessStatus.Stale;
572                     }
573 
574                     /*
575                     The max-age directive takes priority over Expires, so if max-age is
576                     present in a response, the calculation is simply:
577                             freshness_lifetime = max_age_value
578                     */
579                     if (ctx.CacheCacheControl.MaxAge != -1) {
580                         ctx.CacheMaxAge = TimeSpan.FromSeconds(ctx.CacheCacheControl.MaxAge);
581                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_max_age_cache_max_age, ((int)ctx.CacheMaxAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)));
582                         if (ctx.CacheAge < ctx.CacheMaxAge) {
583                             return CacheFreshnessStatus.Fresh;
584                         }
585                         return CacheFreshnessStatus.Stale;
586                     }
587                 }
588 
589                 /*
590                  Otherwise, if Expires is present in the response, the calculation is:
591                         freshness_lifetime = expires_value - date_value
592                 */
593                 if (date == DateTime.MinValue) {
594                     date = ctx.CacheEntry.LastSynchronizedUtc;
595                 }
596 
597                 DateTime expiresDate = ctx.CacheEntry.ExpiresUtc;
598                 if (ctx.CacheExpires != DateTime.MinValue && ctx.CacheExpires < expiresDate) {
599                     expiresDate = ctx.CacheExpires;
600                 }
601 
602                 // If absolute Expires and Response Date and Cache Age can be recovered
603                 if (expiresDate != DateTime.MinValue && date != DateTime.MinValue && ctx.CacheAge != TimeSpan.MinValue) {
604                     ctx.CacheMaxAge = expiresDate - date;
605                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_max_age_expires_date, ((int)((expiresDate - date).TotalSeconds)).ToString(NumberFormatInfo.InvariantInfo), expiresDate.ToString("r", CultureInfo.InvariantCulture)));
606                     if (ctx.CacheAge < ctx.CacheMaxAge) {
607                         return CacheFreshnessStatus.Fresh;
608                     }
609                     return CacheFreshnessStatus.Stale;
610                 }
611 
612                 // If absolute Expires can be recovered
613                 if (expiresDate != DateTime.MinValue) {
614                     ctx.CacheMaxAge = expiresDate - DateTime.UtcNow;
615                     //Take absolute Expires value
616                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_max_age_absolute, expiresDate.ToString("r", CultureInfo.InvariantCulture)));
617                     if (expiresDate < DateTime.UtcNow) {
618                         return CacheFreshnessStatus.Fresh;
619                     }
620                     return CacheFreshnessStatus.Stale;
621                 }
622 
623                 /*
624                    If none of Expires, Cache-Control: max-age, or Cache-Control: s-
625                    maxage (see section 14.9.3) appears in the response, and the response
626                    does not include other restrictions on caching, the cache MAY compute
627                    a freshness lifetime using a heuristic. The cache MUST attach Warning
628                    113 to any response whose age is more than 24 hours if such warning
629                    has not already been added.
630 
631                    Also, if the response does have a Last-Modified time, the heuristic
632                    expiration value SHOULD be no more than some fraction of the interval
633                    since that time. A typical setting of this fraction might be 10%.
634 
635                         response_is_fresh = (freshness_lifetime > current_age)
636                */
637 
638                 ctx.HeuristicExpiration = true;
639 
640                 DateTime lastModifiedDate = ctx.CacheEntry.LastModifiedUtc;
641                 if (ctx.CacheLastModified > lastModifiedDate) {
642                     lastModifiedDate = ctx.CacheLastModified;
643                 }                   ctx.CacheMaxAge = ctx.UnspecifiedMaxAge;
644 
645                 if (lastModifiedDate != DateTime.MinValue) {
646                     TimeSpan span = (nowDate - lastModifiedDate);
647                     int maxAgeSeconds = (int)(span.TotalSeconds/10);
648                     ctx.CacheMaxAge = TimeSpan.FromSeconds(maxAgeSeconds);
649                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_no_max_age_use_10_percent, maxAgeSeconds.ToString(NumberFormatInfo.InvariantInfo), lastModifiedDate.ToString("r", CultureInfo.InvariantCulture)));
650                     if (ctx.CacheAge.TotalSeconds < maxAgeSeconds) {
651                         return CacheFreshnessStatus.Fresh;
652                     }
653                     return CacheFreshnessStatus.Stale;
654                 }
655 
656                 // Else we can only rely on UnspecifiedMaxAge hint
657                 ctx.CacheMaxAge = ctx.UnspecifiedMaxAge;
658                 if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_no_max_age_use_default, ((int)(ctx.UnspecifiedMaxAge.TotalSeconds)).ToString(NumberFormatInfo.InvariantInfo)));
659                 if (ctx.CacheMaxAge >= ctx.CacheAge) {
660                     return CacheFreshnessStatus.Fresh;
661                 }
662                 return CacheFreshnessStatus.Stale;
663             }
664 
665             /*
666                 Returns:
667                 - Valid     : The cache can be updated with the response
668                 - Unknown   : The response should not go into cache
669             */
OnUpdateCache(HttpRequestCacheValidator ctx, HttpWebResponse resp)670             internal static TriState OnUpdateCache(HttpRequestCacheValidator ctx, HttpWebResponse resp) {
671                 /*
672                     Quick check on supported methods.
673                 */
674                 if (ctx.RequestMethod != HttpMethod.Head &&
675                     ctx.RequestMethod != HttpMethod.Get  &&
676                     ctx.RequestMethod != HttpMethod.Post) {
677                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_not_a_get_head_post));
678                     return TriState.Unknown;
679                 }
680 
681                 //If the entry did not exist ...
682                 if (ctx.CacheStream == Stream.Null || (int)ctx.CacheStatusCode == 0) {
683                     if(resp.StatusCode == HttpStatusCode.NotModified) {
684                         // Protection from some weird case when user has changed things
685                         if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_cannot_update_cache_if_304));
686                         return TriState.Unknown;
687                     }
688                     if (ctx.RequestMethod == HttpMethod.Head) {
689                         // Protection from some caching Head response when entry does not exist.
690                         if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_cannot_update_cache_with_head_resp));
691                         return TriState.Unknown;
692                     }
693                 }
694 
695 
696                 if (resp == null) {
697                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_http_resp_is_null));
698                     return TriState.Unknown;
699                 }
700 
701                 //
702                 // We assume that ctx.ResponseCacheControl is already updated based on a live response
703                 //
704 
705                 /*
706                 no-store
707                       ... If sent in a response,
708                       a cache MUST NOT store any part of either this response or the
709                       request that elicited it.
710                 */
711                 if (ctx.ResponseCacheControl.NoStore) {
712                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_cache_control_is_no_store));
713                     return TriState.Unknown;
714                 }
715 
716 
717                 /*
718                 If there is neither a cache validator nor an explicit expiration time
719                    associated with a response, we do not expect it to be cached, but
720                    certain caches MAY violate this expectation (for example, when little
721                    or no network connectivity is available). A client can usually detect
722                    that such a response was taken from a cache by comparing the Date
723                    header to the current time.
724                 */
725 
726                 // NOTE: If no Expire and no Validator peresnt we choose to CACHE
727                 //===============================================================
728 
729 
730                 /*
731                     Note: a new response that has an older Date header value than
732                     existing cached responses is not cacheable.
733                 */
734                 if (ctx.ResponseDate != DateTime.MinValue && ctx.CacheDate != DateTime.MinValue) {
735                     if (ctx.ResponseDate < ctx.CacheDate) {
736                         if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_older_than_cache));
737                         return TriState.Unknown;
738                     }
739                 }
740 
741                 /*
742                 public
743                       Indicates that the response MAY be cached by any cache, even if it
744                       would normally be non-cacheable or cacheable only within a non-
745                       shared cache. (See also Authorization, section 14.8, for
746                       additional details.)
747                 */
748                 if (ctx.ResponseCacheControl.Public) {
749                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_cache_control_is_public));
750                     return TriState.Valid;
751                 }
752 
753                 // sometiem public cache can cache a response with "private" directive, subject to other restrictions
754                 TriState result = TriState.Unknown;
755 
756                 /*
757                 private
758                       Indicates that all or part of the response message is intended for
759                       a single user and MUST NOT be cached by a shared cache. This
760                       allows an origin server to state that the specified parts of the
761 
762                       response are intended for only one user and are not a valid
763                       response for requests by other users. A private (non-shared) cache
764                       MAY cache the response.
765                 */
766                 if (ctx.ResponseCacheControl.Private) {
767                     if (!ctx.CacheEntry.IsPrivateEntry) {
768                         if (ctx.ResponseCacheControl.PrivateHeaders == null) {
769                             if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_cache_control_is_private));
770                             return TriState.Unknown;
771                         }
772                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_cache_control_is_private_plus_headers));
773                         for (int i = 0; i < ctx.ResponseCacheControl.PrivateHeaders.Length; ++i) {
774                             ctx.CacheHeaders.Remove(ctx.ResponseCacheControl.PrivateHeaders[i]);
775                             result = TriState.Valid;
776                         }
777                     }
778                     else {
779                         result = TriState.Valid;
780                     }
781                 }
782 
783 
784                 /*
785                     The RFC is funky on no-cache directive.
786                     But the bottom line is sometime you CAN cache no-cache responses.
787 
788                 */
789                 if (ctx.ResponseCacheControl.NoCache)
790                 {
791                         if (ctx.ResponseLastModified == DateTime.MinValue && ctx.Response.Headers.ETag == null) {
792                             if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_revalidation_required));
793                             return TriState.Unknown;
794                         }
795                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_needs_revalidation));
796                         return TriState.Valid;
797                 }
798 
799                 if (ctx.ResponseCacheControl.SMaxAge != -1 || ctx.ResponseCacheControl.MaxAge != -1) {
800                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_allows_caching, ctx.ResponseCacheControl.ToString()));
801                     return TriState.Valid;
802                 }
803 
804                 /*
805                   When a shared cache (see section 13.7) receives a request
806                   containing an Authorization field, it MUST NOT return the
807                   corresponding response as a reply to any other request, unless one
808                   of the following specific exceptions holds:
809 
810                   1. If the response includes the "s-maxage" cache-control
811 
812                   2. If the response includes the "must-revalidate" cache-control
813 
814                   3. If the response includes the "public" cache-control directive,
815                 */
816                 if (!ctx.CacheEntry.IsPrivateEntry && ctx.Request.Headers[HttpKnownHeaderNames.Authorization] != null) {
817                     // we've already passed an opportunity to cache.
818                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_auth_header_and_no_s_max_age));
819                     return TriState.Unknown;
820                 }
821 
822                 /*
823                     POST
824                     Responses to this method are not cacheable, unless the response
825                     includes appropriate Cache-Control or Expires header fields.
826                 */
827                 if (ctx.RequestMethod == HttpMethod.Post && resp.Headers.Expires == null) {
828                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_post_resp_without_cache_control_or_expires));
829                     return TriState.Unknown;
830                 }
831 
832                 /*
833                  A response received with a status code of 200, 203, 206, 300, 301 or
834                    410 MAY be stored by a cache and used in reply to a subsequent
835                    request, subject to the expiration mechanism, unless a cache-control
836                    directive prohibits caching. However, a cache that does not support
837                    the Range and Content-Range headers MUST NOT cache 206 (Partial
838                    Content) responses.
839 
840                    NOTE: We added 304 here which is correct
841                 */
842                 if (resp.StatusCode == HttpStatusCode.NotModified ||
843                     resp.StatusCode == HttpStatusCode.OK ||
844                     resp.StatusCode == HttpStatusCode.NonAuthoritativeInformation ||
845                     resp.StatusCode == HttpStatusCode.PartialContent ||
846                     resp.StatusCode == HttpStatusCode.MultipleChoices ||
847                     resp.StatusCode == HttpStatusCode.MovedPermanently ||
848                     resp.StatusCode == HttpStatusCode.Gone)
849                 {
850                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_valid_based_on_status_code, (int)resp.StatusCode));
851                     return TriState.Valid;
852                 }
853 
854                 /*
855                    A response received with any other status code (e.g. status codes 302
856                    and 307) MUST NOT be returned in a reply to a subsequent request
857                    unless there are cache-control directives or another header(s) that
858                    explicitly allow it. For example, these include the following: an
859                    Expires header (section 14.21); a "max-age", "s-maxage",  "must-
860                    revalidate", "proxy-revalidate", "public" or "private" cache-control
861                    directive (section 14.9).
862                  */
863                 if (result != TriState.Valid) {
864                     // otheriwse there was a "safe" private directive that allows caching
865                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_no_cache_control, (int)resp.StatusCode));
866                 }
867                 return result;
868             }
869 
870             /*----------*/
871             //
872             // This method checks sutability of cached entry based on the client policy.
873             //
874             /*
875                 Returns:
876                 - true      : The cache is still good
877                 - false     : The cache age does not fit into client policy
878             */
ValidateCacheByClientPolicy(HttpRequestCacheValidator ctx)879             public static bool ValidateCacheByClientPolicy(HttpRequestCacheValidator ctx) {
880 
881                 if (ctx.Policy.Level == HttpRequestCacheLevel.Default)
882                 {
883                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_age, (ctx.CacheAge != TimeSpan.MinValue ? ((int)ctx.CacheAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo) : SR.GetString(SR.net_log_unknown)), (ctx.CacheMaxAge != TimeSpan.MinValue? ((int)ctx.CacheMaxAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo): SR.GetString(SR.net_log_unknown))));
884 
885                     if (ctx.Policy.MinFresh > TimeSpan.Zero)
886                     {
887                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_policy_min_fresh, ((int)ctx.Policy.MinFresh.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)));
888                         if (ctx.CacheAge + ctx.Policy.MinFresh >= ctx.CacheMaxAge) {return false;}
889                     }
890 
891                     if (ctx.Policy.MaxAge != TimeSpan.MaxValue)
892                     {
893                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_policy_max_age, ((int)ctx.Policy.MaxAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)));
894                         if (ctx.CacheAge >= ctx.Policy.MaxAge) {return false;}
895                     }
896 
897                     if (ctx.Policy.InternalCacheSyncDateUtc != DateTime.MinValue)
898                     {
899                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_policy_cache_sync_date, ctx.Policy.InternalCacheSyncDateUtc.ToString("r", CultureInfo.CurrentCulture), ctx.CacheEntry.LastSynchronizedUtc.ToString(CultureInfo.CurrentCulture)));
900                         if (ctx.CacheEntry.LastSynchronizedUtc < ctx.Policy.InternalCacheSyncDateUtc) {
901                             return false;
902                         }
903                     }
904 
905                     TimeSpan adjustedMaxAge = ctx.CacheMaxAge;
906                     if (ctx.Policy.MaxStale > TimeSpan.Zero)
907                     {
908                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_policy_max_stale, ((int)ctx.Policy.MaxStale.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)));
909                         if (adjustedMaxAge < TimeSpan.MaxValue - ctx.Policy.MaxStale)
910                         {
911                             adjustedMaxAge = adjustedMaxAge + ctx.Policy.MaxStale;
912                         }
913                         else
914                         {
915                             adjustedMaxAge = TimeSpan.MaxValue;
916                         }
917 
918                         if (ctx.CacheAge >= adjustedMaxAge)
919                             return false;
920                         else
921                             return true;
922                     }
923 
924                 }
925                 // not stale means "fresh enough"
926                 return ctx.CacheFreshnessStatus == CacheFreshnessStatus.Fresh;
927             }
928 
929             /*
930                 This Validator should be called ONLY before submitting any response
931             */
932             /*
933                 Returns:
934                 - Valid     : Cache can be returned to the app subject to effective policy
935                 - Invalid   : A Conditional request MUST be made (unconditional request is also fine)
936             */
ValidateCacheBySpecialCases(HttpRequestCacheValidator ctx)937             internal static TriState ValidateCacheBySpecialCases(HttpRequestCacheValidator  ctx) {
938 
939                 /*
940                    no-cache
941                        If the no-cache directive does not specify a field-name, then a
942                       cache MUST NOT use the response to satisfy a subsequent request
943                       without successful revalidation with the origin server. This
944                       allows an origin server to prevent caching even by caches that
945                       have been configured to return stale responses to client requests.
946                 */
947                 if (ctx.CacheCacheControl.NoCache) {
948                     if (ctx.CacheCacheControl.NoCacheHeaders == null)
949                     {
950                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_control_no_cache));
951                         return TriState.Invalid;
952                     }
953                     /*
954                         If the no-cache directive does specify one or more field-names, then a cache MAY
955                         use the response to satisfy a subsequent request, subject to any other restrictions
956                         on caching.
957                         However, the specified field-name(s) MUST NOT be sent in the response to
958                         a subsequent request without successful revalidation with the origin server.
959                         This allows an origin server to prevent the re-use of certain header fields
960                         in a response, while still allowing caching of the rest of the response.
961                     */
962                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_control_no_cache_removing_some_headers));
963                     for (int i = 0; i < ctx.CacheCacheControl.NoCacheHeaders.Length; ++i) {
964                         ctx.CacheHeaders.Remove(ctx.CacheCacheControl.NoCacheHeaders[i]);
965                     }
966                 }
967 
968                 /*
969                  must-revalidate
970 
971                     When the must-revalidate
972                     directive is present in a response received by a cache, that cache
973                     MUST NOT use the entry after it becomes stale to respond to a
974                     subsequent request without first revalidating it with the origin
975                     server. (I.e., the cache MUST do an end-to-end revalidation every
976                     time, if, based solely on the origin server's Expires or max-age
977                     value, the cached response is stale.)
978 
979                  proxy-revalidate
980                     The proxy-revalidate directive has the same meaning as the must-
981                     revalidate directive, except that it does not apply to non-shared
982                     user agent caches.
983                 */
984                 if (ctx.CacheCacheControl.MustRevalidate ||
985                     (!ctx.CacheEntry.IsPrivateEntry && ctx.CacheCacheControl.ProxyRevalidate))
986                 {
987                     if (ctx.CacheFreshnessStatus != CacheFreshnessStatus.Fresh) {
988                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_control_must_revalidate));
989                         return TriState.Invalid;
990                     }
991                 }
992                 /*
993                   When a shared cache (see section 13.7) receives a request
994                   containing an Authorization field, it MUST NOT return the
995                   corresponding response as a reply to any other request, unless one
996                   of the following specific exceptions holds:
997 
998                   1. If the response includes the "s-maxage" cache-control
999                      directive, the cache MAY use that response in replying to a
1000                      subsequent request. But (if the specified maximum age has
1001                      passed) a proxy cache MUST first revalidate it with the origin
1002                      server, using the request-headers from the new request to allow
1003                      the origin server to authenticate the new request. (This is the
1004                      defined behavior for s-maxage.) If the response includes "s-
1005                      maxage=0", the proxy MUST always revalidate it before re-using
1006                      it.
1007 
1008                   2. If the response includes the "must-revalidate" cache-control
1009                      directive, the cache MAY use that response in replying to a
1010                      subsequent request. But if the response is stale, all caches
1011                      MUST first revalidate it with the origin server, using the
1012                      request-headers from the new request to allow the origin server
1013                      to authenticate the new request.
1014 
1015                   3. If the response includes the "public" cache-control directive,
1016                      it MAY be returned in reply to any subsequent request.
1017                 */
1018                 if (ctx.Request.Headers[HttpKnownHeaderNames.Authorization] != null) {
1019                     if (ctx.CacheFreshnessStatus != CacheFreshnessStatus.Fresh) {
1020                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_cached_auth_header));
1021                         return TriState.Invalid;
1022                     }
1023 
1024                     if (!ctx.CacheEntry.IsPrivateEntry && ctx.CacheCacheControl.SMaxAge == -1 && !ctx.CacheCacheControl.MustRevalidate && !ctx.CacheCacheControl.Public) {
1025                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_cached_auth_header_no_control_directive));
1026                         return TriState.Invalid;
1027                     }
1028                 }
1029                 return TriState.Valid;
1030             }
1031 
1032 
1033             //
1034             // Second Time (after response) cache validation always goes through this method.
1035             //
1036             // Returns
1037             // - ReturnCachedResponse   = Take from cache, cache stream may be replaced and response stream is closed
1038             // - DoNotTakeFromCache     = Disregard the cache
1039             // - RemoveFromCache        = Disregard and remove cache entry
1040             // - CombineCachedAndServerResponse  = The combined cache+live stream has been constructed.
1041             //
ValidateCacheAfterResponse(HttpRequestCacheValidator ctx, HttpWebResponse resp)1042             public static CacheValidationStatus ValidateCacheAfterResponse(HttpRequestCacheValidator ctx, HttpWebResponse resp) {
1043 
1044                 if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_after_validation));
1045 
1046                 if ((ctx.CacheStream == Stream.Null || (int)ctx.CacheStatusCode == 0) && resp.StatusCode == HttpStatusCode.NotModified) {
1047                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_status_304));
1048                     return CacheValidationStatus.DoNotTakeFromCache;
1049                 }
1050 
1051                 if (ctx.RequestMethod == HttpMethod.Head) {
1052                     /*
1053                            The response to a HEAD request MAY be cacheable in the sense that the
1054                            information contained in the response MAY be used to update a
1055                            previously cached entity from that resource. If the new field values
1056                            indicate that the cached entity differs from the current entity (as
1057                            would be indicated by a change in Content-Length, Content-MD5, ETag
1058                            or Last-Modified), then the cache MUST treat the cache entry as
1059                            stale.
1060                     */
1061                     bool invalidate = false;
1062 
1063                     if (ctx.ResponseEntityLength != -1 && ctx.ResponseEntityLength != ctx.CacheEntityLength) {
1064                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_head_resp_has_different_content_length));
1065                         invalidate = true;
1066                     }
1067                     if (resp.Headers[HttpKnownHeaderNames.ContentMD5] != ctx.CacheHeaders[HttpKnownHeaderNames.ContentMD5]) {
1068                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_head_resp_has_different_content_md5));
1069                         invalidate = true;
1070                     }
1071                     if (resp.Headers.ETag != ctx.CacheHeaders.ETag) {
1072                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_head_resp_has_different_etag));
1073                         invalidate = true;
1074                     }
1075                     if (resp.StatusCode != HttpStatusCode.NotModified && resp.Headers.LastModified != ctx.CacheHeaders.LastModified)
1076                     {
1077                         if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_304_head_resp_has_different_last_modified));
1078                         invalidate = true;
1079                     }
1080                     if (invalidate) {
1081                         if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_existing_entry_has_to_be_discarded));
1082                         return CacheValidationStatus.RemoveFromCache;
1083                     }
1084                 }
1085 
1086                 // If server has returned 206 partial content
1087                 if (resp.StatusCode == HttpStatusCode.PartialContent) {
1088                     /*
1089                            A cache MUST NOT combine a 206 response with other previously cached
1090                            content if the ETag or Last-Modified headers do not match exactly,
1091                            see 13.5.4.
1092                     */
1093 
1094                     // Sometime if ETag has been used the server won't include Last-Modified, which seems to be OK
1095                     if (ctx.CacheHeaders.ETag != ctx.Response.Headers.ETag ||
1096                         (ctx.CacheHeaders.LastModified != ctx.Response.Headers.LastModified
1097                          && (ctx.Response.Headers.LastModified != null || ctx.Response.Headers.ETag == null)))
1098                     {
1099                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_206_resp_non_matching_entry));
1100                         if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_existing_entry_should_be_discarded));
1101                         return CacheValidationStatus.RemoveFromCache;
1102                     }
1103 
1104 
1105                     // check does the live stream fit exactly into our cache tail
1106                     if (ctx.CacheEntry.StreamSize != ctx.ResponseRangeStart) {
1107                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_206_resp_starting_position_not_adjusted));
1108                         return CacheValidationStatus.DoNotTakeFromCache;
1109                     }
1110 
1111                     Common.ReplaceOrUpdateCacheHeaders(ctx, resp);
1112                     if (ctx.RequestRangeUser) {
1113                         // This happens when a response is being downloaded page by page
1114 
1115                         // We request combining the streams
1116                         // A user will see data starting CacheStreamOffset of a combined stream
1117                         ctx.CacheStreamOffset       = ctx.CacheEntry.StreamSize;
1118                         // This is a user response content length
1119                         ctx.CacheStreamLength       = ctx.ResponseRangeEnd - ctx.ResponseRangeStart + 1;
1120                         // This is a new cache stream size
1121                         ctx.CacheEntityLength       = ctx.ResponseEntityLength;
1122 
1123                         ctx.CacheStatusCode         = resp.StatusCode;
1124                         ctx.CacheStatusDescription  = resp.StatusDescription;
1125                         ctx.CacheHttpVersion        = resp.ProtocolVersion;
1126                     }
1127                     else {
1128                         // This happens when previous response was downloaded partly
1129 
1130                         ctx.CacheStreamOffset       = 0;
1131                         ctx.CacheStreamLength       = ctx.ResponseEntityLength;
1132                         ctx.CacheEntityLength       = ctx.ResponseEntityLength;
1133 
1134                         ctx.CacheStatusCode         = HttpStatusCode.OK;
1135                         ctx.CacheStatusDescription  = Common.OkDescription;
1136                         ctx.CacheHttpVersion        = resp.ProtocolVersion;
1137                         ctx.CacheHeaders.Remove(HttpKnownHeaderNames.ContentRange);
1138 
1139                         if (ctx.CacheStreamLength == -1)
1140                             {ctx.CacheHeaders.Remove(HttpKnownHeaderNames.ContentLength);}
1141                         else
1142                             {ctx.CacheHeaders[HttpKnownHeaderNames.ContentLength] = ctx.CacheStreamLength.ToString(NumberFormatInfo.InvariantInfo);}
1143 
1144                     }
1145                     // At this point the protocol should create a combined stream made up of the cached and live streams
1146                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_combined_resp_requested));
1147                     return CacheValidationStatus.CombineCachedAndServerResponse;
1148                 }
1149 
1150                 /*
1151                 304 Not Modified
1152                     The response MUST include the following header fields:
1153 
1154                       - Date, unless its omission is required by section 14.18.1
1155 
1156                    If a clockless origin server obeys these rules, and proxies and
1157                    clients add their own Date to any response received without one (as
1158                    already specified by [RFC 2068], section 14.19), caches will operate
1159                    correctly.
1160 
1161                       - ETag and/or Content-Location, if the header would have been sent
1162                         in a 200 response to the same request
1163 
1164                       - Expires, Cache-Control, and/or Vary, if the field-value might
1165                         differ from that sent in any previous response for the same
1166                         variant
1167                 */
1168 
1169                 if (resp.StatusCode == HttpStatusCode.NotModified) {
1170                     // We will return the response from cache.
1171 
1172                     // We try to avoid to update Cache update in case the server has
1173                     // sent only headers that are "safe" to ignore
1174                     // It's not the best way but WinInet does not work well with headers update.
1175 
1176                     WebHeaderCollection cc = resp.Headers;
1177 
1178                     string  location = null;
1179                     string  etag = null;
1180 
1181                     if ((ctx.CacheExpires != ctx.ResponseExpires) ||
1182                         (ctx.CacheLastModified != ctx.ResponseLastModified) ||
1183                         (ctx.CacheDate != ctx.ResponseDate) ||
1184                         (ctx.ResponseCacheControl.IsNotEmpty) ||
1185                         ((location=cc[HttpKnownHeaderNames.ContentLocation]) != null && location != ctx.CacheHeaders[HttpKnownHeaderNames.ContentLocation]) ||
1186                         ((etag=cc.ETag) != null && etag != ctx.CacheHeaders.ETag)) {
1187                         // Headers have to be updated
1188                         // Note that would allow a new E-Tag header to come in without changing the content.
1189                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_updating_headers_on_304));
1190                         Common.ReplaceOrUpdateCacheHeaders(ctx, resp);
1191                         return CacheValidationStatus.ReturnCachedResponse;
1192                     }
1193 
1194                     //Try to not update headers if they are invariant or the same
1195                     int ignoredHeaders = 0;
1196                     if (etag != null) {
1197                         ++ignoredHeaders;
1198                     }
1199                     if (location != null) {
1200                         ++ignoredHeaders;
1201                     }
1202                     if (ctx.ResponseAge != TimeSpan.MinValue) {
1203                         ++ignoredHeaders;
1204                     }
1205                     if (ctx.ResponseLastModified != DateTime.MinValue) {
1206                         ++ignoredHeaders;
1207                     }
1208                     if (ctx.ResponseExpires != DateTime.MinValue) {
1209                         ++ignoredHeaders;
1210                     }
1211                     if (ctx.ResponseDate != DateTime.MinValue) {
1212                         ++ignoredHeaders;
1213                     }
1214                     if (cc.Via != null) {
1215                         ++ignoredHeaders;
1216                     }
1217                     if (cc[HttpKnownHeaderNames.Connection] != null) {
1218                         ++ignoredHeaders;
1219                     }
1220                     if (cc[HttpKnownHeaderNames.KeepAlive] != null) {
1221                         ++ignoredHeaders;
1222                     }
1223                     if (cc.ProxyAuthenticate != null) {
1224                         ++ignoredHeaders;
1225                     }
1226                     if (cc[HttpKnownHeaderNames.ProxyAuthorization] != null) {
1227                         ++ignoredHeaders;
1228                     }
1229                     if (cc[HttpKnownHeaderNames.TE] != null) {
1230                         ++ignoredHeaders;
1231                     }
1232                     if (cc[HttpKnownHeaderNames.TransferEncoding] != null) {
1233                         ++ignoredHeaders;
1234                     }
1235                     if (cc[HttpKnownHeaderNames.Trailer] != null) {
1236                         ++ignoredHeaders;
1237                     }
1238                     if (cc[HttpKnownHeaderNames.Upgrade] != null) {
1239                         ++ignoredHeaders;
1240                     }
1241 
1242                     if (resp.Headers.Count <= ignoredHeaders) {
1243                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_suppressing_headers_update_on_304));
1244                         ctx.CacheDontUpdateHeaders = true;
1245                     }
1246                     else {
1247                         Common.ReplaceOrUpdateCacheHeaders(ctx, resp);
1248                     }
1249                     return CacheValidationStatus.ReturnCachedResponse;
1250                 }
1251 
1252                 // Any other response
1253                 if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_status_code_not_304_206));
1254                 return CacheValidationStatus.DoNotTakeFromCache;
1255             }
1256 
1257             /*
1258                 Returns:
1259                 - ReturnCachedResponse  : Cache may be returned to the app
1260                 - DoNotTakeFromCache    : Cache must not be returned to the app
1261             */
ValidateCacheOn5XXResponse(HttpRequestCacheValidator ctx)1262             public static CacheValidationStatus ValidateCacheOn5XXResponse(HttpRequestCacheValidator ctx) {
1263                 /*
1264                    If a cache receives a 5xx response while attempting to revalidate an
1265                    entry, it MAY either forward this response to the requesting client,
1266                    or act as if the server failed to respond. In the latter case, it MAY
1267                    return a previously received response unless the cached entry
1268                    includes the "must-revalidate" cache-control directive
1269 
1270                 */
1271                 // Do we have cached item?
1272                 if (ctx.CacheStream == Stream.Null || ctx.CacheStatusCode == (HttpStatusCode)0) {
1273                     return CacheValidationStatus.DoNotTakeFromCache;
1274                 }
1275 
1276                 if (ctx.CacheEntityLength != ctx.CacheEntry.StreamSize || ctx.CacheStatusCode == HttpStatusCode.PartialContent) {
1277                     // Partial cache remains partial, user will not know that.
1278                     // This is because user either did not provide a Range Header or
1279                     // the user range was just forwarded to the server bypassing cache
1280                     return CacheValidationStatus.DoNotTakeFromCache;
1281                 }
1282 
1283                 if (ValidateCacheBySpecialCases(ctx) != TriState.Valid) {
1284                     // This response cannot be used without _successful_ revalidation
1285                     return CacheValidationStatus.DoNotTakeFromCache;
1286                 }
1287 
1288                 if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly || ctx.Policy.Level == HttpRequestCacheLevel.CacheIfAvailable || ctx.Policy.Level == HttpRequestCacheLevel.CacheOrNextCacheOnly)
1289                 {
1290                     // that was a cache only request
1291                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_sxx_resp_cache_only));
1292                     return CacheValidationStatus.ReturnCachedResponse;
1293                 }
1294 
1295                 if (ctx.Policy.Level == HttpRequestCacheLevel.Default || ctx.Policy.Level == HttpRequestCacheLevel.Revalidate)
1296                 {
1297                     if (ValidateCacheByClientPolicy(ctx)) {
1298                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_sxx_resp_can_be_replaced));
1299                         ctx.CacheHeaders.Add(HttpKnownHeaderNames.Warning, HttpRequestCacheValidator.Warning_111);
1300                         return CacheValidationStatus.ReturnCachedResponse;
1301                     }
1302                 }
1303                 return CacheValidationStatus.DoNotTakeFromCache;
1304             }
1305 
1306 
1307             /*
1308                When the cache receives a subsequent request whose Request-URI
1309                specifies one or more cache entries including a Vary header field,
1310                the cache MUST NOT use such a cache entry to construct a response to
1311                the new request unless all of the selecting request-headers present
1312                in the new request match the corresponding stored request-headers in
1313                the original request.
1314 
1315                The selecting request-headers from two requests are defined to match
1316                if and only if the selecting request-headers in the first request can
1317                be transformed to the selecting request-headers in the second request
1318                by adding or removing linear white space (LWS) at places where this
1319                is allowed by the corresponding BNF, and/or combining multiple
1320                message-header fields with the same field name following the rules
1321                about message headers in section 4.2.
1322 
1323                A Vary header field-value of "*" always fails to match and subsequent
1324                requests on that resource can only be properly interpreted by the
1325                origin server.
1326             */
1327             /*
1328                 Returns:
1329                 - Valid     : Vary header values match in both request and cache
1330                 - Invalid   : Vary header values do not match
1331                 - Unknown   : Vary header is not present in cache
1332             */
ValidateCacheByVaryHeader(HttpRequestCacheValidator ctx)1333             internal static TriState ValidateCacheByVaryHeader(HttpRequestCacheValidator  ctx) {
1334                 string[] cacheVary = ctx.CacheHeaders.GetValues(HttpKnownHeaderNames.Vary);
1335                 if (cacheVary == null) {
1336                     return TriState.Unknown;
1337                 }
1338 
1339                 ArrayList varyValues = new ArrayList();
1340                 HttpRequestCacheValidator.ParseHeaderValues(cacheVary,
1341                                                             HttpRequestCacheValidator.ParseValuesCallback,
1342                                                             varyValues);
1343                 if (varyValues.Count == 0) {
1344                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_empty));
1345                     return TriState.Invalid;
1346                 }
1347 
1348                 if (((string)(varyValues[0]))[0] == '*') {
1349                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_contains_asterisks));
1350                     return TriState.Invalid;
1351                 }
1352 
1353                 if (ctx.SystemMeta == null || ctx.SystemMeta.Count == 0) {
1354                     // We keep there previous request headers
1355                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_no_headers_in_metadata));
1356                     return TriState.Invalid;
1357                 }
1358 
1359                 /*
1360                    A Vary field value consisting of a list of field-names signals that
1361                    the representation selected for the response is based on a selection
1362                    algorithm which considers ONLY the listed request-header field values
1363                    in selecting the most appropriate representation. A cache MAY assume
1364                    that the same selection will be made for future requests with the
1365                    same values for the listed field names, for the duration of time for
1366                    which the response is fresh.
1367                 */
1368 
1369                 for (int i = 0; i < varyValues.Count; ++i) {
1370 
1371                     string[] requestValues  = ctx.Request.Headers.GetValues((string)varyValues[i]);
1372                     ArrayList requestFields = new ArrayList();
1373                     if (requestValues != null) {
1374                         HttpRequestCacheValidator.ParseHeaderValues(requestValues,
1375                                                                     HttpRequestCacheValidator.ParseValuesCallback,
1376                                                                     requestFields);
1377                     }
1378 
1379                     string[] cacheValues    =  ctx.SystemMeta.GetValues((string)varyValues[i]);
1380                     ArrayList cacheFields = new ArrayList();
1381                     if (cacheValues != null) {
1382                         HttpRequestCacheValidator.ParseHeaderValues(cacheValues,
1383                                                                 HttpRequestCacheValidator.ParseValuesCallback,
1384                                                                 cacheFields);
1385                     }
1386 
1387                     if (requestFields.Count != cacheFields.Count) {
1388                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_mismatched_count, (string)varyValues[i]));
1389                         return TriState.Invalid;
1390                     }
1391 
1392                     // NB: fields order is significant as per RFC.
1393                     for (int j = 0; j < cacheFields.Count; ++j) {
1394                         if (!AsciiLettersNoCaseEqual((string)cacheFields[j], (string)requestFields[j])) {
1395                             if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_mismatched_field, (string)varyValues[i], (string)cacheFields[j], (string)requestFields[j]));
1396                             return TriState.Invalid;
1397                         }
1398                     }
1399                 }
1400                 if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_match));
1401                 // The Vary header is in cache and all headers values referenced to are equal to those in the Request.
1402                 return TriState.Valid;
1403             }
1404 
1405             // Returns
1406             // - DoNotTakeFromCache = A request shall go as is and the cache will be dropped
1407             // - Continue           = Cache should be preserved and after-response validator should be called
TryConditionalRequest(HttpRequestCacheValidator ctx)1408             public static CacheValidationStatus TryConditionalRequest(HttpRequestCacheValidator ctx) {
1409 
1410                 string ranges;
1411                 TriState isPartial = CheckForRangeRequest(ctx, out ranges);
1412 
1413                 if (isPartial == TriState.Invalid) {
1414                     // This is a user requested range, pass it as is
1415                     return CacheValidationStatus.Continue;
1416                 }
1417 
1418                 if(isPartial == TriState.Valid) {
1419                     // Not all proxy servers, support requesting a range on an FTP
1420                     // command, so to be safe, never try to mix the cache with a range
1421                     // response. Always get the whole thing fresh in the case of FTP
1422                     // over proxy.
1423                     if (ctx is FtpRequestCacheValidator)
1424                         return CacheValidationStatus.DoNotTakeFromCache;
1425                     // We only have a partial response, need to complete it
1426                     if (TryConditionalRangeRequest(ctx)){
1427                         // We can do a conditional range request
1428                         ctx.RequestRangeCache = true;
1429                         ((HttpWebRequest)ctx.Request).AddRange((int)ctx.CacheEntry.StreamSize);
1430                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_range, ctx.Request.Headers[HttpKnownHeaderNames.Range]));
1431                         return CacheValidationStatus.Continue;
1432                     }
1433                     return CacheValidationStatus.DoNotTakeFromCache;
1434                 }
1435 
1436                 //This is not a range request
1437                 return  ConstructConditionalRequest(ctx);
1438             }
1439 
1440 
1441             // Returns:
1442             // ReturnFromCache   = Take it from cache
1443             // DoNotTakeFromCache= Reload from server and disregard current cache
1444             // Continue          = Send a request that may have added a conditional header
TryResponseFromCache(HttpRequestCacheValidator ctx)1445             public static CacheValidationStatus TryResponseFromCache(HttpRequestCacheValidator ctx) {
1446 
1447                 string ranges;
1448                 TriState isRange = CheckForRangeRequest(ctx, out ranges);
1449 
1450                 if (isRange == TriState.Unknown) {
1451                     return  CacheValidationStatus.ReturnCachedResponse;
1452                 }
1453 
1454                 if (isRange == TriState.Invalid) {
1455                     // user range request
1456                     long start = 0;
1457                     long end   = 0;
1458                     long total = 0;
1459 
1460                     if (!GetBytesRange(ranges, ref start, ref end, ref total, true)) {
1461                         if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_range_invalid_format, ranges));
1462                         return CacheValidationStatus.DoNotTakeFromCache;
1463                     }
1464 
1465                     if (start >= ctx.CacheEntry.StreamSize
1466                         || end > ctx.CacheEntry.StreamSize
1467                         || (end == -1 && ctx.CacheEntityLength == -1)
1468                         || (end == -1 && ctx.CacheEntityLength > ctx.CacheEntry.StreamSize)
1469                         || (start == -1 && (end == -1
1470                                             || ctx.CacheEntityLength == -1
1471                                             || (ctx.CacheEntityLength - end >= ctx.CacheEntry.StreamSize))))
1472                     {
1473                         // we don't have such a range in cache
1474                         if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_range_not_in_cache, ranges));
1475                         return  CacheValidationStatus.Continue;
1476                     }
1477 
1478                     if (start == -1) {
1479                         start = ctx.CacheEntityLength - end;
1480                     }
1481 
1482                     if (end <= 0) {
1483                         end = ctx.CacheEntry.StreamSize - 1;
1484                     }
1485 
1486                     ctx.CacheStreamOffset = start;
1487                     ctx.CacheStreamLength = end-start+1;
1488                     Construct206PartialContent(ctx, (int) start);
1489 
1490                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_range_in_cache, ctx.CacheHeaders[HttpKnownHeaderNames.ContentRange]));
1491 
1492                     return CacheValidationStatus.ReturnCachedResponse;
1493                 }
1494                 //
1495                 // Here we got a partially cached response and the user wants a whole response
1496                 //
1497                 if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly &&
1498                     ((object)ctx.Uri.Scheme == (object)Uri.UriSchemeHttp ||
1499                      (object)ctx.Uri.Scheme == (object)Uri.UriSchemeHttps))
1500                 {
1501                     // Here we should strictly report a failure
1502                     // Only for HTTP and HTTPS we choose to return a partial content even user did not ask for it
1503                     ctx.CacheStreamOffset = 0;
1504                     ctx.CacheStreamLength = ctx.CacheEntry.StreamSize;
1505                     Construct206PartialContent(ctx, 0);
1506 
1507                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_partial_resp, ctx.CacheHeaders[HttpKnownHeaderNames.ContentRange]));
1508                     return CacheValidationStatus.ReturnCachedResponse;
1509                 }
1510 
1511                 if (ctx.CacheEntry.StreamSize >= Int32.MaxValue) {
1512                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_entry_size_too_big, ctx.CacheEntry.StreamSize));
1513                     return CacheValidationStatus.DoNotTakeFromCache;
1514                 }
1515 
1516                 if (TryConditionalRangeRequest(ctx)) {
1517                     ctx.RequestRangeCache = true;
1518                     ((HttpWebRequest)ctx.Request).AddRange((int)ctx.CacheEntry.StreamSize);
1519                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_range, ctx.Request.Headers[HttpKnownHeaderNames.Range]));
1520                     return CacheValidationStatus.Continue;
1521                 }
1522                 // This will let an unconditional request go
1523                 return CacheValidationStatus.Continue;
1524             }
1525 
1526             /*
1527                 Discovers the fact that cached response is a partial one.
1528                 Returns:
1529                 - Invalid   : It's a user range request
1530                 - Valid     : It's a partial cached response
1531                 - Unknown   : It's neither a range request nor the cache does have a partial response
1532             */
CheckForRangeRequest(HttpRequestCacheValidator ctx, out string ranges)1533             private static TriState CheckForRangeRequest(HttpRequestCacheValidator ctx, out string ranges) {
1534 
1535                 if ((ranges = ctx.Request.Headers[HttpKnownHeaderNames.Range]) != null) {
1536                     // A request already contains range.
1537                     // The caller will either return it from cache or pass as is to the server
1538                     ctx.RequestRangeUser = true;
1539                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_range_request_range, ctx.Request.Headers[HttpKnownHeaderNames.Range]));
1540                     return TriState.Invalid;
1541                 }
1542 
1543                 if (ctx.CacheStatusCode == HttpStatusCode.PartialContent && ctx.CacheEntityLength == ctx.CacheEntry.StreamSize)
1544                 {
1545                     // this is a whole resposne
1546                     ctx.CacheStatusCode = HttpStatusCode.OK;
1547                     ctx.CacheStatusDescription = Common.OkDescription;
1548                     return TriState.Unknown;
1549                 }
1550                 if (ctx.CacheEntry.IsPartialEntry || (ctx.CacheEntityLength != -1 && ctx.CacheEntityLength != ctx.CacheEntry.StreamSize) || ctx.CacheStatusCode == HttpStatusCode.PartialContent)
1551                 {                    //The cache may contain a partial response
1552                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_could_be_partial, ctx.CacheEntry.StreamSize, ctx.CacheEntityLength));
1553                     return TriState.Valid;
1554                 }
1555 
1556                 return TriState.Unknown;
1557             }
1558 
1559             /*
1560                 HTTP/1.1 clients:
1561 
1562                 - If an entity tag has been provided by the origin server, MUST
1563                 use that entity tag in any cache-conditional request (using If-
1564                 Match or If-None-Match).
1565 
1566                 - If only a Last-Modified value has been provided by the origin
1567                 server, SHOULD use that value in non-subrange cache-conditional
1568                 requests (using If-Modified-Since).
1569 
1570                 - If only a Last-Modified value has been provided by an HTTP/1.0
1571                 origin server, MAY use that value in subrange cache-conditional
1572                 requests (using If-Unmodified-Since:). The user agent SHOULD
1573                 provide a way to disable this, in case of difficulty.
1574 
1575                 - If both an entity tag and a Last-Modified value have been
1576                 provided by the origin server, SHOULD use both validators in
1577                 cache-conditional requests. This allows both HTTP/1.0 and
1578                 HTTP/1.1 caches to respond appropriately.
1579 
1580             */
1581             /*
1582                 Returns:
1583                 - Continue            : Conditional request has been constructed
1584                 - DoNotTakeFromCache  : Conditional request cannot be constructed
1585             */
ConstructConditionalRequest(HttpRequestCacheValidator ctx)1586             public static CacheValidationStatus ConstructConditionalRequest(HttpRequestCacheValidator  ctx) {
1587 
1588                 CacheValidationStatus result = CacheValidationStatus.DoNotTakeFromCache;
1589 
1590                 // The assumption is that a _user_ conditional request was already filtered out
1591 
1592                 bool validator2 = false;
1593                 string str = ctx.CacheHeaders.ETag;
1594                 if (str != null) {
1595                     result = CacheValidationStatus.Continue;
1596                     ctx.Request.Headers[HttpKnownHeaderNames.IfNoneMatch] = str;
1597                     ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfNoneMatch;
1598                     ctx.RequestValidator1 = str;
1599                     validator2 = true;
1600                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_condition_if_none_match, ctx.Request.Headers[HttpKnownHeaderNames.IfNoneMatch]));
1601                 }
1602 
1603                 if (ctx.CacheEntry.LastModifiedUtc != DateTime.MinValue) {
1604                     result = CacheValidationStatus.Continue;
1605                     str = ctx.CacheEntry.LastModifiedUtc.ToString("r", CultureInfo.InvariantCulture);
1606                     ctx.Request.Headers.ChangeInternal(HttpKnownHeaderNames.IfModifiedSince, str);
1607                     if (validator2) {
1608                         ctx.RequestIfHeader2  = HttpKnownHeaderNames.IfModifiedSince;
1609                         ctx.RequestValidator2 = str;
1610                     }
1611                     else {
1612                         ctx.RequestIfHeader1  = HttpKnownHeaderNames.IfModifiedSince;
1613                         ctx.RequestValidator1 = str;
1614                     }
1615                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_condition_if_modified_since, ctx.Request.Headers[HttpKnownHeaderNames.IfModifiedSince]));
1616                 }
1617 
1618                 if(Logging.On) {
1619                     if (result == CacheValidationStatus.DoNotTakeFromCache) {
1620                         Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_cannot_construct_conditional_request));
1621                     }
1622                 }
1623                 return result;
1624             }
1625 
1626 
1627             /*
1628                 Returns:
1629                 - true: Conditional Partial request has been constructed
1630                 - false: Conditional Partial request cannot be constructed
1631             */
TryConditionalRangeRequest(HttpRequestCacheValidator ctx)1632             private static bool TryConditionalRangeRequest(HttpRequestCacheValidator ctx) {
1633                 //
1634                 // The response is partially cached (that has been checked before calling this method)
1635                 //
1636                 if (ctx.CacheEntry.StreamSize >= Int32.MaxValue) {
1637                     //This is a restriction of HttpWebRequest implementation as on 01/28/03
1638                     if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_entry_size_too_big, ctx.CacheEntry.StreamSize));
1639                     return false;
1640                 }
1641 
1642                 /*
1643                     If the entity tag given in the If-Range header matches the current
1644                     entity tag for the entity, then the server SHOULD provide the
1645                     specified sub-range of the entity using a 206 (Partial content)
1646                     response. If the entity tag does not match, then the server SHOULD
1647                     return the entire entity using a 200 (OK) response.
1648                 */
1649                 string str = ctx.CacheHeaders.ETag;
1650                 if (str != null) {
1651                     ctx.Request.Headers[HttpKnownHeaderNames.IfRange] = str;
1652                     ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfRange;
1653                     ctx.RequestValidator1 =str;
1654                     if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_condition_if_range, ctx.Request.Headers[HttpKnownHeaderNames.IfRange]));
1655                     return true;
1656                 }
1657 
1658                 /*
1659                     - If only a Last-Modified value has been provided by an HTTP/1.0
1660                     origin server, MAY use that value in subrange cache-conditional
1661                     requests (using If-Unmodified-Since:). The user agent SHOULD
1662                     provide a way to disable this, in case of difficulty.
1663                 */
1664 
1665                 if (ctx.CacheEntry.LastModifiedUtc != DateTime.MinValue)
1666                 {
1667                     str = ctx.CacheEntry.LastModifiedUtc.ToString("r", CultureInfo.InvariantCulture);
1668                     if (ctx.CacheHttpVersion.Major == 1 && ctx.CacheHttpVersion.Minor == 0)
1669                     {
1670                         // Well If-Unmodified-Since would require an additional request in case it WAS modified
1671                         // A User may want to excerise this path without relying on our implementation.
1672                         if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_conditional_range_not_implemented_on_http_10));
1673                         return false;
1674                     /*
1675                         //Http == 1.0
1676                         ctx.Request.Headers[HttpKnownHeaderNames.IfUnmodifiedSince] = str;
1677                         ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfUnmodifiedSince;
1678                         ctx.RequestValidator1 = str;
1679                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, "Request Condition = If-Unmodified-Since:" + ctx.Request.Headers[HttpKnownHeaderNames.IfUnmodifiedSince]);
1680                         return true;
1681                     */
1682                     }
1683                     else
1684                     {
1685                         //Http > 1.0
1686                         ctx.Request.Headers[HttpKnownHeaderNames.IfRange] = str;
1687                         ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfRange;
1688                         ctx.RequestValidator1 =str;
1689                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_condition_if_range, ctx.Request.Headers[HttpKnownHeaderNames.IfRange]));
1690                         return true;
1691                     }
1692                 }
1693 
1694                 if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_cannot_construct_conditional_range_request));
1695                 //Cannot construct a conditional request
1696                 return false;
1697             }
1698 
1699             //
1700             // A template for 206 response that we serve from cache on a user range request
1701             // It's also used for cache update.
1702             //
Construct206PartialContent(HttpRequestCacheValidator ctx, int rangeStart)1703             public static void Construct206PartialContent(HttpRequestCacheValidator ctx, int rangeStart) {
1704                 ctx.CacheStatusCode         = HttpStatusCode.PartialContent;
1705                 ctx.CacheStatusDescription  = PartialContentDescription;
1706                 if (ctx.CacheHttpVersion == null) {
1707                     ctx.CacheHttpVersion = new Version(1,1);
1708                 }
1709                 string ranges = "bytes " + rangeStart + '-' + (rangeStart + ctx.CacheStreamLength-1) +'/' + (ctx.CacheEntityLength <= 0?"*":ctx.CacheEntityLength.ToString(NumberFormatInfo.InvariantInfo));
1710                 ctx.CacheHeaders[HttpKnownHeaderNames.ContentRange] = ranges;
1711                 ctx.CacheHeaders[HttpKnownHeaderNames.ContentLength] = ctx.CacheStreamLength.ToString(NumberFormatInfo.InvariantInfo);
1712                 ctx.CacheEntry.IsPartialEntry = true;
1713             }
1714             //
1715             // A template for 200 response, used by cache update
1716             //
Construct200ok(HttpRequestCacheValidator ctx)1717             public static void Construct200ok(HttpRequestCacheValidator ctx) {
1718                 ctx.CacheStatusCode         = HttpStatusCode.OK;
1719                 ctx.CacheStatusDescription  = Common.OkDescription;
1720                 if (ctx.CacheHttpVersion == null)
1721                     ctx.CacheHttpVersion = new Version(1,1);
1722 
1723                 ctx.CacheHeaders.Remove(HttpKnownHeaderNames.ContentRange);
1724 
1725                 if (ctx.CacheEntityLength == -1)
1726                     {ctx.CacheHeaders.Remove(HttpKnownHeaderNames.ContentLength);}
1727                 else
1728                     {ctx.CacheHeaders[HttpKnownHeaderNames.ContentLength] = ctx.CacheEntityLength.ToString(NumberFormatInfo.InvariantInfo);}
1729                 ctx.CacheEntry.IsPartialEntry = false;
1730             }
1731             //
1732             // Clear the request from any conditional headers and request no-cache
1733             //
ConstructUnconditionalRefreshRequest(HttpRequestCacheValidator ctx)1734             public static void ConstructUnconditionalRefreshRequest(HttpRequestCacheValidator ctx) {
1735 
1736                 WebHeaderCollection cc = ctx.Request.Headers;
1737                 cc[HttpKnownHeaderNames.CacheControl]="max-age=0";
1738                 cc[HttpKnownHeaderNames.Pragma]="no-cache";
1739                 if (ctx.RequestIfHeader1 != null) {
1740                     cc.RemoveInternal(ctx.RequestIfHeader1);
1741                     ctx.RequestIfHeader1 = null;
1742                 }
1743                 if (ctx.RequestIfHeader2 != null) {
1744                     cc.RemoveInternal(ctx.RequestIfHeader2);
1745                     ctx.RequestIfHeader2 = null;
1746                 }
1747 
1748                 if (ctx.RequestRangeCache) {
1749                     cc.RemoveInternal(HttpKnownHeaderNames.Range);
1750                     ctx.RequestRangeCache = false;
1751                 }
1752             }
1753 
1754             //
1755             // This is called when we have decided to take from Cache or update Cache
1756             //
ReplaceOrUpdateCacheHeaders(HttpRequestCacheValidator ctx, HttpWebResponse resp)1757             public static void ReplaceOrUpdateCacheHeaders(HttpRequestCacheValidator ctx, HttpWebResponse resp) {
1758                 /*
1759                    In other words, the set of end-to-end headers received in the
1760                    incoming response overrides all corresponding end-to-end headers
1761                    stored with the cache entry (except for stored Warning headers with
1762                    warn-code 1xx, which are deleted even if not overridden).
1763 
1764                     This rule does not allow an origin server to use
1765                     a 304 (Not Modified) or a 206 (Partial Content) response to
1766                     entirely delete a header that it had provided with a previous
1767                     response.
1768 
1769                */
1770 
1771                 if (ctx.CacheHeaders == null || (resp.StatusCode != HttpStatusCode.NotModified && resp.StatusCode != HttpStatusCode.PartialContent))
1772                 {
1773                     // existing context is dropped
1774                     ctx.CacheHeaders = new WebHeaderCollection();
1775                 }
1776 
1777                 // Here we preserve Request headers that are present in the response Vary header
1778                 string[] respVary = resp.Headers.GetValues(HttpKnownHeaderNames.Vary);
1779                 if (respVary != null) {
1780                     ArrayList varyValues = new ArrayList();
1781                     HttpRequestCacheValidator.ParseHeaderValues(respVary,
1782                                                                 HttpRequestCacheValidator.ParseValuesCallback,
1783                                                                 varyValues);
1784                     if (varyValues.Count != 0 && ((string)(varyValues[0]))[0] != '*') {
1785                         // we got some request headers to save
1786                         if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_saving_request_headers, resp.Headers[HttpKnownHeaderNames.Vary]));
1787                         if (ctx.SystemMeta == null) {
1788                             ctx.SystemMeta = new NameValueCollection(varyValues.Count+1, CaseInsensitiveAscii.StaticInstance);
1789                         }
1790                         for (int i = 0; i < varyValues.Count; ++i) {
1791                             string headerValue  = ctx.Request.Headers[(string)varyValues[i]];
1792                             ctx.SystemMeta[(string)varyValues[i]] = headerValue;
1793                         }
1794                     }
1795                 }
1796 
1797 
1798                 /*
1799                       - Hop-by-hop headers, which are meaningful only for a single
1800                         transport-level connection, and are not stored by caches or
1801                         forwarded by proxies.
1802 
1803                    The following HTTP/1.1 headers are hop-by-hop headers:
1804 
1805                       - Connection
1806                       - Keep-Alive
1807                       - Proxy-Authenticate
1808                       - Proxy-Authorization
1809                       - TE
1810                       - Trailers
1811                       - Transfer-Encoding
1812                       - Upgrade
1813                 */
1814 
1815 
1816                 // We add or Replace headers from the live response
1817                 for (int i = 0; i < ctx.Response.Headers.Count; ++i) {
1818                     string key = ctx.Response.Headers.GetKey(i);
1819                     if (AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.Connection) ||
1820                         AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.KeepAlive) ||
1821                         AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.ProxyAuthenticate) ||
1822                         AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.ProxyAuthorization) ||
1823                         AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.TE) ||
1824                         AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.TransferEncoding) ||
1825                         AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.Trailer) ||
1826                         AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.Upgrade))
1827                     {
1828                         continue;
1829 
1830                     }
1831                     if (resp.StatusCode == HttpStatusCode.NotModified && AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.ContentLength)) {
1832                          continue;
1833                     }
1834                     ctx.CacheHeaders.ChangeInternal(key, ctx.Response.Headers[i]);
1835                 }
1836             }
1837             //
1838             //
1839             //
AsciiLettersNoCaseEqual(string s1, string s2)1840             private static bool AsciiLettersNoCaseEqual(string s1, string s2) {
1841                 if (s1.Length != s2.Length) {
1842                     return false;
1843                 }
1844                 for (int i = 0; i < s1.Length; ++i) {
1845                     if ((s1[i]|0x20) != (s2[i]|0x20)) {
1846                         return false;
1847                     }
1848                 }
1849                 return true;
1850             }
1851             //
1852             //
1853             //
UnsafeAsciiLettersNoCaseEqual(char* s1, int start, int length, string s2)1854             internal unsafe static bool UnsafeAsciiLettersNoCaseEqual(char* s1, int start, int length, string s2) {
1855                 if (length-start < s2.Length) {
1856                     return false;
1857                 }
1858                 for (int i = 0; i < s2.Length; ++i) {
1859                     if ((s1[start+i]|0x20) != (s2[i]|0x20)) {
1860                         return false;
1861                     }
1862                 }
1863                 return true;
1864             }
1865 
1866             //
1867             // Parses the string on "bytes = start - end" or "bytes start-end/xxx"
1868             //
1869             // Returns
1870             //   true      = take start/end for range
1871             //   false     = parsing error
GetBytesRange(string ranges, ref long start, ref long end, ref long total, bool isRequest)1872             public static bool GetBytesRange(string ranges, ref long start, ref long end, ref long total, bool isRequest) {
1873 
1874                 ranges = ranges.ToLower(CultureInfo.InvariantCulture);
1875 
1876                 int idx = 0;
1877                 while (idx < ranges.Length && ranges[idx] == ' ') {
1878                     ++idx;
1879                 }
1880 
1881                 idx+=5;
1882                 // The "ranges" string is already in lowercase
1883                 if( idx >= ranges.Length || ranges[idx-5] != 'b' || ranges[idx-4] != 'y' || ranges[idx-3] != 't' || ranges[idx-2] != 'e' || ranges[idx-1] != 's')
1884                 {
1885                     if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_only_byte_range_implemented));
1886                     return false;
1887                 }
1888 
1889                 if (isRequest) {
1890                     while (idx < ranges.Length && ranges[idx] == ' ') {
1891                         ++idx;
1892                     }
1893                     if (ranges[idx] != '=') {
1894                         return false;
1895                     }
1896                 }
1897                 else {
1898                     if (ranges[idx] != ' ') {
1899                         return false;
1900                     }
1901                 }
1902 
1903                 char ch = (char)0;
1904                 while (++idx < ranges.Length && (ch=ranges[idx]) == ' ') {
1905                     ;
1906                 }
1907 
1908                 start = -1;
1909                 if (ch != '-') {
1910                     // parsing start
1911                     if (idx < ranges.Length && ch >= '0' && ch <= '9') {
1912                         start = ch-'0';
1913                         while(++idx < ranges.Length && (ch = ranges[idx]) >= '0' && ch <= '9') {
1914                             start = start*10 + (ch-'0');
1915                         }
1916                     }
1917 
1918                     while (idx < ranges.Length && ch == ' ') {ch = ranges[++idx];}
1919                     if (ch != '-') {return false;}
1920                 }
1921 
1922                 // parsing end
1923                 while (idx < ranges.Length && (ch = ranges[++idx]) == ' ') {
1924                     ;
1925                 }
1926 
1927                 end = -1;
1928                 if (idx < ranges.Length && ch >= '0' && ch <= '9') {
1929                     end = ch-'0';
1930                     while(++idx < ranges.Length && (ch = ranges[idx]) >= '0' && ch <= '9') {
1931                         end = end*10 + (ch-'0');
1932                     }
1933                 }
1934                 if (isRequest) {
1935                     while (idx < ranges.Length) {
1936                         if (ranges[idx++] != ' ') {
1937                             if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_multiple_complex_range_not_implemented));
1938                             return false;
1939                         }
1940                     }
1941                 }
1942                 else {
1943                     // parsing total
1944                     while (idx < ranges.Length && (ch = ranges[idx]) == ' ') {
1945                         ++idx;
1946                     }
1947 
1948                     if (ch != '/') {
1949                         return false;
1950                     }
1951                     while (++idx < ranges.Length && (ch=ranges[idx]) == ' ') {
1952                         ;
1953                     }
1954 
1955                     total = -1;
1956                     if (ch != '*') {
1957                         if (idx < ranges.Length && ch >= '0' && ch <= '9') {
1958                             total = ch-'0';
1959                             while (++idx < ranges.Length && (ch = ranges[idx]) >= '0' && ch <= '9') {
1960                                 total = total*10 + (ch-'0');
1961                             }
1962                         }
1963                     }
1964                 }
1965 
1966                 if (!isRequest && (start == -1 || end == -1)) {
1967                     return false;
1968                 }
1969                 return true;
1970             }
1971         }
1972     }
1973 
1974 }
1975