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