diff --git a/ArchiSteamFarm/IPC/Controllers/Api/IPCController.cs b/ArchiSteamFarm/IPC/Controllers/Api/IPCController.cs
new file mode 100644
index 000000000..89e379152
--- /dev/null
+++ b/ArchiSteamFarm/IPC/Controllers/Api/IPCController.cs
@@ -0,0 +1,78 @@
+// _ _ _ ____ _ _____
+// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
+// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
+// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
+// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
+// |
+// Copyright 2015-2022 Ł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.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using ArchiSteamFarm.IPC.Integration;
+using ArchiSteamFarm.IPC.Responses;
+using ArchiSteamFarm.Localization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ArchiSteamFarm.IPC.Controllers.Api;
+
+[Route("Api/IPC")]
+public sealed class IPCController : ArchiController {
+ ///
+ /// Clears the list of all IP addresses currently blocked by ASFs IPC module
+ ///
+ [HttpDelete("Bans")]
+ [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
+ public ActionResult BansDelete() {
+ ApiAuthenticationMiddleware.ClearFailedAuthorizations();
+
+ return Ok(new GenericResponse(true));
+ }
+
+ ///
+ /// Removes an IP address from the list of addresses currently blocked by ASFs IPC module
+ ///
+ [HttpDelete("Bans/{ipAddress:required}")]
+ [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
+ public ActionResult BansDeleteSpecific(string ipAddress) {
+ if (string.IsNullOrEmpty(ipAddress)) {
+ throw new ArgumentNullException(nameof(ipAddress));
+ }
+
+ if (!IPAddress.TryParse(ipAddress, out IPAddress? remoteAddress)) {
+ return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, nameof(ipAddress))));
+ }
+
+ bool result = ApiAuthenticationMiddleware.UnbanIP(remoteAddress);
+
+ if (!result) {
+ return BadRequest(new GenericResponse(false, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIPNotBanned, ipAddress)));
+ }
+
+ return Ok(new GenericResponse(true));
+ }
+
+ ///
+ /// Gets all IP addresses currently blocked by ASFs IPC module
+ ///
+ [HttpGet("Bans")]
+ [ProducesResponseType(typeof(GenericResponse>), (int) HttpStatusCode.OK)]
+ public ActionResult>> BansGet() => Ok(new GenericResponse>(ApiAuthenticationMiddleware.GetCurrentlyBannedIPs().Select(static ip => ip.ToString()).ToHashSet()));
+}
diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs
index 4a8031afe..15dcdb937 100644
--- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs
+++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs
@@ -24,6 +24,7 @@ using MvcNewtonsoftJsonOptions = Microsoft.AspNetCore.Mvc.MvcJsonOptions;
#endif
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
@@ -88,7 +89,19 @@ internal sealed class ApiAuthenticationMiddleware {
await context.Response.WriteJsonAsync(new GenericResponse(false, statusCodeResponse), jsonOptions.Value.SerializerSettings).ConfigureAwait(false);
}
- private static void ClearFailedAuthorizations(object? state = null) => FailedAuthorizations.Clear();
+ internal static void ClearFailedAuthorizations(object? state = null) => FailedAuthorizations.Clear();
+
+ internal static HashSet GetCurrentlyBannedIPs() => FailedAuthorizations.Where(static kv => kv.Value >= MaxFailedAuthorizationAttempts).Select(static kv => kv.Key).ToHashSet();
+
+ internal static bool UnbanIP(IPAddress ipAddress) {
+ ArgumentNullException.ThrowIfNull(ipAddress);
+
+ if (!FailedAuthorizations.TryGetValue(ipAddress, out byte attempts) || (attempts < MaxFailedAuthorizationAttempts)) {
+ return false;
+ }
+
+ return FailedAuthorizations.TryRemove(ipAddress, out _);
+ }
private async Task<(HttpStatusCode StatusCode, bool Permanent)> GetAuthenticationStatus(HttpContext context) {
ArgumentNullException.ThrowIfNull(context);
diff --git a/ArchiSteamFarm/Localization/Strings.Designer.cs b/ArchiSteamFarm/Localization/Strings.Designer.cs
index b94098af3..bceea327e 100644
--- a/ArchiSteamFarm/Localization/Strings.Designer.cs
+++ b/ArchiSteamFarm/Localization/Strings.Designer.cs
@@ -981,6 +981,15 @@ namespace ArchiSteamFarm.Localization {
}
}
+ ///
+ /// Looks up a localized string similar to The IP address {0} is not banned!.
+ ///
+ public static string ErrorIPNotBanned {
+ get {
+ return ResourceManager.GetString("ErrorIPNotBanned", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to {0} is empty!.
///
diff --git a/ArchiSteamFarm/Localization/Strings.resx b/ArchiSteamFarm/Localization/Strings.resx
index 40290ea7d..fb52eb28f 100644
--- a/ArchiSteamFarm/Localization/Strings.resx
+++ b/ArchiSteamFarm/Localization/Strings.resx
@@ -740,4 +740,8 @@ Process uptime: {1}
Please enter your cryptkey:
Please note that this translation should end with space
+
+ The IP address {0} is not banned!
+ {0} will be replaced by an IP address which was requested to be unbanned from using IPC
+