Code:
/ 4.0 / 4.0 / untmp / DEVDIV_TFS / Dev10 / Releases / RTMRel / ndp / fx / src / Net / System / Net / Cache / _Rfc2616CacheValidators.cs / 1305376 / _Rfc2616CacheValidators.cs
/*++ Copyright (c) Microsoft Corporation Module Name: _Rfc2616CacheValidators.cs Abstract: The class implements set of HTTP validators as per RFC2616 Author: Alexei Vopilov 21-Dec-2002 Revision History: --*/ namespace System.Net.Cache { using System; using System.Net; using System.IO; using System.Globalization; using System.Collections; using System.Collections.Specialized; // // Caching RFC // internal class Rfc2616 { private Rfc2616() { } internal enum TriState { Unknown, Valid, Invalid } /*----------*/ // Continue = Proceed to the next protocol stage. // DoNotTakeFromCache = Don't used caches value for this request // DoNotUseCache = Cache is not used for this request and response is not cached. public static CacheValidationStatus OnValidateRequest(HttpRequestCacheValidator ctx) { CacheValidationStatus result = Common.OnValidateRequest(ctx); if (result == CacheValidationStatus.DoNotUseCache) { return result; } /* HTTP/1.1 caches SHOULD treat "Pragma: no-cache" as if the client had sent "Cache-Control: no-cache". No new Pragma directives will be defined in HTTP. we use above information to remove pragma header (we control it itself) */ ctx.Request.Headers.RemoveInternal(HttpKnownHeaderNames.Pragma); /* we want to control cache-control header as well, any specifi extensions should be done using a derived validator class and custom policy */ ctx.Request.Headers.RemoveInternal(HttpKnownHeaderNames.CacheControl); if (ctx.Policy.Level == HttpRequestCacheLevel.NoCacheNoStore) { //adjust request headers since retrieval validators will be suppressed upon return. ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "no-store"); ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "no-cache"); ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.Pragma, "no-cache"); result = CacheValidationStatus.DoNotTakeFromCache; } else if (result == CacheValidationStatus.Continue) { if (ctx.Policy.Level == HttpRequestCacheLevel.Reload || ctx.Policy.Level == HttpRequestCacheLevel.NoCacheNoStore) { //adjust request headers since retrieval validators will be suppressed upon return. ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "no-cache"); ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.Pragma, "no-cache"); result = CacheValidationStatus.DoNotTakeFromCache; } else if (ctx.Policy.Level == HttpRequestCacheLevel.Refresh) { //adjust request headers since retrieval validators will be suppressed upon return. ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "max-age=0"); ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.Pragma, "no-cache"); result = CacheValidationStatus.DoNotTakeFromCache; } else if (ctx.Policy.Level == HttpRequestCacheLevel.Default) { //Transfer Policy into CacheControl directives if (ctx.Policy.MinFresh > TimeSpan.Zero) { ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "min-fresh=" + (int)ctx.Policy.MinFresh.TotalSeconds); } if (ctx.Policy.MaxAge != TimeSpan.MaxValue) { ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "max-age=" + (int)ctx.Policy.MaxAge.TotalSeconds); } if (ctx.Policy.MaxStale > TimeSpan.Zero) { ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "max-stale=" + (int)ctx.Policy.MaxStale.TotalSeconds); } } else if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly || ctx.Policy.Level == HttpRequestCacheLevel.CacheOrNextCacheOnly) { // In case other validators will not be called ctx.Request.Headers.AddInternal(HttpKnownHeaderNames.CacheControl, "only-if-cached"); } } return result; } /*----------*/ public static CacheFreshnessStatus OnValidateFreshness(HttpRequestCacheValidator ctx) { // This will figure out ctx.CacheAge and ctx.CacheMaxAge memebers CacheFreshnessStatus result = Common.ComputeFreshness(ctx); /* We note one exception to this rule: since some applications have traditionally used GETs and HEADs with query URLs (those containing a "?" in the rel_path part) to perform operations with significant side effects, caches MUST NOT treat responses to such URIs as fresh unless the server provides an explicit expiration time. This specifically means that responses from HTTP/1.0 servers for such URIs SHOULD NOT be taken from a cache. See section 9.1.1 for related information. */ if (ctx.Uri.Query.Length != 0) { if (ctx.CacheHeaders.Expires == null && (ctx.CacheEntry.IsPrivateEntry?ctx.CacheCacheControl.MaxAge == -1:ctx.CacheCacheControl.SMaxAge == -1)) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_uri_with_query_has_no_expiration)); return CacheFreshnessStatus.Stale; } if (ctx.CacheHttpVersion.Major <= 1 && ctx.CacheHttpVersion.Minor < 1) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_uri_with_query_and_cached_resp_from_http_10)); return CacheFreshnessStatus.Stale; } } return result; } /*----------*/ // ReturnCachedResponse = Return cached response to the application // DoNotTakeFromCache = Don't used caches value for this request // Continue = Proceed to the next protocol stage. public static CacheValidationStatus OnValidateCache(HttpRequestCacheValidator ctx) { if (Common.ValidateCacheByVaryHeader(ctx) == TriState.Invalid) { // RFC 2616 is tricky on this. In theory we could make a conditional request. // However we rather will not. // And the reason can be deducted from the RFC definitoin of the response Vary Header. return CacheValidationStatus.DoNotTakeFromCache; } // For Revalidate option we perform a wire request anyway if (ctx.Policy.Level == HttpRequestCacheLevel.Revalidate) { return Common.TryConditionalRequest(ctx); } if (Common.ValidateCacheBySpecialCases(ctx) == TriState.Invalid) { // This takes over the cache policy since the cache content may be sematically incorrect if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly) { // Cannot do a wire request return CacheValidationStatus.DoNotTakeFromCache; } return Common.TryConditionalRequest(ctx); } // So now we have either fresh or stale entry that might be used in place of the response // At this point it's safe to consider cache freshness and effective Policy as the core decision rules // Reminder: This method should not be executed with Level >= CacheLevel.Refresh bool enoughFresh = Common.ValidateCacheByClientPolicy(ctx); if (enoughFresh || ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly || ctx.Policy.Level == HttpRequestCacheLevel.CacheIfAvailable || ctx.Policy.Level == HttpRequestCacheLevel.CacheOrNextCacheOnly) { // The freshness does not matter, check does user requested Range fits into cached entry CacheValidationStatus result = Common.TryResponseFromCache(ctx); if (result != CacheValidationStatus.ReturnCachedResponse) { if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly) { // Cannot do a wire request return CacheValidationStatus.DoNotTakeFromCache; } return result; } if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_valid_as_fresh_or_because_policy, ctx.Policy.ToString())); return CacheValidationStatus.ReturnCachedResponse; } // This will return either Continue=conditional request or DoNotTakeFromCache==Unconditional request return Common.TryConditionalRequest(ctx); } /*----------*/ // Returns // RetryResponseFromServer = Retry this request as the result of invalid response received // Continue = The response can be accepted public static CacheValidationStatus OnValidateResponse(HttpRequestCacheValidator ctx) { // // At this point we assume that policy >= CacheOrNextCacheOnly && policy < Refresh // // If there was a retry already, it should go with cache disabled so by default we won't retry it again if (ctx.ResponseCount > 1) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_accept_based_on_retry_count, ctx.ResponseCount)); return CacheValidationStatus.Continue; } // We don't convert user-range request to a conditional one if (ctx.RequestRangeUser) { // was a user range request, we did not touch it. return CacheValidationStatus.Continue; } //If a live response has older Date, then request should be retried if (ctx.CacheDate != DateTime.MinValue && ctx.ResponseDate != DateTime.MinValue && ctx.CacheDate > ctx.ResponseDate) { if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_date_header_older_than_cache_entry)); Common.ConstructUnconditionalRefreshRequest(ctx); return CacheValidationStatus.RetryResponseFromServer; } HttpWebResponse resp = ctx.Response as HttpWebResponse; if (ctx.RequestRangeCache && resp.StatusCode == HttpStatusCode.RequestedRangeNotSatisfiable) { if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_server_didnt_satisfy_range, ctx.Request.Headers[HttpKnownHeaderNames.Range])); Common.ConstructUnconditionalRefreshRequest(ctx); return CacheValidationStatus.RetryResponseFromServer; } if (resp.StatusCode == HttpStatusCode.NotModified) { if (ctx.RequestIfHeader1 == null) { // something is really broken on the wire if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_304_received_on_unconditional_request)); Common.ConstructUnconditionalRefreshRequest(ctx); return CacheValidationStatus.RetryResponseFromServer; } else if (ctx.RequestRangeCache) { // The way _we_ create range requests shoyuld never result in 304 if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_304_received_on_unconditional_request_expected_200_206)); Common.ConstructUnconditionalRefreshRequest(ctx); return CacheValidationStatus.RetryResponseFromServer; } } if (ctx.CacheHttpVersion.Major <= 1 && resp.ProtocolVersion.Major <=1 && ctx.CacheHttpVersion.Minor < 1 && resp.ProtocolVersion.Minor <1 && ctx.CacheLastModified > ctx.ResponseLastModified) { if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_last_modified_header_older_than_cache_entry)); // On http <= 1.0 cache LastModified > resp LastModified Common.ConstructUnconditionalRefreshRequest(ctx); return CacheValidationStatus.RetryResponseFromServer; } if (ctx.Policy.Level == HttpRequestCacheLevel.Default && ctx.ResponseAge != TimeSpan.MinValue) { // If the client has requested MaxAge/MinFresh/MaxStale // check does the response meet the requirements if ( (ctx.ResponseAge > ctx.Policy.MaxAge) || (ctx.ResponseExpires != DateTime.MinValue && (ctx.Policy.MinFresh > TimeSpan.Zero && (ctx.ResponseExpires - DateTime.UtcNow) < ctx.Policy.MinFresh) || (ctx.Policy.MaxStale > TimeSpan.Zero && (DateTime.UtcNow - ctx.ResponseExpires) > ctx.Policy.MaxStale))) { if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_freshness_outside_policy_limits)); Common.ConstructUnconditionalRefreshRequest(ctx); return CacheValidationStatus.RetryResponseFromServer; } } //Cleanup what we've done to this request since protcol can resubmit for auth or redirect. if (ctx.RequestIfHeader1 != null) { ctx.Request.Headers.RemoveInternal(ctx.RequestIfHeader1); ctx.RequestIfHeader1 = null; } if (ctx.RequestIfHeader2 != null) { ctx.Request.Headers.RemoveInternal(ctx.RequestIfHeader2); ctx.RequestIfHeader2 = null; } if (ctx.RequestRangeCache) { ctx.Request.Headers.RemoveInternal(HttpKnownHeaderNames.Range); ctx.RequestRangeCache = false; } return CacheValidationStatus.Continue; } /*----------*/ // Returns: // CacheResponse = Replace cache entry with received live response // UpdateResponseInformation = Update Metadata of cache entry using live response headers // RemoveFromCache = Remove cache entry referenced to by a cache key. // Continue = Simply do not update cache. // public static CacheValidationStatus OnUpdateCache(HttpRequestCacheValidator ctx) { // Below condition is to get rid of a broken cache entry, we cannot update cache in that case if (ctx.CacheStatusCode == HttpStatusCode.NotModified) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_need_to_remove_invalid_cache_entry_304)); return CacheValidationStatus.RemoveFromCache; } HttpWebResponse resp = ctx.Response as HttpWebResponse; if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_status, resp.StatusCode)); /********* Vs Whidbey#127214 It was decided not to play with ResponseContentLocation in our implementation. A derived class may still want to play. // Compute new Cache Update Key if Content-Location is present on the response if (ctx.ResponseContentLocation != null) { if (!Uri.TryParse(ctx.ResponseContentLocation, true, true, out cacheUri)) { if(Logging.On)Logging.PrintError(Logging.RequestCache, "Cannot parse Uri from Response Content-Location: " + ctx.ResponseContentLocation); return CacheValidationStatus.RemoveFromCache; } if (!cacheUri.IsAbsoluteUri) { try { ctx.CacheKey = new Uri(ctx.RequestUri, cacheUri); } catch { return CacheValidationStatus.RemoveFromCache; } } } *********/ if (ctx.ValidationStatus == CacheValidationStatus.RemoveFromCache) { return CacheValidationStatus.RemoveFromCache; } CacheValidationStatus noUpdateResult = (ctx.RequestMethod >= HttpMethod.Post && ctx.RequestMethod <= HttpMethod.Delete || ctx.RequestMethod == HttpMethod.Other) ?CacheValidationStatus.RemoveFromCache :CacheValidationStatus.DoNotUpdateCache; if (Common.OnUpdateCache(ctx, resp) != TriState.Valid) { return noUpdateResult; } CacheValidationStatus result = CacheValidationStatus.CacheResponse; ctx.CacheEntry.IsPartialEntry = false; if (resp.StatusCode == HttpStatusCode.NotModified || ctx.RequestMethod == HttpMethod.Head) { result = CacheValidationStatus.UpdateResponseInformation; // This may take a shorter path when updating the entry if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_304_or_request_head)); if (ctx.CacheDontUpdateHeaders) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_dont_update_cached_headers)); ctx.CacheHeaders = null; ctx.CacheEntry.ExpiresUtc = ctx.ResponseExpires; ctx.CacheEntry.LastModifiedUtc = ctx.ResponseLastModified; if (ctx.Policy.Level == HttpRequestCacheLevel.Default) { ctx.CacheEntry.MaxStale = ctx.Policy.MaxStale; } else { ctx.CacheEntry.MaxStale = TimeSpan.MinValue; } ctx.CacheEntry.LastSynchronizedUtc = DateTime.UtcNow; } else { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_update_cached_headers)); } } else if (resp.StatusCode == HttpStatusCode.PartialContent) { // Check on whether the user requested range can be appended to the cache entry // We only support combining of non-overlapped increasing bytes ranges if (ctx.CacheEntry.StreamSize != ctx.ResponseRangeStart && ctx.ResponseRangeStart != 0) { 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)); return noUpdateResult; } // We might be appending a live stream to cache BUT user has asked for a specific range. // Hence don't reset CacheStreamOffset here so the protocol will create a cache forwarding stream that will hide first bytes from the user if (!ctx.RequestRangeUser) { ctx.CacheStreamOffset = 0; } // Below code assumes that a combined response has been given to the user, Common.ReplaceOrUpdateCacheHeaders(ctx, resp); ctx.CacheHttpVersion = resp.ProtocolVersion; ctx.CacheEntityLength = ctx.ResponseEntityLength; ctx.CacheStreamLength = ctx.CacheEntry.StreamSize = ctx.ResponseRangeEnd+1; if (ctx.CacheEntityLength > 0 && ctx.CacheEntityLength == ctx.CacheEntry.StreamSize) { //eventually cache is about to store a complete response Common.Construct200ok(ctx); } else Common.Construct206PartialContent(ctx, 0); } else { Common.ReplaceOrUpdateCacheHeaders(ctx, resp); ctx.CacheHttpVersion = resp.ProtocolVersion; ctx.CacheStatusCode = resp.StatusCode; ctx.CacheStatusDescription = resp.StatusDescription; ctx.CacheEntry.StreamSize = resp.ContentLength; } return result; } // // Implements various cache validation helper methods // internal static class Common { public const string PartialContentDescription = "Partial Content"; public const string OkDescription = "OK"; // // Implements logic as of the Request caching suitability. // // Returns: // Continue = Proceed to the next protocol stage. // DoNotTakeFromCache = Don't use cached response for this request // DoNotUseCache = Cache is not used for this request and response is not cached. public static CacheValidationStatus OnValidateRequest(HttpRequestCacheValidator ctx) { /* Some HTTP methods MUST cause a cache to invalidate an entity. This is either the entity referred to by the Request-URI, or by the Location or Content-Location headers (if present). These methods are: PUT, DELETE, POST. A cache that passes through requests for methods it does not understand SHOULD invalidate any entities referred to by the Request-URI */ if (ctx.RequestMethod >= HttpMethod.Post && ctx.RequestMethod <= HttpMethod.Delete) { if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly) { // Throw because the request must hit the wire and it's cache-only policy ctx.FailRequest(WebExceptionStatus.RequestProhibitedByCachePolicy); } // here we could return a hint on removing existing entry, but UpdateCache should handle this case correctly return CacheValidationStatus.DoNotTakeFromCache; } // // Additionally to said above we can only cache GET or HEAD, for any other methods we request bypassing cache. // if (ctx.RequestMethod < HttpMethod.Head || ctx.RequestMethod > HttpMethod.Get ) { if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly) { // Throw because the request must hit the wire and it's cache-only policy ctx.FailRequest(WebExceptionStatus.RequestProhibitedByCachePolicy); } return CacheValidationStatus.DoNotUseCache; } if (ctx.Request.Headers[HttpKnownHeaderNames.IfModifiedSince] != null || ctx.Request.Headers[HttpKnownHeaderNames.IfNoneMatch] != null || ctx.Request.Headers[HttpKnownHeaderNames.IfRange] != null || ctx.Request.Headers[HttpKnownHeaderNames.IfMatch] != null || ctx.Request.Headers[HttpKnownHeaderNames.IfUnmodifiedSince] != null ) { // The _user_ request contains conditonal cache directives // Those will conflict with the caching engine => do not lookup a cached item. if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_request_contains_conditional_header)); if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly) { // Throw because the request must hit the wire and it's cache-only policy ctx.FailRequest(WebExceptionStatus.RequestProhibitedByCachePolicy); } return CacheValidationStatus.DoNotTakeFromCache; } return CacheValidationStatus.Continue; } // // Implements logic as to compute cache freshness. // Client Policy is not considered // public static CacheFreshnessStatus ComputeFreshness(HttpRequestCacheValidator ctx) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_now_time, DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture))); /* apparent_age = max(0, response_time - date_value); */ DateTime nowDate = DateTime.UtcNow; TimeSpan age = TimeSpan.MaxValue; DateTime date = ctx.CacheDate; if (date != DateTime.MinValue) { age = (nowDate - date); 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))); } else if (ctx.CacheEntry.LastSynchronizedUtc != DateTime.MinValue) { /* Another way to compute cache age but only if Date header is absent. */ age = nowDate - ctx.CacheEntry.LastSynchronizedUtc; if (ctx.CacheAge != TimeSpan.MinValue) { age += ctx.CacheAge; } if(Logging.On) { if (ctx.CacheAge != TimeSpan.MinValue) 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))); else 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))); } } /* corrected_received_age = max(apparent_age, age_value); */ if (ctx.CacheAge != TimeSpan.MinValue) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_age2, ((int)ctx.CacheAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo))); if (ctx.CacheAge > age || age == TimeSpan.MaxValue) { age = ctx.CacheAge; } } // Updating CacheAge ... // Note we don't account on response "transit" delay // Also undefined cache entry Age is reported as TimeSpan.MaxValue (which is impossble to get from HTTP) // Also a negative age is reset to 0 as per RFC ctx.CacheAge = (age < TimeSpan.Zero? TimeSpan.Zero: age); // Now we start checking the server specified requirements /* The calculation to determine if a response has expired is quite simple: response_is_fresh = (freshness_lifetime > current_age) */ // If we managed to compute the Cache Age if (ctx.CacheAge != TimeSpan.MinValue) { /* s-maxage If a response includes an s-maxage directive, then for a shared cache (but not for a private cache), the maximum age specified by this directive overrides the maximum age specified by either the max-age directive or the Expires header. */ if (!ctx.CacheEntry.IsPrivateEntry && ctx.CacheCacheControl.SMaxAge != -1) { ctx.CacheMaxAge = TimeSpan.FromSeconds(ctx.CacheCacheControl.SMaxAge); 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))); if (ctx.CacheAge < ctx.CacheMaxAge) { return CacheFreshnessStatus.Fresh; } return CacheFreshnessStatus.Stale; } /* The max-age directive takes priority over Expires, so if max-age is present in a response, the calculation is simply: freshness_lifetime = max_age_value */ if (ctx.CacheCacheControl.MaxAge != -1) { ctx.CacheMaxAge = TimeSpan.FromSeconds(ctx.CacheCacheControl.MaxAge); 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))); if (ctx.CacheAge < ctx.CacheMaxAge) { return CacheFreshnessStatus.Fresh; } return CacheFreshnessStatus.Stale; } } /* Otherwise, if Expires is present in the response, the calculation is: freshness_lifetime = expires_value - date_value */ if (date == DateTime.MinValue) { date = ctx.CacheEntry.LastSynchronizedUtc; } DateTime expiresDate = ctx.CacheEntry.ExpiresUtc; if (ctx.CacheExpires != DateTime.MinValue && ctx.CacheExpires < expiresDate) { expiresDate = ctx.CacheExpires; } // If absolute Expires and Response Date and Cache Age can be recovered if (expiresDate != DateTime.MinValue && date != DateTime.MinValue && ctx.CacheAge != TimeSpan.MinValue) { ctx.CacheMaxAge = expiresDate - date; 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))); if (ctx.CacheAge < ctx.CacheMaxAge) { return CacheFreshnessStatus.Fresh; } return CacheFreshnessStatus.Stale; } // If absolute Expires can be recovered if (expiresDate != DateTime.MinValue) { ctx.CacheMaxAge = expiresDate - DateTime.UtcNow; //Take absolute Expires value if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_max_age_absolute, expiresDate.ToString("r", CultureInfo.InvariantCulture))); if (expiresDate < DateTime.UtcNow) { return CacheFreshnessStatus.Fresh; } return CacheFreshnessStatus.Stale; } /* If none of Expires, Cache-Control: max-age, or Cache-Control: s- maxage (see section 14.9.3) appears in the response, and the response does not include other restrictions on caching, the cache MAY compute a freshness lifetime using a heuristic. The cache MUST attach Warning 113 to any response whose age is more than 24 hours if such warning has not already been added. Also, if the response does have a Last-Modified time, the heuristic expiration value SHOULD be no more than some fraction of the interval since that time. A typical setting of this fraction might be 10%. response_is_fresh = (freshness_lifetime > current_age) */ ctx.HeuristicExpiration = true; DateTime lastModifiedDate = ctx.CacheEntry.LastModifiedUtc; if (ctx.CacheLastModified > lastModifiedDate) { lastModifiedDate = ctx.CacheLastModified; } ctx.CacheMaxAge = ctx.UnspecifiedMaxAge; if (lastModifiedDate != DateTime.MinValue) { TimeSpan span = (nowDate - lastModifiedDate); int maxAgeSeconds = (int)(span.TotalSeconds/10); ctx.CacheMaxAge = TimeSpan.FromSeconds(maxAgeSeconds); 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))); if (ctx.CacheAge.TotalSeconds < maxAgeSeconds) { return CacheFreshnessStatus.Fresh; } return CacheFreshnessStatus.Stale; } // Else we can only rely on UnspecifiedMaxAge hint ctx.CacheMaxAge = ctx.UnspecifiedMaxAge; 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))); if (ctx.CacheMaxAge >= ctx.CacheAge) { return CacheFreshnessStatus.Fresh; } return CacheFreshnessStatus.Stale; } /* Returns: - Valid : The cache can be updated with the response - Unknown : The response should not go into cache */ internal static TriState OnUpdateCache(HttpRequestCacheValidator ctx, HttpWebResponse resp) { /* Quick check on supported methods. */ if (ctx.RequestMethod != HttpMethod.Head && ctx.RequestMethod != HttpMethod.Get && ctx.RequestMethod != HttpMethod.Post) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_not_a_get_head_post)); return TriState.Unknown; } //If the entry did not exist ... if (ctx.CacheStream == Stream.Null || (int)ctx.CacheStatusCode == 0) { if(resp.StatusCode == HttpStatusCode.NotModified) { // Protection from some weird case when user has changed things if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_cannot_update_cache_if_304)); return TriState.Unknown; } if (ctx.RequestMethod == HttpMethod.Head) { // Protection from some caching Head response when entry does not exist. if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_cannot_update_cache_with_head_resp)); return TriState.Unknown; } } if (resp == null) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_http_resp_is_null)); return TriState.Unknown; } // // We assume that ctx.ResponseCacheControl is already updated based on a live response // /* no-store ... If sent in a response, a cache MUST NOT store any part of either this response or the request that elicited it. */ if (ctx.ResponseCacheControl.NoStore) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_cache_control_is_no_store)); return TriState.Unknown; } /* If there is neither a cache validator nor an explicit expiration time associated with a response, we do not expect it to be cached, but certain caches MAY violate this expectation (for example, when little or no network connectivity is available). A client can usually detect that such a response was taken from a cache by comparing the Date header to the current time. */ // NOTE: If no Expire and no Validator peresnt we choose to CACHE //================================================================ /* Note: a new response that has an older Date header value than existing cached responses is not cacheable. */ if (ctx.ResponseDate != DateTime.MinValue && ctx.CacheDate != DateTime.MinValue) { if (ctx.ResponseDate < ctx.CacheDate) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_older_than_cache)); return TriState.Unknown; } } /* public Indicates that the response MAY be cached by any cache, even if it would normally be non-cacheable or cacheable only within a non- shared cache. (See also Authorization, section 14.8, for additional details.) */ if (ctx.ResponseCacheControl.Public) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_cache_control_is_public)); return TriState.Valid; } // sometiem public cache can cache a response with "private" directive, subject to other restrictions TriState result = TriState.Unknown; /* private Indicates that all or part of the response message is intended for a single user and MUST NOT be cached by a shared cache. This allows an origin server to state that the specified parts of the response are intended for only one user and are not a valid response for requests by other users. A private (non-shared) cache MAY cache the response. */ if (ctx.ResponseCacheControl.Private) { if (!ctx.CacheEntry.IsPrivateEntry) { if (ctx.ResponseCacheControl.PrivateHeaders == null) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_cache_control_is_private)); return TriState.Unknown; } if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_cache_control_is_private_plus_headers)); for (int i = 0; i < ctx.ResponseCacheControl.PrivateHeaders.Length; ++i) { ctx.CacheHeaders.Remove(ctx.ResponseCacheControl.PrivateHeaders[i]); result = TriState.Valid; } } else { result = TriState.Valid; } } /* The RFC is funky on no-cache directive. But the bottom line is sometime you CAN cache no-cache responses. */ if (ctx.ResponseCacheControl.NoCache) { if (ctx.ResponseLastModified == DateTime.MinValue && ctx.Response.Headers.ETag == null) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_revalidation_required)); return TriState.Unknown; } if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_needs_revalidation)); return TriState.Valid; } if (ctx.ResponseCacheControl.SMaxAge != -1 || ctx.ResponseCacheControl.MaxAge != -1) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_allows_caching, ctx.ResponseCacheControl.ToString())); return TriState.Valid; } /* When a shared cache (see section 13.7) receives a request containing an Authorization field, it MUST NOT return the corresponding response as a reply to any other request, unless one of the following specific exceptions holds: 1. If the response includes the "s-maxage" cache-control 2. If the response includes the "must-revalidate" cache-control 3. If the response includes the "public" cache-control directive, */ if (!ctx.CacheEntry.IsPrivateEntry && ctx.Request.Headers[HttpKnownHeaderNames.Authorization] != null) { // we've already passed an opportunity to cache. if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_auth_header_and_no_s_max_age)); return TriState.Unknown; } /* POST Responses to this method are not cacheable, unless the response includes appropriate Cache-Control or Expires header fields. */ if (ctx.RequestMethod == HttpMethod.Post && resp.Headers.Expires == null) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_post_resp_without_cache_control_or_expires)); return TriState.Unknown; } /* A response received with a status code of 200, 203, 206, 300, 301 or 410 MAY be stored by a cache and used in reply to a subsequent request, subject to the expiration mechanism, unless a cache-control directive prohibits caching. However, a cache that does not support the Range and Content-Range headers MUST NOT cache 206 (Partial Content) responses. NOTE: We added 304 here which is correct */ if (resp.StatusCode == HttpStatusCode.NotModified || resp.StatusCode == HttpStatusCode.OK || resp.StatusCode == HttpStatusCode.NonAuthoritativeInformation || resp.StatusCode == HttpStatusCode.PartialContent || resp.StatusCode == HttpStatusCode.MultipleChoices || resp.StatusCode == HttpStatusCode.MovedPermanently || resp.StatusCode == HttpStatusCode.Gone) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_valid_based_on_status_code, (int)resp.StatusCode)); return TriState.Valid; } /* A response received with any other status code (e.g. status codes 302 and 307) MUST NOT be returned in a reply to a subsequent request unless there are cache-control directives or another header(s) that explicitly allow it. For example, these include the following: an Expires header (section 14.21); a "max-age", "s-maxage", "must- revalidate", "proxy-revalidate", "public" or "private" cache-control directive (section 14.9). */ if (result != TriState.Valid) { // otheriwse there was a "safe" private directive that allows caching if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_no_cache_control, (int)resp.StatusCode)); } return result; } /*----------*/ // // This method checks sutability of cached entry based on the client policy. // /* Returns: - true : The cache is still good - false : The cache age does not fit into client policy */ public static bool ValidateCacheByClientPolicy(HttpRequestCacheValidator ctx) { if (ctx.Policy.Level == HttpRequestCacheLevel.Default) { 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)))); if (ctx.Policy.MinFresh > TimeSpan.Zero) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_policy_min_fresh, ((int)ctx.Policy.MinFresh.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo))); if (ctx.CacheAge + ctx.Policy.MinFresh >= ctx.CacheMaxAge) {return false;} } if (ctx.Policy.MaxAge != TimeSpan.MaxValue) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_policy_max_age, ((int)ctx.Policy.MaxAge.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo))); if (ctx.CacheAge >= ctx.Policy.MaxAge) {return false;} } if (ctx.Policy.InternalCacheSyncDateUtc != DateTime.MinValue) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_policy_cache_[....]_date, ctx.Policy.InternalCacheSyncDateUtc.ToString("r", CultureInfo.CurrentCulture), ctx.CacheEntry.LastSynchronizedUtc.ToString(CultureInfo.CurrentCulture))); if (ctx.CacheEntry.LastSynchronizedUtc < ctx.Policy.InternalCacheSyncDateUtc) { return false; } } TimeSpan adjustedMaxAge = ctx.CacheMaxAge; if (ctx.Policy.MaxStale > TimeSpan.Zero) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_policy_max_stale, ((int)ctx.Policy.MaxStale.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo))); if (adjustedMaxAge < TimeSpan.MaxValue - ctx.Policy.MaxStale) { adjustedMaxAge = adjustedMaxAge + ctx.Policy.MaxStale; } else { adjustedMaxAge = TimeSpan.MaxValue; } if (ctx.CacheAge >= adjustedMaxAge) return false; else return true; } } // not stale means "fresh enough" return ctx.CacheFreshnessStatus == CacheFreshnessStatus.Fresh; } /* This Validator should be called ONLY before submitting any response */ /* Returns: - Valid : Cache can be returned to the app subject to effective policy - Invalid : A Conditional request MUST be made (unconditional request is also fine) */ internal static TriState ValidateCacheBySpecialCases(HttpRequestCacheValidator ctx) { /* no-cache If the no-cache directive does not specify a field-name, then a cache MUST NOT use the response to satisfy a subsequent request without successful revalidation with the origin server. This allows an origin server to prevent caching even by caches that have been configured to return stale responses to client requests. */ if (ctx.CacheCacheControl.NoCache) { if (ctx.CacheCacheControl.NoCacheHeaders == null) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_control_no_cache)); return TriState.Invalid; } /* If the no-cache directive does specify one or more field-names, then a cache MAY use the response to satisfy a subsequent request, subject to any other restrictions on caching. However, the specified field-name(s) MUST NOT be sent in the response to a subsequent request without successful revalidation with the origin server. This allows an origin server to prevent the re-use of certain header fields in a response, while still allowing caching of the rest of the response. */ if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_control_no_cache_removing_some_headers)); for (int i = 0; i < ctx.CacheCacheControl.NoCacheHeaders.Length; ++i) { ctx.CacheHeaders.Remove(ctx.CacheCacheControl.NoCacheHeaders[i]); } } /* must-revalidate When the must-revalidate directive is present in a response received by a cache, that cache MUST NOT use the entry after it becomes stale to respond to a subsequent request without first revalidating it with the origin server. (I.e., the cache MUST do an end-to-end revalidation every time, if, based solely on the origin server's Expires or max-age value, the cached response is stale.) proxy-revalidate The proxy-revalidate directive has the same meaning as the must- revalidate directive, except that it does not apply to non-shared user agent caches. */ if (ctx.CacheCacheControl.MustRevalidate || (!ctx.CacheEntry.IsPrivateEntry && ctx.CacheCacheControl.ProxyRevalidate)) { if (ctx.CacheFreshnessStatus != CacheFreshnessStatus.Fresh) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_control_must_revalidate)); return TriState.Invalid; } } /* When a shared cache (see section 13.7) receives a request containing an Authorization field, it MUST NOT return the corresponding response as a reply to any other request, unless one of the following specific exceptions holds: 1. If the response includes the "s-maxage" cache-control directive, the cache MAY use that response in replying to a subsequent request. But (if the specified maximum age has passed) a proxy cache MUST first revalidate it with the origin server, using the request-headers from the new request to allow the origin server to authenticate the new request. (This is the defined behavior for s-maxage.) If the response includes "s- maxage=0", the proxy MUST always revalidate it before re-using it. 2. If the response includes the "must-revalidate" cache-control directive, the cache MAY use that response in replying to a subsequent request. But if the response is stale, all caches MUST first revalidate it with the origin server, using the request-headers from the new request to allow the origin server to authenticate the new request. 3. If the response includes the "public" cache-control directive, it MAY be returned in reply to any subsequent request. */ if (ctx.Request.Headers[HttpKnownHeaderNames.Authorization] != null) { if (ctx.CacheFreshnessStatus != CacheFreshnessStatus.Fresh) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_cached_auth_header)); return TriState.Invalid; } if (!ctx.CacheEntry.IsPrivateEntry && ctx.CacheCacheControl.SMaxAge == -1 && !ctx.CacheCacheControl.MustRevalidate && !ctx.CacheCacheControl.Public) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_cached_auth_header_no_control_directive)); return TriState.Invalid; } } return TriState.Valid; } // // Second Time (after response) cache validation always goes through this method. // // Returns // - ReturnCachedResponse = Take from cache, cache stream may be replaced and response stream is closed // - DoNotTakeFromCache = Disregard the cache // - RemoveFromCache = Disregard and remove cache entry // - CombineCachedAndServerResponse = The combined cache+live stream has been constructed. // public static CacheValidationStatus ValidateCacheAfterResponse(HttpRequestCacheValidator ctx, HttpWebResponse resp) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_after_validation)); if ((ctx.CacheStream == Stream.Null || (int)ctx.CacheStatusCode == 0) && resp.StatusCode == HttpStatusCode.NotModified) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_resp_status_304)); return CacheValidationStatus.DoNotTakeFromCache; } if (ctx.RequestMethod == HttpMethod.Head) { /* The response to a HEAD request MAY be cacheable in the sense that the information contained in the response MAY be used to update a previously cached entity from that resource. If the new field values indicate that the cached entity differs from the current entity (as would be indicated by a change in Content-Length, Content-MD5, ETag or Last-Modified), then the cache MUST treat the cache entry as stale. */ bool invalidate = false; if (ctx.ResponseEntityLength != -1 && ctx.ResponseEntityLength != ctx.CacheEntityLength) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_head_resp_has_different_content_length)); invalidate = true; } if (resp.Headers[HttpKnownHeaderNames.ContentMD5] != ctx.CacheHeaders[HttpKnownHeaderNames.ContentMD5]) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_head_resp_has_different_content_md5)); invalidate = true; } if (resp.Headers.ETag != ctx.CacheHeaders.ETag) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_head_resp_has_different_etag)); invalidate = true; } if (resp.StatusCode != HttpStatusCode.NotModified && resp.Headers.LastModified != ctx.CacheHeaders.LastModified) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_304_head_resp_has_different_last_modified)); invalidate = true; } if (invalidate) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_existing_entry_has_to_be_discarded)); return CacheValidationStatus.RemoveFromCache; } } // If server has returned 206 partial content if (resp.StatusCode == HttpStatusCode.PartialContent) { /* A cache MUST NOT combine a 206 response with other previously cached content if the ETag or Last-Modified headers do not match exactly, see 13.5.4. */ // Sometime if ETag has been used the server won't include Last-Modified, which seems to be OK if (ctx.CacheHeaders.ETag != ctx.Response.Headers.ETag || (ctx.CacheHeaders.LastModified != ctx.Response.Headers.LastModified && (ctx.Response.Headers.LastModified != null || ctx.Response.Headers.ETag == null))) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_206_resp_non_matching_entry)); if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_existing_entry_should_be_discarded)); return CacheValidationStatus.RemoveFromCache; } // check does the live stream fit exactly into our cache tail if (ctx.CacheEntry.StreamSize != ctx.ResponseRangeStart) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_206_resp_starting_position_not_adjusted)); return CacheValidationStatus.DoNotTakeFromCache; } Common.ReplaceOrUpdateCacheHeaders(ctx, resp); if (ctx.RequestRangeUser) { // This happens when a response is being downloaded page by page // We request combining the streams // A user will see data starting CacheStreamOffset of a combined stream ctx.CacheStreamOffset = ctx.CacheEntry.StreamSize; // This is a user response content length ctx.CacheStreamLength = ctx.ResponseRangeEnd - ctx.ResponseRangeStart + 1; // This is a new cache stream size ctx.CacheEntityLength = ctx.ResponseEntityLength; ctx.CacheStatusCode = resp.StatusCode; ctx.CacheStatusDescription = resp.StatusDescription; ctx.CacheHttpVersion = resp.ProtocolVersion; } else { // This happens when previous response was downloaded partly ctx.CacheStreamOffset = 0; ctx.CacheStreamLength = ctx.ResponseEntityLength; ctx.CacheEntityLength = ctx.ResponseEntityLength; ctx.CacheStatusCode = HttpStatusCode.OK; ctx.CacheStatusDescription = Common.OkDescription; ctx.CacheHttpVersion = resp.ProtocolVersion; ctx.CacheHeaders.Remove(HttpKnownHeaderNames.ContentRange); if (ctx.CacheStreamLength == -1) {ctx.CacheHeaders.Remove(HttpKnownHeaderNames.ContentLength);} else {ctx.CacheHeaders[HttpKnownHeaderNames.ContentLength] = ctx.CacheStreamLength.ToString(NumberFormatInfo.InvariantInfo);} } // At this point the protocol should create a combined stream made up of the cached and live streams if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_combined_resp_requested)); return CacheValidationStatus.CombineCachedAndServerResponse; } /* 304 Not Modified The response MUST include the following header fields: - Date, unless its omission is required by section 14.18.1 If a clockless origin server obeys these rules, and proxies and clients add their own Date to any response received without one (as already specified by [RFC 2068], section 14.19), caches will operate correctly. - ETag and/or Content-Location, if the header would have been sent in a 200 response to the same request - Expires, Cache-Control, and/or Vary, if the field-value might differ from that sent in any previous response for the same variant */ if (resp.StatusCode == HttpStatusCode.NotModified) { // We will return the response from cache. // We try to avoid to update Cache update in case the server has // sent only headers that are "safe" to ignore // It's not the best way but WinInet does not work well with headers update. WebHeaderCollection cc = resp.Headers; string location = null; string etag = null; if ((ctx.CacheExpires != ctx.ResponseExpires) || (ctx.CacheLastModified != ctx.ResponseLastModified) || (ctx.CacheDate != ctx.ResponseDate) || (ctx.ResponseCacheControl.IsNotEmpty) || ((location=cc[HttpKnownHeaderNames.ContentLocation]) != null && location != ctx.CacheHeaders[HttpKnownHeaderNames.ContentLocation]) || ((etag=cc.ETag) != null && etag != ctx.CacheHeaders.ETag)) { // Headers have to be updated // Note that would allow a new E-Tag header to come in without changing the content. if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_updating_headers_on_304)); Common.ReplaceOrUpdateCacheHeaders(ctx, resp); return CacheValidationStatus.ReturnCachedResponse; } //Try to not update headers if they are invariant or the same int ignoredHeaders = 0; if (etag != null) { ++ignoredHeaders; } if (location != null) { ++ignoredHeaders; } if (ctx.ResponseAge != TimeSpan.MinValue) { ++ignoredHeaders; } if (ctx.ResponseLastModified != DateTime.MinValue) { ++ignoredHeaders; } if (ctx.ResponseExpires != DateTime.MinValue) { ++ignoredHeaders; } if (ctx.ResponseDate != DateTime.MinValue) { ++ignoredHeaders; } if (cc.Via != null) { ++ignoredHeaders; } if (cc[HttpKnownHeaderNames.Connection] != null) { ++ignoredHeaders; } if (cc[HttpKnownHeaderNames.KeepAlive] != null) { ++ignoredHeaders; } if (cc.ProxyAuthenticate != null) { ++ignoredHeaders; } if (cc[HttpKnownHeaderNames.ProxyAuthorization] != null) { ++ignoredHeaders; } if (cc[HttpKnownHeaderNames.TE] != null) { ++ignoredHeaders; } if (cc[HttpKnownHeaderNames.TransferEncoding] != null) { ++ignoredHeaders; } if (cc[HttpKnownHeaderNames.Trailer] != null) { ++ignoredHeaders; } if (cc[HttpKnownHeaderNames.Upgrade] != null) { ++ignoredHeaders; } if (resp.Headers.Count <= ignoredHeaders) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_suppressing_headers_update_on_304)); ctx.CacheDontUpdateHeaders = true; } else { Common.ReplaceOrUpdateCacheHeaders(ctx, resp); } return CacheValidationStatus.ReturnCachedResponse; } // Any other response if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_status_code_not_304_206)); return CacheValidationStatus.DoNotTakeFromCache; } /* Returns: - ReturnCachedResponse : Cache may be returned to the app - DoNotTakeFromCache : Cache must not be returned to the app */ public static CacheValidationStatus ValidateCacheOn5XXResponse(HttpRequestCacheValidator ctx) { /* If a cache receives a 5xx response while attempting to revalidate an entry, it MAY either forward this response to the requesting client, or act as if the server failed to respond. In the latter case, it MAY return a previously received response unless the cached entry includes the "must-revalidate" cache-control directive */ // Do we have cached item? if (ctx.CacheStream == Stream.Null || ctx.CacheStatusCode == (HttpStatusCode)0) { return CacheValidationStatus.DoNotTakeFromCache; } if (ctx.CacheEntityLength != ctx.CacheEntry.StreamSize || ctx.CacheStatusCode == HttpStatusCode.PartialContent) { // Partial cache remains partial, user will not know that. // This is because user either did not provide a Range Header or // the user range was just forwarded to the server bypassing cache return CacheValidationStatus.DoNotTakeFromCache; } if (ValidateCacheBySpecialCases(ctx) != TriState.Valid) { // This response cannot be used without _successful_ revalidation return CacheValidationStatus.DoNotTakeFromCache; } if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly || ctx.Policy.Level == HttpRequestCacheLevel.CacheIfAvailable || ctx.Policy.Level == HttpRequestCacheLevel.CacheOrNextCacheOnly) { // that was a cache only request if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_sxx_resp_cache_only)); return CacheValidationStatus.ReturnCachedResponse; } if (ctx.Policy.Level == HttpRequestCacheLevel.Default || ctx.Policy.Level == HttpRequestCacheLevel.Revalidate) { if (ValidateCacheByClientPolicy(ctx)) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_sxx_resp_can_be_replaced)); ctx.CacheHeaders.Add(HttpKnownHeaderNames.Warning, HttpRequestCacheValidator.Warning_111); return CacheValidationStatus.ReturnCachedResponse; } } return CacheValidationStatus.DoNotTakeFromCache; } /* When the cache receives a subsequent request whose Request-URI specifies one or more cache entries including a Vary header field, the cache MUST NOT use such a cache entry to construct a response to the new request unless all of the selecting request-headers present in the new request match the corresponding stored request-headers in the original request. The selecting request-headers from two requests are defined to match if and only if the selecting request-headers in the first request can be transformed to the selecting request-headers in the second request by adding or removing linear white space (LWS) at places where this is allowed by the corresponding BNF, and/or combining multiple message-header fields with the same field name following the rules about message headers in section 4.2. A Vary header field-value of "*" always fails to match and subsequent requests on that resource can only be properly interpreted by the origin server. */ /* Returns: - Valid : Vary header values match in both request and cache - Invalid : Vary header values do not match - Unknown : Vary header is not present in cache */ internal static TriState ValidateCacheByVaryHeader(HttpRequestCacheValidator ctx) { string[] cacheVary = ctx.CacheHeaders.GetValues(HttpKnownHeaderNames.Vary); if (cacheVary == null) { return TriState.Unknown; } ArrayList varyValues = new ArrayList(); HttpRequestCacheValidator.ParseHeaderValues(cacheVary, HttpRequestCacheValidator.ParseValuesCallback, varyValues); if (varyValues.Count == 0) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_empty)); return TriState.Invalid; } if (((string)(varyValues[0]))[0] == '*') { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_contains_asterisks)); return TriState.Invalid; } if (ctx.SystemMeta == null || ctx.SystemMeta.Count == 0) { // We keep there previous request headers if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_no_headers_in_metadata)); return TriState.Invalid; } /* A Vary field value consisting of a list of field-names signals that the representation selected for the response is based on a selection algorithm which considers ONLY the listed request-header field values in selecting the most appropriate representation. A cache MAY assume that the same selection will be made for future requests with the same values for the listed field names, for the duration of time for which the response is fresh. */ for (int i = 0; i < varyValues.Count; ++i) { string[] requestValues = ctx.Request.Headers.GetValues((string)varyValues[i]); ArrayList requestFields = new ArrayList(); if (requestValues != null) { HttpRequestCacheValidator.ParseHeaderValues(requestValues, HttpRequestCacheValidator.ParseValuesCallback, requestFields); } string[] cacheValues = ctx.SystemMeta.GetValues((string)varyValues[i]); ArrayList cacheFields = new ArrayList(); if (cacheValues != null) { HttpRequestCacheValidator.ParseHeaderValues(cacheValues, HttpRequestCacheValidator.ParseValuesCallback, cacheFields); } if (requestFields.Count != cacheFields.Count) { if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_mismatched_count, (string)varyValues[i])); return TriState.Invalid; } // NB: fields order is significant as per RFC. for (int j = 0; j < cacheFields.Count; ++j) { if (!AsciiLettersNoCaseEqual((string)cacheFields[j], (string)requestFields[j])) { 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])); return TriState.Invalid; } } } if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_vary_header_match)); // The Vary header is in cache and all headers values referenced to are equal to those in the Request. return TriState.Valid; } // Returns // - DoNotTakeFromCache = A request shall go as is and the cache will be dropped // - Continue = Cache should be preserved and after-response validator should be called public static CacheValidationStatus TryConditionalRequest(HttpRequestCacheValidator ctx) { string ranges; TriState isPartial = CheckForRangeRequest(ctx, out ranges); if (isPartial == TriState.Invalid) { // This is a user requested range, pass it as is return CacheValidationStatus.Continue; } if(isPartial == TriState.Valid) { // Not all proxy servers, support requesting a range on an FTP // command, so to be safe, never try to mix the cache with a range // response. Always get the whole thing fresh in the case of FTP // over proxy. if (ctx is FtpRequestCacheValidator) return CacheValidationStatus.DoNotTakeFromCache; // We only have a partial response, need to complete it if (TryConditionalRangeRequest(ctx)){ // We can do a conditional range request ctx.RequestRangeCache = true; ((HttpWebRequest)ctx.Request).AddRange((int)ctx.CacheEntry.StreamSize); if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_range, ctx.Request.Headers[HttpKnownHeaderNames.Range])); return CacheValidationStatus.Continue; } return CacheValidationStatus.DoNotTakeFromCache; } //This is not a range request return ConstructConditionalRequest(ctx); } // Returns: // ReturnFromCache = Take it from cache // DoNotTakeFromCache= Reload from server and disregard current cache // Continue = Send a request that may have added a conditional header public static CacheValidationStatus TryResponseFromCache(HttpRequestCacheValidator ctx) { string ranges; TriState isRange = CheckForRangeRequest(ctx, out ranges); if (isRange == TriState.Unknown) { return CacheValidationStatus.ReturnCachedResponse; } if (isRange == TriState.Invalid) { // user range request long start = 0; long end = 0; long total = 0; if (!GetBytesRange(ranges, ref start, ref end, ref total, true)) { if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_range_invalid_format, ranges)); return CacheValidationStatus.DoNotTakeFromCache; } if (start >= ctx.CacheEntry.StreamSize || end > ctx.CacheEntry.StreamSize || (end == -1 && ctx.CacheEntityLength == -1) || (end == -1 && ctx.CacheEntityLength > ctx.CacheEntry.StreamSize) || (start == -1 && (end == -1 || ctx.CacheEntityLength == -1 || (ctx.CacheEntityLength - end >= ctx.CacheEntry.StreamSize)))) { // we don't have such a range in cache if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_range_not_in_cache, ranges)); return CacheValidationStatus.Continue; } if (start == -1) { start = ctx.CacheEntityLength - end; } if (end <= 0) { end = ctx.CacheEntry.StreamSize - 1; } ctx.CacheStreamOffset = start; ctx.CacheStreamLength = end-start+1; Construct206PartialContent(ctx, (int) start); if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_range_in_cache, ctx.CacheHeaders[HttpKnownHeaderNames.ContentRange])); return CacheValidationStatus.ReturnCachedResponse; } // // Here we got a partially cached response and the user wants a whole response // if (ctx.Policy.Level == HttpRequestCacheLevel.CacheOnly && ((object)ctx.Uri.Scheme == (object)Uri.UriSchemeHttp || (object)ctx.Uri.Scheme == (object)Uri.UriSchemeHttps)) { // Here we should strictly report a failure // Only for HTTP and HTTPS we choose to return a partial content even user did not ask for it ctx.CacheStreamOffset = 0; ctx.CacheStreamLength = ctx.CacheEntry.StreamSize; Construct206PartialContent(ctx, 0); if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_partial_resp, ctx.CacheHeaders[HttpKnownHeaderNames.ContentRange])); return CacheValidationStatus.ReturnCachedResponse; } if (ctx.CacheEntry.StreamSize >= Int32.MaxValue) { if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_entry_size_too_big, ctx.CacheEntry.StreamSize)); return CacheValidationStatus.DoNotTakeFromCache; } if (TryConditionalRangeRequest(ctx)) { ctx.RequestRangeCache = true; ((HttpWebRequest)ctx.Request).AddRange((int)ctx.CacheEntry.StreamSize); if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_range, ctx.Request.Headers[HttpKnownHeaderNames.Range])); return CacheValidationStatus.Continue; } // This will let an unconditional request go return CacheValidationStatus.Continue; } /* Discovers the fact that cached response is a partial one. Returns: - Invalid : It's a user range request - Valid : It's a partial cached response - Unknown : It's neither a range request nor the cache does have a partial response */ private static TriState CheckForRangeRequest(HttpRequestCacheValidator ctx, out string ranges) { if ((ranges = ctx.Request.Headers[HttpKnownHeaderNames.Range]) != null) { // A request already contains range. // The caller will either return it from cache or pass as is to the server ctx.RequestRangeUser = true; if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_range_request_range, ctx.Request.Headers[HttpKnownHeaderNames.Range])); return TriState.Invalid; } if (ctx.CacheStatusCode == HttpStatusCode.PartialContent && ctx.CacheEntityLength == ctx.CacheEntry.StreamSize) { // this is a whole resposne ctx.CacheStatusCode = HttpStatusCode.OK; ctx.CacheStatusDescription = Common.OkDescription; return TriState.Unknown; } if (ctx.CacheEntry.IsPartialEntry || (ctx.CacheEntityLength != -1 && ctx.CacheEntityLength != ctx.CacheEntry.StreamSize) || ctx.CacheStatusCode == HttpStatusCode.PartialContent) { //The cache may contain a partial response if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_could_be_partial, ctx.CacheEntry.StreamSize, ctx.CacheEntityLength)); return TriState.Valid; } return TriState.Unknown; } /* HTTP/1.1 clients: - If an entity tag has been provided by the origin server, MUST use that entity tag in any cache-conditional request (using If- Match or If-None-Match). - If only a Last-Modified value has been provided by the origin server, SHOULD use that value in non-subrange cache-conditional requests (using If-Modified-Since). - If only a Last-Modified value has been provided by an HTTP/1.0 origin server, MAY use that value in subrange cache-conditional requests (using If-Unmodified-Since:). The user agent SHOULD provide a way to disable this, in case of difficulty. - If both an entity tag and a Last-Modified value have been provided by the origin server, SHOULD use both validators in cache-conditional requests. This allows both HTTP/1.0 and HTTP/1.1 caches to respond appropriately. */ /* Returns: - Continue : Conditional request has been constructed - DoNotTakeFromCache : Conditional request cannot be constructed */ public static CacheValidationStatus ConstructConditionalRequest(HttpRequestCacheValidator ctx) { CacheValidationStatus result = CacheValidationStatus.DoNotTakeFromCache; // The assumption is that a _user_ conditional request was already filtered out bool validator2 = false; string str = ctx.CacheHeaders.ETag; if (str != null) { result = CacheValidationStatus.Continue; ctx.Request.Headers[HttpKnownHeaderNames.IfNoneMatch] = str; ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfNoneMatch; ctx.RequestValidator1 = str; validator2 = true; if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_condition_if_none_match, ctx.Request.Headers[HttpKnownHeaderNames.IfNoneMatch])); } if (ctx.CacheEntry.LastModifiedUtc != DateTime.MinValue) { result = CacheValidationStatus.Continue; str = ctx.CacheEntry.LastModifiedUtc.ToString("r", CultureInfo.InvariantCulture); ctx.Request.Headers.ChangeInternal(HttpKnownHeaderNames.IfModifiedSince, str); if (validator2) { ctx.RequestIfHeader2 = HttpKnownHeaderNames.IfModifiedSince; ctx.RequestValidator2 = str; } else { ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfModifiedSince; ctx.RequestValidator1 = str; } if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_condition_if_modified_since, ctx.Request.Headers[HttpKnownHeaderNames.IfModifiedSince])); } if(Logging.On) { if (result == CacheValidationStatus.DoNotTakeFromCache) { Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_cannot_construct_conditional_request)); } } return result; } /* Returns: - true: Conditional Partial request has been constructed - false: Conditional Partial request cannot be constructed */ private static bool TryConditionalRangeRequest(HttpRequestCacheValidator ctx) { // // The response is partially cached (that has been checked before calling this method) // if (ctx.CacheEntry.StreamSize >= Int32.MaxValue) { //This is a restriction of HttpWebRequest implementation as on 01/28/03 if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_entry_size_too_big, ctx.CacheEntry.StreamSize)); return false; } /* If the entity tag given in the If-Range header matches the current entity tag for the entity, then the server SHOULD provide the specified sub-range of the entity using a 206 (Partial content) response. If the entity tag does not match, then the server SHOULD return the entire entity using a 200 (OK) response. */ string str = ctx.CacheHeaders.ETag; if (str != null) { ctx.Request.Headers[HttpKnownHeaderNames.IfRange] = str; ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfRange; ctx.RequestValidator1 =str; if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_condition_if_range, ctx.Request.Headers[HttpKnownHeaderNames.IfRange])); return true; } /* - If only a Last-Modified value has been provided by an HTTP/1.0 origin server, MAY use that value in subrange cache-conditional requests (using If-Unmodified-Since:). The user agent SHOULD provide a way to disable this, in case of difficulty. */ if (ctx.CacheEntry.LastModifiedUtc != DateTime.MinValue) { str = ctx.CacheEntry.LastModifiedUtc.ToString("r", CultureInfo.InvariantCulture); if (ctx.CacheHttpVersion.Major == 1 && ctx.CacheHttpVersion.Minor == 0) { // Well If-Unmodified-Since would require an additional request in case it WAS modified // A User may want to excerise this path without relying on our implementation. if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_conditional_range_not_implemented_on_http_10)); return false; /* //Http == 1.0 ctx.Request.Headers[HttpKnownHeaderNames.IfUnmodifiedSince] = str; ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfUnmodifiedSince; ctx.RequestValidator1 = str; if(Logging.On)Logging.PrintInfo(Logging.RequestCache, "Request Condition = If-Unmodified-Since:" + ctx.Request.Headers[HttpKnownHeaderNames.IfUnmodifiedSince]); return true; */ } else { //Http > 1.0 ctx.Request.Headers[HttpKnownHeaderNames.IfRange] = str; ctx.RequestIfHeader1 = HttpKnownHeaderNames.IfRange; ctx.RequestValidator1 =str; if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_condition_if_range, ctx.Request.Headers[HttpKnownHeaderNames.IfRange])); return true; } } if(Logging.On)Logging.PrintWarning(Logging.RequestCache, SR.GetString(SR.net_log_cache_cannot_construct_conditional_range_request)); //Cannot construct a conditional request return false; } // // A template for 206 response that we serve from cache on a user range request // It's also used for cache update. // public static void Construct206PartialContent(HttpRequestCacheValidator ctx, int rangeStart) { ctx.CacheStatusCode = HttpStatusCode.PartialContent; ctx.CacheStatusDescription = PartialContentDescription; if (ctx.CacheHttpVersion == null) { ctx.CacheHttpVersion = new Version(1,1); } string ranges = "bytes " + rangeStart + '-' + (rangeStart + ctx.CacheStreamLength-1) +'/' + (ctx.CacheEntityLength <= 0?"*":ctx.CacheEntityLength.ToString(NumberFormatInfo.InvariantInfo)); ctx.CacheHeaders[HttpKnownHeaderNames.ContentRange] = ranges; ctx.CacheHeaders[HttpKnownHeaderNames.ContentLength] = ctx.CacheStreamLength.ToString(NumberFormatInfo.InvariantInfo); ctx.CacheEntry.IsPartialEntry = true; } // // A template for 200 response, used by cache update // public static void Construct200ok(HttpRequestCacheValidator ctx) { ctx.CacheStatusCode = HttpStatusCode.OK; ctx.CacheStatusDescription = Common.OkDescription; if (ctx.CacheHttpVersion == null) ctx.CacheHttpVersion = new Version(1,1); ctx.CacheHeaders.Remove(HttpKnownHeaderNames.ContentRange); if (ctx.CacheEntityLength == -1) {ctx.CacheHeaders.Remove(HttpKnownHeaderNames.ContentLength);} else {ctx.CacheHeaders[HttpKnownHeaderNames.ContentLength] = ctx.CacheEntityLength.ToString(NumberFormatInfo.InvariantInfo);} ctx.CacheEntry.IsPartialEntry = false; } // // Clear the request from any conditional headers and request no-cache // public static void ConstructUnconditionalRefreshRequest(HttpRequestCacheValidator ctx) { WebHeaderCollection cc = ctx.Request.Headers; cc[HttpKnownHeaderNames.CacheControl]="max-age=0"; cc[HttpKnownHeaderNames.Pragma]="no-cache"; if (ctx.RequestIfHeader1 != null) { cc.RemoveInternal(ctx.RequestIfHeader1); ctx.RequestIfHeader1 = null; } if (ctx.RequestIfHeader2 != null) { cc.RemoveInternal(ctx.RequestIfHeader2); ctx.RequestIfHeader2 = null; } if (ctx.RequestRangeCache) { cc.RemoveInternal(HttpKnownHeaderNames.Range); ctx.RequestRangeCache = false; } } // // This is called when we have decided to take from Cache or update Cache // public static void ReplaceOrUpdateCacheHeaders(HttpRequestCacheValidator ctx, HttpWebResponse resp) { /* In other words, the set of end-to-end headers received in the incoming response overrides all corresponding end-to-end headers stored with the cache entry (except for stored Warning headers with warn-code 1xx, which are deleted even if not overridden). This rule does not allow an origin server to use a 304 (Not Modified) or a 206 (Partial Content) response to entirely delete a header that it had provided with a previous response. */ if (ctx.CacheHeaders == null || (resp.StatusCode != HttpStatusCode.NotModified && resp.StatusCode != HttpStatusCode.PartialContent)) { // existing context is dropped ctx.CacheHeaders = new WebHeaderCollection(); } // Here we preserve Request headers that are present in the response Vary header string[] respVary = resp.Headers.GetValues(HttpKnownHeaderNames.Vary); if (respVary != null) { ArrayList varyValues = new ArrayList(); HttpRequestCacheValidator.ParseHeaderValues(respVary, HttpRequestCacheValidator.ParseValuesCallback, varyValues); if (varyValues.Count != 0 && ((string)(varyValues[0]))[0] != '*') { // we got some request headers to save if(Logging.On)Logging.PrintInfo(Logging.RequestCache, SR.GetString(SR.net_log_cache_saving_request_headers, resp.Headers[HttpKnownHeaderNames.Vary])); if (ctx.SystemMeta == null) { ctx.SystemMeta = new NameValueCollection(varyValues.Count+1, CaseInsensitiveAscii.StaticInstance); } for (int i = 0; i < varyValues.Count; ++i) { string headerValue = ctx.Request.Headers[(string)varyValues[i]]; ctx.SystemMeta[(string)varyValues[i]] = headerValue; } } } /* - Hop-by-hop headers, which are meaningful only for a single transport-level connection, and are not stored by caches or forwarded by proxies. The following HTTP/1.1 headers are hop-by-hop headers: - Connection - Keep-Alive - Proxy-Authenticate - Proxy-Authorization - TE - Trailers - Transfer-Encoding - Upgrade */ // We add or Replace headers from the live response for (int i = 0; i < ctx.Response.Headers.Count; ++i) { string key = ctx.Response.Headers.GetKey(i); if (AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.Connection) || AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.KeepAlive) || AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.ProxyAuthenticate) || AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.ProxyAuthorization) || AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.TE) || AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.TransferEncoding) || AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.Trailer) || AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.Upgrade)) { continue; } if (resp.StatusCode == HttpStatusCode.NotModified && AsciiLettersNoCaseEqual(key, HttpKnownHeaderNames.ContentLength)) { continue; } ctx.CacheHeaders.ChangeInternal(key, ctx.Response.Headers[i]); } } // // // private static bool AsciiLettersNoCaseEqual(string s1, string s2) { if (s1.Length != s2.Length) { return false; } for (int i = 0; i < s1.Length; ++i) { if ((s1[i]|0x20) != (s2[i]|0x20)) { return false; } } return true; } // // // internal unsafe static bool UnsafeAsciiLettersNoCaseEqual(char* s1, int start, int length, string s2) { if (length-start < s2.Length) { return false; } for (int i = 0; i < s2.Length; ++i) { if ((s1[start+i]|0x20) != (s2[i]|0x20)) { return false; } } return true; } // // Parses the string on "bytes = start - end" or "bytes start-end/xxx" // // Returns // true = take start/end for range // false = parsing error public static bool GetBytesRange(string ranges, ref long start, ref long end, ref long total, bool isRequest) { ranges = ranges.ToLower(CultureInfo.InvariantCulture); int idx = 0; while (idx < ranges.Length && ranges[idx] == ' ') { ++idx; } idx+=5; // The "ranges" string is already in lowercase if( idx >= ranges.Length || ranges[idx-5] != 'b' || ranges[idx-4] != 'y' || ranges[idx-3] != 't' || ranges[idx-2] != 'e' || ranges[idx-1] != 's') { if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_only_byte_range_implemented)); return false; } if (isRequest) { while (idx < ranges.Length && ranges[idx] == ' ') { ++idx; } if (ranges[idx] != '=') { return false; } } else { if (ranges[idx] != ' ') { return false; } } char ch = (char)0; while (++idx < ranges.Length && (ch=ranges[idx]) == ' ') { ; } start = -1; if (ch != '-') { // parsing start if (idx < ranges.Length && ch >= '0' && ch <= '9') { start = ch-'0'; while(++idx < ranges.Length && (ch = ranges[idx]) >= '0' && ch <= '9') { start = start*10 + (ch-'0'); } } while (idx < ranges.Length && ch == ' ') {ch = ranges[++idx];} if (ch != '-') {return false;} } // parsing end while (idx < ranges.Length && (ch = ranges[++idx]) == ' ') { ; } end = -1; if (idx < ranges.Length && ch >= '0' && ch <= '9') { end = ch-'0'; while(++idx < ranges.Length && (ch = ranges[idx]) >= '0' && ch <= '9') { end = end*10 + (ch-'0'); } } if (isRequest) { while (idx < ranges.Length) { if (ranges[idx++] != ' ') { if(Logging.On)Logging.PrintError(Logging.RequestCache, SR.GetString(SR.net_log_cache_multiple_complex_range_not_implemented)); return false; } } } else { // parsing total while (idx < ranges.Length && (ch = ranges[idx]) == ' ') { ++idx; } if (ch != '/') { return false; } while (++idx < ranges.Length && (ch=ranges[idx]) == ' ') { ; } total = -1; if (ch != '*') { if (idx < ranges.Length && ch >= '0' && ch <= '9') { total = ch-'0'; while (++idx < ranges.Length && (ch = ranges[idx]) >= '0' && ch <= '9') { total = total*10 + (ch-'0'); } } } } if (!isRequest && (start == -1 || end == -1)) { return false; } return true; } } } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu
This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- AnnotationResourceChangedEventArgs.cs
- PrefixHandle.cs
- ADMembershipUser.cs
- ToolStripRendererSwitcher.cs
- BrowserTree.cs
- ThreadNeutralSemaphore.cs
- DocumentEventArgs.cs
- ConfigurationManager.cs
- OleDbSchemaGuid.cs
- DefaultTextStore.cs
- SmtpTransport.cs
- TextSimpleMarkerProperties.cs
- TemplateEditingFrame.cs
- ValueHandle.cs
- DynamicControl.cs
- AsmxEndpointPickerExtension.cs
- RegisteredScript.cs
- StopStoryboard.cs
- RelatedCurrencyManager.cs
- Tracking.cs
- Constraint.cs
- BitmapSourceSafeMILHandle.cs
- LayoutEngine.cs
- TransformGroup.cs
- PageParser.cs
- SystemResourceKey.cs
- ExternalFile.cs
- FormsAuthenticationUserCollection.cs
- SecurityPolicySection.cs
- FieldToken.cs
- WebColorConverter.cs
- ExpressionLexer.cs
- DataGridViewCheckBoxCell.cs
- XamlTreeBuilderBamlRecordWriter.cs
- WindowsTooltip.cs
- JavaScriptSerializer.cs
- TypeSystemProvider.cs
- AsyncResult.cs
- Roles.cs
- ConfigXmlSignificantWhitespace.cs
- StubHelpers.cs
- LinearGradientBrush.cs
- ServiceObjectContainer.cs
- ProfessionalColors.cs
- WinEventTracker.cs
- MsmqReceiveParameters.cs
- DataGridViewCellLinkedList.cs
- FormParameter.cs
- StickyNoteAnnotations.cs
- DataRecordInternal.cs
- InputMethodStateTypeInfo.cs
- ModuleConfigurationInfo.cs
- DoubleKeyFrameCollection.cs
- NativeMethods.cs
- ChangeDirector.cs
- TextSelection.cs
- InvokeMethod.cs
- ResourceWriter.cs
- Identity.cs
- ConfigXmlSignificantWhitespace.cs
- PropertyFilterAttribute.cs
- SelectionListComponentEditor.cs
- MemberBinding.cs
- Marshal.cs
- DesignTimeTemplateParser.cs
- IISMapPath.cs
- Intellisense.cs
- ToolBarPanel.cs
- UIElementParaClient.cs
- DataGridViewAutoSizeColumnsModeEventArgs.cs
- RoleGroupCollectionEditor.cs
- Set.cs
- DockPattern.cs
- DataContractSerializerMessageContractImporter.cs
- StylusButton.cs
- BooleanStorage.cs
- DbConnectionOptions.cs
- WebPartUserCapability.cs
- PenThreadWorker.cs
- XmlSchemaObjectCollection.cs
- StringFormat.cs
- TimeSpan.cs
- CompositeActivityTypeDescriptor.cs
- Vector3D.cs
- SystemPens.cs
- DependencyPropertyAttribute.cs
- Tag.cs
- CachingParameterInspector.cs
- XmlTextReader.cs
- LassoSelectionBehavior.cs
- GradientSpreadMethodValidation.cs
- WindowsAuthenticationModule.cs
- xamlnodes.cs
- VirtualizingStackPanel.cs
- TextViewBase.cs
- DataSet.cs
- AccessViolationException.cs
- SchemaElementDecl.cs
- DecimalConstantAttribute.cs
- XmlSchemaComplexContent.cs