Browse Source

Fix etags.

pull/845/head
Sebastian 4 years ago
parent
commit
453f6b5ddd
  1. 22
      backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs
  2. 24
      backend/src/Squidex.Web/ETagExtensions.cs
  3. 54
      backend/src/Squidex.Web/ETagUtils.cs
  4. 15
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  5. 18
      backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs
  6. 8
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  7. 12
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

22
backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs

@ -38,7 +38,7 @@ namespace Squidex.Web.CommandMiddlewares
if (command.ExpectedVersion == EtagVersion.Auto)
{
if (TryParseEtag(httpContext, out var expectedVersion))
if (httpContext.TryParseEtagVersion(HeaderNames.IfMatch, out var expectedVersion))
{
command.ExpectedVersion = expectedVersion;
}
@ -64,25 +64,5 @@ namespace Squidex.Web.CommandMiddlewares
{
httpContext.Response.Headers[HeaderNames.ETag] = version.ToString(CultureInfo.InvariantCulture);
}
private static bool TryParseEtag(HttpContext httpContext, out long version)
{
version = default;
if (httpContext.Request.Headers.TryGetString(HeaderNames.IfMatch, out var etag))
{
if (etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{
etag = etag[2..];
}
if (long.TryParse(etag, NumberStyles.Any, CultureInfo.InvariantCulture, out version))
{
return true;
}
}
return false;
}
}
}

24
backend/src/Squidex.Web/ETagExtensions.cs

@ -8,6 +8,8 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure;
@ -58,5 +60,27 @@ namespace Squidex.Web
{
return entity.Version.ToString(CultureInfo.InvariantCulture);
}
public static bool TryParseEtagVersion(this HttpContext httpContext, string header, out long version)
{
version = default;
if (httpContext.Request.Headers.TryGetString(header, out var etag))
{
var span = etag.AsSpan();
if (span.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{
span = span[2..];
}
if (long.TryParse(span, NumberStyles.Any, CultureInfo.InvariantCulture, out version))
{
return true;
}
}
return false;
}
}
}

54
backend/src/Squidex.Web/ETagUtils.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Web
{
public static class ETagUtils
{
public static string ToWeakEtag(string? etag)
{
return $"W/{etag}";
}
public static bool IsWeakEtag(string etag)
{
return IsWeakEtag(etag.AsSpan());
}
public static bool IsWeakEtag(ReadOnlySpan<char> etag)
{
return etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase);
}
public static bool IsSameEtag(string lhs, string rhs)
{
return IsSameEtag(lhs.AsSpan(), rhs.AsSpan());
}
public static bool IsSameEtag(ReadOnlySpan<char> lhs, ReadOnlySpan<char> rhs)
{
var isMatch = lhs.Equals(rhs, StringComparison.Ordinal);
if (isMatch)
{
return true;
}
if (lhs.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{
lhs = lhs[2..];
}
if (rhs.StartsWith("W/", StringComparison.OrdinalIgnoreCase))
{
rhs = rhs[2..];
}
return lhs.Equals(rhs, StringComparison.Ordinal);
}
}
}

15
backend/src/Squidex.Web/Pipeline/CachingFilter.cs

@ -41,10 +41,17 @@ namespace Squidex.Web.Pipeline
private static bool IsCacheable(HttpContext httpContext, string etag)
{
return HttpMethods.IsGet(httpContext.Request.Method) &&
httpContext.Response.StatusCode == 200 &&
httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatch) &&
string.Equals(etag, noneMatch, StringComparison.Ordinal);
if (!HttpMethods.IsGet(httpContext.Request.Method) || httpContext.Response.StatusCode != 200)
{
return false;
}
if (!httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatchValue))
{
return false;
}
return ETagUtils.IsSameEtag(noneMatchValue, etag);
}
}
}

18
backend/src/Squidex.Web/Pipeline/CachingKeysMiddleware.cs

@ -32,15 +32,15 @@ namespace Squidex.Web.Pipeline
AppendAuthHeaders(context);
context.Response.OnStarting(x =>
context.Response.OnStarting(_ =>
{
var httpContext = (HttpContext)x;
var httpContext = (HttpContext)_;
if (httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag))
{
if (!cachingOptions.StrongETag && IsWeakEtag(etag))
if (!cachingOptions.StrongETag && !ETagUtils.IsWeakEtag(etag))
{
httpContext.Response.Headers[HeaderNames.ETag] = ToWeakEtag(etag);
httpContext.Response.Headers[HeaderNames.ETag] = ETagUtils.ToWeakEtag(etag);
}
}
@ -65,15 +65,5 @@ namespace Squidex.Web.Pipeline
cachingManager.AddHeader("Auth-ClientId");
}
}
private static string ToWeakEtag(string? etag)
{
return $"W/{etag}";
}
private static bool IsWeakEtag(string etag)
{
return !etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase);
}
}
}

8
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities;
@ -206,7 +207,12 @@ namespace Squidex.Areas.Api.Controllers.Contents
return NotFound();
}
var response = ContentDto.FromContent(content, Resources);
var response = Deferred.Response(() =>
{
return ContentDto.FromContent(content, Resources);
});
Response.Headers[HeaderNames.ETag] = content.ToEtag();
return Ok(response);
}

12
backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

@ -46,13 +46,17 @@ namespace Squidex.Web.Pipeline
sut = new CachingFilter(cachingManager);
}
[Fact]
public async Task Should_return_304_for_same_etags()
[Theory]
[InlineData("13", "13")]
[InlineData("13", "W/13")]
[InlineData("W/13", "13")]
[InlineData("W/13", "W/13")]
public async Task Should_return_304_for_same_etags(string ifNoneMatch, string etag)
{
httpContext.Request.Method = HttpMethods.Get;
httpContext.Request.Headers[HeaderNames.IfNoneMatch] = "W/13";
httpContext.Request.Headers[HeaderNames.IfNoneMatch] = ifNoneMatch;
httpContext.Response.Headers[HeaderNames.ETag] = "W/13";
httpContext.Response.Headers[HeaderNames.ETag] = etag;
await sut.OnActionExecutionAsync(executingContext, Next());

Loading…
Cancel
Save