From 3c0484cc9730c06892b996d0b884a05ecada07af Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 30 Aug 2020 09:32:14 -0600 Subject: [PATCH 1/6] Allow for dynamic cors response --- .../ApiServiceCollectionExtensions.cs | 8 ++- .../Middleware/DynamicCorsMiddleware.cs | 68 +++++++++++++++++++ Jellyfin.Server/Models/ServerCorsPolicy.cs | 43 ++++++++---- Jellyfin.Server/Startup.cs | 8 ++- .../Configuration/ServerConfiguration.cs | 6 ++ 5 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 0fd599cfcd..b2f861542a 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -135,13 +135,17 @@ namespace Jellyfin.Server.Extensions /// /// The service collection. /// The base url for the API. + /// The configured cors hosts. /// The MVC builder. - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl) + public static IMvcBuilder AddJellyfinApi( + this IServiceCollection serviceCollection, + string baseUrl, + string[] corsHosts) { return serviceCollection .AddCors(options => { - options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy); + options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, new ServerCorsPolicy(corsHosts).Policy); }) .Configure(options => { diff --git a/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs b/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs new file mode 100644 index 0000000000..4fad898a73 --- /dev/null +++ b/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Dynamic cors middleware. + /// + public class DynamicCorsMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly CorsMiddleware _corsMiddleware; + + /// + /// Initializes a new instance of the class. + /// + /// Next request delegate. + /// Instance of the interface. + /// Instance of the interface. + /// The cors policy name. + public DynamicCorsMiddleware( + RequestDelegate next, + ICorsService corsService, + ILoggerFactory loggerFactory, + string policyName) + { + _corsMiddleware = new CorsMiddleware(next, corsService, loggerFactory, policyName); + _next = next; + _logger = loggerFactory.CreateLogger(); + } + + /// + /// Invoke request. + /// + /// Request context. + /// Instance of the interface. + /// Task. + /// + public async Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider) + { + // Only execute if is preflight request. + if (string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase)) + { + // Invoke original cors middleware. + await _corsMiddleware.Invoke(context, corsPolicyProvider).ConfigureAwait(false); + if (context.Response.Headers.TryGetValue(HeaderNames.AccessControlAllowOrigin, out var headerValue) + && string.Equals(headerValue, "*", StringComparison.Ordinal)) + { + context.Response.Headers[HeaderNames.AccessControlAllowOrigin] = context.Request.Host.Value; + _logger.LogDebug("Overwriting CORS response header: {HeaderName}: {HeaderValue}", HeaderNames.AccessControlAllowOrigin, context.Request.Host.Value); + + if (!context.Response.Headers.ContainsKey(HeaderNames.AccessControlAllowCredentials)) + { + context.Response.Headers[HeaderNames.AccessControlAllowCredentials] = "true"; + } + } + } + + // Call the next delegate/middleware in the pipeline + await this._next(context).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Models/ServerCorsPolicy.cs b/Jellyfin.Server/Models/ServerCorsPolicy.cs index ae010c042e..3a45db3b44 100644 --- a/Jellyfin.Server/Models/ServerCorsPolicy.cs +++ b/Jellyfin.Server/Models/ServerCorsPolicy.cs @@ -1,30 +1,47 @@ -using Microsoft.AspNetCore.Cors.Infrastructure; +using System; +using Microsoft.AspNetCore.Cors.Infrastructure; namespace Jellyfin.Server.Models { /// /// Server Cors Policy. /// - public static class ServerCorsPolicy + public class ServerCorsPolicy { /// /// Default policy name. /// - public const string DefaultPolicyName = "DefaultCorsPolicy"; + public const string DefaultPolicyName = nameof(ServerCorsPolicy); /// - /// Default Policy. Allow Everything. + /// Initializes a new instance of the class. /// - public static readonly CorsPolicy DefaultPolicy = new CorsPolicy + /// The configured cors hosts. + public ServerCorsPolicy(string[] corsHosts) { - // Allow any origin - Origins = { "*" }, + var builder = new CorsPolicyBuilder() + .AllowAnyMethod() + .AllowAnyHeader(); - // Allow any method - Methods = { "*" }, + // No hosts configured or only default configured. + if (corsHosts.Length == 0 + || (corsHosts.Length == 1 + && string.Equals(corsHosts[0], "*", StringComparison.Ordinal))) + { + builder.AllowAnyOrigin(); + } + else + { + builder.WithOrigins(corsHosts) + .AllowCredentials(); + } - // Allow any header - Headers = { "*" } - }; + Policy = builder.Build(); + } + + /// + /// Gets the cors policy. + /// + public CorsPolicy Policy { get; } } -} \ No newline at end of file +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index d0dd183c68..76f5e69ced 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -38,7 +38,9 @@ namespace Jellyfin.Server { services.AddResponseCompression(); services.AddHttpContextAccessor(); - services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/')); + services.AddJellyfinApi( + _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), + _serverConfigurationManager.Configuration.CorsHosts); services.AddJellyfinApiSwagger(); @@ -78,11 +80,11 @@ namespace Jellyfin.Server app.UseAuthentication(); app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); - app.UseCors(ServerCorsPolicy.DefaultPolicyName); + app.UseMiddleware(ServerCorsPolicy.DefaultPolicyName); app.UseAuthorization(); if (_serverConfigurationManager.Configuration.EnableMetrics) { - // Must be registered after any middleware that could chagne HTTP response codes or the data will be bad + // Must be registered after any middleware that could change HTTP response codes or the data will be bad app.UseHttpMetrics(); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index c66091f9d5..a743277d7a 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -264,6 +264,11 @@ namespace MediaBrowser.Model.Configuration /// public long SlowResponseThresholdMs { get; set; } + /// + /// Gets or sets the cors hosts. + /// + public string[] CorsHosts { get; set; } + /// /// Initializes a new instance of the class. /// @@ -372,6 +377,7 @@ namespace MediaBrowser.Model.Configuration EnableSlowResponseWarning = true; SlowResponseThresholdMs = 500; + CorsHosts = new[] { "*" }; } } From eba0d9e387dc0375628acfd81957ff180c82d8df Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 30 Aug 2020 10:05:21 -0600 Subject: [PATCH 2/6] Always allow set credentials header --- Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs b/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs index 4fad898a73..c4c491cdd8 100644 --- a/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs +++ b/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs @@ -52,12 +52,10 @@ namespace Jellyfin.Server.Middleware && string.Equals(headerValue, "*", StringComparison.Ordinal)) { context.Response.Headers[HeaderNames.AccessControlAllowOrigin] = context.Request.Host.Value; - _logger.LogDebug("Overwriting CORS response header: {HeaderName}: {HeaderValue}", HeaderNames.AccessControlAllowOrigin, context.Request.Host.Value); - if (!context.Response.Headers.ContainsKey(HeaderNames.AccessControlAllowCredentials)) - { - context.Response.Headers[HeaderNames.AccessControlAllowCredentials] = "true"; - } + // Always allow credentials. + context.Response.Headers[HeaderNames.AccessControlAllowCredentials] = "true"; + _logger.LogDebug("Overwriting CORS response header: {HeaderName}: {HeaderValue}", HeaderNames.AccessControlAllowOrigin, context.Request.Host.Value); } } From e97ccd87fb74c34222eccf03493a56144065eaa4 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 31 Aug 2020 07:21:07 -0600 Subject: [PATCH 3/6] Remove DynamicCorsMiddleware --- .../Middleware/DynamicCorsMiddleware.cs | 66 ------------------- Jellyfin.Server/Startup.cs | 2 +- 2 files changed, 1 insertion(+), 67 deletions(-) delete mode 100644 Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs diff --git a/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs b/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs deleted file mode 100644 index c4c491cdd8..0000000000 --- a/Jellyfin.Server/Middleware/DynamicCorsMiddleware.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Jellyfin.Server.Middleware -{ - /// - /// Dynamic cors middleware. - /// - public class DynamicCorsMiddleware - { - private readonly RequestDelegate _next; - private readonly ILogger _logger; - private readonly CorsMiddleware _corsMiddleware; - - /// - /// Initializes a new instance of the class. - /// - /// Next request delegate. - /// Instance of the interface. - /// Instance of the interface. - /// The cors policy name. - public DynamicCorsMiddleware( - RequestDelegate next, - ICorsService corsService, - ILoggerFactory loggerFactory, - string policyName) - { - _corsMiddleware = new CorsMiddleware(next, corsService, loggerFactory, policyName); - _next = next; - _logger = loggerFactory.CreateLogger(); - } - - /// - /// Invoke request. - /// - /// Request context. - /// Instance of the interface. - /// Task. - /// - public async Task Invoke(HttpContext context, ICorsPolicyProvider corsPolicyProvider) - { - // Only execute if is preflight request. - if (string.Equals(context.Request.Method, CorsConstants.PreflightHttpMethod, StringComparison.OrdinalIgnoreCase)) - { - // Invoke original cors middleware. - await _corsMiddleware.Invoke(context, corsPolicyProvider).ConfigureAwait(false); - if (context.Response.Headers.TryGetValue(HeaderNames.AccessControlAllowOrigin, out var headerValue) - && string.Equals(headerValue, "*", StringComparison.Ordinal)) - { - context.Response.Headers[HeaderNames.AccessControlAllowOrigin] = context.Request.Host.Value; - - // Always allow credentials. - context.Response.Headers[HeaderNames.AccessControlAllowCredentials] = "true"; - _logger.LogDebug("Overwriting CORS response header: {HeaderName}: {HeaderValue}", HeaderNames.AccessControlAllowOrigin, context.Request.Host.Value); - } - } - - // Call the next delegate/middleware in the pipeline - await this._next(context).ConfigureAwait(false); - } - } -} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 76f5e69ced..966587a6a0 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -80,7 +80,7 @@ namespace Jellyfin.Server app.UseAuthentication(); app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); - app.UseMiddleware(ServerCorsPolicy.DefaultPolicyName); + app.UseCors(ServerCorsPolicy.DefaultPolicyName); app.UseAuthorization(); if (_serverConfigurationManager.Configuration.EnableMetrics) { From 2c05d53b06855eab76012ba70da00ec62af787c3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 5 Sep 2020 09:10:05 -0600 Subject: [PATCH 4/6] Convert to ICorsPolicyProvider --- .../ApiServiceCollectionExtensions.cs | 14 +++--- .../Middleware/CorsPolicyProvider.cs | 48 +++++++++++++++++-- Jellyfin.Server/Models/ServerCorsPolicy.cs | 47 ------------------ Jellyfin.Server/Startup.cs | 7 +-- 4 files changed, 53 insertions(+), 63 deletions(-) delete mode 100644 Jellyfin.Server/Models/ServerCorsPolicy.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 8dcce93a45..65db331555 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -16,12 +16,13 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; -using Jellyfin.Server.Models; +using Jellyfin.Server.Middleware; using MediaBrowser.Common.Json; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; @@ -134,18 +135,15 @@ namespace Jellyfin.Server.Extensions /// /// The service collection. /// An IEnumerable containing all plugin assemblies with API controllers. - /// /// The configured cors hosts. + /// /// /// The MVC builder. public static IMvcBuilder AddJellyfinApi( this IServiceCollection serviceCollection, - IEnumerable pluginAssemblies, - string[] corsHosts) + IEnumerable pluginAssemblies) { IMvcBuilder mvcBuilder = serviceCollection - .AddCors(options => - { - options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, new ServerCorsPolicy(corsHosts).Policy); - }) + .AddCors() + .AddTransient() .Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; diff --git a/Jellyfin.Server/Middleware/CorsPolicyProvider.cs b/Jellyfin.Server/Middleware/CorsPolicyProvider.cs index 7c2b28ed8f..02178e29c7 100644 --- a/Jellyfin.Server/Middleware/CorsPolicyProvider.cs +++ b/Jellyfin.Server/Middleware/CorsPolicyProvider.cs @@ -1,7 +1,49 @@ -namespace Jellyfin.Server.Middleware +using System; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware { - public class CorsPolicyProvider + /// + /// Cors policy provider. + /// + public class CorsPolicyProvider : ICorsPolicyProvider { - + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public CorsPolicyProvider(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + /// + public Task GetPolicyAsync(HttpContext context, string policyName) + { + var corsHosts = _serverConfigurationManager.Configuration.CorsHosts; + var builder = new CorsPolicyBuilder() + .AllowAnyMethod() + .AllowAnyHeader(); + + // No hosts configured or only default configured. + if (corsHosts.Length == 0 + || (corsHosts.Length == 1 + && string.Equals(corsHosts[0], CorsConstants.AnyOrigin, StringComparison.Ordinal))) + { + builder.AllowAnyOrigin(); + } + else + { + builder.WithOrigins(corsHosts) + .AllowCredentials(); + } + + return Task.FromResult(builder.Build()); + } } } diff --git a/Jellyfin.Server/Models/ServerCorsPolicy.cs b/Jellyfin.Server/Models/ServerCorsPolicy.cs deleted file mode 100644 index 3a45db3b44..0000000000 --- a/Jellyfin.Server/Models/ServerCorsPolicy.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Microsoft.AspNetCore.Cors.Infrastructure; - -namespace Jellyfin.Server.Models -{ - /// - /// Server Cors Policy. - /// - public class ServerCorsPolicy - { - /// - /// Default policy name. - /// - public const string DefaultPolicyName = nameof(ServerCorsPolicy); - - /// - /// Initializes a new instance of the class. - /// - /// The configured cors hosts. - public ServerCorsPolicy(string[] corsHosts) - { - var builder = new CorsPolicyBuilder() - .AllowAnyMethod() - .AllowAnyHeader(); - - // No hosts configured or only default configured. - if (corsHosts.Length == 0 - || (corsHosts.Length == 1 - && string.Equals(corsHosts[0], "*", StringComparison.Ordinal))) - { - builder.AllowAnyOrigin(); - } - else - { - builder.WithOrigins(corsHosts) - .AllowCredentials(); - } - - Policy = builder.Build(); - } - - /// - /// Gets the cors policy. - /// - public CorsPolicy Policy { get; } - } -} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 5601915a33..16629b5d95 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -5,7 +5,6 @@ using Jellyfin.Api.TypeConverters; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using Jellyfin.Server.Middleware; -using Jellyfin.Server.Models; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; @@ -53,9 +52,7 @@ namespace Jellyfin.Server { options.HttpsPort = _serverApplicationHost.HttpsPort; }); - services.AddJellyfinApi( - _serverApplicationHost.GetApiPluginAssemblies(), - _serverConfigurationManager.Configuration.CorsHosts); + services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies()); services.AddJellyfinApiSwagger(); @@ -118,7 +115,7 @@ namespace Jellyfin.Server mainApp.UseResponseCompression(); - mainApp.UseCors(ServerCorsPolicy.DefaultPolicyName); + mainApp.UseCors(); if (_serverConfigurationManager.Configuration.RequireHttps && _serverApplicationHost.ListenWithHttps) From 527ffaa90c5f041474b24f68735ba7e24e1b8358 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 5 Sep 2020 09:12:50 -0600 Subject: [PATCH 5/6] clean docs --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 65db331555..ce9e245588 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -135,11 +135,8 @@ namespace Jellyfin.Server.Extensions /// /// The service collection. /// An IEnumerable containing all plugin assemblies with API controllers. - /// /// /// The MVC builder. - public static IMvcBuilder AddJellyfinApi( - this IServiceCollection serviceCollection, - IEnumerable pluginAssemblies) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies) { IMvcBuilder mvcBuilder = serviceCollection .AddCors() From 342de39d78431503a0429b76e0ba9d3501b746db Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 5 Sep 2020 13:02:53 -0600 Subject: [PATCH 6/6] Move CorsPolicyProvider to Jellyfin.Server.Configuration --- .../{Middleware => Configuration}/CorsPolicyProvider.cs | 2 +- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename Jellyfin.Server/{Middleware => Configuration}/CorsPolicyProvider.cs (97%) diff --git a/Jellyfin.Server/Middleware/CorsPolicyProvider.cs b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs similarity index 97% rename from Jellyfin.Server/Middleware/CorsPolicyProvider.cs rename to Jellyfin.Server/Configuration/CorsPolicyProvider.cs index 02178e29c7..0d04b6bb13 100644 --- a/Jellyfin.Server/Middleware/CorsPolicyProvider.cs +++ b/Jellyfin.Server/Configuration/CorsPolicyProvider.cs @@ -4,7 +4,7 @@ using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Http; -namespace Jellyfin.Server.Middleware +namespace Jellyfin.Server.Configuration { /// /// Cors policy provider. diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index ce9e245588..9319b573a4 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -15,6 +15,7 @@ using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Server.Configuration; using Jellyfin.Server.Formatters; using Jellyfin.Server.Middleware; using MediaBrowser.Common.Json;