diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 4fb62c39d..58e5ca682 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -14,6 +14,7 @@ using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; using Squidex.Pipeline; @@ -62,6 +63,8 @@ namespace Squidex.Areas.Api.Controllers.Apps var response = entities.Select(a => AppDto.FromApp(a, subject, appPlansProvider)).ToList(); + Response.Headers["Etag"] = string.Join(";", response.Select(x => $"{x.Id}{x.Version}")).Sha256Base64(); + return Ok(response); } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index 0b3f722de..015c70773 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -12,10 +12,11 @@ using NodaTime; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Reflection; +using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.Assets.Models { - public sealed class AssetDto + public sealed class AssetDto : IGenerateEtag { /// /// The id of the asset. @@ -97,6 +98,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// public long Version { get; set; } + public string GenerateETag() + { + return $"{Id}{Version}"; + } + public static AssetDto FromAsset(IAssetEntity asset) { return SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() }); diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs index 6e81fa113..c4192c9e6 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs @@ -9,10 +9,11 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; +using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.Assets.Models { - public sealed class AssetsDto + public sealed class AssetsDto : IGenerateEtag { /// /// The assets. @@ -25,6 +26,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models /// public long Total { get; set; } + public string GenerateETag() + { + return string.Join(";", Items?.Select(x => x.GenerateETag()) ?? Enumerable.Empty()).Sha256Base64(); + } + public static AssetsDto FromAssets(IResultList assets) { return new AssetsDto { Total = assets.Total, Items = assets.Select(AssetDto.FromAsset).ToArray() }; diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 97093cdc2..e111a4edd 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -16,10 +16,11 @@ using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Reflection; +using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.Contents.Models { - public sealed class ContentDto + public sealed class ContentDto : IGenerateEtag { /// /// The if of the content item. @@ -79,6 +80,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// public long Version { get; set; } + public string GenerateETag() + { + return $"{Id}{Version}"; + } + public static ContentDto FromCommand(CreateContent command, EntityCreatedResult result) { var now = SystemClock.Instance.GetCurrentInstant(); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 12c19cd9c..71c54dd66 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -5,9 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Linq; +using Squidex.Infrastructure; +using Squidex.Pipeline; + namespace Squidex.Areas.Api.Controllers.Contents.Models { - public sealed class ContentsDto + public sealed class ContentsDto : IGenerateEtag { /// /// The total number of content items. @@ -18,5 +22,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// The content items. /// public ContentDto[] Items { get; set; } + + public string GenerateETag() + { + return string.Join(";", Items?.Select(x => x.GenerateETag()) ?? Enumerable.Empty()).Sha256Base64(); + } } } diff --git a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs index cceb3a4b7..4d145f1fa 100644 --- a/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs +++ b/src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs @@ -27,18 +27,18 @@ namespace Squidex.Areas.Frontend.Middlewares public async Task Invoke(HttpContext context) { - var buffer = new MemoryStream(); - var body = context.Response.Body; + var responseBuffer = new MemoryStream(); + var responseBody = context.Response.Body; - context.Response.Body = buffer; + context.Response.Body = responseBuffer; await next(context); - buffer.Seek(0, SeekOrigin.Begin); + responseBuffer.Seek(0, SeekOrigin.Begin); if (context.Response.StatusCode == 200 && IsIndex(context) && IsHtml(context)) { - using (var reader = new StreamReader(buffer)) + using (var reader = new StreamReader(responseBuffer)) { var response = await reader.ReadToEndAsync(); @@ -56,17 +56,17 @@ namespace Squidex.Areas.Frontend.Middlewares context.Response.Headers["Content-Length"] = memoryStream.Length.ToString(); - await memoryStream.CopyToAsync(body); + await memoryStream.CopyToAsync(responseBody); } } } } else if (context.Response.StatusCode != 304) { - await buffer.CopyToAsync(body); + await responseBuffer.CopyToAsync(responseBody); } - context.Response.Body = body; + context.Response.Body = responseBody; } private static string InjectStyles(string response) diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs index fbb865fa1..2e0190fcb 100644 --- a/src/Squidex/Config/Web/WebServices.cs +++ b/src/Squidex/Config/Web/WebServices.cs @@ -33,7 +33,11 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsSelf(); - services.AddMvc().AddMySerializers(); + services.AddMvc(options => + { + options.Filters.Add(); + }).AddMySerializers(); + services.AddCors(); services.AddRouting(); } diff --git a/src/Squidex/Pipeline/ETagFilter.cs b/src/Squidex/Pipeline/ETagFilter.cs new file mode 100644 index 000000000..960791b9c --- /dev/null +++ b/src/Squidex/Pipeline/ETagFilter.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Squidex.Pipeline +{ + public sealed class ETagFilter : IAsyncActionFilter + { + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var resultContext = await next(); + + var httpContext = context.HttpContext; + + if (!httpContext.Response.Headers.TryGetValue("Etag", out _) && resultContext.Result is ObjectResult obj && obj.Value is IGenerateEtag g) + { + var calculatedEtag = g.GenerateETag(); + + if (!string.IsNullOrWhiteSpace(calculatedEtag)) + { + httpContext.Response.Headers.Add("Etag", calculatedEtag); + } + } + + if (httpContext.Request.Method == "GET" && + httpContext.Request.Headers.TryGetValue("If-None-Match", out var noneMatch) && + httpContext.Response.StatusCode == 200 && + httpContext.Response.Headers.TryGetValue("Etag", out var etag) && + !string.IsNullOrWhiteSpace(noneMatch) && + !string.IsNullOrWhiteSpace(etag) && + string.Equals(etag, noneMatch, System.StringComparison.Ordinal)) + { + resultContext.Result = new StatusCodeResult(304); + } + } + } +} diff --git a/src/Squidex/Pipeline/IGenerateEtag.cs b/src/Squidex/Pipeline/IGenerateEtag.cs new file mode 100644 index 000000000..4755fcf27 --- /dev/null +++ b/src/Squidex/Pipeline/IGenerateEtag.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Pipeline +{ + public interface IGenerateEtag + { + string GenerateETag(); + } +}