< Summary - Jellyfin

Information
Class: Jellyfin.Server.Extensions.ApiServiceCollectionExtensions
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
Line coverage
98%
Covered lines: 238
Uncovered lines: 3
Coverable lines: 241
Total lines: 388
Line coverage: 98.7%
Branch coverage
90%
Covered branches: 27
Total branches: 30
Branch coverage: 90%
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%
AddJellyfinApiSwagger(...)100%11100%
AddPolicy(...)100%11100%
AddProxyAddresses(...)87.5%18.051680%
AddIPAddress(...)91.66%12.281287.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.Extensions.Json;
 23using Jellyfin.Server.Configuration;
 24using Jellyfin.Server.Filters;
 25using MediaBrowser.Common.Api;
 26using MediaBrowser.Common.Net;
 27using MediaBrowser.Model.Entities;
 28using MediaBrowser.Model.Session;
 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.OpenApi.Any;
 36using Microsoft.OpenApi.Interfaces;
 37using Microsoft.OpenApi.Models;
 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                    // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeader
 22119                    // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help inv
 22120
 22121                    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | Forwa
 22122
 22123                    if (config.KnownProxies.Length == 0)
 22124                    {
 22125                        options.KnownNetworks.Clear();
 22126                        options.KnownProxies.Clear();
 22127                    }
 22128                    else
 22129                    {
 22130                        AddProxyAddresses(config, config.KnownProxies, options);
 22131                    }
 22132
 22133                    // Only set forward limit if we have some known proxies or some known networks.
 22134                    if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
 22135                    {
 22136                        options.ForwardLimit = null;
 22137                    }
 22138                })
 22139                .AddMvc(opts =>
 22140                {
 22141                    // Allow requester to change between camelCase and PascalCase
 22142                    opts.RespectBrowserAcceptHeader = true;
 22143
 22144                    opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
 22145                    opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
 22146
 22147                    opts.OutputFormatters.Add(new CssOutputFormatter());
 22148                    opts.OutputFormatters.Add(new XmlOutputFormatter());
 22149
 22150                    opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider());
 22151                })
 22152
 22153                // Clear app parts to avoid other assemblies being picked up
 22154                .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
 22155                .AddApplicationPart(typeof(StartupController).Assembly)
 22156                .AddJsonOptions(options =>
 22157                {
 22158                    // Update all properties that are set in JsonDefaults
 22159                    var jsonOptions = JsonDefaults.PascalCaseOptions;
 22160
 22161                    // From JsonDefaults
 22162                    options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
 22163                    options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
 22164                    options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
 22165                    options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
 22166
 22167                    options.JsonSerializerOptions.Converters.Clear();
 22168                    foreach (var converter in jsonOptions.Converters)
 22169                    {
 22170                        options.JsonSerializerOptions.Converters.Add(converter);
 22171                    }
 22172
 22173                    // From JsonDefaults.PascalCase
 22174                    options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
 22175                });
 176
 132177            foreach (Assembly pluginAssembly in pluginAssemblies)
 178            {
 44179                mvcBuilder.AddApplicationPart(pluginAssembly);
 180            }
 181
 22182            return mvcBuilder.AddControllersAsServices();
 183        }
 184
 185        /// <summary>
 186        /// Adds Swagger to the service collection.
 187        /// </summary>
 188        /// <param name="serviceCollection">The service collection.</param>
 189        /// <returns>The updated service collection.</returns>
 190        public static IServiceCollection AddJellyfinApiSwagger(this IServiceCollection serviceCollection)
 191        {
 22192            return serviceCollection.AddSwaggerGen(c =>
 22193            {
 22194                var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1";
 22195                c.SwaggerDoc("api-docs", new OpenApiInfo
 22196                {
 22197                    Title = "Jellyfin API",
 22198                    Version = version,
 22199                    Extensions = new Dictionary<string, IOpenApiExtension>
 22200                    {
 22201                        {
 22202                            "x-jellyfin-version",
 22203                            new OpenApiString(version)
 22204                        }
 22205                    }
 22206                });
 22207
 22208                c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
 22209                {
 22210                    Type = SecuritySchemeType.ApiKey,
 22211                    In = ParameterLocation.Header,
 22212                    Name = "Authorization",
 22213                    Description = "API key header parameter"
 22214                });
 22215
 22216                // Add all xml doc files to swagger generator.
 22217                var xmlFiles = Directory.GetFiles(
 22218                    AppContext.BaseDirectory,
 22219                    "*.xml",
 22220                    SearchOption.TopDirectoryOnly);
 22221
 22222                foreach (var xmlFile in xmlFiles)
 22223                {
 22224                    c.IncludeXmlComments(xmlFile);
 22225                }
 22226
 22227                // Order actions by route path, then by http method.
 22228                c.OrderActionsBy(description =>
 22229                    $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
 22230
 22231                // Use method name as operationId
 22232                c.CustomOperationIds(
 22233                    description =>
 22234                    {
 22235                        description.TryGetMethodInfo(out MethodInfo methodInfo);
 22236                        // Attribute name, method name, none.
 22237                        return description?.ActionDescriptor.AttributeRouteInfo?.Name
 22238                               ?? methodInfo?.Name
 22239                               ?? null;
 22240                    });
 22241
 22242                // Allow parameters to properly be nullable.
 22243                c.UseAllOfToExtendReferenceSchemas();
 22244                c.SupportNonNullableReferenceTypes();
 22245
 22246                // TODO - remove when all types are supported in System.Text.Json
 22247                c.AddSwaggerTypeMappings();
 22248
 22249                c.SchemaFilter<IgnoreEnumSchemaFilter>();
 22250                c.OperationFilter<SecurityRequirementsOperationFilter>();
 22251                c.OperationFilter<FileResponseFilter>();
 22252                c.OperationFilter<FileRequestFilter>();
 22253                c.OperationFilter<ParameterObsoleteFilter>();
 22254                c.DocumentFilter<AdditionalModelFilter>();
 22255            });
 256        }
 257
 258        private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationR
 259        {
 352260            authorizationOptions.AddPolicy(policyName, policy =>
 352261            {
 352262                policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizatio
 352263            });
 352264        }
 265
 266        /// <summary>
 267        /// Sets up the proxy configuration based on the addresses/subnets in <paramref name="allowedProxies"/>.
 268        /// </summary>
 269        /// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param>
 270        /// <param name="allowedProxies">The string array to parse.</param>
 271        /// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param>
 272        internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOpt
 273        {
 38274            for (var i = 0; i < allowedProxies.Length; i++)
 275            {
 12276                if (IPAddress.TryParse(allowedProxies[i], out var addr))
 277                {
 4278                    AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? NetworkConsta
 279                }
 8280                else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet))
 281                {
 0282                    if (subnet is not null)
 283                    {
 0284                        AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
 285                    }
 286                }
 8287                else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.Enabl
 288                {
 24289                    foreach (var address in addresses)
 290                    {
 8291                        AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Net
 292                    }
 293                }
 294            }
 7295        }
 296
 297        private static void AddIPAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, i
 298        {
 12299            if (addr.IsIPv4MappedToIPv6)
 300            {
 0301                addr = addr.MapToIPv4();
 302            }
 303
 12304            if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.
 305            {
 4306                return;
 307            }
 308
 8309            if (prefixLength == NetworkConstants.MinimumIPv4PrefixSize)
 310            {
 4311                options.KnownProxies.Add(addr);
 312            }
 313            else
 314            {
 4315                options.KnownNetworks.Add(new Microsoft.AspNetCore.HttpOverrides.IPNetwork(addr, prefixLength));
 316            }
 4317        }
 318
 319        private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
 320        {
 321            /*
 322             * TODO remove when System.Text.Json properly supports non-string keys.
 323             * Used in BaseItemDto.ImageBlurHashes
 324             */
 21325            options.MapType<Dictionary<ImageType, string>>(() =>
 21326                new OpenApiSchema
 21327                {
 21328                    Type = "object",
 21329                    AdditionalProperties = new OpenApiSchema
 21330                    {
 21331                        Type = "string"
 21332                    }
 21333                });
 334
 335            /*
 336             * Support BlurHash dictionary
 337             */
 21338            options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() =>
 21339                new OpenApiSchema
 21340                {
 21341                    Type = "object",
 21342                    Properties = typeof(ImageType).GetEnumNames().ToDictionary(
 21343                        name => name,
 21344                        _ => new OpenApiSchema
 21345                        {
 21346                            Type = "object",
 21347                            AdditionalProperties = new OpenApiSchema
 21348                            {
 21349                                Type = "string"
 21350                            }
 21351                        })
 21352                });
 353
 354            // Support dictionary with nullable string value.
 21355            options.MapType<Dictionary<string, string?>>(() =>
 21356                new OpenApiSchema
 21357                {
 21358                    Type = "object",
 21359                    AdditionalProperties = new OpenApiSchema
 21360                    {
 21361                        Type = "string",
 21362                        Nullable = true
 21363                    }
 21364                });
 365
 366            // Manually describe Flags enum.
 21367            options.MapType<TranscodeReason>(() =>
 21368                new OpenApiSchema
 21369                {
 21370                    Type = "array",
 21371                    Items = new OpenApiSchema
 21372                    {
 21373                        Reference = new OpenApiReference
 21374                        {
 21375                            Id = nameof(TranscodeReason),
 21376                            Type = ReferenceType.Schema,
 21377                        }
 21378                    }
 21379                });
 380
 381            // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
 21382            options.MapType<Version>(() => new OpenApiSchema
 21383            {
 21384                Type = "string"
 21385            });
 21386        }
 387    }
 388}