private async Task GenerateResponseFromStore(HttpContext httpContext, bool updateLastModified = true) { var logInformation = string.Empty; var headers = httpContext.Response.Headers; // set the ETag & Last-Modified date. // remove any other ETag and Last-Modified headers (could be set // by other pieces of code) headers.Remove(HeaderNames.ETag); headers.Remove(HeaderNames.LastModified); // generate key var storeKey = await _storeKeyGenerator.GenerateStoreKey( ConstructStoreKeyContext(httpContext.Request, _validationModelOptions)); // take ETag value from the store (if it's found) var savedResponse = await _store.GetAsync(storeKey); if (savedResponse?.ETag != null) { var eTag = new ETag(savedResponse.ETag.ETagType, savedResponse.ETag.Value); headers[HeaderNames.ETag] = savedResponse.ETag.ToString(); logInformation = $"ETag: {eTag.ETagType}, {eTag}, "; } DateTimeOffset lastModified; if (updateLastModified) { // set LastModified lastModified = await _lastModifiedInjector.CalculateLastModified( new ResourceContext(httpContext.Request, storeKey, savedResponse)); } else { lastModified = savedResponse.LastModified; } var lastModifiedValue = await _dateParser.LastModifiedToString(lastModified); headers[HeaderNames.LastModified] = lastModifiedValue; logInformation += $"Last-Modified: {lastModifiedValue}."; _logger.LogInformation($"Generation done. {logInformation}"); }
private async Task GenerateResponseFromStore(HttpContext httpContext) { var headers = httpContext.Response.Headers; // set the ETag & Last-Modified date. // remove any other ETag and Last-Modified headers (could be set // by other pieces of code) headers.Remove(HeaderNames.ETag); headers.Remove(HeaderNames.LastModified); // generate key var requestKey = GenerateRequestKey(httpContext.Request); // set LastModified // r = RFC1123 pattern (https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx) var lastModified = GetUtcNowWithoutMilliseconds(); headers[HeaderNames.LastModified] = lastModified.ToString("r", CultureInfo.InvariantCulture); ETag eTag = null; // take ETag value from the store (if it's found) var savedResponse = await _store.GetAsync(requestKey); if (savedResponse?.ETag != null) { eTag = new ETag(savedResponse.ETag.ETagType, savedResponse.ETag.Value); headers[HeaderNames.ETag] = savedResponse.ETag.Value; } // store (overwrite) await _store.SetAsync(requestKey, new ValidationValue(eTag, lastModified)); var logInformation = string.Empty; if (eTag != null) { logInformation = $"ETag: {eTag.ETagType.ToString()}, {eTag.Value}, "; } logInformation += $"Last-Modified: {lastModified.ToString("r", CultureInfo.InvariantCulture)}."; _logger.LogInformation($"Generation done. {logInformation}"); }
private static bool ETagsMatch( ETag eTag, string eTagToCompare, bool useStrongComparisonFunction) { // for If-None-Match (cache) checks, weak comparison should be used. // for If-Match (concurrency) check, strong comparison should be used. //The example below shows the results for a set of entity-tag pairs and //both the weak and strong comparison function results: //+--------+--------+-------------------+-----------------+ //| ETag 1 | ETag 2 | Strong Comparison | Weak Comparison | //+--------+--------+-------------------+-----------------+ //| W/"1" | W/"1" | no match | match | //| W/"1" | W/"2" | no match | no match | //| W/"1" | "1" | no match | match | //| "1" | "1" | match | match | //+--------+--------+-------------------+-----------------+ if (useStrongComparisonFunction) { // to match, both eTags must be strong & be an exact match. var eTagToCompareIsStrong = !eTagToCompare.StartsWith("W/"); return(eTagToCompareIsStrong && eTag.ETagType == ETagType.Strong && string.Equals(eTag.ToString(), eTagToCompare, StringComparison.OrdinalIgnoreCase)); } // for weak comparison, we only compare the parts of the eTags after the "W/" var firstValueToCompare = eTag.ETagType == ETagType.Weak ? eTag.ToString().Substring(2) : eTag.ToString(); var secondValueToCompare = eTagToCompare.StartsWith("W/") ? eTagToCompare.Substring(2) : eTagToCompare; return(string.Equals(firstValueToCompare, secondValueToCompare, StringComparison.OrdinalIgnoreCase)); }
private void GenerateValidationHeadersOnResponse(HttpContext httpContext) { // don't generate these for 304 - that's taken care of at the // start of the request if (httpContext.Response.StatusCode == StatusCodes.Status304NotModified) { return; } // This takes care of storing new tags, also after a succesful PUT/POST/PATCH. // Other PUT/POST/PATCH requests must thus include the new ETag as If-Match, // otherwise the precondition will fail. // // If an API returns a 204 No Content after PUT/PATCH, the ETag will be generated // from an empty response - any other user/cache must GET the response again // before updating it. Getting it will result in a new ETag value being generated. // // If an API returns a 200 Ok after PUT/PATCH, the ETag will be generated from // that response body - if the update was succesful but nothing was changed, // in those cases the original ETag for other users/caches will still be sufficient. // if the response body cannot be read, we can never // generate correct ETags (and it should never be cached) if (!httpContext.Response.Body.CanRead) { return; } _logger.LogInformation("Generating Validation headers."); var headers = httpContext.Response.Headers; // remove any other ETag and Last-Modified headers (could be set // by other pieces of code) headers.Remove(HeaderNames.ETag); headers.Remove(HeaderNames.LastModified); // Save the ETag in a store. // Key = generated from request URI & headers (if VaryBy is // set, only use those headers) // ETag itself is generated from the key + response body // (strong ETag) // get the request key var requestKey = GenerateRequestKey(httpContext.Request); var requestKeyAsBytes = Encoding.UTF8.GetBytes(requestKey); // get the response bytes if (httpContext.Response.Body.CanSeek) { httpContext.Response.Body.Position = 0; } var responseBodyContent = new StreamReader(httpContext.Response.Body).ReadToEnd(); var responseBodyContentAsBytes = Encoding.UTF8.GetBytes(responseBodyContent); // combine both to generate an etag var combinedBytes = Combine(requestKeyAsBytes, responseBodyContentAsBytes); var eTag = new ETag(ETagType.Strong, GenerateETag(combinedBytes)); var lastModified = GetUtcNowWithoutMilliseconds(); // store the ETag & LastModified date with the request key as key in the ETag store _store.SetAsync(requestKey, new ValidationValue(eTag, lastModified)); // set the ETag and LastModified header headers[HeaderNames.ETag] = eTag.Value; headers[HeaderNames.LastModified] = lastModified.ToString("r", CultureInfo.InvariantCulture); _logger.LogInformation($"Validation headers generated. ETag: {eTag.Value}. Last-Modified: {lastModified.ToString("r", CultureInfo.InvariantCulture)}"); }
private async Task <bool> ConditionalPUTorPATCHIsValid(HttpContext httpContext) { _logger.LogInformation("Checking for conditional PUT/PATCH."); // Preconditional checks are used for concurrency checks only, // on updates: PUT or PATCH if (!(httpContext.Request.Method == HttpMethod.Put.ToString() || httpContext.Request.Method == "PATCH")) { _logger.LogInformation("Not valid - method isn't PUT or PATCH."); // for all the other methods, return true (no 412 response) return(true); } // the precondition is valid if one of the ETags submitted through // IfMatch matches with the saved ETag, AND if the If-UnModified-Since // value is smaller than the saved date. Both must be valid if both // are submitted. // If both headers are missing, we should // always return true (the precondition is missing, so it's valid) // We don't need to check anything, and can never return a 412 response if (!(httpContext.Request.Headers.Keys.Contains(HeaderNames.IfMatch) || httpContext.Request.Headers.Keys.Contains(HeaderNames.IfUnmodifiedSince))) { _logger.LogInformation("Not valid - no If Match or If Unmodified-Since headers."); return(true); } // generate the request key var requestKey = GenerateRequestKey(httpContext.Request); // find the validationValue with this key in the store var validationValue = await _store.GetAsync(requestKey); // if there is no validation value in the store, we return false: // there is nothing to compare to, so the precondition can // never be ok - return a 412 response if (validationValue == null || validationValue.ETag == null) { _logger.LogInformation("No saved response found in store."); return(false); } var eTagIsValid = false; var ifUnModifiedSinceIsValid = false; // check the ETags if (httpContext.Request.Headers.Keys.Contains(HeaderNames.IfMatch)) { var ifMatchHeaderValue = httpContext.Request.Headers[HeaderNames.IfMatch].ToString().Trim(); _logger.LogInformation($"Checking If-Match: {ifMatchHeaderValue}."); // if the value is *, the check is valid. if (ifMatchHeaderValue == "*") { eTagIsValid = true; } else { // otherwise, check the actual ETag(s) var ETagsFromIfMatchHeader = ifMatchHeaderValue .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (var ETag in ETagsFromIfMatchHeader) { // check the ETag. If one of the ETags matches, the // ETag precondition is valid. // for concurrency checks, we use the strong // comparison function. if (ETagsMatch(validationValue.ETag, ETag.Trim(), true)) { _logger.LogInformation($"ETag valid: {validationValue.ETag}."); eTagIsValid = true; break; } } } } else { _logger.LogInformation("No If-Match header, don't check ETag."); // if there is no IfMatch header, the tag precondition is valid. eTagIsValid = true; } // if there is an IfMatch header but none of the ETags match, // the precondition is already invalid. We don't have to // continue checking. if (!eTagIsValid) { _logger.LogInformation("Not valid. No match found for ETag."); return(false); } // Either the ETag matches (or one of them), or there was no IfMatch header. // Continue with checking the IfUnModifiedSince header, if it exists. if (httpContext.Request.Headers.Keys.Contains(HeaderNames.IfUnmodifiedSince)) { // if the LastModified date is smaller than the IfUnmodifiedSince date, // the precondition is valid. var ifUnModifiedSinceValue = httpContext.Request.Headers[HeaderNames.IfUnmodifiedSince].ToString(); _logger.LogInformation($"Checking If-Unmodified-Since: {ifUnModifiedSinceValue}"); DateTimeOffset parsedIfUnModifiedSince; if (DateTimeOffset.TryParseExact(ifUnModifiedSinceValue, "r", CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AdjustToUniversal, out parsedIfUnModifiedSince)) { // If the LastModified date is smaller than the IfUnmodifiedSince date, // the precondition is valid. ifUnModifiedSinceIsValid = validationValue.LastModified.CompareTo(parsedIfUnModifiedSince) < 0; } else { // can only check if we can parse it. Invalid values must // be ignored. ifUnModifiedSinceIsValid = true; _logger.LogInformation("Cannot parse If-Unmodified-Since value as date, header is ignored."); } } else { _logger.LogInformation("No If-Unmodified-Since header."); // if there is no IfUnmodifiedSince header, the check is valid. ifUnModifiedSinceIsValid = true; } // return the combined result of all validators. return(ifUnModifiedSinceIsValid && eTagIsValid); }
private async Task <bool> ConditionalGETorHEADIsValid(HttpContext httpContext) { _logger.LogInformation("Checking for conditional GET/HEAD."); if (!(httpContext.Request.Method == HttpMethod.Get.ToString()) || httpContext.Request.Method == "HEAD") { _logger.LogInformation("Not valid - method isn't GET or HEAD."); return(false); } // we should check ALL If-None-Match values (can be multiple eTags) (if available), // and the If-Modified-Since date (if available AND an eTag matches). See issue #2 @Github. // So, this is a valid conditional GET/HEAD (304) if one of the ETags match and, if it's // available, the If-Modified-Since date is larger than what's saved. // if both headers are missing, we should // always return false - we don't need to check anything, and // can never return a 304 response if (!(httpContext.Request.Headers.Keys.Contains(HeaderNames.IfNoneMatch) || httpContext.Request.Headers.Keys.Contains(HeaderNames.IfModifiedSince))) { _logger.LogInformation("Not valid - no If-None-Match or If-Modified-Since headers."); return(false); } // generate the request key var requestKey = GenerateRequestKey(httpContext.Request); // find the validationValue with this key in the store var validationValue = await _store.GetAsync(requestKey); // if there is no validation value in the store, always // return false - we have nothing to compare to, and can // never return a 304 response if (validationValue == null || validationValue.ETag == null) { _logger.LogInformation("No saved response found in store."); return(false); } bool eTagIsValid = false; bool ifModifiedSinceIsValid = false; // check the ETags if (httpContext.Request.Headers.Keys.Contains(HeaderNames.IfNoneMatch)) { _logger.LogInformation("Checking If-None-Match."); var ifNoneMatchHeaderValue = httpContext.Request.Headers[HeaderNames.IfNoneMatch].ToString().Trim(); _logger.LogInformation($"Checking If-None-Match: {ifNoneMatchHeaderValue}."); // if the value is *, the check is valid. if (ifNoneMatchHeaderValue == "*") { eTagIsValid = true; } else { var ETagsFromIfNoneMatchHeader = ifNoneMatchHeaderValue .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (var ETag in ETagsFromIfNoneMatchHeader) { // check the ETag. If one of the ETags matches, we're good to // go and can return a 304 Not Modified. // For conditional GET/HEAD, we use weak comparison. if (ETagsMatch(validationValue.ETag, ETag.Trim(), false)) { eTagIsValid = true; _logger.LogInformation($"ETag valid: {validationValue.ETag}."); break; } } // if there is an IfNoneMatch header, but none of the eTags match, we don't take the // If-Modified-Since headers into account. // // cfr: "If none of the entity tags match, then the server MAY perform the requested method as if the // If-None-Match header field did not exist, but MUST also ignore any If-Modified-Since header field(s) // in the request. That is, if no entity tags match, then the server MUST NOT return a 304(Not Modified) response." if (!eTagIsValid) { _logger.LogInformation("Not valid. No match found for ETag."); return(false); } } } else { _logger.LogInformation("No If-None-Match header, don't check ETag."); eTagIsValid = true; } if (httpContext.Request.Headers.Keys.Contains(HeaderNames.IfModifiedSince)) { // if the LastModified date is smaller than the IfModifiedSince date, // we can return a 304 Not Modified (IF there's also a matching ETag). // By adding an If-Modified-Since date // to a GET/HEAD request, the consumer is stating that (s)he only wants the resource // to be returned if if has been modified after that. var ifModifiedSinceValue = httpContext.Request.Headers[HeaderNames.IfModifiedSince].ToString(); _logger.LogInformation($"Checking If-Modified-Since: {ifModifiedSinceValue}"); DateTimeOffset parsedIfModifiedSince; if (DateTimeOffset.TryParseExact(ifModifiedSinceValue, "r", CultureInfo.InvariantCulture.DateTimeFormat, DateTimeStyles.AdjustToUniversal, out parsedIfModifiedSince)) { // can only check if we can parse it. ifModifiedSinceIsValid = validationValue.LastModified.CompareTo(parsedIfModifiedSince) < 0; } else { ifModifiedSinceIsValid = true; _logger.LogInformation("Cannot parse If-Modified-Since value as date, header is ignored."); } } else { _logger.LogInformation("No If-Modified-Since header."); ifModifiedSinceIsValid = true; } return(eTagIsValid && ifModifiedSinceIsValid); }
public ValidatorValue(ETag eTag, DateTimeOffset lastModified) { ETag = eTag; LastModified = lastModified; }