diff --git a/ArchiSteamFarm/IPC/Controllers/Api/ArchiController.cs b/ArchiSteamFarm/IPC/Controllers/Api/ArchiController.cs index 306b7aa71..6c4064808 100644 --- a/ArchiSteamFarm/IPC/Controllers/Api/ArchiController.cs +++ b/ArchiSteamFarm/IPC/Controllers/Api/ArchiController.cs @@ -30,8 +30,8 @@ namespace ArchiSteamFarm.IPC.Controllers.Api { [Produces("application/json")] [Route("Api")] [SwaggerResponse((int) HttpStatusCode.BadRequest, "The request has failed, check " + nameof(GenericResponse.Message) + " from response body for actual reason. Most of the time this is ASF, understanding the request, but refusing to execute it due to provided reason.", typeof(GenericResponse))] - [SwaggerResponse((int) HttpStatusCode.Unauthorized, "ASF has " + nameof(GlobalConfig.IPCPassword) + " set, but you've failed to authenticate. See " + SharedInfo.ProjectURL + "/wiki/IPC#authentication.")] - [SwaggerResponse((int) HttpStatusCode.Forbidden, "ASF has " + nameof(GlobalConfig.IPCPassword) + " set and you've failed to authenticate too many times, try again in an hour. See " + SharedInfo.ProjectURL + "/wiki/IPC#authentication.")] + [SwaggerResponse((int) HttpStatusCode.Unauthorized, "ASF has " + nameof(GlobalConfig.IPCPassword) + " set, but you've failed to authenticate. See " + SharedInfo.ProjectURL + "/wiki/IPC#authentication.", typeof(GenericResponse))] + [SwaggerResponse((int) HttpStatusCode.Forbidden, "ASF lacks " + nameof(GlobalConfig.IPCPassword) + " and you're not permitted to access the API, or " + nameof(GlobalConfig.IPCPassword) + " is set and you've failed to authenticate too many times (try again in an hour). See " + SharedInfo.ProjectURL + "/wiki/IPC#authentication.", typeof(GenericResponse))] [SwaggerResponse((int) HttpStatusCode.InternalServerError, "ASF has encountered an unexpected error while serving the request. The log may include extra info related to this issue.")] [SwaggerResponse((int) HttpStatusCode.ServiceUnavailable, "ASF has encountered an error while requesting a third-party resource. Try again later.")] public abstract class ArchiController : ControllerBase { } diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs index 19d77d5e6..7376fcd97 100644 --- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs +++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs @@ -28,10 +28,12 @@ using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; +using ArchiSteamFarm.IPC.Responses; using ArchiSteamFarm.Storage; using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -71,23 +73,35 @@ namespace ArchiSteamFarm.IPC.Integration { } [PublicAPI] - public async Task InvokeAsync(HttpContext context) { +#if NETFRAMEWORK + public async Task InvokeAsync(HttpContext context, IOptions jsonOptions) { +#else + public async Task InvokeAsync(HttpContext context, IOptions jsonOptions) { +#endif if (context == null) { throw new ArgumentNullException(nameof(context)); } - HttpStatusCode authenticationStatus = await GetAuthenticationStatus(context).ConfigureAwait(false); + if (jsonOptions == null) { + throw new ArgumentNullException(nameof(jsonOptions)); + } - if (authenticationStatus != HttpStatusCode.OK) { - await context.Response.Generate(authenticationStatus).ConfigureAwait(false); + (HttpStatusCode statusCode, bool permanent) = await GetAuthenticationStatus(context).ConfigureAwait(false); + + if (statusCode == HttpStatusCode.OK) { + await Next(context).ConfigureAwait(false); return; } - await Next(context).ConfigureAwait(false); + context.Response.StatusCode = (int) statusCode; + + StatusCodeResponse statusCodeResponse = new(statusCode, permanent); + + await context.Response.WriteJsonAsync(new GenericResponse(false, statusCodeResponse), jsonOptions.Value.SerializerSettings).ConfigureAwait(false); } - private async Task GetAuthenticationStatus(HttpContext context) { + private async Task<(HttpStatusCode StatusCode, bool Permanent)> GetAuthenticationStatus(HttpContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } @@ -106,38 +120,38 @@ namespace ArchiSteamFarm.IPC.Integration { if (string.IsNullOrEmpty(ipcPassword)) { if (IPAddress.IsLoopback(clientIP)) { - return HttpStatusCode.OK; + return (HttpStatusCode.OK, true); } if (ForwardedHeadersOptions.KnownNetworks.Count == 0) { - return HttpStatusCode.Forbidden; + return (HttpStatusCode.Forbidden, true); } if (clientIP.IsIPv4MappedToIPv6) { IPAddress mappedClientIP = clientIP.MapToIPv4(); if (ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(mappedClientIP))) { - return HttpStatusCode.OK; + return (HttpStatusCode.OK, true); } } - return ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; + return (ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden, true); } if (FailedAuthorizations.TryGetValue(clientIP, out byte attempts)) { if (attempts >= MaxFailedAuthorizationAttempts) { - return HttpStatusCode.Forbidden; + return (HttpStatusCode.Forbidden, false); } } if (!context.Request.Headers.TryGetValue(HeadersField, out StringValues passwords) && !context.Request.Query.TryGetValue("password", out passwords)) { - return HttpStatusCode.Unauthorized; + return (HttpStatusCode.Unauthorized, true); } string? inputPassword = passwords.FirstOrDefault(password => !string.IsNullOrEmpty(password)); if (string.IsNullOrEmpty(inputPassword)) { - return HttpStatusCode.Unauthorized; + return (HttpStatusCode.Unauthorized, true); } ArchiCryptoHelper.EHashingMethod ipcPasswordFormat = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPasswordFormat : GlobalConfig.DefaultIPCPasswordFormat; @@ -151,7 +165,7 @@ namespace ArchiSteamFarm.IPC.Integration { try { if (FailedAuthorizations.TryGetValue(clientIP, out attempts)) { if (attempts >= MaxFailedAuthorizationAttempts) { - return HttpStatusCode.Forbidden; + return (HttpStatusCode.Forbidden, false); } } @@ -162,7 +176,7 @@ namespace ArchiSteamFarm.IPC.Integration { AuthorizationSemaphore.Release(); } - return authorized ? HttpStatusCode.OK : HttpStatusCode.Unauthorized; + return (authorized ? HttpStatusCode.OK : HttpStatusCode.Unauthorized, true); } } } diff --git a/ArchiSteamFarm/IPC/Integration/EnumSchemaFilter.cs b/ArchiSteamFarm/IPC/Integration/EnumSchemaFilter.cs index e22f40b13..d3c3533d7 100644 --- a/ArchiSteamFarm/IPC/Integration/EnumSchemaFilter.cs +++ b/ArchiSteamFarm/IPC/Integration/EnumSchemaFilter.cs @@ -65,6 +65,11 @@ namespace ArchiSteamFarm.IPC.Integration { } } + if (definition.ContainsKey(enumName)) { + // This is possible if we have multiple names for the same enum value, we'll ignore additional ones + continue; + } + IOpenApiAny enumObject; if (TryCast(enumValue, out int intValue)) { diff --git a/ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs b/ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs new file mode 100644 index 000000000..2738b713c --- /dev/null +++ b/ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs @@ -0,0 +1,47 @@ +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// | +// Copyright 2015-2021 Ɓ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.ComponentModel.DataAnnotations; +using System.Net; +using Newtonsoft.Json; + +namespace ArchiSteamFarm.IPC.Responses { + public sealed class StatusCodeResponse { + /// + /// Value indicating whether the status is permanent. If yes, retrying the request with exactly the same payload doesn't make sense due to a permanent problem (e.g. ASF misconfiguration). + /// + [JsonProperty(Required = Required.Always)] + [Required] + public bool Permanent { get; private set; } + + /// + /// Status code transmitted in addition to the one in HTTP spec. + /// + [JsonProperty(Required = Required.Always)] + [Required] + public HttpStatusCode StatusCode { get; private set; } + + internal StatusCodeResponse(HttpStatusCode statusCode, bool permanent) { + StatusCode = statusCode; + Permanent = permanent; + } + } +} diff --git a/ArchiSteamFarm/IPC/WebUtilities.cs b/ArchiSteamFarm/IPC/WebUtilities.cs index 6e9b06f4d..cb5e29373 100644 --- a/ArchiSteamFarm/IPC/WebUtilities.cs +++ b/ArchiSteamFarm/IPC/WebUtilities.cs @@ -23,24 +23,15 @@ using ArchiSteamFarm.Compatibility; #endif using System; +using System.IO; using System.Linq; -using System.Net; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; namespace ArchiSteamFarm.IPC { internal static class WebUtilities { - internal static async Task Generate(this HttpResponse httpResponse, HttpStatusCode statusCode) { - if (httpResponse == null) { - throw new ArgumentNullException(nameof(httpResponse)); - } - - ushort statusCodeNumber = (ushort) statusCode; - - httpResponse.StatusCode = statusCodeNumber; - await httpResponse.WriteAsync(statusCodeNumber + " - " + statusCode).ConfigureAwait(false); - } - internal static string? GetUnifiedName(this Type type) { if (type == null) { throw new ArgumentNullException(nameof(type)); @@ -69,5 +60,25 @@ namespace ArchiSteamFarm.IPC { return Type.GetType(typeText + "," + typeText[..index]); } + + internal static async Task WriteJsonAsync(this HttpResponse response, TValue? value, JsonSerializerSettings? jsonSerializerSettings = null) { + if (response == null) { + throw new ArgumentNullException(nameof(response)); + } + + response.ContentType = "application/json; charset=utf-8"; + + StreamWriter streamWriter = new(response.Body, Encoding.UTF8); + + await using (streamWriter.ConfigureAwait(false)) { + using JsonTextWriter jsonWriter = new(streamWriter); + + JsonSerializer serializer = JsonSerializer.CreateDefault(jsonSerializerSettings); + + serializer.Serialize(jsonWriter, value); + + await jsonWriter.FlushAsync().ConfigureAwait(false); + } + } } }