< 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: 227
Uncovered lines: 6
Coverable lines: 233
Total lines: 398
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 9/14/2025 - 12:09:49 AM Line coverage: 97.4% (226/232) Branch coverage: 83.3% (30/36) Total lines: 39512/4/2025 - 12:11:49 AM Line coverage: 97.4% (227/233) Branch coverage: 83.3% (30/36) Total lines: 398 9/14/2025 - 12:09:49 AM Line coverage: 97.4% (226/232) Branch coverage: 83.3% (30/36) Total lines: 39512/4/2025 - 12:11:49 AM Line coverage: 97.4% (227/233) Branch coverage: 83.3% (30/36) Total lines: 398

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.Extensions.DependencyInjection.Extensions;
 37using Microsoft.OpenApi.Any;
 38using Microsoft.OpenApi.Interfaces;
 39using Microsoft.OpenApi.Models;
 40using Swashbuckle.AspNetCore.Swagger;
 41using Swashbuckle.AspNetCore.SwaggerGen;
 42using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
 43
 44namespace Jellyfin.Server.Extensions
 45{
 46    /// <summary>
 47    /// API specific extensions for the service collection.
 48    /// </summary>
 49    public static class ApiServiceCollectionExtensions
 50    {
 51        /// <summary>
 52        /// Adds jellyfin API authorization policies to the DI container.
 53        /// </summary>
 54        /// <param name="serviceCollection">The service collection.</param>
 55        /// <returns>The updated service collection.</returns>
 56        public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
 57        {
 58            // The default handler must be first so that it is evaluated first
 2159            serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
 2160            serviceCollection.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
 2161            serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
 2162            serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
 2163            serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
 2164            serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
 65
 2166            return serviceCollection.AddAuthorizationCore(options =>
 2167            {
 2168                options.DefaultPolicy = new AuthorizationPolicyBuilder()
 2169                    .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
 2170                    .AddRequirements(new DefaultAuthorizationRequirement())
 2171                    .Build();
 2172
 2173                options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement());
 2174                options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableColl
 2175                options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloadi
 2176                options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false));
 2177                options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement());
 2178                options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, f
 2179                options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSc
 2180                options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess
 2181                options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvMa
 2182                options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement
 2183                options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2184                options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementT
 2185                options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2186                options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementTyp
 2187                options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtit
 2188                options.AddPolicy(Policies.LyricManagement, new UserPermissionRequirement(PermissionKind.EnableLyricMana
 2189                options.AddPolicy(
 2190                    Policies.RequiresElevation,
 2191                    policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
 2192                        .RequireClaim(ClaimTypes.Role, UserRoles.Administrator));
 2193            });
 94        }
 95
 96        /// <summary>
 97        /// Adds custom legacy authentication to the service collection.
 98        /// </summary>
 99        /// <param name="serviceCollection">The service collection.</param>
 100        /// <returns>The updated service collection.</returns>
 101        public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection serviceCollection)
 102        {
 21103            return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication)
 21104                .AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(AuthenticationSchemes.CustomAuthent
 105        }
 106
 107        /// <summary>
 108        /// Extension method for adding the Jellyfin API to the service collection.
 109        /// </summary>
 110        /// <param name="serviceCollection">The service collection.</param>
 111        /// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
 112        /// <param name="config">The <see cref="NetworkConfiguration"/>.</param>
 113        /// <returns>The MVC builder.</returns>
 114        public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> plugin
 115        {
 21116            IMvcBuilder mvcBuilder = serviceCollection
 21117                .AddCors()
 21118                .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
 21119                .Configure<ForwardedHeadersOptions>(options =>
 21120                {
 21121                    ConfigureForwardHeaders(config, options);
 21122                })
 21123                .AddMvc(opts =>
 21124                {
 21125                    // Allow requester to change between camelCase and PascalCase
 21126                    opts.RespectBrowserAcceptHeader = true;
 21127
 21128                    opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
 21129                    opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
 21130
 21131                    opts.OutputFormatters.Add(new CssOutputFormatter());
 21132                    opts.OutputFormatters.Add(new XmlOutputFormatter());
 21133
 21134                    opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider());
 21135                })
 21136
 21137                // Clear app parts to avoid other assemblies being picked up
 21138                .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
 21139                .AddApplicationPart(typeof(StartupController).Assembly)
 21140                .AddJsonOptions(options =>
 21141                {
 21142                    // Update all properties that are set in JsonDefaults
 21143                    var jsonOptions = JsonDefaults.PascalCaseOptions;
 21144
 21145                    // From JsonDefaults
 21146                    options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
 21147                    options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
 21148                    options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
 21149                    options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
 21150
 21151                    options.JsonSerializerOptions.Converters.Clear();
 21152                    foreach (var converter in jsonOptions.Converters)
 21153                    {
 21154                        options.JsonSerializerOptions.Converters.Add(converter);
 21155                    }
 21156
 21157                    // From JsonDefaults.PascalCase
 21158                    options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
 21159                });
 160
 126161            foreach (Assembly pluginAssembly in pluginAssemblies)
 162            {
 42163                mvcBuilder.AddApplicationPart(pluginAssembly);
 164            }
 165
 21166            return mvcBuilder.AddControllersAsServices();
 167        }
 168
 169        internal static void ConfigureForwardHeaders(NetworkConfiguration config, ForwardedHeadersOptions options)
 170        {
 171            // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddlew
 172            // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate
 173
 21174            if (config.KnownProxies.Length == 0)
 175            {
 21176                options.ForwardedHeaders = ForwardedHeaders.None;
 21177                options.KnownNetworks.Clear();
 21178                options.KnownProxies.Clear();
 179            }
 180            else
 181            {
 0182                options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | Forwarded
 0183                AddProxyAddresses(config, config.KnownProxies, options);
 184            }
 185
 186            // Only set forward limit if we have some known proxies or some known networks.
 21187            if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
 188            {
 0189                options.ForwardLimit = null;
 190            }
 21191        }
 192
 193        /// <summary>
 194        /// Adds Swagger to the service collection.
 195        /// </summary>
 196        /// <param name="serviceCollection">The service collection.</param>
 197        /// <returns>The updated service collection.</returns>
 198        public static IServiceCollection AddJellyfinApiSwagger(this IServiceCollection serviceCollection)
 199        {
 21200            return serviceCollection.AddSwaggerGen(c =>
 21201            {
 21202                var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1";
 21203                c.SwaggerDoc("api-docs", new OpenApiInfo
 21204                {
 21205                    Title = "Jellyfin API",
 21206                    Version = version,
 21207                    Extensions = new Dictionary<string, IOpenApiExtension>
 21208                    {
 21209                        {
 21210                            "x-jellyfin-version",
 21211                            new OpenApiString(version)
 21212                        }
 21213                    }
 21214                });
 21215
 21216                c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
 21217                {
 21218                    Type = SecuritySchemeType.ApiKey,
 21219                    In = ParameterLocation.Header,
 21220                    Name = "Authorization",
 21221                    Description = "API key header parameter"
 21222                });
 21223
 21224                // Add all xml doc files to swagger generator.
 21225                var xmlFiles = Directory.EnumerateFiles(
 21226                    AppContext.BaseDirectory,
 21227                    "*.xml",
 21228                    SearchOption.TopDirectoryOnly);
 21229
 21230                foreach (var xmlFile in xmlFiles)
 21231                {
 21232                    c.IncludeXmlComments(xmlFile);
 21233                }
 21234
 21235                // Order actions by route path, then by http method.
 21236                c.OrderActionsBy(description =>
 21237                    $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
 21238
 21239                // Use method name as operationId
 21240                c.CustomOperationIds(
 21241                    description =>
 21242                    {
 21243                        description.TryGetMethodInfo(out MethodInfo methodInfo);
 21244                        // Attribute name, method name, none.
 21245                        return description?.ActionDescriptor.AttributeRouteInfo?.Name
 21246                               ?? methodInfo?.Name
 21247                               ?? null;
 21248                    });
 21249
 21250                // Allow parameters to properly be nullable.
 21251                c.UseAllOfToExtendReferenceSchemas();
 21252                c.SupportNonNullableReferenceTypes();
 21253
 21254                // TODO - remove when all types are supported in System.Text.Json
 21255                c.AddSwaggerTypeMappings();
 21256
 21257                c.SchemaFilter<IgnoreEnumSchemaFilter>();
 21258                c.OperationFilter<RetryOnTemporarilyUnavailableFilter>();
 21259                c.OperationFilter<SecurityRequirementsOperationFilter>();
 21260                c.OperationFilter<FileResponseFilter>();
 21261                c.OperationFilter<FileRequestFilter>();
 21262                c.OperationFilter<ParameterObsoleteFilter>();
 21263                c.DocumentFilter<AdditionalModelFilter>();
 21264            })
 21265            .Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
 266        }
 267
 268        private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationR
 269        {
 336270            authorizationOptions.AddPolicy(policyName, policy =>
 336271            {
 336272                policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizatio
 336273            });
 336274        }
 275
 276        /// <summary>
 277        /// Sets up the proxy configuration based on the addresses/subnets in <paramref name="allowedProxies"/>.
 278        /// </summary>
 279        /// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param>
 280        /// <param name="allowedProxies">The string array to parse.</param>
 281        /// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param>
 282        internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOpt
 283        {
 38284            for (var i = 0; i < allowedProxies.Length; i++)
 285            {
 12286                if (IPAddress.TryParse(allowedProxies[i], out var addr))
 287                {
 4288                    AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? NetworkConsta
 289                }
 8290                else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet))
 291                {
 0292                    if (subnet is not null)
 293                    {
 0294                        AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
 295                    }
 296                }
 8297                else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.Enabl
 298                {
 24299                    foreach (var address in addresses)
 300                    {
 8301                        AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Net
 302                    }
 303                }
 304            }
 7305        }
 306
 307        private static void AddIPAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, i
 308        {
 12309            if (addr.IsIPv4MappedToIPv6)
 310            {
 0311                addr = addr.MapToIPv4();
 312            }
 313
 12314            if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.
 315            {
 4316                return;
 317            }
 318
 8319            if (prefixLength == NetworkConstants.MinimumIPv4PrefixSize)
 320            {
 4321                options.KnownProxies.Add(addr);
 322            }
 323            else
 324            {
 4325                options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(addr, prefixLength));
 326            }
 4327        }
 328
 329        private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
 330        {
 331            /*
 332             * TODO remove when System.Text.Json properly supports non-string keys.
 333             * Used in BaseItemDto.ImageBlurHashes
 334             */
 20335            options.MapType<Dictionary<ImageType, string>>(() =>
 20336                new OpenApiSchema
 20337                {
 20338                    Type = "object",
 20339                    AdditionalProperties = new OpenApiSchema
 20340                    {
 20341                        Type = "string"
 20342                    }
 20343                });
 344
 345            /*
 346             * Support BlurHash dictionary
 347             */
 20348            options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() =>
 20349                new OpenApiSchema
 20350                {
 20351                    Type = "object",
 20352                    Properties = typeof(ImageType).GetEnumNames().ToDictionary(
 20353                        name => name,
 20354                        _ => new OpenApiSchema
 20355                        {
 20356                            Type = "object",
 20357                            AdditionalProperties = new OpenApiSchema
 20358                            {
 20359                                Type = "string"
 20360                            }
 20361                        })
 20362                });
 363
 364            // Support dictionary with nullable string value.
 20365            options.MapType<Dictionary<string, string?>>(() =>
 20366                new OpenApiSchema
 20367                {
 20368                    Type = "object",
 20369                    AdditionalProperties = new OpenApiSchema
 20370                    {
 20371                        Type = "string",
 20372                        Nullable = true
 20373                    }
 20374                });
 375
 376            // Manually describe Flags enum.
 20377            options.MapType<TranscodeReason>(() =>
 20378                new OpenApiSchema
 20379                {
 20380                    Type = "array",
 20381                    Items = new OpenApiSchema
 20382                    {
 20383                        Reference = new OpenApiReference
 20384                        {
 20385                            Id = nameof(TranscodeReason),
 20386                            Type = ReferenceType.Schema,
 20387                        }
 20388                    }
 20389                });
 390
 391            // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
 20392            options.MapType<Version>(() => new OpenApiSchema
 20393            {
 20394                Type = "string"
 20395            });
 20396        }
 397    }
 398}