diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj
index 95541f9b9c55b..8280155183810 100644
--- a/ArchiSteamFarm/ArchiSteamFarm.csproj
+++ b/ArchiSteamFarm/ArchiSteamFarm.csproj
@@ -16,7 +16,8 @@
-
+
+
diff --git a/ArchiSteamFarm/IPC/ArchiKestrel.cs b/ArchiSteamFarm/IPC/ArchiKestrel.cs
index e7aac2ce52f85..9303ec9f9282b 100644
--- a/ArchiSteamFarm/IPC/ArchiKestrel.cs
+++ b/ArchiSteamFarm/IPC/ArchiKestrel.cs
@@ -36,6 +36,7 @@
using ArchiSteamFarm.IPC.Controllers.Api;
using ArchiSteamFarm.IPC.Integration;
using ArchiSteamFarm.IPC.OpenApi;
+using ArchiSteamFarm.IPC.Swagger;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using ArchiSteamFarm.NLog.Targets;
@@ -53,6 +54,7 @@
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;
@@ -255,7 +257,11 @@ private static void ConfigureApp([SuppressMessage("ReSharper", "SuggestBaseTypeF
app.MapControllers();
// Add support for OpenAPI, responsible for automatic API documentation generation, this should be on the end, once we're done with API
- app.MapOpenApi("/swagger/{documentName}/swagger.json");
+ if (Program.UseOpenApi) {
+ app.MapOpenApi("/swagger/{documentName}/swagger.json");
+ } else {
+ app.UseSwagger();
+ }
// Add support for swagger UI, this should be after swagger, obviously
app.UseSwaggerUI(
@@ -332,13 +338,71 @@ private static void ConfigureServices([SuppressMessage("ReSharper", "SuggestBase
}
// Add support for OpenAPI, responsible for automatic API documentation generation
- services.AddOpenApi(
- SharedInfo.ASF, static options => {
- options.AddDocumentTransformer();
- options.AddOperationTransformer();
- options.AddSchemaTransformer();
- }
- );
+ if (Program.UseOpenApi) {
+ services.AddOpenApi(
+ SharedInfo.ASF, static options => {
+ options.AddDocumentTransformer();
+ options.AddOperationTransformer();
+ options.AddSchemaTransformer();
+ }
+ );
+ } else {
+ 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
+ }
+ },
+
+ []
+ }
+ }
+ );
+
+ // 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();
+ options.SchemaFilter();
+ options.SchemaFilter();
+
+ 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()
+ }
+ );
+ }
+ );
+ }
// Add support for optional healtchecks
services.AddHealthChecks();
diff --git a/ArchiSteamFarm/IPC/Swagger/CustomAttributesSchemaFilter.cs b/ArchiSteamFarm/IPC/Swagger/CustomAttributesSchemaFilter.cs
new file mode 100644
index 0000000000000..d99c18dcdbd1a
--- /dev/null
+++ b/ArchiSteamFarm/IPC/Swagger/CustomAttributesSchemaFilter.cs
@@ -0,0 +1,53 @@
+// ----------------------------------------------------------------------------------------------
+// _ _ _ ____ _ _____
+// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
+// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
+// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
+// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
+// ----------------------------------------------------------------------------------------------
+// |
+// Copyright 2015-2025 Ł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.Reflection;
+using ArchiSteamFarm.IPC.Integration;
+using JetBrains.Annotations;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace ArchiSteamFarm.IPC.Swagger;
+
+[UsedImplicitly]
+internal sealed class CustomAttributesSchemaFilter : ISchemaFilter {
+ public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
+ ArgumentNullException.ThrowIfNull(schema);
+ ArgumentNullException.ThrowIfNull(context);
+
+ ICustomAttributeProvider attributesProvider;
+
+ if (context.MemberInfo != null) {
+ attributesProvider = context.MemberInfo;
+ } else if (context.ParameterInfo != null) {
+ attributesProvider = context.ParameterInfo;
+ } else {
+ return;
+ }
+
+ foreach (CustomSwaggerAttribute customSwaggerAttribute in attributesProvider.GetCustomAttributes(typeof(CustomSwaggerAttribute), true)) {
+ customSwaggerAttribute.Apply(schema);
+ }
+ }
+}
diff --git a/ArchiSteamFarm/IPC/Swagger/EnumSchemaFilter.cs b/ArchiSteamFarm/IPC/Swagger/EnumSchemaFilter.cs
new file mode 100644
index 0000000000000..4eb9c87b732bb
--- /dev/null
+++ b/ArchiSteamFarm/IPC/Swagger/EnumSchemaFilter.cs
@@ -0,0 +1,107 @@
+// ----------------------------------------------------------------------------------------------
+// _ _ _ ____ _ _____
+// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
+// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
+// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
+// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
+// ----------------------------------------------------------------------------------------------
+// |
+// Copyright 2015-2025 Ł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.Globalization;
+using JetBrains.Annotations;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Extensions;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace ArchiSteamFarm.IPC.Swagger;
+
+[UsedImplicitly]
+internal sealed class EnumSchemaFilter : ISchemaFilter {
+ public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
+ ArgumentNullException.ThrowIfNull(schema);
+ ArgumentNullException.ThrowIfNull(context);
+
+ if (context.Type is not { IsEnum: true }) {
+ return;
+ }
+
+ if (context.Type.IsDefined(typeof(FlagsAttribute), false)) {
+ schema.Format = "flags";
+ }
+
+ OpenApiObject definition = new();
+
+ foreach (object? enumValue in context.Type.GetEnumValues()) {
+ if (enumValue == null) {
+ throw new InvalidOperationException(nameof(enumValue));
+ }
+
+ string? enumName = Enum.GetName(context.Type, enumValue);
+
+ if (string.IsNullOrEmpty(enumName)) {
+ // Fallback
+ enumName = enumValue.ToString();
+
+ if (string.IsNullOrEmpty(enumName)) {
+ throw new InvalidOperationException(nameof(enumName));
+ }
+ }
+
+ if (definition.ContainsKey(enumName)) {
+ // This is possible if we have multiple names for the same enum value, we'll ignore additional ones
+ continue;
+ }
+
+ IOpenApiPrimitive enumObject;
+
+ if (TryCast(enumValue, out int intValue)) {
+ enumObject = new OpenApiInteger(intValue);
+ } else if (TryCast(enumValue, out long longValue)) {
+ enumObject = new OpenApiLong(longValue);
+ } else if (TryCast(enumValue, out ulong ulongValue)) {
+ // OpenApi spec doesn't support ulongs as of now
+ enumObject = new OpenApiString(ulongValue.ToString(CultureInfo.InvariantCulture));
+ } else {
+ throw new InvalidOperationException(nameof(enumValue));
+ }
+
+ definition.Add(enumName, enumObject);
+ }
+
+ schema.AddExtension("x-definition", definition);
+ }
+
+ private static bool TryCast(object value, out T typedValue) where T : struct {
+ ArgumentNullException.ThrowIfNull(value);
+
+ try {
+ typedValue = (T) Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
+
+ return true;
+ } catch (InvalidCastException) {
+ typedValue = default(T);
+
+ return false;
+ } catch (OverflowException) {
+ typedValue = default(T);
+
+ return false;
+ }
+ }
+}
diff --git a/ArchiSteamFarm/IPC/Swagger/ReadOnlyFixesSchemaFilter.cs b/ArchiSteamFarm/IPC/Swagger/ReadOnlyFixesSchemaFilter.cs
new file mode 100644
index 0000000000000..d798fc1e23e33
--- /dev/null
+++ b/ArchiSteamFarm/IPC/Swagger/ReadOnlyFixesSchemaFilter.cs
@@ -0,0 +1,42 @@
+// ----------------------------------------------------------------------------------------------
+// _ _ _ ____ _ _____
+// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
+// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
+// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
+// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
+// ----------------------------------------------------------------------------------------------
+// |
+// Copyright 2015-2025 Ł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.Reflection;
+using JetBrains.Annotations;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace ArchiSteamFarm.IPC.Swagger;
+
+[UsedImplicitly]
+internal sealed class ReadOnlyFixesSchemaFilter : ISchemaFilter {
+ public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
+ ArgumentNullException.ThrowIfNull(schema);
+ ArgumentNullException.ThrowIfNull(context);
+
+ if (schema.ReadOnly && context.MemberInfo is PropertyInfo { CanWrite: true }) {
+ schema.ReadOnly = false;
+ }
+ }
+}
diff --git a/ArchiSteamFarm/Program.cs b/ArchiSteamFarm/Program.cs
index ddc247b5d333c..4124058182889 100644
--- a/ArchiSteamFarm/Program.cs
+++ b/ArchiSteamFarm/Program.cs
@@ -59,6 +59,7 @@ internal static class Program {
internal static bool Service { get; private set; }
internal static bool ShutdownSequenceInitialized { get; private set; }
internal static bool SteamParentalGeneration { get; private set; } = true;
+ internal static bool UseOpenApi { get; private set; }
private static readonly Dictionary RegisteredPosixSignals = new();
private static readonly TaskCompletionSource ShutdownResetEvent = new();
@@ -610,6 +611,10 @@ private static async Task ParseArgs(IReadOnlyCollection args) {
case "--SYSTEM-REQUIRED" when noArgumentValueNext():
SystemRequired = true;
+ break;
+ case "--USE-OPENAPI" when noArgumentValueNext():
+ UseOpenApi = true;
+
break;
default:
if (cryptKeyNext) {
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5686d11752d0b..e07c2dbb9615f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,7 +17,8 @@
-
+
+