From 32e5aee9bfc72c76978ba357797d9a91da70e83a Mon Sep 17 00:00:00 2001 From: solidDoWant Date: Fri, 3 Oct 2025 07:27:32 +0000 Subject: [PATCH] Fix: incompatibility with reverse proxy forward auth providers Signed-off-by: solidDoWant --- frontend/src/Utilities/createAjaxRequest.js | 1 + src/NzbDrone.Common/Options/ServerOptions.cs | 1 + .../Configuration/ConfigFileProvider.cs | 3 + src/NzbDrone.Host/Startup.cs | 60 ++++++++++++++++++- 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/frontend/src/Utilities/createAjaxRequest.js b/frontend/src/Utilities/createAjaxRequest.js index 6715833a70..4a5a2f9dda 100644 --- a/frontend/src/Utilities/createAjaxRequest.js +++ b/frontend/src/Utilities/createAjaxRequest.js @@ -27,6 +27,7 @@ function addContentType(ajaxOptions) { export default function createAjaxRequest(originalAjaxOptions) { const requestXHR = new window.XMLHttpRequest(); + requestXHR.withCredentials = true; // Needed for CORS requests with cookies, which some reverse proxies with forward auth require let aborted = false; let complete = false; diff --git a/src/NzbDrone.Common/Options/ServerOptions.cs b/src/NzbDrone.Common/Options/ServerOptions.cs index d21e12b2a2..8bf54a40ca 100644 --- a/src/NzbDrone.Common/Options/ServerOptions.cs +++ b/src/NzbDrone.Common/Options/ServerOptions.cs @@ -4,6 +4,7 @@ public class ServerOptions { public string UrlBase { get; set; } public string BindAddress { get; set; } + public string AllowedCORSOrigins { get; set; } // TODO public int? Port { get; set; } public bool? EnableSsl { get; set; } public int? SslPort { get; set; } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 1f88db6281..2b36483832 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -31,6 +31,7 @@ public interface IConfigFileProvider : IHandleAsync, void EnsureDefaultConfigFile(); string BindAddress { get; } + string AllowedCORSOrigins { get; } int Port { get; } int SslPort { get; } bool EnableSsl { get; } @@ -174,6 +175,8 @@ public string BindAddress } } + public string AllowedCORSOrigins => _serverOptions.AllowedCORSOrigins ?? GetValue("AllowedCORSOrigins", "*"); + public int Port => _serverOptions.Port ?? GetValueInt("Port", 7878); public int SslPort => _serverOptions.SslPort ?? GetValueInt("SslPort", 9898); diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs index 265ba58bd5..40d779d071 100644 --- a/src/NzbDrone.Host/Startup.cs +++ b/src/NzbDrone.Host/Startup.cs @@ -4,6 +4,7 @@ using DryIoc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; @@ -11,6 +12,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using NLog.Extensions.Logging; using NzbDrone.Common.EnvironmentInfo; @@ -70,15 +72,18 @@ public void ConfigureServices(IServiceCollection services) services.AddCors(options => { + // Origin policy will be added after configuration is complete, because it depends on config file values options.AddPolicy(VersionedApiControllerAttribute.API_CORS_POLICY, builder => - builder.AllowAnyOrigin() + builder + .AllowCredentials() .AllowAnyMethod() .AllowAnyHeader()); options.AddPolicy("AllowGet", builder => - builder.AllowAnyOrigin() + builder + .AllowCredentials() .WithMethods("GET", "OPTIONS") .AllowAnyHeader()); }); @@ -194,6 +199,8 @@ public void ConfigureServices(IServiceCollection services) services.AddAppAuthentication(); + services.ConfigureOptions(); + services.PostConfigure(options => { var builtInFactory = options.InvalidModelStateResponseFactory; @@ -327,3 +334,52 @@ private void EnsureSingleInstance(bool isService, IStartupContext startupContext } } } + +public class CORSOriginConfigurator : IPostConfigureOptions +{ + private readonly IConfigFileProvider _configFileProvider; + private readonly NLog.Logger _logger; + + public CORSOriginConfigurator(IConfigFileProvider configFileProvider, NLog.Logger logger) + { + _configFileProvider = configFileProvider; + _logger = logger; + } + + public void PostConfigure(string name, CorsOptions options) => PostConfigure(options); + + public void PostConfigure(CorsOptions options) + { + _logger.Info("Configuring CORS. Allowed origins: {0}", _configFileProvider.AllowedCORSOrigins); + options.GetPolicy(VersionedApiControllerAttribute.API_CORS_POLICY).IsOriginAllowed = CorsOriginCheck; + options.GetPolicy("AllowGet").IsOriginAllowed = CorsOriginCheck; + } + + protected bool CorsOriginCheck(string requestOrigin) + { + var allowedOrigin = _configFileProvider.AllowedCORSOrigins; + if (allowedOrigin.Equals(CorsConstants.AnyOrigin, StringComparison.Ordinal)) + { + return true; + } + + if (string.IsNullOrWhiteSpace(requestOrigin)) + { + return false; + } + + var allowedOriginAsUri = new Uri(allowedOrigin); + Uri requestOriginAsUri; + try + { + requestOriginAsUri = new Uri(requestOrigin); + } + catch (UriFormatException) + { + return false; + } + + // Compare the scheme and authority (host + port) of the two URIs, according to RFC 6454 + return Uri.Compare(allowedOriginAsUri, requestOriginAsUri, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0; + } +}