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); }