Skip to content

Commit

Permalink
Go back to swagger in regards to #3376, add cmdline switch to force o…
Browse files Browse the repository at this point in the history
…penapi
  • Loading branch information
JustArchi committed Jan 20, 2025
1 parent 2a66fce commit ac01cca
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 10 deletions.
3 changes: 2 additions & 1 deletion ArchiSteamFarm/ArchiSteamFarm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
<PackageReference Include="Nito.AsyncEx.Coordination" />
<PackageReference Include="NLog.Web.AspNetCore" />
<PackageReference Include="SteamKit2" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="System.Composition" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
Expand Down
80 changes: 72 additions & 8 deletions ArchiSteamFarm/IPC/ArchiKestrel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<DocumentTransformer>();
options.AddOperationTransformer<OperationTransformer>();
options.AddSchemaTransformer<SchemaTransformer>();
}
);
if (Program.UseOpenApi) {
services.AddOpenApi(
SharedInfo.ASF, static options => {
options.AddDocumentTransformer<DocumentTransformer>();
options.AddOperationTransformer<OperationTransformer>();
options.AddSchemaTransformer<SchemaTransformer>();
}
);
} 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<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()
}
);
}
);
}

// Add support for optional healtchecks
services.AddHealthChecks();
Expand Down
53 changes: 53 additions & 0 deletions ArchiSteamFarm/IPC/Swagger/CustomAttributesSchemaFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2025 Ł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 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);
}
}
}
107 changes: 107 additions & 0 deletions ArchiSteamFarm/IPC/Swagger/EnumSchemaFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2025 Ł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 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<T>(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;
}
}
}
42 changes: 42 additions & 0 deletions ArchiSteamFarm/IPC/Swagger/ReadOnlyFixesSchemaFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// ----------------------------------------------------------------------------------------------
// _ _ _ ____ _ _____
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
// |
// Copyright 2015-2025 Ł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 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;
}
}
}
5 changes: 5 additions & 0 deletions ArchiSteamFarm/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PosixSignal, PosixSignalRegistration> RegisteredPosixSignals = new();
private static readonly TaskCompletionSource<byte> ShutdownResetEvent = new();
Expand Down Expand Up @@ -610,6 +611,10 @@ private static async Task<bool> ParseArgs(IReadOnlyCollection<string> args) {
case "--SYSTEM-REQUIRED" when noArgumentValueNext():
SystemRequired = true;

break;
case "--USE-OPENAPI" when noArgumentValueNext():
UseOpenApi = true;

break;
default:
if (cryptKeyNext) {
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.10.0" />
<PackageVersion Include="SteamKit2" Version="3.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="7.2.0" />
<PackageVersion Include="System.Composition" Version="9.0.1" />
<PackageVersion Include="System.Composition.AttributedModel" Version="9.0.1" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
Expand Down

0 comments on commit ac01cca

Please sign in to comment.