Add support for supplying additional information on 401/403 status codes

In particular permanent: true/false value indicating whether 403 comes from rate limiting or ASF block due to lack of IPCPassword
This commit is contained in:
Archi
2021-07-20 14:32:53 +02:00
parent 37326c7ab6
commit 1b3ef7a41d
5 changed files with 106 additions and 29 deletions

View File

@@ -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<StatusCodeResponse>))]
[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<StatusCodeResponse>))]
[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 { }

View File

@@ -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<MvcJsonOptions> jsonOptions) {
#else
public async Task InvokeAsync(HttpContext context, IOptions<MvcNewtonsoftJsonOptions> 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<StatusCodeResponse>(false, statusCodeResponse), jsonOptions.Value.SerializerSettings).ConfigureAwait(false);
}
private async Task<HttpStatusCode> 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);
}
}
}

View File

@@ -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)) {

View File

@@ -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 {
/// <summary>
/// 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).
/// </summary>
[JsonProperty(Required = Required.Always)]
[Required]
public bool Permanent { get; private set; }
/// <summary>
/// Status code transmitted in addition to the one in HTTP spec.
/// </summary>
[JsonProperty(Required = Required.Always)]
[Required]
public HttpStatusCode StatusCode { get; private set; }
internal StatusCodeResponse(HttpStatusCode statusCode, bool permanent) {
StatusCode = statusCode;
Permanent = permanent;
}
}
}

View File

@@ -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<TValue>(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);
}
}
}
}