diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs index c994cbc89..205e898ac 100644 --- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs +++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs @@ -30,7 +30,9 @@ using ArchiSteamFarm.Core; using ArchiSteamFarm.Helpers; using ArchiSteamFarm.Storage; using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace ArchiSteamFarm.IPC.Integration { @@ -46,11 +48,18 @@ namespace ArchiSteamFarm.IPC.Integration { private static Timer? ClearFailedAuthorizationsTimer; + private readonly ForwardedHeadersOptions ForwardedHeadersOptions; private readonly RequestDelegate Next; - public ApiAuthenticationMiddleware(RequestDelegate next) { + public ApiAuthenticationMiddleware(RequestDelegate next, IOptions forwardedHeadersOptions) { Next = next ?? throw new ArgumentNullException(nameof(next)); + if (forwardedHeadersOptions == null) { + throw new ArgumentNullException(nameof(forwardedHeadersOptions)); + } + + ForwardedHeadersOptions = forwardedHeadersOptions.Value ?? throw new InvalidOperationException(nameof(forwardedHeadersOptions)); + lock (FailedAuthorizations) { ClearFailedAuthorizationsTimer ??= new Timer( _ => FailedAuthorizations.Clear(), @@ -78,7 +87,7 @@ namespace ArchiSteamFarm.IPC.Integration { await Next(context).ConfigureAwait(false); } - private static async Task GetAuthenticationStatus(HttpContext context) { + private async Task GetAuthenticationStatus(HttpContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } @@ -87,18 +96,34 @@ namespace ArchiSteamFarm.IPC.Integration { throw new InvalidOperationException(nameof(ClearFailedAuthorizationsTimer)); } - string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword; - - if (string.IsNullOrEmpty(ipcPassword)) { - return HttpStatusCode.OK; - } - IPAddress? clientIP = context.Connection.RemoteIpAddress; if (clientIP == null) { throw new InvalidOperationException(nameof(clientIP)); } + string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword; + + if (string.IsNullOrEmpty(ipcPassword)) { + if (IPAddress.IsLoopback(clientIP)) { + return HttpStatusCode.OK; + } + + if (ForwardedHeadersOptions.KnownNetworks.Count == 0) { + return HttpStatusCode.Forbidden; + } + + if (clientIP.IsIPv4MappedToIPv6) { + IPAddress mappedClientIP = clientIP.MapToIPv4(); + + if (ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(mappedClientIP))) { + return HttpStatusCode.OK; + } + } + + return ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; + } + if (FailedAuthorizations.TryGetValue(clientIP, out byte attempts)) { if (attempts >= MaxFailedAuthorizationAttempts) { return HttpStatusCode.Forbidden; diff --git a/ArchiSteamFarm/IPC/Startup.cs b/ArchiSteamFarm/IPC/Startup.cs index 0f377183c..c918e8bc7 100644 --- a/ArchiSteamFarm/IPC/Startup.cs +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -148,12 +148,12 @@ namespace ArchiSteamFarm.IPC { app.UseRouting(); #endif + // We want to protect our API with IPCPassword and additional security, this should be called after routing, so the middleware won't have to deal with API endpoints that do not exist + app.UseWhen(context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), appBuilder => appBuilder.UseMiddleware()); + string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword; if (!string.IsNullOrEmpty(ipcPassword)) { - // We want to protect our API with IPCPassword, this should be called after routing, so the middleware won't have to deal with API endpoints that do not exist - app.UseWhen(context => context.Request.Path.StartsWithSegments("/Api", StringComparison.OrdinalIgnoreCase), appBuilder => appBuilder.UseMiddleware()); - // We want to apply CORS policy in order to allow userscripts and other third-party integrations to communicate with ASF API, this should be called before response compression, but can't be due to how our flow works // We apply CORS policy only with IPCPassword set as an extra authentication measure app.UseCors(); @@ -197,7 +197,8 @@ namespace ArchiSteamFarm.IPC { HashSet? knownNetworks = null; if (knownNetworksTexts?.Count > 0) { - knownNetworks = new HashSet(knownNetworksTexts.Count); + // Use specified known networks + knownNetworks = new HashSet(); foreach (string knownNetworkText in knownNetworksTexts) { string[] addressParts = knownNetworkText.Split('/', StringSplitOptions.RemoveEmptyEntries);