diff --git a/Iceshrimp.Backend/Controllers/Federation/ActivityPubController.cs b/Iceshrimp.Backend/Controllers/Federation/ActivityPubController.cs index 422d7a46..9d4246d1 100644 --- a/Iceshrimp.Backend/Controllers/Federation/ActivityPubController.cs +++ b/Iceshrimp.Backend/Controllers/Federation/ActivityPubController.cs @@ -12,6 +12,7 @@ using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Queues; using Iceshrimp.Backend.Core.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; @@ -108,6 +109,7 @@ public class ActivityPubController( [HttpGet("/users/{id}")] [AuthorizedFetch] + [OutputCache(PolicyName = "federation")] [MediaTypeRouteFilter("application/activity+json", "application/ld+json")] [OverrideResultType] [ProducesResults(HttpStatusCode.OK, HttpStatusCode.MovedPermanently)] @@ -129,6 +131,7 @@ public class ActivityPubController( [HttpGet("/users/{id}/collections/featured")] [AuthorizedFetch] + [OutputCache(PolicyName = "federation")] [OverrideResultType] [ProducesResults(HttpStatusCode.OK)] [ProducesErrors(HttpStatusCode.NotFound)] @@ -159,6 +162,7 @@ public class ActivityPubController( [HttpGet("/@{acct}")] [AuthorizedFetch] + [OutputCache(PolicyName = "federation")] [MediaTypeRouteFilter("application/activity+json", "application/ld+json")] [OverrideResultType] [ProducesResults(HttpStatusCode.OK, HttpStatusCode.MovedPermanently)] @@ -212,6 +216,7 @@ public class ActivityPubController( [HttpGet("/emoji/{name}")] [AuthorizedFetch] + [OutputCache(PolicyName = "federation")] [MediaTypeRouteFilter("application/activity+json", "application/ld+json")] [OverrideResultType] [ProducesResults(HttpStatusCode.OK)] diff --git a/Iceshrimp.Backend/Controllers/Federation/WellKnownController.cs b/Iceshrimp.Backend/Controllers/Federation/WellKnownController.cs index ea6f6ab8..2ff048d6 100644 --- a/Iceshrimp.Backend/Controllers/Federation/WellKnownController.cs +++ b/Iceshrimp.Backend/Controllers/Federation/WellKnownController.cs @@ -9,6 +9,7 @@ using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Middleware; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OutputCaching; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -17,6 +18,7 @@ namespace Iceshrimp.Backend.Controllers.Federation; [FederationApiController] [Route("/.well-known")] [EnableCors("well-known")] +[OutputCache(PolicyName = "federation")] public class WellKnownController(IOptions config, DatabaseContext db) : ControllerBase { [HttpGet("webfinger")] diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index 53c12680..1bf63bbb 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -374,10 +374,22 @@ public static class ServiceExtensions options.AddScheme("StubAuthenticationHandler", null); }); } + + public static void AddOutputCacheWithOptions(this IServiceCollection services) + { + services.AddOutputCache(options => + { + options.AddPolicy("conditional", o => o.With(ctx => ctx.HttpContext.ShouldCacheOutput())); + options.AddPolicy("federation", o => o.SetVaryByHeader("Accept").Expire(TimeSpan.FromSeconds(60))); + options.DefaultExpirationTimeSpan = TimeSpan.FromDays(365); + }); + } } public static partial class HttpContextExtensions { + private const string CacheKey = "shouldCache"; + public static string GetRateLimitPartition(this HttpContext ctx, bool includeRoute) => (includeRoute ? ctx.Request.Path.ToString() + "#" : "") + (GetRateLimitPartitionInternal(ctx) ?? ""); @@ -385,6 +397,11 @@ public static partial class HttpContextExtensions ctx.GetUser()?.Id ?? ctx.Request.Headers["X-Forwarded-For"].FirstOrDefault() ?? ctx.Connection.RemoteIpAddress?.ToString(); + + public static void CacheOutput(this HttpContext ctx) => ctx.Items[CacheKey] = true; + + public static bool ShouldCacheOutput(this HttpContext ctx) => + ctx.Items.TryGetValue(CacheKey, out var s) && s is true; } #region AsyncDataProtection handlers diff --git a/Iceshrimp.Backend/Startup.cs b/Iceshrimp.Backend/Startup.cs index 5a1e2716..aee64c7e 100644 --- a/Iceshrimp.Backend/Startup.cs +++ b/Iceshrimp.Backend/Startup.cs @@ -24,6 +24,7 @@ builder.Services .AddPlugins(PluginLoader.Assemblies); builder.Services.AddSwaggerGenWithOptions(); +builder.Services.AddOutputCacheWithOptions(); builder.Services.AddLogging(logging => logging.AddCustomConsoleFormatter()); builder.Services.AddDatabaseContext(builder.Configuration); builder.Services.AddSlidingWindowRateLimiter(); @@ -38,6 +39,7 @@ builder.Services.AddAntiforgery(o => o.Cookie.Name = "CSRF-Token"); builder.Services.AddServices(builder.Configuration); builder.Services.ConfigureServices(builder.Configuration); + builder.WebHost.ConfigureKestrel(builder.Configuration); builder.WebHost.UseStaticWebAssets(); @@ -64,6 +66,7 @@ app.UseAuthorization(); app.UseWebSockets(new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(30) }); app.UseCustomMiddleware(); app.UseAntiforgery(); +app.UseOutputCache(); app.MapStaticAssetsWithTransparentDecompression(); app.MapControllers();