< Summary - Jellyfin

Information
Class: Jellyfin.Server.Extensions.ApiServiceCollectionExtensions
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
Line coverage
97%
Covered lines: 226
Uncovered lines: 6
Coverable lines: 232
Total lines: 395
Line coverage: 97.4%
Branch coverage
83%
Covered branches: 30
Total branches: 36
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddJellyfinApiAuthorization(...)100%11100%
AddCustomAuthentication(...)100%11100%
AddJellyfinApi(...)100%22100%
ConfigureForwardHeaders(...)50%7666.66%
AddJellyfinApiSwagger(...)100%11100%
AddPolicy(...)100%11100%
AddProxyAddresses(...)87.5%181680%
AddIPAddress(...)91.66%121287.5%
AddSwaggerTypeMappings(...)100%11100%

File(s)

/srv/git/jellyfin/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.IO;
 4using System.Linq;
 5using System.Net;
 6using System.Net.Sockets;
 7using System.Reflection;
 8using System.Security.Claims;
 9using Emby.Server.Implementations;
 10using Jellyfin.Api.Auth;
 11using Jellyfin.Api.Auth.AnonymousLanAccessPolicy;
 12using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
 13using Jellyfin.Api.Auth.FirstTimeSetupPolicy;
 14using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
 15using Jellyfin.Api.Auth.SyncPlayAccessPolicy;
 16using Jellyfin.Api.Auth.UserPermissionPolicy;
 17using Jellyfin.Api.Constants;
 18using Jellyfin.Api.Controllers;
 19using Jellyfin.Api.Formatters;
 20using Jellyfin.Api.ModelBinders;
 21using Jellyfin.Data.Enums;
 22using Jellyfin.Database.Implementations.Enums;
 23using Jellyfin.Extensions.Json;
 24using Jellyfin.Server.Configuration;
 25using Jellyfin.Server.Filters;
 26using MediaBrowser.Common.Api;
 27using MediaBrowser.Common.Net;
 28using MediaBrowser.Model.Entities;
 29using MediaBrowser.Model.Session;
 30using Microsoft.AspNetCore.Authentication;
 31using Microsoft.AspNetCore.Authorization;
 32using Microsoft.AspNetCore.Builder;
 33using Microsoft.AspNetCore.Cors.Infrastructure;
 34using Microsoft.AspNetCore.HttpOverrides;
 35using Microsoft.Extensions.DependencyInjection;
 36using Microsoft.OpenApi.Any;
 37using Microsoft.OpenApi.Interfaces;
 38using Microsoft.OpenApi.Models;
 39using Swashbuckle.AspNetCore.SwaggerGen;
 40using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
 41
 42namespace Jellyfin.Server.Extensions
 43{
 44    /// <summary>
 45    /// API specific extensions for the service collection.
 46    /// </summary>
 47    public static class ApiServiceCollectionExtensions
 48    {
 49        /// <summary>
 50        /// Adds jellyfin API authorization policies to the DI container.
 51        /// </summary>
 52        /// <param name="serviceCollection">The service collection.</param>
 53        /// <returns>The updated service collection.</returns>
 54        public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
 55        {
 56            // The default handler must be first so that it is evaluated first
 2157            serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
 2158            serviceCollection.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
 2159            serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
 2160            serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
 2161            serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
 2162            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
 63
 2164            return serviceCollection.AddAuthorizationCore(options =>
 2165            {
 2166                options.DefaultPolicy = new AuthorizationPolicyBuilder()
 2167                    .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
 2168                    .AddRequirements(new DefaultAuthorizationRequirement())
 2169                    .Build();
 2170
 2171                options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement());
 2172                options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableColl
 2173                options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloadi
 2174                options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false));
 2175                options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement());
 2176                options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, f
 2177                options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSc
 2178                options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess
 2179                options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvMa
 2180                options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement
 2181                options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2182                options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementT
 2183                options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2184                options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2185                options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtit
 2186                options.AddPolicy(Policies.LyricManagement, new UserPermissionRequirement(PermissionKind.EnableLyricMana
 2187                options.AddPolicy(
 2188                    Policies.RequiresElevation,
 2189                    policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
 2190                        .RequireClaim(ClaimTypes.Role, UserRoles.Administrator));
 2191            });
 92        }
 93
 94        /// <summary>
 95        /// Adds custom legacy authentication to the service collection.
 96        /// </summary>
 97        /// <param name="serviceCollection">The service collection.</param>
 98        /// <returns>The updated service collection.</returns>
 99        public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection serviceCollection)
 100        {
 21101            return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication)
 21102                .AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(AuthenticationSchemes.CustomAuthent
 103        }
 104
 105        /// <summary>
 106        /// Extension method for adding the Jellyfin API to the service collection.
 107        /// </summary>
 108        /// <param name="serviceCollection">The service collection.</param>
 109        /// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
 110        /// <param name="config">The <see cref="NetworkConfiguration"/>.</param>
 111        /// <returns>The MVC builder.</returns>
 112        public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> plugin
 113        {
 21114            IMvcBuilder mvcBuilder = serviceCollection
 21115                .AddCors()
 21116                .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
 21117                .Configure<ForwardedHeadersOptions>(options =>
 21118                {
 21119                    ConfigureForwardHeaders(config, options);
 21120                })
 21121                .AddMvc(opts =>
 21122                {
 21123                    // Allow requester to change between camelCase and PascalCase
 21124                    opts.RespectBrowserAcceptHeader = true;
 21125
 21126                    opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
 21127                    opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
 21128
 21129                    opts.OutputFormatters.Add(new CssOutputFormatter());
 21130                    opts.OutputFormatters.Add(new XmlOutputFormatter());
 21131
 21132                    opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider());
 21133                })
 21134
 21135                // Clear app parts to avoid other assemblies being picked up
 21136                .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
 21137                .AddApplicationPart(typeof(StartupController).Assembly)
 21138                .AddJsonOptions(options =>
 21139                {
 21140                    // Update all properties that are set in JsonDefaults
 21141                    var jsonOptions = JsonDefaults.PascalCaseOptions;
 21142
 21143                    // From JsonDefaults
 21144                    options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
 21145                    options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
 21146                    options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
 21147                    options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
 21148
 21149                    options.JsonSerializerOptions.Converters.Clear();
 21150                    foreach (var converter in jsonOptions.Converters)
 21151                    {
 21152                        options.JsonSerializerOptions.Converters.Add(converter);
 21153                    }
 21154
 21155                    // From JsonDefaults.PascalCase
 21156                    options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
 21157                });
 158
 126159            foreach (Assembly pluginAssembly in pluginAssemblies)
 160            {
 42161                mvcBuilder.AddApplicationPart(pluginAssembly);
 162            }
 163
 21164            return mvcBuilder.AddControllersAsServices();
 165        }
 166
 167        internal static void ConfigureForwardHeaders(NetworkConfiguration config, ForwardedHeadersOptions options)
 168        {
 169            // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddlew
 170            // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate
 171
 21172            if (config.KnownProxies.Length == 0)
 173            {
 21174                options.ForwardedHeaders = ForwardedHeaders.None;
 21175                options.KnownNetworks.Clear();
 21176                options.KnownProxies.Clear();
 177            }
 178            else
 179            {
 0180                options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | Forwarded
 0181                AddProxyAddresses(config, config.KnownProxies, options);
 182            }
 183
 184            // Only set forward limit if we have some known proxies or some known networks.
 21185            if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
 186            {
 0187                options.ForwardLimit = null;
 188            }
 21189        }
 190
 191        /// <summary>
 192        /// Adds Swagger to the service collection.
 193        /// </summary>
 194        /// <param name="serviceCollection">The service collection.</param>
 195        /// <returns>The updated service collection.</returns>
 196        public static IServiceCollection AddJellyfinApiSwagger(this IServiceCollection serviceCollection)
 197        {
 21198            return serviceCollection.AddSwaggerGen(c =>
 21199            {
 21200                var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1";
 21201                c.SwaggerDoc("api-docs", new OpenApiInfo
 21202                {
 21203                    Title = "Jellyfin API",
 21204                    Version = version,
 21205                    Extensions = new Dictionary<string, IOpenApiExtension>
 21206                    {
 21207                        {
 21208                            "x-jellyfin-version",
 21209                            new OpenApiString(version)
 21210                        }
 21211                    }
 21212                });
 21213
 21214                c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
 21215                {
 21216                    Type = SecuritySchemeType.ApiKey,
 21217                    In = ParameterLocation.Header,
 21218                    Name = "Authorization",
 21219                    Description = "API key header parameter"
 21220                });
 21221
 21222                // Add all xml doc files to swagger generator.
 21223                var xmlFiles = Directory.EnumerateFiles(
 21224                    AppContext.BaseDirectory,
 21225                    "*.xml",
 21226                    SearchOption.TopDirectoryOnly);
 21227
 21228                foreach (var xmlFile in xmlFiles)
 21229                {
 21230                    c.IncludeXmlComments(xmlFile);
 21231                }
 21232
 21233                // Order actions by route path, then by http method.
 21234                c.OrderActionsBy(description =>
 21235                    $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
 21236
 21237                // Use method name as operationId
 21238                c.CustomOperationIds(
 21239                    description =>
 21240                    {
 21241                        description.TryGetMethodInfo(out MethodInfo methodInfo);
 21242                        // Attribute name, method name, none.
 21243                        return description?.ActionDescriptor.AttributeRouteInfo?.Name
 21244                               ?? methodInfo?.Name
 21245                               ?? null;
 21246                    });
 21247
 21248                // Allow parameters to properly be nullable.
 21249                c.UseAllOfToExtendReferenceSchemas();
 21250                c.SupportNonNullableReferenceTypes();
 21251
 21252                // TODO - remove when all types are supported in System.Text.Json
 21253                c.AddSwaggerTypeMappings();
 21254
 21255                c.SchemaFilter<IgnoreEnumSchemaFilter>();
 21256                c.OperationFilter<RetryOnTemporarilyUnavailableFilter>();
 21257                c.OperationFilter<SecurityRequirementsOperationFilter>();
 21258                c.OperationFilter<FileResponseFilter>();
 21259                c.OperationFilter<FileRequestFilter>();
 21260                c.OperationFilter<ParameterObsoleteFilter>();
 21261                c.DocumentFilter<AdditionalModelFilter>();
 21262            });
 263        }
 264
 265        private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationR
 266        {
 336267            authorizationOptions.AddPolicy(policyName, policy =>
 336268            {
 336269                policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizatio
 336270            });
 336271        }
 272
 273        /// <summary>
 274        /// Sets up the proxy configuration based on the addresses/subnets in <paramref name="allowedProxies"/>.
 275        /// </summary>
 276        /// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param>
 277        /// <param name="allowedProxies">The string array to parse.</param>
 278        /// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param>
 279        internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOpt
 280        {
 38281            for (var i = 0; i < allowedProxies.Length; i++)
 282            {
 12283                if (IPAddress.TryParse(allowedProxies[i], out var addr))
 284                {
 4285                    AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? NetworkConsta
 286                }
 8287                else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet))
 288                {
 0289                    if (subnet is not null)
 290                    {
 0291                        AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
 292                    }
 293                }
 8294                else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.Enabl
 295                {
 24296                    foreach (var address in addresses)
 297                    {
 8298                        AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Net
 299                    }
 300                }
 301            }
 7302        }
 303
 304        private static void AddIPAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, i
 305        {
 12306            if (addr.IsIPv4MappedToIPv6)
 307            {
 0308                addr = addr.MapToIPv4();
 309            }
 310
 12311            if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.
 312            {
 4313                return;
 314            }
 315
 8316            if (prefixLength == NetworkConstants.MinimumIPv4PrefixSize)
 317            {
 4318                options.KnownProxies.Add(addr);
 319            }
 320            else
 321            {
 4322                options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(addr, prefixLength));
 323            }
 4324        }
 325
 326        private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
 327        {
 328            /*
 329             * TODO remove when System.Text.Json properly supports non-string keys.
 330             * Used in BaseItemDto.ImageBlurHashes
 331             */
 21332            options.MapType<Dictionary<ImageType, string>>(() =>
 21333                new OpenApiSchema
 21334                {
 21335                    Type = "object",
 21336                    AdditionalProperties = new OpenApiSchema
 21337                    {
 21338                        Type = "string"
 21339                    }
 21340                });
 341
 342            /*
 343             * Support BlurHash dictionary
 344             */
 21345            options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() =>
 21346                new OpenApiSchema
 21347                {
 21348                    Type = "object",
 21349                    Properties = typeof(ImageType).GetEnumNames().ToDictionary(
 21350                        name => name,
 21351                        _ => new OpenApiSchema
 21352                        {
 21353                            Type = "object",
 21354                            AdditionalProperties = new OpenApiSchema
 21355                            {
 21356                                Type = "string"
 21357                            }
 21358                        })
 21359                });
 360
 361            // Support dictionary with nullable string value.
 21362            options.MapType<Dictionary<string, string?>>(() =>
 21363                new OpenApiSchema
 21364                {
 21365                    Type = "object",
 21366                    AdditionalProperties = new OpenApiSchema
 21367                    {
 21368                        Type = "string",
 21369                        Nullable = true
 21370                    }
 21371                });
 372
 373            // Manually describe Flags enum.
 21374            options.MapType<TranscodeReason>(() =>
 21375                new OpenApiSchema
 21376                {
 21377                    Type = "array",
 21378                    Items = new OpenApiSchema
 21379                    {
 21380                        Reference = new OpenApiReference
 21381                        {
 21382                            Id = nameof(TranscodeReason),
 21383                            Type = ReferenceType.Schema,
 21384                        }
 21385                    }
 21386                });
 387
 388            // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
 21389            options.MapType<Version>(() => new OpenApiSchema
 21390            {
 21391                Type = "string"
 21392            });
 21393        }
 394    }
 395}