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 (command.ExpectedVersion == EtagVersion.Auto)
{ {
if (TryParseEtag(httpContext, out var expectedVersion)) if (httpContext.TryParseEtagVersion(HeaderNames.IfMatch, out var expectedVersion))
{ {
command.ExpectedVersion = expectedVersion; command.ExpectedVersion = expectedVersion;
} }
@ -64,25 +64,5 @@ namespace Squidex.Web.CommandMiddlewares
{ {
httpContext.Response.Headers[HeaderNames.ETag] = version.ToString(CultureInfo.InvariantCulture); 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.Globalization;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -58,5 +60,27 @@ namespace Squidex.Web
{ {
return entity.Version.ToString(CultureInfo.InvariantCulture); 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) private static bool IsCacheable(HttpContext httpContext, string etag)
{ {
return HttpMethods.IsGet(httpContext.Request.Method) && if (!HttpMethods.IsGet(httpContext.Request.Method) || httpContext.Response.StatusCode != 200)
httpContext.Response.StatusCode == 200 && {
httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatch) && return false;
string.Equals(etag, noneMatch, StringComparison.Ordinal); }
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); 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 (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"); 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.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
@ -206,7 +207,12 @@ namespace Squidex.Areas.Api.Controllers.Contents
return NotFound(); 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); return Ok(response);
} }

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

@ -46,13 +46,17 @@ namespace Squidex.Web.Pipeline
sut = new CachingFilter(cachingManager); sut = new CachingFilter(cachingManager);
} }
[Fact] [Theory]
public async Task Should_return_304_for_same_etags() [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.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()); await sut.OnActionExecutionAsync(executingContext, Next());

Loading…
Cancel
Save