From f58a9be02a76c4c2f30ad53490d8f15989cbdaad Mon Sep 17 00:00:00 2001 From: Archi Date: Sun, 4 Jul 2021 18:51:35 +0200 Subject: [PATCH] IPC: Add optional SRI support for ASF-ui In theory, this is required only in specific proxy/CDN solutions accessing ASF data over http that would somehow want to transform the responses https://github.com/JustArchiNET/ASF-ui/pull/1470 --- ArchiSteamFarm/IPC/Startup.cs | 82 ++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/ArchiSteamFarm/IPC/Startup.cs b/ArchiSteamFarm/IPC/Startup.cs index 190b3e84b..5bb021d90 100644 --- a/ArchiSteamFarm/IPC/Startup.cs +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -41,10 +41,12 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -70,35 +72,60 @@ namespace ArchiSteamFarm.IPC { throw new ArgumentNullException(nameof(env)); } + // The order of dependency injection is super important, doing things in wrong order will break everything + // https://docs.microsoft.com/aspnet/core/fundamentals/middleware + + // This one is easy, it's always in the beginning if (Debugging.IsUserDebugging) { app.UseDeveloperExceptionPage(); } - // The order of dependency injection matters, pay attention to it + // Add support for proxies, this one comes usually after developer exception page, but could be before + app.UseForwardedHeaders(); - // TODO: Try to get rid of this workaround for missing PathBase feature, https://github.com/aspnet/AspNetCore/issues/5898 + // Add support for response compression - must be called before static files as we want to compress those as well + app.UseResponseCompression(); + + // It's not apparent when UsePathBase() should be called, but definitely before we get down to static files + // TODO: Maybe eventually we can get rid of this, https://github.com/aspnet/AspNetCore/issues/5898 PathString pathBase = Configuration.GetSection("Kestrel").GetValue("PathBase"); if (!string.IsNullOrEmpty(pathBase) && (pathBase != "/")) { app.UsePathBase(pathBase); } - // Add support for proxies - app.UseForwardedHeaders(); - - // Add support for response compression - app.UseResponseCompression(); - - // Add support for websockets used in /Api/NLog - app.UseWebSockets(); - - // We're using index for URL routing in our static files so re-execute all non-API calls on / + // The default HTML file (usually index.html) is responsible for IPC GUI routing, so re-execute all non-API calls on / + // This must be called before default files, because we don't know the exact file name that will be used for index page app.UseWhen(context => !context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), appBuilder => appBuilder.UseStatusCodePagesWithReExecute("/")); - // We need static files support for IPC GUI + // Add support for default root path redirection (GET / -> GET /index.html), must come before static files app.UseDefaultFiles(); - app.UseStaticFiles(); + // Add support for static files (e.g. HTML, CSS and JS from IPC GUI) + app.UseStaticFiles( + new StaticFileOptions { + OnPrepareResponse = context => { + // Add support for SRI-protected static files + if (context.File.Exists && !context.File.IsDirectory && !string.IsNullOrEmpty(context.File.Name)) { + string extension = Path.GetExtension(context.File.Name); + + switch (extension.ToUpperInvariant()) { + case ".CSS": + case ".JS": + ResponseHeaders headers = context.Context.Response.GetTypedHeaders(); + + headers.CacheControl = new CacheControlHeaderValue { + NoTransform = true + }; + + break; + } + } + } + } + ); + + // Use routing for our API controllers, this should be called once we're done with all the static files mess #if !NETFRAMEWORK app.UseRouting(); #endif @@ -106,25 +133,28 @@ namespace ArchiSteamFarm.IPC { string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword; if (!string.IsNullOrEmpty(ipcPassword)) { - // We need ApiAuthenticationMiddleware for IPCPassword + // We want to protect our API with IPCPassword, this should be called after routing, so the middleware won't have to deal with API endpoints that do not exist app.UseWhen(context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), appBuilder => appBuilder.UseMiddleware()); - // We want to apply CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API - // We apply CORS policy only with IPCPassword set as extra authentication measure + // We want to apply CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API, this should be called before response compression, but can't be due to how our flow works + // We apply CORS policy only with IPCPassword set as an extra authentication measure app.UseCors(); } - // Add support for mapping controllers + // Add support for websockets that we use e.g. in /Api/NLog + app.UseWebSockets(); + + // Finally register proper API endpoints once we're done with routing #if NETFRAMEWORK app.UseMvcWithDefaultRoute(); #else app.UseEndpoints(endpoints => endpoints.MapControllers()); #endif - // Use swagger for automatic API documentation generation + // Add support for swagger, responsible for automatic API documentation generation, this should be on the end, once we're done with API app.UseSwagger(); - // Use friendly swagger UI + // Add support for swagger UI, this should be after swagger, obviously app.UseSwaggerUI( options => { options.DisplayRequestDuration(); @@ -140,9 +170,10 @@ namespace ArchiSteamFarm.IPC { throw new ArgumentNullException(nameof(services)); } - // The order of dependency injection matters, pay attention to it + // The order of dependency injection is super important, doing things in wrong order will break everything + // Order in Configure() method is a good start - // Add support for custom reverse proxy endpoints + // Prepare knownNetworks that we'll use in a second HashSet? knownNetworksTexts = Configuration.GetSection("Kestrel:KnownNetworks").Get>(); HashSet? knownNetworks = null; @@ -182,12 +213,13 @@ namespace ArchiSteamFarm.IPC { string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword; - // Add CORS to allow userscripts and third-party apps if (!string.IsNullOrEmpty(ipcPassword)) { + // We want to apply CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API + // We apply CORS policy only with IPCPassword set as an extra authentication measure services.AddCors(options => options.AddDefaultPolicy(policyBuilder => policyBuilder.AllowAnyOrigin())); } - // Add swagger documentation generation + // Add support for swagger, responsible for automatic API documentation generation services.AddSwaggerGen( options => { options.AddSecurityDefinition( @@ -244,7 +276,7 @@ namespace ArchiSteamFarm.IPC { } ); - // Add Newtonsoft.Json support for SwaggerGen, this one must be executed after AddSwaggerGen() + // Add support for Newtonsoft.Json in swagger, this one must be executed after AddSwaggerGen() services.AddSwaggerGenNewtonsoftSupport(); // We need MVC for /Api, but we're going to use only a small subset of all available features