mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-01 22:20:52 +00:00
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:
@@ -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 { }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
47
ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs
Normal file
47
ArchiSteamFarm/IPC/Responses/StatusCodeResponse.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user