diff --git a/ArchiSteamFarm/IPC/ArchiKestrel.cs b/ArchiSteamFarm/IPC/ArchiKestrel.cs index 81c3930895dc7..c55cbdfdc0800 100644 --- a/ArchiSteamFarm/IPC/ArchiKestrel.cs +++ b/ArchiSteamFarm/IPC/ArchiKestrel.cs @@ -123,7 +123,7 @@ private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeF ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(app); - // The order of dependency injection is super important, doing things in wrong order will break everything + // The order of dependency injection is super important, doing things in wrong order will most likely break everything // https://docs.microsoft.com/aspnet/core/fundamentals/middleware // This one is easy, it's always in the beginning @@ -134,8 +134,9 @@ private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeF // Add support for proxies, this one comes usually after developer exception page, but could be before app.UseForwardedHeaders(); + // Add support for response caching - must be called before static files as we want to cache those as well 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 + // As previously in services, we skip it if memory usage is super important for us app.UseResponseCaching(); } @@ -157,42 +158,54 @@ private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeF // Add support for default root path redirection (GET / -> GET /index.html), must come before static files app.UseDefaultFiles(); + // Add support for additional default files provided by plugins Dictionary pluginPaths = new(StringComparer.Ordinal); - if (PluginsCore.ActivePlugins.Count > 0) { - foreach (IWebInterface plugin in PluginsCore.ActivePlugins.OfType()) { - if (string.IsNullOrEmpty(plugin.PhysicalPath) || string.IsNullOrEmpty(plugin.WebPath)) { - // Invalid path provided - continue; - } + foreach (IWebInterface plugin in PluginsCore.ActivePlugins.OfType()) { + string physicalPath = plugin.PhysicalPath; - string physicalPath = plugin.PhysicalPath; + if (string.IsNullOrEmpty(physicalPath)) { + // Invalid path provided + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, $"{nameof(physicalPath)} ({plugin.Name})")); - if (!Path.IsPathRooted(physicalPath)) { - // Relative path - string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); + continue; + } - if (string.IsNullOrEmpty(assemblyDirectory)) { - throw new InvalidOperationException(nameof(assemblyDirectory)); - } + string webPath = plugin.WebPath; - physicalPath = Path.Combine(assemblyDirectory, plugin.PhysicalPath); - } + if (string.IsNullOrEmpty(webPath)) { + // Invalid path provided + ASF.ArchiLogger.LogGenericError(string.Format(CultureInfo.CurrentCulture, Strings.ErrorObjectIsNull, $"{nameof(webPath)} ({plugin.Name})")); - if (!Directory.Exists(physicalPath)) { - // Non-existing path provided - continue; - } + continue; + } - pluginPaths[physicalPath] = plugin.WebPath; + if (!Path.IsPathRooted(physicalPath)) { + // Relative path + string? assemblyDirectory = Path.GetDirectoryName(plugin.GetType().Assembly.Location); - if (plugin.WebPath != "/") { - app.UseDefaultFiles(plugin.WebPath); + if (string.IsNullOrEmpty(assemblyDirectory)) { + throw new InvalidOperationException(nameof(assemblyDirectory)); } + + physicalPath = Path.Combine(assemblyDirectory, physicalPath); + } + + if (!Directory.Exists(physicalPath)) { + // Non-existing path provided + ASF.ArchiLogger.LogGenericWarning(string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, $"{nameof(physicalPath)} ({plugin.Name})")); + + continue; + } + + pluginPaths[physicalPath] = webPath; + + if (webPath != "/") { + app.UseDefaultFiles(webPath); } } - // Add support for static files from custom plugins (e.g. HTML, CSS and JS) + // Add support for additional static files from custom plugins (e.g. HTML, CSS and JS) foreach ((string physicalPath, string webPath) in pluginPaths) { StaticFileOptions options = new() { FileProvider = new PhysicalFileProvider(physicalPath), @@ -219,10 +232,10 @@ private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeF // 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()); + // Add support for CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API 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(); } @@ -230,19 +243,18 @@ private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeF // 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()); - - if (PluginsCore.ActivePlugins.Count > 0) { - foreach (IWebServiceProvider plugin in PluginsCore.ActivePlugins.OfType()) { - try { - plugin.OnConfiguringEndpoints(app); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - } + // Add additional endpoints provided by plugins + foreach (IWebServiceProvider plugin in PluginsCore.ActivePlugins.OfType()) { + try { + plugin.OnConfiguringEndpoints(app); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); } } + // 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(); @@ -262,8 +274,8 @@ private static void ConfigureServices([SuppressMessage("ReSharper", "SuggestBase 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 + // The order of dependency injection is super important, doing things in wrong order will most likely break everything + // https://docs.microsoft.com/aspnet/core/fundamentals/middleware // Prepare knownNetworks that we'll use in a second HashSet? knownNetworksTexts = configuration.GetSection("Kestrel:KnownNetworks").Get>(); @@ -301,18 +313,19 @@ private static void ConfigureServices([SuppressMessage("ReSharper", "SuggestBase } ); + // Add support for response caching if (ASF.GlobalConfig?.OptimizationMode != GlobalConfig.EOptimizationMode.MinMemoryUsage) { - // Add support for response caching + // We can skip it if memory usage is super important for us services.AddResponseCaching(); } // Add support for response compression services.AddResponseCompression(static options => options.EnableForHttps = true); + // Add support for CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API 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())); } @@ -383,30 +396,31 @@ private static void ConfigureServices([SuppressMessage("ReSharper", "SuggestBase // Add support for optional healtchecks services.AddHealthChecks(); + // Add support for additional services provided by plugins + foreach (IWebServiceProvider plugin in PluginsCore.ActivePlugins.OfType()) { + try { + plugin.OnConfiguringServices(services); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + } + } + // 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? assemblies = PluginsCore.LoadAssemblies(); - - if (assemblies != null) { - foreach (Assembly assembly in assemblies) { - mvc.AddApplicationPart(assembly); - } - } + // Add support for additional controllers provided by plugins + HashSet? assemblies = PluginsCore.LoadAssemblies(); - foreach (IWebServiceProvider plugin in PluginsCore.ActivePlugins.OfType()) { - try { - plugin.OnConfiguringServices(services); - } catch (Exception e) { - ASF.ArchiLogger.LogGenericException(e); - } + if (assemblies != null) { + foreach (Assembly assembly in assemblies) { + mvc.AddApplicationPart(assembly); } } + // Register discovered controllers mvc.AddControllersAsServices(); + // Modify default JSON options mvc.AddJsonOptions( static options => { JsonSerializerOptions jsonSerializerOptions = Debugging.IsUserDebugging ? JsonUtilities.IndentedJsonSerialierOptions : JsonUtilities.DefaultJsonSerialierOptions;