diff options
Diffstat (limited to 'Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs')
| -rw-r--r-- | Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 215 |
1 files changed, 167 insertions, 48 deletions
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 0fd599cfc..f19e87aba 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; +using System.Net.Sockets; using System.Reflection; -using Jellyfin.Api; +using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.DownloadPolicy; @@ -14,19 +16,29 @@ using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; +using Jellyfin.Api.Auth.SyncPlayAccessPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; +using Jellyfin.Extensions.Json; +using Jellyfin.Networking.Configuration; +using Jellyfin.Server.Configuration; +using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; -using Jellyfin.Server.Models; -using MediaBrowser.Common.Json; +using MediaBrowser.Common.Net; 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.Any; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; +using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; namespace Jellyfin.Server.Extensions { @@ -51,6 +63,7 @@ namespace Jellyfin.Server.Extensions serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>(); + serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>(); return serviceCollection.AddAuthorizationCore(options => { options.AddPolicy( @@ -116,6 +129,34 @@ namespace Jellyfin.Server.Extensions policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddRequirements(new RequiresElevationRequirement()); }); + options.AddPolicy( + Policies.SyncPlayHasAccess, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); + }); + options.AddPolicy( + Policies.SyncPlayCreateGroup, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); + }); + options.AddPolicy( + Policies.SyncPlayJoinGroup, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); + }); + options.AddPolicy( + Policies.SyncPlayIsInGroup, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); + }); }); } @@ -134,27 +175,48 @@ namespace Jellyfin.Server.Extensions /// Extension method for adding the jellyfin API to the service collection. /// </summary> /// <param name="serviceCollection">The service collection.</param> - /// <param name="baseUrl">The base url for the API.</param> + /// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param> + /// <param name="config">The <see cref="NetworkConfiguration"/>.</param> /// <returns>The MVC builder.</returns> - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies, NetworkConfiguration config) { - return serviceCollection - .AddCors(options => - { - options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy); - }) + IMvcBuilder mvcBuilder = serviceCollection + .AddCors() + .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>() .Configure<ForwardedHeadersOptions>(options => { + // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs + // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues. + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + if (config.KnownProxies.Length == 0) + { + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + } + else + { + AddProxyAddresses(config, config.KnownProxies, options); + } + + // Only set forward limit if we have some known proxies or some known networks. + if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0) + { + options.ForwardLimit = null; + } }) .AddMvc(opts => { - opts.UseGeneralRoutePrefix(baseUrl); + // Allow requester to change between camelCase and PascalCase + opts.RespectBrowserAcceptHeader = true; + opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter()); opts.OutputFormatters.Add(new CssOutputFormatter()); opts.OutputFormatters.Add(new XmlOutputFormatter()); + + opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider()); }) // Clear app parts to avoid other assemblies being picked up @@ -163,7 +225,7 @@ namespace Jellyfin.Server.Extensions .AddJsonOptions(options => { // Update all properties that are set in JsonDefaults - var jsonOptions = JsonDefaults.GetPascalCaseOptions(); + var jsonOptions = JsonDefaults.PascalCaseOptions; // From JsonDefaults options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling; @@ -179,8 +241,14 @@ namespace Jellyfin.Server.Extensions // From JsonDefaults.PascalCase options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; - }) - .AddControllersAsServices(); + }); + + foreach (Assembly pluginAssembly in pluginAssemblies) + { + mvcBuilder.AddApplicationPart(pluginAssembly); + } + + return mvcBuilder.AddControllersAsServices(); } /// <summary> @@ -192,27 +260,28 @@ namespace Jellyfin.Server.Extensions { return serviceCollection.AddSwaggerGen(c => { - c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); + var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1"; + c.SwaggerDoc("api-docs", new OpenApiInfo + { + Title = "Jellyfin API", + Version = version, + Extensions = new Dictionary<string, IOpenApiExtension> + { + { + "x-jellyfin-version", + new OpenApiString(version) + } + } + }); + c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, In = ParameterLocation.Header, - Name = "X-Emby-Token", + Name = "X-Emby-Authorization", Description = "API key header parameter" }); - var securitySchemeRef = new OpenApiSecurityScheme - { - Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = AuthenticationSchemes.CustomAuthentication }, - }; - - // TODO: Apply this with an operation filter instead of globally - // https://github.com/domaindrivendev/Swashbuckle.AspNetCore#add-security-definitions-and-requirements - c.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { securitySchemeRef, Array.Empty<string>() } - }); - // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( AppContext.BaseDirectory, @@ -234,33 +303,89 @@ namespace Jellyfin.Server.Extensions { description.TryGetMethodInfo(out MethodInfo methodInfo); // Attribute name, method name, none. - return description?.ActionDescriptor?.AttributeRouteInfo?.Name + return description?.ActionDescriptor.AttributeRouteInfo?.Name ?? methodInfo?.Name ?? null; }); + // Allow parameters to properly be nullable. + c.UseAllOfToExtendReferenceSchemas(); + c.SupportNonNullableReferenceTypes(); + // TODO - remove when all types are supported in System.Text.Json c.AddSwaggerTypeMappings(); + + c.OperationFilter<SecurityRequirementsOperationFilter>(); + c.OperationFilter<FileResponseFilter>(); + c.OperationFilter<FileRequestFilter>(); + c.OperationFilter<ParameterObsoleteFilter>(); + c.DocumentFilter<AdditionalModelFilter>(); }); } + /// <summary> + /// Sets up the proxy configuration based on the addresses in <paramref name="allowedProxies"/>. + /// </summary> + /// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param> + /// <param name="allowedProxies">The string array to parse.</param> + /// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param> + internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOptions options) + { + for (var i = 0; i < allowedProxies.Length; i++) + { + if (IPNetAddress.TryParse(allowedProxies[i], out var addr)) + { + AddIpAddress(config, options, addr.Address, addr.PrefixLength); + } + else if (IPHost.TryParse(allowedProxies[i], out var host)) + { + foreach (var address in host.GetAddresses()) + { + AddIpAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + } + } + } + } + + private static void AddIpAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength) + { + if ((!config.EnableIPV4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPV6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) + { + return; + } + + // In order for dual-mode sockets to be used, IP6 has to be enabled in JF and an interface has to have an IP6 address. + if (addr.AddressFamily == AddressFamily.InterNetwork && config.EnableIPV6) + { + // If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format. + // https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0 . + addr = addr.MapToIPv6(); + } + + if (prefixLength == 32) + { + options.KnownProxies.Add(addr); + } + else + { + options.KnownNetworks.Add(new IPNetwork(addr, prefixLength)); + } + } + private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) { /* - * TODO remove when System.Text.Json supports non-string keys. - * Used in Jellyfin.Api.Controller.GetChannels. + * TODO remove when System.Text.Json properly supports non-string keys. + * Used in BaseItemDto.ImageBlurHashes */ options.MapType<Dictionary<ImageType, string>>(() => new OpenApiSchema { Type = "object", - Properties = typeof(ImageType).GetEnumNames().ToDictionary( - name => name, - name => new OpenApiSchema - { - Type = "string", - Format = "string" - }) + AdditionalProperties = new OpenApiSchema + { + Type = "string" + } }); /* @@ -272,18 +397,12 @@ namespace Jellyfin.Server.Extensions Type = "object", Properties = typeof(ImageType).GetEnumNames().ToDictionary( name => name, - name => new OpenApiSchema + _ => new OpenApiSchema { - Type = "object", Properties = new Dictionary<string, OpenApiSchema> + Type = "object", + AdditionalProperties = new OpenApiSchema { - { - "string", - new OpenApiSchema - { - Type = "string", - Format = "string" - } - } + Type = "string" } }) }); |
