Skip to content

Commit

Permalink
Implemented native web GUI support for plugins inside the ASF IPC. Cl…
Browse files Browse the repository at this point in the history
…oses #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 5d7d9ac.

* 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
  • Loading branch information
fazelukario authored Apr 20, 2023
1 parent 69cb599 commit 97da56d
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 32 deletions.
98 changes: 67 additions & 31 deletions ArchiSteamFarm/IPC/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@
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;
Expand All @@ -45,6 +47,7 @@
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;
Expand Down Expand Up @@ -97,44 +100,43 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
// 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);

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;
}
#if !NETFRAMEWORK && !NETSTANDARD
Dictionary<string, string> pluginsPaths = new();

if (PluginsCore.ActivePlugins?.Count > 0) {
foreach (IWebInterface plugin in PluginsCore.ActivePlugins.OfType<IWebInterface>()) {
if (string.IsNullOrEmpty(plugin.PhysicalPath) || string.IsNullOrEmpty(plugin.WebPath)) {
continue;
}

string staticFilesDirectory = Path.IsPathRooted(plugin.PhysicalPath)
? plugin.PhysicalPath
: Path.Combine(Path.GetDirectoryName(plugin.GetType().Assembly.Location)!, plugin.PhysicalPath);

ResponseHeaders headers = context.Context.Response.GetTypedHeaders();
if (Directory.Exists(staticFilesDirectory)) {
pluginsPaths.Add(staticFilesDirectory, plugin.WebPath);

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

Expand Down Expand Up @@ -173,6 +175,40 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
);
}

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);
Expand Down
33 changes: 33 additions & 0 deletions ArchiSteamFarm/Plugins/Interfaces/IWebInterface.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// |
// Copyright 2015-2023 Łukasz "JustArchi" Domeradzki
// Contact: [email protected]
// |
// 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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down

0 comments on commit 97da56d

Please sign in to comment.