Implemented native web GUI support for plugins inside the ASF IPC. Closes #2876 (#2877)

* Implemented native web GUI support for plugins inside the ASF IPC

ref #2876

* calm down netframework

* less `#if`'s

* code optimization

* misc

* Code improvements

* Support nested paths

* Revert "Support nested paths"

This reverts commit 5d7d9ac6ae.

* Support for nested paths (no errors now I guess)

* better path naming

* fixed typos

* Use `HashSet<string>` instead of `List<string>`

* Code improvements

* Code improvements

* Code improvements

* Code improvements

* Code improvements

* Added support for overriding ASF-ui files

* Removed a modified file from pull request

* Added `IWebInterface`

* less `#if`'s

* Code improvements

* Code improvements

* Added license

* Code improvements

* Added default implementation of `IWebInterface`

* Code improvements:
*Use of `OfType<>` instead `Where` and casting.

* Code improvements:
*Null checking

* Removed useless code

* shut up `netf`

* Removed useless null check

* Code improvements:
*Misc: kvp deconstaction

* Added support for absolute path
This commit is contained in:
fazelukario
2023-04-20 23:02:49 +03:00
committed by GitHub
parent 69cb5999ea
commit 97da56d016
3 changed files with 99 additions and 30 deletions

View File

@@ -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<string, string> pluginsPaths = new();
CacheControlHeaderValue cacheControl = new();
if (PluginsCore.ActivePlugins?.Count > 0) {
foreach (IWebInterface plugin in PluginsCore.ActivePlugins.OfType<IWebInterface>()) {
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<string> 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);

View File

@@ -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

View File

@@ -90,7 +90,7 @@
<DebugType>none</DebugType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<WarningsNotAsErrors>CS8002,IL2026,IL2057,IL2072,IL2075,IL2104</WarningsNotAsErrors>
<WarningsNotAsErrors>CS8002,IL2026,IL2057,IL2072,IL2075,IL2104,IL3000</WarningsNotAsErrors>
</PropertyGroup>
<!-- Enable public signing if not part of Visual Studio, which is too stupid to understand what public signing is -->