< 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: 200
Uncovered lines: 5
Coverable lines: 205
Total lines: 359
Line coverage: 97.5%
Branch coverage
85%
Covered branches: 29
Total branches: 34
Branch coverage: 85.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/6/2025 - 12:11:15 AM Line coverage: 97.4% (227/233) Branch coverage: 83.3% (30/36) Total lines: 3981/3/2026 - 12:11:48 AM Line coverage: 97% (200/206) Branch coverage: 83.3% (30/36) Total lines: 3651/19/2026 - 12:13:54 AM Line coverage: 97.5% (200/205) Branch coverage: 85.2% (29/34) Total lines: 3622/27/2026 - 12:13:29 AM Line coverage: 97.5% (200/205) Branch coverage: 85.2% (29/34) Total lines: 359 12/6/2025 - 12:11:15 AM Line coverage: 97.4% (227/233) Branch coverage: 83.3% (30/36) Total lines: 3981/3/2026 - 12:11:48 AM Line coverage: 97% (200/206) Branch coverage: 83.3% (30/36) Total lines: 3651/19/2026 - 12:13:54 AM Line coverage: 97.5% (200/205) Branch coverage: 85.2% (29/34) Total lines: 3622/27/2026 - 12:13:29 AM Line coverage: 97.5% (200/205) Branch coverage: 85.2% (29/34) Total lines: 359

Coverage delta

