internal Func <HttpRequestMessage, Task <HttpResponseMessage> > PutIfUnmodifiedSince() { return((request) => { if (request.Method != HttpMethod.Put) { return null; } DateTimeOffset?ifUnmodifiedSince = request.Headers.IfUnmodifiedSince; if (ifUnmodifiedSince == null) { return null; } DateTimeOffset modifiedInQuestion = ifUnmodifiedSince.Value; var entityTagKey = GenerateCacheKey(request); TimedEntityTagHeaderValue actualEtag = null; bool isModified = true; if (_entityTagStore.TryGetValue(entityTagKey, out actualEtag)) { isModified = actualEtag.LastModified > modifiedInQuestion; } return isModified ? request.CreateResponse(HttpStatusCode.PreconditionFailed) .ToTask() : null; }); }
internal Func <HttpRequestMessage, Task <HttpResponseMessage> > PutIfMatch() { return((request) => { if (request.Method != HttpMethod.Put) { return null; } ICollection <EntityTagHeaderValue> matchTags = request.Headers.IfMatch; if (matchTags == null || matchTags.Count == 0) { return null; } var entityTagKey = GenerateCacheKey(request); TimedEntityTagHeaderValue actualEtag = null; bool matchFound = false; if (_entityTagStore.TryGetValue(entityTagKey, out actualEtag)) { if (matchTags.Any(etag => etag.Tag == actualEtag.Tag)) { matchFound = true; } } return matchFound ? null : request.CreateResponse(HttpStatusCode.PreconditionFailed) .ToTask(); }); }
protected virtual void CheckExpiry(HttpRequestMessage request) { // not interested if not GET if (request.Method != HttpMethod.Get) { return; } var cacheExpiry = CacheRefreshPolicyProvider(request, _configuration); if (cacheExpiry == TimeSpan.MaxValue) { return; // infinity } var cacheKey = GenerateCacheKey(request); TimedEntityTagHeaderValue value = null; if (!_entityTagStore.TryGetValue(cacheKey, out value)) { return; } if (value.LastModified.Add(cacheExpiry) < DateTimeOffset.Now) { _entityTagStore.TryRemove(cacheKey); } }
public static void ApplyTimedETag(this HttpResponse response, TimedEntityTagHeaderValue timedETag) { if (timedETag.LastModified.HasValue) { response.Headers[HttpHeaderNames.LastModified] = timedETag.LastModified.Value.ToUniversalTime().ToString("r"); } else if (timedETag.ETag != null) { response.Headers[HttpHeaderNames.ETag] = timedETag.ETag.ToString(); } }
internal Func <HttpRequestMessage, Task <HttpResponseMessage> > GetIfMatchNoneMatch() { return((request) => { try { if (request.Method != HttpMethod.Get) { return null; } ICollection <EntityTagHeaderValue> noneMatchTags = request.Headers.IfNoneMatch; ICollection <EntityTagHeaderValue> matchTags = request.Headers.IfMatch; if (matchTags.Count == 0 && noneMatchTags.Count == 0) { return null; // no etag } if (matchTags.Count > 0 && noneMatchTags.Count > 0) // both if-match and if-none-match exist { return request.CreateResponse(HttpStatusCode.BadRequest) .ToTask(); } var isNoneMatch = noneMatchTags.Count > 0; var etags = isNoneMatch ? noneMatchTags : matchTags; var entityTagKey = GenerateCacheKey(request); // compare the Etag with the one in the cache // do conditional get. TimedEntityTagHeaderValue actualEtag = null; bool matchFound = false; if (_entityTagStore.TryGetValue(entityTagKey, out actualEtag)) { if (etags.Any(etag => etag.Tag == actualEtag.Tag)) { matchFound = true; } } return matchFound ^ isNoneMatch ? null : new NotModifiedResponse(request, null, actualEtag.ToEntityTagHeaderValue()).ToTask(); } catch (Exception e) { throw new CacheCowServerException("Error in GetIfMatchNoneMatch", e); } }); }
internal Func <HttpRequestMessage, Task <HttpResponseMessage> > GetIfModifiedUnmodifiedSince() { return((request) => { try { if (request.Method != HttpMethod.Get) { return null; } DateTimeOffset?ifModifiedSince = request.Headers.IfModifiedSince; DateTimeOffset?ifUnmodifiedSince = request.Headers.IfUnmodifiedSince; if (ifModifiedSince == null && ifUnmodifiedSince == null) { return null; // no etag } if (ifModifiedSince != null && ifUnmodifiedSince != null) // both exist { return request.CreateResponse(HttpStatusCode.BadRequest) .ToTask(); } bool ifModified = (ifUnmodifiedSince == null); DateTimeOffset modifiedInQuestion = ifModified ? ifModifiedSince.Value : ifUnmodifiedSince.Value; var entityTagKey = GenerateCacheKey(request); TimedEntityTagHeaderValue actualEtag = null; bool isModified = true; if (_entityTagStore.TryGetValue(entityTagKey, out actualEtag)) { isModified = actualEtag.LastModified > modifiedInQuestion; } return isModified ^ ifModified ? new NotModifiedResponse(request, null, actualEtag.ToEntityTagHeaderValue()).ToTask() : null; } catch (Exception e) { throw new CacheCowServerException("Error in GetIfModifiedUnmodifiedSince", e); } }); }
public void AddOrUpdate(CacheKey key, TimedEntityTagHeaderValue eTag) { _eTagCache.Set(key.HashBase64, eTag, DateTimeOffset.MaxValue); // route pattern var bag = new ConcurrentBag <CacheKey>(); bag = (ConcurrentBag <CacheKey>)_routePatternCache.AddOrGetExisting(key.RoutePattern, bag , DateTimeOffset.MaxValue) ?? bag; bag.Add(key); // resource var rbag = new ConcurrentBag <CacheKey>(); rbag = (ConcurrentBag <CacheKey>)_resourceCache.AddOrGetExisting(key.ResourceUri, rbag , DateTimeOffset.MaxValue) ?? rbag; rbag.Add(key); }
/// <summary> /// Adds caching for GET and PUT if /// cache control provided is not null /// With PUT, since cache has been alreay invalidated, /// we provide the new ETag (old one has been cleared in invalidation phase) /// </summary> /// <param name="cacheKey"></param> /// <param name="request"></param> /// <param name="response"></param> /// <param name="varyHeaders"></param> /// <returns></returns> internal Action AddCaching( CacheKey cacheKey, HttpRequestMessage request, HttpResponseMessage response) { return (() => { var cacheControlHeaderValue = CacheControlHeaderProvider(request, _configuration); if (cacheControlHeaderValue == null) { return; } TimedEntityTagHeaderValue eTagValue; string uri = UriTrimmer(request.RequestUri); // in case of GET and no ETag // in case of PUT, we should return the new ETag of the resource // NOTE: No need to check if it is in the cache. If it were, it would not get // here if (request.Method == HttpMethod.Get || request.Method == HttpMethod.Put) { // create new ETag only if it does not already exist if (!_entityTagStore.TryGetValue(cacheKey, out eTagValue)) { eTagValue = new TimedEntityTagHeaderValue(ETagValueGenerator(uri, request.Headers)); _entityTagStore.AddOrUpdate(cacheKey, eTagValue); } // set ETag response.Headers.ETag = eTagValue.ToEntityTagHeaderValue(); // set last-modified if (AddLastModifiedHeader && response.Content != null && !response.Content.Headers.Any(x => x.Key.Equals(HttpHeaderNames.LastModified, StringComparison.CurrentCultureIgnoreCase))) { response.Content.Headers.Add(HttpHeaderNames.LastModified, eTagValue.LastModified.ToString("r")); } // set Vary if (AddVaryHeader && _varyByHeaders != null && _varyByHeaders.Length > 0) { response.Headers.Add(HttpHeaderNames.Vary, _varyByHeaders); } // harmonise Pragma header with cachecontrol header if (cacheControlHeaderValue.NoStore) { response.Headers.TryAddWithoutValidation(HttpHeaderNames.Pragma, "no-cache"); if (response.Content != null) { response.Content.Headers.Expires = DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(1)); } } else { if (response.Headers.Contains(HttpHeaderNames.Pragma)) { response.Headers.Remove(HttpHeaderNames.Pragma); } } response.Headers.TryAddWithoutValidation(HttpHeaderNames.CacheControl, cacheControlHeaderValue.ToString()); } }); }
/// <summary> /// Happens at the incoming (executING) /// </summary> /// <param name="timedEtag"></param> /// <param name="cacheValidationStatus"></param> /// <param name="context"> /// </param> /// <returns> /// True: applied and the call can exit (short-circuit) /// False: tried to apply but did not match hence the call should continue /// null: could not apply (timedEtag was null) /// </returns> protected bool?ApplyCacheValidation(TimedEntityTagHeaderValue timedEtag, CacheValidationStatus cacheValidationStatus, HttpContext context) { if (timedEtag == null) { return(null); } var headers = context.Request.GetTypedHeadersWithCaching(); switch (cacheValidationStatus) { case CacheValidationStatus.GetIfModifiedSince: if (timedEtag.LastModified == null) { return(false); } else { if (timedEtag.LastModified > headers.IfModifiedSince.Value) { return(false); } else { context.Response.StatusCode = StatusCodes.Status304NotModified; return(true); } } case CacheValidationStatus.GetIfNoneMatch: if (timedEtag.ETag == null) { return(false); } else { if (headers.IfNoneMatch.Any(x => x.Tag == timedEtag.ETag.Tag)) { context.Response.StatusCode = StatusCodes.Status304NotModified; return(true); } else { return(false); } } case CacheValidationStatus.PutPatchDeleteIfMatch: if (timedEtag.ETag == null) { return(false); } else { if (headers.IfMatch.Any(x => x.Tag == timedEtag.ETag.Tag)) { return(false); } else { context.Response.StatusCode = StatusCodes.Status412PreconditionFailed; return(true); } } case CacheValidationStatus.PutPatchDeleteIfUnModifiedSince: if (timedEtag.LastModified == null) { return(false); } else { if (timedEtag.LastModified > headers.IfUnmodifiedSince.Value) { context.Response.StatusCode = StatusCodes.Status412PreconditionFailed; return(true); } else { return(false); } } default: return(null); } }
/// <summary> /// Applies after the controller assigned a result to the action /// </summary> /// <param name="context">HttpContext</param> /// <param name="viewModel">action result</param> public async Task After(HttpContext context, object viewModel) { var ms = context.Response.Body as MemoryStream; bool mustReflush = ms != null && ms.Length > 0; context.Response.Body = _stream; try { if (HttpMethods.IsGet(context.Request.Method)) { context.Response.Headers.Add(HeaderNames.Vary, string.Join(";", _cacheDirectiveProvider.GetVaryHeaders(context))); var cacheControl = _cacheDirectiveProvider.GetCacheControl(context, this.ConfiguredExpiry); var isResponseCacheable = _validator.IsCacheable(context.Response); if (!cacheControl.NoStore && isResponseCacheable) // _______ is cacheable { TimedEntityTagHeaderValue tet = null; if (viewModel != null) { tet = _cacheDirectiveProvider.Extract(viewModel); } if (_cacheValidated == null && // could not validate tet != null && _cacheValidationStatus != CacheValidationStatus.None) // can only do GET validation, PUT is already impacted backend stores { _cacheValidated = ApplyCacheValidation(tet, _cacheValidationStatus, context); _cacheCowHeader.ValidationApplied = true; // the response would have been set and no need to run the rest of the pipeline if (_cacheValidated ?? false) { _cacheCowHeader.ValidationMatched = true; context.Response.Headers.Add(CacheCowHeader.Name, _cacheCowHeader.ToString()); return; } } if (tet != null) { context.Response.ApplyTimedETag(tet); } } if (!_isRequestCacheable || !isResponseCacheable) { context.Response.MakeNonCacheable(); } else { context.Response.Headers[HttpHeaderNames.CacheControl] = cacheControl.ToString(); } context.Response.Headers.Add(CacheCowHeader.Name, _cacheCowHeader.ToString()); } } finally { if (mustReflush) { ms.Position = 0; await ms.CopyToAsync(context.Response.Body); } } }
public bool TryGetValue(CacheKey key, out TimedEntityTagHeaderValue eTag) { eTag = (TimedEntityTagHeaderValue)_eTagCache.Get(key.HashBase64); return(eTag != null); }