< Summary - Jellyfin

Information
Class: Jellyfin.Server.Extensions.ApiServiceCollectionExtensions
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
Line coverage
96%
Covered lines: 198
Uncovered lines: 7
Coverable lines: 205
Total lines: 359
Line coverage: 96.5%
Branch coverage
80%
Covered branches: 32
Total branches: 40
Branch coverage: 80%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/26/2026 - 12:14:14 AM Line coverage: 97.5% (200/205) Branch coverage: 85.2% (29/34) Total lines: 3594/29/2026 - 12:14:58 AM Line coverage: 96.5% (198/205) Branch coverage: 82.5% (33/40) Total lines: 3595/20/2026 - 12:15:44 AM Line coverage: 96.5% (198/205) Branch coverage: 80% (32/40) Total lines: 359 3/26/2026 - 12:14:14 AM Line coverage: 97.5% (200/205) Branch coverage: 85.2% (29/34) Total lines: 3594/29/2026 - 12:14:58 AM Line coverage: 96.5% (198/205) Branch coverage: 82.5% (33/40) Total lines: 3595/20/2026 - 12:15:44 AM Line coverage: 96.5% (198/205) Branch coverage: 80% (32/40) Total lines: 359

Coverage delta

Coverage delta 3 -3

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(...)77.77%351862.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
 2256            serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
 2257            serviceCollection.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
 2258            serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
 2259            serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
 2260            serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
 2261            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
 62
 2263            return serviceCollection.AddAuthorizationCore(options =>
 2264            {
 2265                options.DefaultPolicy = new AuthorizationPolicyBuilder()
 2266                    .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
 2267                    .AddRequirements(new DefaultAuthorizationRequirement())
 2268                    .Build();
 2269
 2270                options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement());
 2271                options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableColl
 2272                options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloadi
 2273                options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false));
 2274                options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement());
 2275                options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, f
 2276                options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSc
 2277                options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess
 2278                options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvMa
 2279                options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement
 2280                options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2281                options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementT
 2282                options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2283                options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2284                options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtit
 2285                options.AddPolicy(Policies.LyricManagement, new UserPermissionRequirement(PermissionKind.EnableLyricMana
 2286                options.AddPolicy(
 2287                    Policies.RequiresElevation,
 2288                    policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
 2289                        .RequireClaim(ClaimTypes.Role, UserRoles.Administrator));
 2290            });
 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        {
 22100            return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication)
 22101                .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        {
 22113            IMvcBuilder mvcBuilder = serviceCollection
 22114                .AddCors()
 22115                .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
 22116                .Configure<ForwardedHeadersOptions>(options =>
 22117                {
 22118                    ConfigureForwardHeaders(config, options);
 22119                })
 22120                .AddMvc(opts =>
 22121                {
 22122                    // Allow requester to change between camelCase and PascalCase
 22123                    opts.RespectBrowserAcceptHeader = true;
 22124
 22125                    opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
 22126                    opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
 22127
 22128                    opts.OutputFormatters.Add(new CssOutputFormatter());
 22129                    opts.OutputFormatters.Add(new XmlOutputFormatter());
 22130
 22131                    opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider());
 22132                })
 22133
 22134                // Clear app parts to avoid other assemblies being picked up
 22135                .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
 22136                .AddApplicationPart(typeof(StartupController).Assembly)
 22137                .AddJsonOptions(options =>
 22138                {
 22139                    // Update all properties that are set in JsonDefaults
 22140                    var jsonOptions = JsonDefaults.PascalCaseOptions;
 22141
 22142                    // From JsonDefaults
 22143                    options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
 22144                    options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
 22145                    options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
 22146                    options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
 22147
 22148                    options.JsonSerializerOptions.Converters.Clear();
 22149                    foreach (var converter in jsonOptions.Converters)
 22150                    {
 22151                        options.JsonSerializerOptions.Converters.Add(converter);
 22152                    }
 22153
 22154                    // From JsonDefaults.PascalCase
 22155                    options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
 22156                });
 157
 132158            foreach (Assembly pluginAssembly in pluginAssemblies)
 159            {
 44160                mvcBuilder.AddApplicationPart(pluginAssembly);
 161            }
 162
 22163            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
 22171            if (config.KnownProxies.Length == 0)
 172            {
 22173                options.ForwardedHeaders = ForwardedHeaders.None;
 22174                options.KnownIPNetworks.Clear();
 22175                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.
 22184            if (options.KnownProxies.Count != 0 || options.KnownIPNetworks.Count != 0)
 185            {
 0186                options.ForwardLimit = null;
 187            }
 22188        }
 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        {
 22197            return serviceCollection.AddSwaggerGen(c =>
 22198            {
 22199                var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1";
 22200                c.SwaggerDoc("api-docs", new OpenApiInfo
 22201                {
 22202                    Title = "Jellyfin API",
 22203                    Version = version,
 22204                    Extensions = new Dictionary<string, IOpenApiExtension>
 22205                    {
 22206                        {
 22207                            "x-jellyfin-version",
 22208                            new JsonNodeExtension(JsonValue.Create(version))
 22209                        }
 22210                    }
 22211                });
 22212
 22213                c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
 22214                {
 22215                    Type = SecuritySchemeType.ApiKey,
 22216                    In = ParameterLocation.Header,
 22217                    Name = "Authorization",
 22218                    Description = "API key header parameter"
 22219                });
 22220
 22221                // Add all xml doc files to swagger generator.
 22222                var xmlFiles = Directory.EnumerateFiles(
 22223                    AppContext.BaseDirectory,
 22224                    "*.xml",
 22225                    SearchOption.TopDirectoryOnly);
 22226
 22227                foreach (var xmlFile in xmlFiles)
 22228                {
 22229                    c.IncludeXmlComments(xmlFile);
 22230                }
 22231
 22232                // Order actions by route path, then by http method.
 22233                c.OrderActionsBy(description =>
 22234                    $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
 22235
 22236                // Use method name as operationId
 22237                c.CustomOperationIds(
 22238                    description =>
 22239                    {
 22240                        description.TryGetMethodInfo(out MethodInfo methodInfo);
 22241                        // Attribute name, method name, none.
 22242                        return description?.ActionDescriptor.AttributeRouteInfo?.Name
 22243                               ?? methodInfo?.Name
 22244                               ?? null;
 22245                    });
 22246
 22247                // Allow parameters to properly be nullable.
 22248                c.UseAllOfToExtendReferenceSchemas();
 22249                c.SupportNonNullableReferenceTypes();
 22250
 22251                // TODO - remove when all types are supported in System.Text.Json
 22252                c.AddSwaggerTypeMappings();
 22253
 22254                c.SchemaFilter<IgnoreEnumSchemaFilter>();
 22255                c.SchemaFilter<FlagsEnumSchemaFilter>();
 22256                c.OperationFilter<RetryOnTemporarilyUnavailableFilter>();
 22257                c.OperationFilter<SecurityRequirementsOperationFilter>();
 22258                c.OperationFilter<FileResponseFilter>();
 22259                c.OperationFilter<FileRequestFilter>();
 22260                c.OperationFilter<ParameterObsoleteFilter>();
 22261                c.DocumentFilter<AdditionalModelFilter>();
 22262                c.DocumentFilter<SecuritySchemeReferenceFixupFilter>();
 22263            })
 22264            .Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
 265        }
 266
 267        private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationR
 268        {
 352269            authorizationOptions.AddPolicy(policyName, policy =>
 352270            {
 352271                policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizatio
 352272            });
 352273        }
 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 ((addr.AddressFamily == AddressFamily.InterNetwork && prefixLength == NetworkConstants.MinimumIPv4PrefixS
 316            {
 8317                options.KnownProxies.Add(addr);
 318            }
 319            else
 320            {
 0321                options.KnownIPNetworks.Add(new System.Net.IPNetwork(addr, prefixLength));
 322            }
 0323        }
 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             */
 21331            options.MapType<Dictionary<ImageType, string>>(() =>
 21332                new OpenApiSchema
 21333                {
 21334                    Type = JsonSchemaType.Object,
 21335                    AdditionalProperties = new OpenApiSchema
 21336                    {
 21337                        Type = JsonSchemaType.String
 21338                    }
 21339                });
 340
 341            // Support dictionary with nullable string value.
 21342            options.MapType<Dictionary<string, string?>>(() =>
 21343                new OpenApiSchema
 21344                {
 21345                    Type = JsonSchemaType.Object,
 21346                    AdditionalProperties = new OpenApiSchema
 21347                    {
 21348                        Type = JsonSchemaType.String | JsonSchemaType.Null
 21349                    }
 21350                });
 351
 352            // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
 21353            options.MapType<Version>(() => new OpenApiSchema
 21354            {
 21355                Type = JsonSchemaType.String
 21356            });
 21357        }
 358    }
 359}