Coverage delta 2 -2

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(...)92.85%141488.88%
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.Net;
 5using System.Net.Sockets;
 6using System.Reflection;
 7using System.Security.Claims;
 8using System.Text.Json.Nodes;
 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 Microsoft.AspNetCore.Authentication;
 30using Microsoft.AspNetCore.Authorization;
 31using Microsoft.AspNetCore.Builder;
 32using Microsoft.AspNetCore.Cors.Infrastructure;
 33using Microsoft.AspNetCore.HttpOverrides;
 34using Microsoft.Extensions.DependencyInjection;
 35using Microsoft.Extensions.DependencyInjection.Extensions;
 36using Microsoft.OpenApi;
 37using Swashbuckle.AspNetCore.Swagger;
 38using Swashbuckle.AspNetCore.SwaggerGen;
 39using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
 40
 41namespace Jellyfin.Server.Extensions
 42{
 43    /// <summary>
 44    /// API specific extensions for the service collection.
 45    /// </summary>
 46    public static class ApiServiceCollectionExtensions
 47    {
 48        /// <summary>
 49        /// Adds jellyfin API authorization policies to the DI container.
 50        /// </summary>
 51        /// <param name="serviceCollection">The service collection.</param>
 52        /// <returns>The updated service collection.</returns>
 53        public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
 54        {
 55            // The default handler must be first so that it is evaluated first
 2156            serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
 2157            serviceCollection.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
 2158            serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
 2159            serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
 2160            serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
 2161            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
 62
 2163            return serviceCollection.AddAuthorizationCore(options =>
 2164            {
 2165                options.DefaultPolicy = new AuthorizationPolicyBuilder()
 2166                    .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
 2167                    .AddRequirements(new DefaultAuthorizationRequirement())
 2168                    .Build();
 2169
 2170                options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement());
 2171                options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableColl
 2172                options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloadi
 2173                options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false));
 2174                options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement());
 2175                options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, f
 2176                options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSc
 2177                options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess
 2178                options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvMa
 2179                options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement
 2180                options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2181                options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementT
 2182                options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2183                options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2184                options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtit
 2185                options.AddPolicy(Policies.LyricManagement, new UserPermissionRequirement(PermissionKind.EnableLyricMana
 2186                options.AddPolicy(
 2187                    Policies.RequiresElevation,
 2188                    policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
 2189                        .RequireClaim(ClaimTypes.Role, UserRoles.Administrator));
 2190            });
 91        }
 92
 93        /// <summary>
 94        /// Adds custom legacy authentication to the service collection.
 95        /// </summary>
 96        /// <param name="serviceCollection">The service collection.</param>
 97        /// <returns>The updated service collection.</returns>
 98        public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection serviceCollection)
 99        {
 21100            return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication)
 21101                .AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(AuthenticationSchemes.CustomAuthent
 102        }
 103
 104        /// <summary>
 105        /// Extension method for adding the Jellyfin API to the service collection.
 106        /// </summary>
 107        /// <param name="serviceCollection">The service collection.</param>
 108        /// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
 109        /// <param name="config">The <see cref="NetworkConfiguration"/>.</param>
 110        /// <returns>The MVC builder.</returns>
 111        public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> plugin
 112        {
 21113            IMvcBuilder mvcBuilder = serviceCollection
 21114                .AddCors()
 21115                .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
 21116                .Configure<ForwardedHeadersOptions>(options =>
 21117                {
 21118                    ConfigureForwardHeaders(config, options);
 21119                })
 21120                .AddMvc(opts =>
 21121                {
 21122                    // Allow requester to change between camelCase and PascalCase
 21123                    opts.RespectBrowserAcceptHeader = true;
 21124
 21125                    opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
 21126                    opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
 21127
 21128                    opts.OutputFormatters.Add(new CssOutputFormatter());
 21129                    opts.OutputFormatters.Add(new XmlOutputFormatter());
 21130
 21131                    opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider());
 21132                })
 21133
 21134                // Clear app parts to avoid other assemblies being picked up
 21135                .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
 21136                .AddApplicationPart(typeof(StartupController).Assembly)
 21137                .AddJsonOptions(options =>
 21138                {
 21139                    // Update all properties that are set in JsonDefaults
 21140                    var jsonOptions = JsonDefaults.PascalCaseOptions;
 21141
 21142                    // From JsonDefaults
 21143                    options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
 21144                    options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
 21145                    options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
 21146                    options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
 21147
 21148                    options.JsonSerializerOptions.Converters.Clear();
 21149                    foreach (var converter in jsonOptions.Converters)
 21150                    {
 21151                        options.JsonSerializerOptions.Converters.Add(converter);
 21152                    }
 21153
 21154                    // From JsonDefaults.PascalCase
 21155                    options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
 21156                });
 157
 126158            foreach (Assembly pluginAssembly in pluginAssemblies)
 159            {
 42160                mvcBuilder.AddApplicationPart(pluginAssembly);
 161            }
 162
 21163            return mvcBuilder.AddControllersAsServices();
 164        }
 165
 166        internal static void ConfigureForwardHeaders(NetworkConfiguration config, ForwardedHeadersOptions options)
 167        {
 168            // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddlew
 169            // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate
 170
 21171            if (config.KnownProxies.Length == 0)
 172            {
 21173                options.ForwardedHeaders = ForwardedHeaders.None;
 21174                options.KnownIPNetworks.Clear();
 21175                options.KnownProxies.Clear();
 176            }
 177            else
 178            {
 0179                options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | Forwarded
 0180                AddProxyAddresses(config, config.KnownProxies, options);
 181            }
 182
 183            // Only set forward limit if we have some known proxies or some known networks.
 21184            if (options.KnownProxies.Count != 0 || options.KnownIPNetworks.Count != 0)
 185            {
 0186                options.ForwardLimit = null;
 187            }
 21188        }
 189
 190        /// <summary>
 191        /// Adds Swagger to the service collection.
 192        /// </summary>
 193        /// <param name="serviceCollection">The service collection.</param>
 194        /// <returns>The updated service collection.</returns>
 195        public static IServiceCollection AddJellyfinApiSwagger(this IServiceCollection serviceCollection)
 196        {
 21197            return serviceCollection.AddSwaggerGen(c =>
 21198            {
 21199                var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1";
 21200                c.SwaggerDoc("api-docs", new OpenApiInfo
 21201                {
 21202                    Title = "Jellyfin API",
 21203                    Version = version,
 21204                    Extensions = new Dictionary<string, IOpenApiExtension>
 21205                    {
 21206                        {
 21207                            "x-jellyfin-version",
 21208                            new JsonNodeExtension(JsonValue.Create(version))
 21209                        }
 21210                    }
 21211                });
 21212
 21213                c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
 21214                {
 21215                    Type = SecuritySchemeType.ApiKey,
 21216                    In = ParameterLocation.Header,
 21217                    Name = "Authorization",
 21218                    Description = "API key header parameter"
 21219                });
 21220
 21221                // Add all xml doc files to swagger generator.
 21222                var xmlFiles = Directory.EnumerateFiles(
 21223                    AppContext.BaseDirectory,
 21224                    "*.xml",
 21225                    SearchOption.TopDirectoryOnly);
 21226
 21227                foreach (var xmlFile in xmlFiles)
 21228                {
 21229                    c.IncludeXmlComments(xmlFile);
 21230                }
 21231
 21232                // Order actions by route path, then by http method.
 21233                c.OrderActionsBy(description =>
 21234                    $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
 21235
 21236                // Use method name as operationId
 21237                c.CustomOperationIds(
 21238                    description =>
 21239                    {
 21240                        description.TryGetMethodInfo(out MethodInfo methodInfo);
 21241                        // Attribute name, method name, none.
 21242                        return description?.ActionDescriptor.AttributeRouteInfo?.Name
 21243                               ?? methodInfo?.Name
 21244                               ?? null;
 21245                    });
 21246
 21247                // Allow parameters to properly be nullable.
 21248                c.UseAllOfToExtendReferenceSchemas();
 21249                c.SupportNonNullableReferenceTypes();
 21250
 21251                // TODO - remove when all types are supported in System.Text.Json
 21252                c.AddSwaggerTypeMappings();
 21253
 21254                c.SchemaFilter<IgnoreEnumSchemaFilter>();
 21255                c.SchemaFilter<FlagsEnumSchemaFilter>();
 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                c.DocumentFilter<SecuritySchemeReferenceFixupFilter>();
 21263            })
 21264            .Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
 265        }
 266
 267        private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationR
 268        {
 336269            authorizationOptions.AddPolicy(policyName, policy =>
 336270            {
 336271                policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizatio
 336272            });
 336273        }
 274
 275        /// <summary>
 276        /// Sets up the proxy configuration based on the addresses/subnets in <paramref name="allowedProxies"/>.
 277        /// </summary>
 278        /// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param>
 279        /// <param name="allowedProxies">The string array to parse.</param>
 280        /// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param>
 281        internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOpt
 282        {
 38283            for (var i = 0; i < allowedProxies.Length; i++)
 284            {
 12285                if (IPAddress.TryParse(allowedProxies[i], out var addr))
 286                {
 4287                    AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? NetworkConsta
 288                }
 8289                else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet))
 290                {
 0291                    AddIPAddress(config, options, subnet.Address, subnet.Subnet.PrefixLength);
 292                }
 8293                else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.Enabl
 294                {
 24295                    foreach (var address in addresses)
 296                    {
 8297                        AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Net
 298                    }
 299                }
 300            }
 7301        }
 302
 303        private static void AddIPAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, i
 304        {
 12305            if (addr.IsIPv4MappedToIPv6)
 306            {
 0307                addr = addr.MapToIPv4();
 308            }
 309
 12310            if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.
 311            {
 4312                return;
 313            }
 314
 8315            if (prefixLength == NetworkConstants.MinimumIPv4PrefixSize)
 316            {
 4317                options.KnownProxies.Add(addr);
 318            }
 319            else
 320            {
 4321                options.KnownIPNetworks.Add(new System.Net.IPNetwork(addr, prefixLength));
 322            }
 4323        }
 324
 325        private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
 326        {
 327            /*
 328             * TODO remove when System.Text.Json properly supports non-string keys.
 329             * Used in BaseItemDto.ImageBlurHashes
 330             */
 20331            options.MapType<Dictionary<ImageType, string>>(() =>
 20332                new OpenApiSchema
 20333                {
 20334                    Type = JsonSchemaType.Object,
 20335                    AdditionalProperties = new OpenApiSchema
 20336                    {
 20337                        Type = JsonSchemaType.String
 20338                    }
 20339                });
 340
 341            // Support dictionary with nullable string value.
 20342            options.MapType<Dictionary<string, string?>>(() =>
 20343                new OpenApiSchema
 20344                {
 20345                    Type = JsonSchemaType.Object,
 20346                    AdditionalProperties = new OpenApiSchema
 20347                    {
 20348                        Type = JsonSchemaType.String | JsonSchemaType.Null
 20349                    }
 20350                });
 351
 352            // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
 20353            options.MapType<Version>(() => new OpenApiSchema
 20354            {
 20355                Type = JsonSchemaType.String
 20356            });
 20357        }
 358    }
 359}