mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-01 06:00:46 +00:00
Minimize dependencies for starting IPC server
Previously WebApplication didn't offer any advantages over generic Host, but with release of .NET 8 there is now slim and empty builders, which limit amount of initialized dependencies and allow us to skip some unnecessary features in default pipeline.
This commit is contained in:
@@ -42,6 +42,7 @@ public static class JsonUtilities {
|
||||
public static readonly JsonSerializerOptions IndentedJsonSerialierOptions = CreateDefaultJsonSerializerOptions(true);
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static JsonElement ToJsonElement<T>(this T obj) where T : notnull {
|
||||
ArgumentNullException.ThrowIfNull(obj);
|
||||
|
||||
@@ -49,9 +50,11 @@ public static class JsonUtilities {
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static T? ToJsonObject<T>(this JsonElement jsonElement) => jsonElement.Deserialize<T>(DefaultJsonSerialierOptions);
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static async ValueTask<T?> ToJsonObject<T>(this Stream stream, CancellationToken cancellationToken = default) {
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
@@ -59,6 +62,7 @@ public static class JsonUtilities {
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static T? ToJsonObject<T>([StringSyntax(StringSyntaxAttribute.Json)] this string json) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(json);
|
||||
|
||||
@@ -66,6 +70,7 @@ public static class JsonUtilities {
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
public static string ToJsonText<T>(this T obj, bool writeIndented = false) => JsonSerializer.Serialize(obj, writeIndented ? IndentedJsonSerialierOptions : DefaultJsonSerialierOptions);
|
||||
|
||||
private static void ApplyCustomModifiers(JsonTypeInfo jsonTypeInfo) {
|
||||
@@ -102,6 +107,7 @@ public static class JsonUtilities {
|
||||
}
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static JsonSerializerOptions CreateDefaultJsonSerializerOptions(bool writeIndented = false) =>
|
||||
new() {
|
||||
AllowTrailingCommas = true,
|
||||
|
||||
@@ -20,29 +20,50 @@
|
||||
// limitations under the License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.IPC.Controllers.Api;
|
||||
using ArchiSteamFarm.IPC.Integration;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.NLog;
|
||||
using ArchiSteamFarm.NLog.Targets;
|
||||
using ArchiSteamFarm.Plugins;
|
||||
using ArchiSteamFarm.Plugins.Interfaces;
|
||||
using ArchiSteamFarm.Storage;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Headers;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NLog.Web;
|
||||
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
|
||||
|
||||
namespace ArchiSteamFarm.IPC;
|
||||
|
||||
internal static class ArchiKestrel {
|
||||
internal static bool IsRunning => KestrelWebHost != null;
|
||||
internal static bool IsRunning => WebApplication != null;
|
||||
|
||||
internal static HistoryTarget? HistoryTarget { get; private set; }
|
||||
|
||||
private static IHost? KestrelWebHost;
|
||||
private static WebApplication? WebApplication;
|
||||
|
||||
internal static void OnNewHistoryTarget(HistoryTarget? historyTarget = null) {
|
||||
if (HistoryTarget != null) {
|
||||
@@ -57,102 +78,424 @@ internal static class ArchiKestrel {
|
||||
}
|
||||
|
||||
internal static async Task Start() {
|
||||
if (KestrelWebHost != null) {
|
||||
if (WebApplication != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.IPCStarting);
|
||||
|
||||
// The order of dependency injection matters, pay attention to it
|
||||
HostBuilder builder = new();
|
||||
|
||||
string customDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.WebsiteDirectory);
|
||||
string websiteDirectory = Directory.Exists(customDirectory) ? customDirectory : Path.Combine(AppContext.BaseDirectory, SharedInfo.WebsiteDirectory);
|
||||
|
||||
// Set default content root
|
||||
builder.UseContentRoot(SharedInfo.HomeDirectory);
|
||||
|
||||
// Check if custom config is available
|
||||
string absoluteConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.ConfigDirectory);
|
||||
string customConfigPath = Path.Combine(absoluteConfigDirectory, SharedInfo.IPCConfigFile);
|
||||
|
||||
bool customConfigExists = File.Exists(customConfigPath);
|
||||
|
||||
if (customConfigExists && Debugging.IsDebugConfigured) {
|
||||
try {
|
||||
string json = await File.ReadAllTextAsync(customConfigPath).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(json)) {
|
||||
JsonNode? jsonNode = JsonNode.Parse(json);
|
||||
|
||||
ASF.ArchiLogger.LogGenericDebug($"{SharedInfo.IPCConfigFile}: {jsonNode?.ToJsonText(true) ?? "null"}");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable NLog integration for logging
|
||||
builder.ConfigureLogging(
|
||||
static logging => {
|
||||
logging.ClearProviders();
|
||||
logging.SetMinimumLevel(Debugging.IsUserDebugging ? LogLevel.Trace : LogLevel.Warning);
|
||||
}
|
||||
);
|
||||
|
||||
builder.UseNLog(new NLogAspNetCoreOptions { ShutdownOnDispose = false });
|
||||
|
||||
builder.ConfigureWebHostDefaults(
|
||||
webBuilder => {
|
||||
// Set default web root
|
||||
if (Directory.Exists(websiteDirectory)) {
|
||||
webBuilder.UseWebRoot(websiteDirectory);
|
||||
}
|
||||
|
||||
// Now conditionally initialize settings that are not possible to override
|
||||
if (customConfigExists) {
|
||||
// Set up custom config to be used
|
||||
webBuilder.UseConfiguration(new ConfigurationBuilder().SetBasePath(absoluteConfigDirectory).AddJsonFile(SharedInfo.IPCConfigFile, false, Program.ConfigWatch).Build());
|
||||
|
||||
// Use custom config for Kestrel configuration
|
||||
webBuilder.UseKestrel(static (builderContext, options) => options.Configure(builderContext.Configuration.GetSection("Kestrel")));
|
||||
} else {
|
||||
// Use ASF defaults for Kestrel
|
||||
webBuilder.UseKestrel(static options => options.ListenLocalhost(1242));
|
||||
}
|
||||
|
||||
// Specify Startup class for IPC
|
||||
webBuilder.UseStartup<Startup>();
|
||||
}
|
||||
);
|
||||
|
||||
// Init history logger for /Api/Log usage
|
||||
Logging.InitHistoryLogger();
|
||||
|
||||
// Start the server
|
||||
IHost? kestrelWebHost = null;
|
||||
WebApplication webApplication = await CreateWebApplication().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
kestrelWebHost = builder.Build();
|
||||
await kestrelWebHost.StartAsync().ConfigureAwait(false);
|
||||
// Start the server
|
||||
await webApplication.StartAsync().ConfigureAwait(false);
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
kestrelWebHost?.Dispose();
|
||||
|
||||
await webApplication.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
KestrelWebHost = kestrelWebHost;
|
||||
WebApplication = webApplication;
|
||||
|
||||
ASF.ArchiLogger.LogGenericInfo(Strings.IPCReady);
|
||||
}
|
||||
|
||||
internal static async Task Stop() {
|
||||
if (KestrelWebHost == null) {
|
||||
if (WebApplication == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await KestrelWebHost.StopAsync().ConfigureAwait(false);
|
||||
KestrelWebHost.Dispose();
|
||||
KestrelWebHost = null;
|
||||
await WebApplication.StopAsync().ConfigureAwait(false);
|
||||
await WebApplication.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
WebApplication = null;
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "PathString is a primitive, it's unlikely to be trimmed to the best of our knowledge")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] ConfigurationManager configuration, IApplicationBuilder app) {
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Add support for proxies, this one comes usually after developer exception page, but could be before
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
// Add support for response caching - must be called before static files as we want to cache those as well
|
||||
app.UseResponseCaching();
|
||||
}
|
||||
|
||||
// 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<PathString>("PathBase");
|
||||
|
||||
if (!string.IsNullOrEmpty(pathBase) && (pathBase != "/")) {
|
||||
app.UsePathBase(pathBase);
|
||||
}
|
||||
|
||||
// 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(static context => !context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), static appBuilder => appBuilder.UseStatusCodePagesWithReExecute("/"));
|
||||
|
||||
// Add support for default root path redirection (GET / -> GET /index.html), must come before static files
|
||||
app.UseDefaultFiles();
|
||||
|
||||
Dictionary<string, string> pluginPaths = new(StringComparer.Ordinal);
|
||||
|
||||
if (PluginsCore.ActivePlugins.Count > 0) {
|
||||
foreach (IWebInterface plugin in PluginsCore.ActivePlugins.OfType<IWebInterface>()) {
|
||||
if (string.IsNullOrEmpty(plugin.PhysicalPath) || string.IsNullOrEmpty(plugin.WebPath)) {
|
||||
// Invalid path provided
|
||||
continue;
|
||||
}
|
||||
|
||||
string physicalPath = plugin.PhysicalPath;
|
||||
|
||||
if (!Path.IsPathRooted(physicalPath)) {
|
||||
// Relative path
|
||||
string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location);
|
||||
|
||||
if (string.IsNullOrEmpty(assemblyDirectory)) {
|
||||
// Invalid path provided
|
||||
continue;
|
||||
}
|
||||
|
||||
physicalPath = Path.Combine(assemblyDirectory, plugin.PhysicalPath);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(physicalPath)) {
|
||||
// Non-existing path provided
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginPaths[physicalPath] = plugin.WebPath;
|
||||
|
||||
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 pluginPaths) {
|
||||
app.UseStaticFiles(
|
||||
new StaticFileOptions {
|
||||
FileProvider = new PhysicalFileProvider(physicalPath),
|
||||
OnPrepareResponse = OnPrepareResponse,
|
||||
RequestPath = webPath
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add support for static files (e.g. HTML, CSS and JS from IPC GUI)
|
||||
app.UseStaticFiles(
|
||||
new StaticFileOptions {
|
||||
OnPrepareResponse = OnPrepareResponse
|
||||
}
|
||||
);
|
||||
|
||||
// Use routing for our API controllers, this should be called once we're done with all the static files mess
|
||||
app.UseRouting();
|
||||
|
||||
// We want to protect our API with IPCPassword and additional security, this should be called after routing, so the middleware won't have to deal with API endpoints that do not exist
|
||||
app.UseWhen(static context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), static appBuilder => appBuilder.UseMiddleware<ApiAuthenticationMiddleware>());
|
||||
|
||||
string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword;
|
||||
|
||||
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, 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 websockets that we use e.g. in /Api/NLog
|
||||
app.UseWebSockets();
|
||||
|
||||
// Finally register proper API endpoints once we're done with routing
|
||||
app.UseEndpoints(static endpoints => endpoints.MapControllers());
|
||||
|
||||
// Add support for swagger, responsible for automatic API documentation generation, this should be on the end, once we're done with API
|
||||
app.UseSwagger();
|
||||
|
||||
// Add support for swagger UI, this should be after swagger, obviously
|
||||
app.UseSwaggerUI(
|
||||
static options => {
|
||||
options.DisplayRequestDuration();
|
||||
options.EnableDeepLinking();
|
||||
options.ShowExtensions();
|
||||
options.SwaggerEndpoint($"{SharedInfo.ASF}/swagger.json", $"{SharedInfo.ASF} API");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private static void ConfigureServices([SuppressMessage("ReSharper", "SuggestBaseTypeForParameter")] ConfigurationManager configuration, IServiceCollection services) {
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
// The order of dependency injection is super important, doing things in wrong order will break everything
|
||||
// Order in Configure() method is a good start
|
||||
|
||||
// Prepare knownNetworks that we'll use in a second
|
||||
HashSet<string>? knownNetworksTexts = configuration.GetSection("Kestrel:KnownNetworks").Get<HashSet<string>>();
|
||||
|
||||
HashSet<IPNetwork>? knownNetworks = null;
|
||||
|
||||
if (knownNetworksTexts?.Count > 0) {
|
||||
// Use specified known networks
|
||||
knownNetworks = new HashSet<IPNetwork>();
|
||||
|
||||
foreach (string knownNetworkText in knownNetworksTexts) {
|
||||
string[] addressParts = knownNetworkText.Split('/', 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if ((addressParts.Length != 2) || !IPAddress.TryParse(addressParts[0], out IPAddress? ipAddress) || !byte.TryParse(addressParts[1], out byte prefixLength)) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(knownNetworkText)));
|
||||
ASF.ArchiLogger.LogGenericDebug($"{nameof(knownNetworkText)}: {knownNetworkText}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
knownNetworks.Add(new IPNetwork(ipAddress, prefixLength));
|
||||
}
|
||||
}
|
||||
|
||||
// Add support for proxies
|
||||
services.Configure<ForwardedHeadersOptions>(
|
||||
options => {
|
||||
options.ForwardedHeaders = ForwardedHeaders.All;
|
||||
|
||||
if (knownNetworks != null) {
|
||||
foreach (IPNetwork knownNetwork in knownNetworks) {
|
||||
options.KnownNetworks.Add(knownNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
// Add support for response caching
|
||||
services.AddResponseCaching();
|
||||
}
|
||||
|
||||
// Add support for response compression
|
||||
services.AddResponseCompression(static options => options.EnableForHttps = true);
|
||||
|
||||
string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword;
|
||||
|
||||
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(static options => options.AddDefaultPolicy(static policyBuilder => policyBuilder.AllowAnyOrigin()));
|
||||
}
|
||||
|
||||
// Add support for swagger, responsible for automatic API documentation generation
|
||||
services.AddSwaggerGen(
|
||||
static options => {
|
||||
options.AddSecurityDefinition(
|
||||
nameof(GlobalConfig.IPCPassword), new OpenApiSecurityScheme {
|
||||
Description = $"{nameof(GlobalConfig.IPCPassword)} authentication using request headers. Check {SharedInfo.ProjectURL}/wiki/IPC#authentication for more info.",
|
||||
In = ParameterLocation.Header,
|
||||
Name = ApiAuthenticationMiddleware.HeadersField,
|
||||
Type = SecuritySchemeType.ApiKey
|
||||
}
|
||||
);
|
||||
|
||||
options.AddSecurityRequirement(
|
||||
new OpenApiSecurityRequirement {
|
||||
{
|
||||
new OpenApiSecurityScheme {
|
||||
Reference = new OpenApiReference {
|
||||
Id = nameof(GlobalConfig.IPCPassword),
|
||||
Type = ReferenceType.SecurityScheme
|
||||
}
|
||||
},
|
||||
|
||||
Array.Empty<string>()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// We require custom schema IDs due to conflicting type names, choosing the proper one is tricky as there is no good answer and any kind of convention has a potential to create conflict
|
||||
// FullName and Name both do, ToString() for unknown to me reason doesn't, and I don't have courage to call our WebUtilities.GetUnifiedName() better than what .NET ships with (because it isn't)
|
||||
// Let's use ToString() until we find a good enough reason to change it, also, the name must pass ^[a-zA-Z0-9.-_]+$ regex
|
||||
options.CustomSchemaIds(static type => type.ToString().Replace('+', '-'));
|
||||
|
||||
options.EnableAnnotations(true, true);
|
||||
|
||||
options.SchemaFilter<CustomAttributesSchemaFilter>();
|
||||
options.SchemaFilter<EnumSchemaFilter>();
|
||||
options.SchemaFilter<ReadOnlyFixesSchemaFilter>();
|
||||
|
||||
options.SwaggerDoc(
|
||||
SharedInfo.ASF, new OpenApiInfo {
|
||||
Contact = new OpenApiContact {
|
||||
Name = SharedInfo.GithubRepo,
|
||||
Url = new Uri(SharedInfo.ProjectURL)
|
||||
},
|
||||
|
||||
License = new OpenApiLicense {
|
||||
Name = SharedInfo.LicenseName,
|
||||
Url = new Uri(SharedInfo.LicenseURL)
|
||||
},
|
||||
|
||||
Title = $"{SharedInfo.AssemblyName} API",
|
||||
Version = SharedInfo.Version.ToString()
|
||||
}
|
||||
);
|
||||
|
||||
string xmlDocumentationFile = Path.Combine(AppContext.BaseDirectory, SharedInfo.AssemblyDocumentation);
|
||||
|
||||
if (File.Exists(xmlDocumentationFile)) {
|
||||
options.IncludeXmlComments(xmlDocumentationFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// We need MVC for /Api, but we're going to use only a small subset of all available features
|
||||
IMvcBuilder mvc = services.AddControllers();
|
||||
|
||||
// Add support for controllers declared in custom plugins
|
||||
if (PluginsCore.ActivePlugins.Count > 0) {
|
||||
HashSet<Assembly>? assemblies = PluginsCore.LoadAssemblies();
|
||||
|
||||
if (assemblies != null) {
|
||||
foreach (Assembly assembly in assemblies) {
|
||||
mvc.AddApplicationPart(assembly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mvc.AddControllersAsServices();
|
||||
|
||||
mvc.AddJsonOptions(
|
||||
static options => {
|
||||
JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerialierOptions : JsonUtilities.DefaultJsonSerialierOptions;
|
||||
|
||||
foreach (JsonConverter converter in jsonSerializerOptions.Converters) {
|
||||
options.JsonSerializerOptions.Converters.Add(converter);
|
||||
}
|
||||
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = jsonSerializerOptions.PropertyNamingPolicy;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = jsonSerializerOptions.TypeInfoResolver;
|
||||
options.JsonSerializerOptions.WriteIndented = jsonSerializerOptions.WriteIndented;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static async Task<WebApplication> CreateWebApplication() {
|
||||
string customDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.WebsiteDirectory);
|
||||
string websiteDirectory = Directory.Exists(customDirectory) ? customDirectory : Path.Combine(AppContext.BaseDirectory, SharedInfo.WebsiteDirectory);
|
||||
|
||||
// The order of dependency injection matters, pay attention to it
|
||||
WebApplicationBuilder builder = WebApplication.CreateEmptyBuilder(
|
||||
new WebApplicationOptions {
|
||||
ApplicationName = SharedInfo.AssemblyName,
|
||||
ContentRootPath = SharedInfo.HomeDirectory,
|
||||
WebRootPath = websiteDirectory
|
||||
}
|
||||
);
|
||||
|
||||
// Enable NLog integration for logging
|
||||
builder.Logging.SetMinimumLevel(Debugging.IsUserDebugging ? LogLevel.Trace : LogLevel.Warning);
|
||||
builder.Logging.AddNLogWeb(new NLogAspNetCoreOptions { ShutdownOnDispose = false });
|
||||
|
||||
// Check if custom config is available
|
||||
string absoluteConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), SharedInfo.ConfigDirectory);
|
||||
string customConfigPath = Path.Combine(absoluteConfigDirectory, SharedInfo.IPCConfigFile);
|
||||
bool customConfigExists = File.Exists(customConfigPath);
|
||||
|
||||
if (customConfigExists) {
|
||||
if (Debugging.IsDebugConfigured) {
|
||||
try {
|
||||
string json = await File.ReadAllTextAsync(customConfigPath).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(json)) {
|
||||
JsonNode? jsonNode = JsonNode.Parse(json);
|
||||
|
||||
ASF.ArchiLogger.LogGenericDebug($"{SharedInfo.IPCConfigFile}: {jsonNode?.ToJsonText(true) ?? "null"}");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up custom config to be used
|
||||
builder.WebHost.UseConfiguration(new ConfigurationBuilder().SetBasePath(absoluteConfigDirectory).AddJsonFile(SharedInfo.IPCConfigFile, false, true).Build());
|
||||
}
|
||||
|
||||
builder.WebHost.ConfigureKestrel(
|
||||
options => {
|
||||
options.AddServerHeader = false;
|
||||
|
||||
if (customConfigExists) {
|
||||
// Use custom config for Kestrel configuration
|
||||
options.Configure(builder.Configuration.GetSection("Kestrel"));
|
||||
} else {
|
||||
// Use ASFB defaults for Kestrel
|
||||
options.ListenLocalhost(1242);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
builder.WebHost.UseKestrelCore();
|
||||
|
||||
ConfigureServices(builder.Configuration, builder.Services);
|
||||
|
||||
WebApplication result = builder.Build();
|
||||
|
||||
ConfigureApp(builder.Configuration, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void OnPrepareResponse(StaticFileResponseContext context) {
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (context.File is not { Exists: true, IsDirectory: false } || string.IsNullOrEmpty(context.File.Name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 their cache is still up-to-date
|
||||
// This is used to handle ASF and user updates to WWW root, we don't want 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ internal sealed class ApiAuthenticationMiddleware {
|
||||
}
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
[UsedImplicitly]
|
||||
public async Task InvokeAsync(HttpContext context, IOptions<JsonOptions> jsonOptions) {
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
@@ -1,380 +0,0 @@
|
||||
// _ _ _ ____ _ _____
|
||||
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
|
||||
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
|
||||
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
|
||||
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
|
||||
// |
|
||||
// Copyright 2015-2024 Ł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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ArchiSteamFarm.Core;
|
||||
using ArchiSteamFarm.Helpers.Json;
|
||||
using ArchiSteamFarm.IPC.Integration;
|
||||
using ArchiSteamFarm.Localization;
|
||||
using ArchiSteamFarm.Plugins;
|
||||
using ArchiSteamFarm.Plugins.Interfaces;
|
||||
using ArchiSteamFarm.Storage;
|
||||
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.StaticFiles;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
|
||||
|
||||
namespace ArchiSteamFarm.IPC;
|
||||
|
||||
internal sealed class Startup {
|
||||
private readonly IConfiguration Configuration;
|
||||
|
||||
public Startup(IConfiguration configuration) {
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "PathString is a primitive, it's unlikely to be trimmed to the best of our knowledge")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3000", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
[UsedImplicitly]
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
ArgumentNullException.ThrowIfNull(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();
|
||||
}
|
||||
|
||||
// Add support for proxies, this one comes usually after developer exception page, but could be before
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
// Add support for response caching - must be called before static files as we want to cache those as well
|
||||
app.UseResponseCaching();
|
||||
}
|
||||
|
||||
// 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<PathString>("PathBase");
|
||||
|
||||
if (!string.IsNullOrEmpty(pathBase) && (pathBase != "/")) {
|
||||
app.UsePathBase(pathBase);
|
||||
}
|
||||
|
||||
// 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(static context => !context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), static appBuilder => appBuilder.UseStatusCodePagesWithReExecute("/"));
|
||||
|
||||
// Add support for default root path redirection (GET / -> GET /index.html), must come before static files
|
||||
app.UseDefaultFiles();
|
||||
|
||||
Dictionary<string, string> pluginPaths = new(StringComparer.Ordinal);
|
||||
|
||||
if (PluginsCore.ActivePlugins.Count > 0) {
|
||||
foreach (IWebInterface plugin in PluginsCore.ActivePlugins.OfType<IWebInterface>()) {
|
||||
if (string.IsNullOrEmpty(plugin.PhysicalPath) || string.IsNullOrEmpty(plugin.WebPath)) {
|
||||
// Invalid path provided
|
||||
continue;
|
||||
}
|
||||
|
||||
string physicalPath = plugin.PhysicalPath;
|
||||
|
||||
if (!Path.IsPathRooted(physicalPath)) {
|
||||
// Relative path
|
||||
string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location);
|
||||
|
||||
if (string.IsNullOrEmpty(assemblyDirectory)) {
|
||||
// Invalid path provided
|
||||
continue;
|
||||
}
|
||||
|
||||
physicalPath = Path.Combine(assemblyDirectory, plugin.PhysicalPath);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(physicalPath)) {
|
||||
// Non-existing path provided
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginPaths[physicalPath] = plugin.WebPath;
|
||||
|
||||
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 pluginPaths) {
|
||||
app.UseStaticFiles(
|
||||
new StaticFileOptions {
|
||||
FileProvider = new PhysicalFileProvider(physicalPath),
|
||||
OnPrepareResponse = OnPrepareResponse,
|
||||
RequestPath = webPath
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add support for static files (e.g. HTML, CSS and JS from IPC GUI)
|
||||
app.UseStaticFiles(
|
||||
new StaticFileOptions {
|
||||
OnPrepareResponse = OnPrepareResponse
|
||||
}
|
||||
);
|
||||
|
||||
// Use routing for our API controllers, this should be called once we're done with all the static files mess
|
||||
app.UseRouting();
|
||||
|
||||
// We want to protect our API with IPCPassword and additional security, this should be called after routing, so the middleware won't have to deal with API endpoints that do not exist
|
||||
app.UseWhen(static context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), static appBuilder => appBuilder.UseMiddleware<ApiAuthenticationMiddleware>());
|
||||
|
||||
string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword;
|
||||
|
||||
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, 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 websockets that we use e.g. in /Api/NLog
|
||||
app.UseWebSockets();
|
||||
|
||||
// Finally register proper API endpoints once we're done with routing
|
||||
app.UseEndpoints(static endpoints => endpoints.MapControllers());
|
||||
|
||||
// Add support for swagger, responsible for automatic API documentation generation, this should be on the end, once we're done with API
|
||||
app.UseSwagger();
|
||||
|
||||
// Add support for swagger UI, this should be after swagger, obviously
|
||||
app.UseSwaggerUI(
|
||||
static options => {
|
||||
options.DisplayRequestDuration();
|
||||
options.EnableDeepLinking();
|
||||
options.ShowExtensions();
|
||||
options.SwaggerEndpoint($"{SharedInfo.ASF}/swagger.json", $"{SharedInfo.ASF} API");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[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);
|
||||
|
||||
// The order of dependency injection is super important, doing things in wrong order will break everything
|
||||
// Order in Configure() method is a good start
|
||||
|
||||
// Prepare knownNetworks that we'll use in a second
|
||||
HashSet<string>? knownNetworksTexts = Configuration.GetSection("Kestrel:KnownNetworks").Get<HashSet<string>>();
|
||||
|
||||
HashSet<IPNetwork>? knownNetworks = null;
|
||||
|
||||
if (knownNetworksTexts?.Count > 0) {
|
||||
// Use specified known networks
|
||||
knownNetworks = new HashSet<IPNetwork>();
|
||||
|
||||
foreach (string knownNetworkText in knownNetworksTexts) {
|
||||
string[] addressParts = knownNetworkText.Split('/', 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if ((addressParts.Length != 2) || !IPAddress.TryParse(addressParts[0], out IPAddress? ipAddress) || !byte.TryParse(addressParts[1], out byte prefixLength)) {
|
||||
ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(knownNetworkText)));
|
||||
ASF.ArchiLogger.LogGenericDebug($"{nameof(knownNetworkText)}: {knownNetworkText}");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
knownNetworks.Add(new IPNetwork(ipAddress, prefixLength));
|
||||
}
|
||||
}
|
||||
|
||||
// Add support for proxies
|
||||
services.Configure<ForwardedHeadersOptions>(
|
||||
options => {
|
||||
options.ForwardedHeaders = ForwardedHeaders.All;
|
||||
|
||||
if (knownNetworks != null) {
|
||||
foreach (IPNetwork knownNetwork in knownNetworks) {
|
||||
options.KnownNetworks.Add(knownNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) {
|
||||
// Add support for response caching
|
||||
services.AddResponseCaching();
|
||||
}
|
||||
|
||||
// Add support for response compression
|
||||
services.AddResponseCompression(static options => options.EnableForHttps = true);
|
||||
|
||||
string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword;
|
||||
|
||||
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(static options => options.AddDefaultPolicy(static policyBuilder => policyBuilder.AllowAnyOrigin()));
|
||||
}
|
||||
|
||||
// Add support for swagger, responsible for automatic API documentation generation
|
||||
services.AddSwaggerGen(
|
||||
static options => {
|
||||
options.AddSecurityDefinition(
|
||||
nameof(GlobalConfig.IPCPassword), new OpenApiSecurityScheme {
|
||||
Description = $"{nameof(GlobalConfig.IPCPassword)} authentication using request headers. Check {SharedInfo.ProjectURL}/wiki/IPC#authentication for more info.",
|
||||
In = ParameterLocation.Header,
|
||||
Name = ApiAuthenticationMiddleware.HeadersField,
|
||||
Type = SecuritySchemeType.ApiKey
|
||||
}
|
||||
);
|
||||
|
||||
options.AddSecurityRequirement(
|
||||
new OpenApiSecurityRequirement {
|
||||
{
|
||||
new OpenApiSecurityScheme {
|
||||
Reference = new OpenApiReference {
|
||||
Id = nameof(GlobalConfig.IPCPassword),
|
||||
Type = ReferenceType.SecurityScheme
|
||||
}
|
||||
},
|
||||
|
||||
Array.Empty<string>()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// We require custom schema IDs due to conflicting type names, choosing the proper one is tricky as there is no good answer and any kind of convention has a potential to create conflict
|
||||
// FullName and Name both do, ToString() for unknown to me reason doesn't, and I don't have courage to call our WebUtilities.GetUnifiedName() better than what .NET ships with (because it isn't)
|
||||
// Let's use ToString() until we find a good enough reason to change it, also, the name must pass ^[a-zA-Z0-9.-_]+$ regex
|
||||
options.CustomSchemaIds(static type => type.ToString().Replace('+', '-'));
|
||||
|
||||
options.EnableAnnotations(true, true);
|
||||
|
||||
options.SchemaFilter<CustomAttributesSchemaFilter>();
|
||||
options.SchemaFilter<EnumSchemaFilter>();
|
||||
options.SchemaFilter<ReadOnlyFixesSchemaFilter>();
|
||||
|
||||
options.SwaggerDoc(
|
||||
SharedInfo.ASF, new OpenApiInfo {
|
||||
Contact = new OpenApiContact {
|
||||
Name = SharedInfo.GithubRepo,
|
||||
Url = new Uri(SharedInfo.ProjectURL)
|
||||
},
|
||||
|
||||
License = new OpenApiLicense {
|
||||
Name = SharedInfo.LicenseName,
|
||||
Url = new Uri(SharedInfo.LicenseURL)
|
||||
},
|
||||
|
||||
Title = $"{SharedInfo.AssemblyName} API",
|
||||
Version = SharedInfo.Version.ToString()
|
||||
}
|
||||
);
|
||||
|
||||
string xmlDocumentationFile = Path.Combine(AppContext.BaseDirectory, SharedInfo.AssemblyDocumentation);
|
||||
|
||||
if (File.Exists(xmlDocumentationFile)) {
|
||||
options.IncludeXmlComments(xmlDocumentationFile);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// We need MVC for /Api, but we're going to use only a small subset of all available features
|
||||
IMvcBuilder mvc = services.AddControllers();
|
||||
|
||||
// Add support for controllers declared in custom plugins
|
||||
if (PluginsCore.ActivePlugins.Count > 0) {
|
||||
HashSet<Assembly>? assemblies = PluginsCore.LoadAssemblies();
|
||||
|
||||
if (assemblies != null) {
|
||||
foreach (Assembly assembly in assemblies) {
|
||||
mvc.AddApplicationPart(assembly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mvc.AddControllersAsServices();
|
||||
|
||||
mvc.AddJsonOptions(
|
||||
static options => {
|
||||
JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerialierOptions : JsonUtilities.DefaultJsonSerialierOptions;
|
||||
|
||||
foreach (JsonConverter converter in jsonSerializerOptions.Converters) {
|
||||
options.JsonSerializerOptions.Converters.Add(converter);
|
||||
}
|
||||
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = jsonSerializerOptions.PropertyNamingPolicy;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = jsonSerializerOptions.TypeInfoResolver;
|
||||
options.JsonSerializerOptions.WriteIndented = jsonSerializerOptions.WriteIndented;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static void OnPrepareResponse(StaticFileResponseContext context) {
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
|
||||
if (context.File is not { Exists: true, IsDirectory: false } || string.IsNullOrEmpty(context.File.Name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 their cache is still up-to-date
|
||||
// This is used to handle ASF and user updates to WWW root, we don't want 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;
|
||||
}
|
||||
}
|
||||
@@ -441,9 +441,9 @@ internal static class Logging {
|
||||
|
||||
if (!Debugging.IsUserDebugging) {
|
||||
// Silence default ASP.NET logging
|
||||
config.LoggingRules.Add(new LoggingRule("Microsoft*", target) { FinalMinLevel = LogLevel.Warn });
|
||||
config.LoggingRules.Add(new LoggingRule("Microsoft.Hosting.Lifetime*", target) { FinalMinLevel = LogLevel.Info });
|
||||
config.LoggingRules.Add(new LoggingRule("System*", target) { FinalMinLevel = LogLevel.Warn });
|
||||
config.LoggingRules.Add(new LoggingRule("Microsoft.*", target) { FinalMinLevel = LogLevel.Warn });
|
||||
config.LoggingRules.Add(new LoggingRule("Microsoft.Hosting.Lifetime", target) { FinalMinLevel = LogLevel.Info });
|
||||
config.LoggingRules.Add(new LoggingRule("System.*", target) { FinalMinLevel = LogLevel.Warn });
|
||||
}
|
||||
|
||||
config.LoggingRules.Add(new LoggingRule("*", LogLevel.Debug, target));
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -707,6 +708,7 @@ public sealed class WebBrowser : IDisposable {
|
||||
return await InternalRequest(request, HttpMethod.Post, headers, data, referer, requestOptions, httpCompletionOption, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "We don't care about trimmed assemblies, as we need it to work only with the known (used) ones")]
|
||||
private async Task<HttpResponseMessage?> InternalRequest<T>(Uri request, HttpMethod httpMethod, IReadOnlyCollection<KeyValuePair<string, string>>? headers = null, T? data = null, Uri? referer = null, ERequestOptions requestOptions = ERequestOptions.None, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, byte maxRedirections = MaxTries, CancellationToken cancellationToken = default) where T : class {
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
ArgumentNullException.ThrowIfNull(httpMethod);
|
||||
|
||||
Reference in New Issue
Block a user