From c60b0381b86a9da4e2c2280e01c8d49066878cf2 Mon Sep 17 00:00:00 2001 From: Archi Date: Sat, 10 Jul 2021 18:23:09 +0200 Subject: [PATCH 1/5] Closes #2371 --- .../ApiAuthenticationMiddleware.cs | 16 ++++++----- ArchiSteamFarm/IPC/Startup.cs | 27 ++++++++++++------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs index c994cbc8977bf..128727c27821d 100644 --- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs +++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs @@ -87,18 +87,22 @@ private static async Task GetAuthenticationStatus(HttpContext co 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) || Startup.KnownNetworks.Any(network => network.Contains(clientIP))) { + return HttpStatusCode.OK; + } + + return 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 0f377183cc8e5..1ab43a02d87eb 100644 --- a/ArchiSteamFarm/IPC/Startup.cs +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -29,6 +29,7 @@ #endif using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Net; using System.Reflection; @@ -53,6 +54,8 @@ namespace ArchiSteamFarm.IPC { internal sealed class Startup { + internal static ImmutableHashSet KnownNetworks { get; private set; } = ImmutableHashSet.Empty; + private readonly IConfiguration Configuration; public Startup(IConfiguration configuration) => Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); @@ -148,12 +151,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { 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(); @@ -194,11 +197,10 @@ public void ConfigureServices(IServiceCollection services) { // Prepare knownNetworks that we'll use in a second HashSet? knownNetworksTexts = Configuration.GetSection("Kestrel:KnownNetworks").Get>(); - HashSet? knownNetworks = null; + HashSet knownNetworks = new(); if (knownNetworksTexts?.Count > 0) { - knownNetworks = new HashSet(knownNetworksTexts.Count); - + // Use specified known networks foreach (string knownNetworkText in knownNetworksTexts) { string[] addressParts = knownNetworkText.Split('/', StringSplitOptions.RemoveEmptyEntries); @@ -211,17 +213,22 @@ public void ConfigureServices(IServiceCollection services) { knownNetworks.Add(new IPNetwork(ipAddress, prefixLength)); } + } else { + // Use private address space networks by default, https://datatracker.ietf.org/doc/html/rfc1918#section-3 + knownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); + knownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); + knownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); } + KnownNetworks = knownNetworks.ToImmutableHashSet(); + // Add support for proxies services.Configure( options => { options.ForwardedHeaders = ForwardedHeaders.All; - if (knownNetworks != null) { - foreach (IPNetwork knownNetwork in knownNetworks) { - options.KnownNetworks.Add(knownNetwork); - } + foreach (IPNetwork knownNetwork in KnownNetworks) { + options.KnownNetworks.Add(knownNetwork); } } ); From 86f4ea7d0c5af6e82faadca380e9bf2766a75177 Mon Sep 17 00:00:00 2001 From: Archi Date: Sun, 11 Jul 2021 14:57:50 +0200 Subject: [PATCH 2/5] Change the default to no known networks --- ArchiSteamFarm/IPC/Startup.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/ArchiSteamFarm/IPC/Startup.cs b/ArchiSteamFarm/IPC/Startup.cs index 1ab43a02d87eb..45dd1af6f2ab5 100644 --- a/ArchiSteamFarm/IPC/Startup.cs +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -197,10 +197,10 @@ public void ConfigureServices(IServiceCollection services) { // Prepare knownNetworks that we'll use in a second HashSet? knownNetworksTexts = Configuration.GetSection("Kestrel:KnownNetworks").Get>(); - HashSet knownNetworks = new(); - if (knownNetworksTexts?.Count > 0) { // Use specified known networks + HashSet knownNetworks = new(); + foreach (string knownNetworkText in knownNetworksTexts) { string[] addressParts = knownNetworkText.Split('/', StringSplitOptions.RemoveEmptyEntries); @@ -213,14 +213,9 @@ public void ConfigureServices(IServiceCollection services) { knownNetworks.Add(new IPNetwork(ipAddress, prefixLength)); } - } else { - // Use private address space networks by default, https://datatracker.ietf.org/doc/html/rfc1918#section-3 - knownNetworks.Add(new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)); - knownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)); - knownNetworks.Add(new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)); - } - KnownNetworks = knownNetworks.ToImmutableHashSet(); + KnownNetworks = knownNetworks.ToImmutableHashSet(); + } // Add support for proxies services.Configure( From 282828e81960aa46727bc1382cd333638e410501 Mon Sep 17 00:00:00 2001 From: Archi Date: Mon, 12 Jul 2021 12:17:14 +0200 Subject: [PATCH 3/5] Address @Vital7 note --- .../IPC/Integration/ApiAuthenticationMiddleware.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs index 128727c27821d..ee0a9e5a8c6b1 100644 --- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs +++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs @@ -96,11 +96,17 @@ private static async Task GetAuthenticationStatus(HttpContext co string? ipcPassword = ASF.GlobalConfig != null ? ASF.GlobalConfig.IPCPassword : GlobalConfig.DefaultIPCPassword; if (string.IsNullOrEmpty(ipcPassword)) { - if (IPAddress.IsLoopback(clientIP) || Startup.KnownNetworks.Any(network => network.Contains(clientIP))) { + if (IPAddress.IsLoopback(clientIP)) { return HttpStatusCode.OK; } - return HttpStatusCode.Forbidden; + if (Startup.KnownNetworks.IsEmpty) { + return HttpStatusCode.Forbidden; + } + + IPAddress mappedClientIP = clientIP.IsIPv4MappedToIPv6 ? clientIP.MapToIPv4() : clientIP; + + return Startup.KnownNetworks.Any(network => network.Contains(mappedClientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; } if (FailedAuthorizations.TryGetValue(clientIP, out byte attempts)) { From 9aea006b4265618cc0b63ac46618807ed629ad93 Mon Sep 17 00:00:00 2001 From: Archi Date: Mon, 12 Jul 2021 12:26:52 +0200 Subject: [PATCH 4/5] Handle both IPv4 and IPv6 when mapped This follows ASP.NET Core logic --- .../IPC/Integration/ApiAuthenticationMiddleware.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs index ee0a9e5a8c6b1..04e5f6784327a 100644 --- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs +++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs @@ -104,9 +104,15 @@ private static async Task GetAuthenticationStatus(HttpContext co return HttpStatusCode.Forbidden; } - IPAddress mappedClientIP = clientIP.IsIPv4MappedToIPv6 ? clientIP.MapToIPv4() : clientIP; + if (clientIP.IsIPv4MappedToIPv6) { + IPAddress mappedClientIP = clientIP.MapToIPv4(); - return Startup.KnownNetworks.Any(network => network.Contains(mappedClientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; + if (Startup.KnownNetworks.Any(network => network.Contains(mappedClientIP))) { + return HttpStatusCode.OK; + } + } + + return Startup.KnownNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; } if (FailedAuthorizations.TryGetValue(clientIP, out byte attempts)) { From 75078946f52d452cb2a984ac10bf5522aee1e3ad Mon Sep 17 00:00:00 2001 From: Archi Date: Mon, 12 Jul 2021 12:59:56 +0200 Subject: [PATCH 5/5] Refactor forwarded headers usage --- .../ApiAuthenticationMiddleware.cs | 19 ++++++++++++++----- ArchiSteamFarm/IPC/Startup.cs | 15 +++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs index 04e5f6784327a..205e898ac26b8 100644 --- a/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs +++ b/ArchiSteamFarm/IPC/Integration/ApiAuthenticationMiddleware.cs @@ -30,7 +30,9 @@ 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 @@ internal sealed class ApiAuthenticationMiddleware { 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 @@ public async Task InvokeAsync(HttpContext context) { 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)); } @@ -100,19 +109,19 @@ private static async Task GetAuthenticationStatus(HttpContext co return HttpStatusCode.OK; } - if (Startup.KnownNetworks.IsEmpty) { + if (ForwardedHeadersOptions.KnownNetworks.Count == 0) { return HttpStatusCode.Forbidden; } if (clientIP.IsIPv4MappedToIPv6) { IPAddress mappedClientIP = clientIP.MapToIPv4(); - if (Startup.KnownNetworks.Any(network => network.Contains(mappedClientIP))) { + if (ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(mappedClientIP))) { return HttpStatusCode.OK; } } - return Startup.KnownNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; + return ForwardedHeadersOptions.KnownNetworks.Any(network => network.Contains(clientIP)) ? HttpStatusCode.OK : HttpStatusCode.Forbidden; } if (FailedAuthorizations.TryGetValue(clientIP, out byte attempts)) { diff --git a/ArchiSteamFarm/IPC/Startup.cs b/ArchiSteamFarm/IPC/Startup.cs index 45dd1af6f2ab5..c918e8bc7c746 100644 --- a/ArchiSteamFarm/IPC/Startup.cs +++ b/ArchiSteamFarm/IPC/Startup.cs @@ -29,7 +29,6 @@ #endif using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Globalization; using System.Net; using System.Reflection; @@ -54,8 +53,6 @@ namespace ArchiSteamFarm.IPC { internal sealed class Startup { - internal static ImmutableHashSet KnownNetworks { get; private set; } = ImmutableHashSet.Empty; - private readonly IConfiguration Configuration; public Startup(IConfiguration configuration) => Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); @@ -197,9 +194,11 @@ public void ConfigureServices(IServiceCollection services) { // Prepare knownNetworks that we'll use in a second HashSet? knownNetworksTexts = Configuration.GetSection("Kestrel:KnownNetworks").Get>(); + HashSet? knownNetworks = null; + if (knownNetworksTexts?.Count > 0) { // Use specified known networks - HashSet knownNetworks = new(); + knownNetworks = new HashSet(); foreach (string knownNetworkText in knownNetworksTexts) { string[] addressParts = knownNetworkText.Split('/', StringSplitOptions.RemoveEmptyEntries); @@ -213,8 +212,6 @@ public void ConfigureServices(IServiceCollection services) { knownNetworks.Add(new IPNetwork(ipAddress, prefixLength)); } - - KnownNetworks = knownNetworks.ToImmutableHashSet(); } // Add support for proxies @@ -222,8 +219,10 @@ public void ConfigureServices(IServiceCollection services) { options => { options.ForwardedHeaders = ForwardedHeaders.All; - foreach (IPNetwork knownNetwork in KnownNetworks) { - options.KnownNetworks.Add(knownNetwork); + if (knownNetworks != null) { + foreach (IPNetwork knownNetwork in knownNetworks) { + options.KnownNetworks.Add(knownNetwork); + } } } );