Example #1
0
        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}");
        }
Example #3
0
        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);
        }
Example #7
0
 public ValidatorValue(ETag eTag, DateTimeOffset lastModified)
 {
     ETag         = eTag;
     LastModified = lastModified;
 }