diff --git a/ArchiSteamFarm/IPC/Startup.cs b/ArchiSteamFarm/IPC/Startup.cs index b36d603fa..1676ff9db 100644 --- a/ArchiSteamFarm/IPC/Startup.cs +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -30,12 +30,14 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using System.Linq; using System.Net; using System.Reflection; using ArchiSteamFarm.Core; using ArchiSteamFarm.IPC.Integration; using ArchiSteamFarm.Localization; using ArchiSteamFarm.Plugins; +using ArchiSteamFarm.Plugins.Interfaces; using ArchiSteamFarm.Storage; using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; @@ -45,6 +47,7 @@ using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; using Newtonsoft.Json; @@ -97,44 +100,43 @@ internal sealed class Startup { // Add support for default root path redirection (GET / -> GET /index.html), must come before static files app.UseDefaultFiles(); - // Add support for static files (e.g. HTML, CSS and JS from IPC GUI) - app.UseStaticFiles( - new StaticFileOptions { - OnPrepareResponse = static context => { - if (context.File is { Exists: true, IsDirectory: false } && !string.IsNullOrEmpty(context.File.Name)) { - string extension = Path.GetExtension(context.File.Name); +#if !NETFRAMEWORK && !NETSTANDARD + Dictionary pluginsPaths = new(); - CacheControlHeaderValue cacheControl = new(); + if (PluginsCore.ActivePlugins?.Count > 0) { + foreach (IWebInterface plugin in PluginsCore.ActivePlugins.OfType()) { + if (string.IsNullOrEmpty(plugin.PhysicalPath) || string.IsNullOrEmpty(plugin.WebPath)) { + continue; + } - switch (extension.ToUpperInvariant()) { - case ".CSS" or ".JS": - // Add support for SRI-protected static files - // SRI requires from us to notify the caller (especially proxy) to avoid modifying the data - cacheControl.NoTransform = true; + string staticFilesDirectory = Path.IsPathRooted(plugin.PhysicalPath) + ? plugin.PhysicalPath + : Path.Combine(Path.GetDirectoryName(plugin.GetType().Assembly.Location)!, plugin.PhysicalPath); - goto default; - default: - // Instruct the caller to always ask us first about every file it requests - // Contrary to the name, this doesn't prevent client from caching, but rather informs it that it must verify with us first that his cache is still up-to-date - // This is used to handle ASF and user updates to WWW root, we don't want from the client to ever use outdated scripts - cacheControl.NoCache = true; + if (Directory.Exists(staticFilesDirectory)) { + pluginsPaths.Add(staticFilesDirectory, plugin.WebPath); - // All static files are public by definition, we don't have any authorization here - cacheControl.Public = true; - - break; - } - - ResponseHeaders headers = context.Context.Response.GetTypedHeaders(); - - headers.CacheControl = cacheControl; + if (plugin.WebPath != "/") { + app.UseDefaultFiles(plugin.WebPath); } } } - ); + } + + // Add support for static files from custom plugins (e.g. HTML, CSS and JS) + foreach ((string physicalPath, string webPath) in pluginsPaths) { + StaticFileOptions staticFileOptions = GetNewStaticFileOptionsWithCacheControl(); + staticFileOptions.FileProvider = new PhysicalFileProvider(physicalPath); + staticFileOptions.RequestPath = webPath; + app.UseStaticFiles(staticFileOptions); + } +#endif + + // Add support for static files (e.g. HTML, CSS and JS from IPC GUI) + app.UseStaticFiles(GetNewStaticFileOptionsWithCacheControl()); - // Use routing for our API controllers, this should be called once we're done with all the static files mess #if !NETFRAMEWORK && !NETSTANDARD + // Use routing for our API controllers, this should be called once we're done with all the static files mess app.UseRouting(); #endif @@ -173,6 +175,40 @@ internal sealed class Startup { ); } + private static StaticFileOptions GetNewStaticFileOptionsWithCacheControl() => new() { + OnPrepareResponse = static context => { + if (context.File is { Exists: true, IsDirectory: false } && !string.IsNullOrEmpty(context.File.Name)) { + string extension = Path.GetExtension(context.File.Name); + + CacheControlHeaderValue cacheControl = new(); + + switch (extension.ToUpperInvariant()) { + case ".CSS" or ".JS": + // Add support for SRI-protected static files + // SRI requires from us to notify the caller (especially proxy) to avoid modifying the data + cacheControl.NoTransform = true; + + goto default; + default: + // Instruct the caller to always ask us first about every file it requests + // Contrary to the name, this doesn't prevent client from caching, but rather informs it that it must verify with us first that his cache is still up-to-date + // This is used to handle ASF and user updates to WWW root, we don't want from the client to ever use outdated scripts + cacheControl.NoCache = true; + + // All static files are public by definition, we don't have any authorization here + cacheControl.Public = true; + + break; + } + + ResponseHeaders headers = context.Context.Response.GetTypedHeaders(); + + headers.CacheControl = cacheControl; + } + } + }; + + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "HashSet isn't a primitive, but we widely use the required features everywhere and it's unlikely to be trimmed to the best of our knowledge")] public void ConfigureServices(IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); diff --git a/ArchiSteamFarm/Plugins/Interfaces/IWebInterface.cs b/ArchiSteamFarm/Plugins/Interfaces/IWebInterface.cs new file mode 100644 index 000000000..eebcf6896 --- /dev/null +++ b/ArchiSteamFarm/Plugins/Interfaces/IWebInterface.cs @@ -0,0 +1,33 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2023 Ɓukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Newtonsoft.Json; + +namespace ArchiSteamFarm.Plugins.Interfaces; + +#if !NETFRAMEWORK && !NETSTANDARD +public interface IWebInterface : IPlugin { + string PhysicalPath => "www"; + + [JsonProperty] + string WebPath => "/"; +} +#endif diff --git a/Directory.Build.props b/Directory.Build.props index b07a025b9..f48921569 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -90,7 +90,7 @@ none true - CS8002,IL2026,IL2057,IL2072,IL2075,IL2104 + CS8002,IL2026,IL2057,IL2072,IL2075,IL2104,IL3000