|
/*++
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_sync_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;
}
}
}
}
|