using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Security.Claims; using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.AnonymousLanAccessPolicy; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.FirstTimeSetupPolicy; using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; using Jellyfin.Api.Auth.SyncPlayAccessPolicy; using Jellyfin.Api.Auth.UserPermissionPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Api.Formatters; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions.Json; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Constants; using Jellyfin.Networking.Extensions; using Jellyfin.Server.Configuration; using Jellyfin.Server.Filters; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; namespace Jellyfin.Server.Extensions { /// /// API specific extensions for the service collection. /// public static class ApiServiceCollectionExtensions { /// /// Adds jellyfin API authorization policies to the DI container. /// /// The service collection. /// The updated service collection. public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection) { // The default handler must be first so that it is evaluated first serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); return serviceCollection.AddAuthorizationCore(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) .AddRequirements(new DefaultAuthorizationRequirement()) .Build(); options.AddPolicy(Policies.AnonymousLanAccessPolicy, new AnonymousLanAccessRequirement()); options.AddPolicy(Policies.CollectionManagement, new UserPermissionRequirement(PermissionKind.EnableCollectionManagement)); options.AddPolicy(Policies.Download, new UserPermissionRequirement(PermissionKind.EnableContentDownloading)); options.AddPolicy(Policies.FirstTimeSetupOrDefault, new FirstTimeSetupRequirement(requireAdmin: false)); options.AddPolicy(Policies.FirstTimeSetupOrElevated, new FirstTimeSetupRequirement()); options.AddPolicy(Policies.FirstTimeSetupOrIgnoreParentalControl, new FirstTimeSetupRequirement(false, false)); options.AddPolicy(Policies.IgnoreParentalControl, new DefaultAuthorizationRequirement(validateParentalSchedule: false)); options.AddPolicy(Policies.LiveTvAccess, new UserPermissionRequirement(PermissionKind.EnableLiveTvAccess)); options.AddPolicy(Policies.LiveTvManagement, new UserPermissionRequirement(PermissionKind.EnableLiveTvManagement)); options.AddPolicy(Policies.LocalAccessOrRequiresElevation, new LocalAccessOrRequiresElevationRequirement()); options.AddPolicy(Policies.SyncPlayHasAccess, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess)); options.AddPolicy(Policies.SyncPlayCreateGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup)); options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup)); options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup)); options.AddPolicy( Policies.RequiresElevation, policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication) .RequireClaim(ClaimTypes.Role, UserRoles.Administrator)); }); } /// /// Adds custom legacy authentication to the service collection. /// /// The service collection. /// The updated service collection. public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection serviceCollection) { return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication) .AddScheme(AuthenticationSchemes.CustomAuthentication, null); } /// /// Extension method for adding the Jellyfin API to the service collection. /// /// The service collection. /// An IEnumerable containing all plugin assemblies with API controllers. /// The . /// The MVC builder. public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable pluginAssemblies, NetworkConfiguration config) { IMvcBuilder mvcBuilder = serviceCollection .AddCors() .AddTransient() .Configure(options => { // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues. options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; if (config.KnownProxies.Length == 0) { options.KnownNetworks.Clear(); options.KnownProxies.Clear(); } else { AddProxyAddresses(config, config.KnownProxies, options); } // Only set forward limit if we have some known proxies or some known networks. if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0) { options.ForwardLimit = null; } }) .AddMvc(opts => { // Allow requester to change between camelCase and PascalCase opts.RespectBrowserAcceptHeader = true; opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter()); opts.OutputFormatters.Add(new CssOutputFormatter()); opts.OutputFormatters.Add(new XmlOutputFormatter()); opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider()); }) // Clear app parts to avoid other assemblies being picked up .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) .AddApplicationPart(typeof(StartupController).Assembly) .AddJsonOptions(options => { // Update all properties that are set in JsonDefaults var jsonOptions = JsonDefaults.PascalCaseOptions; // From JsonDefaults options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling; options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented; options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition; options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling; options.JsonSerializerOptions.Converters.Clear(); foreach (var converter in jsonOptions.Converters) { options.JsonSerializerOptions.Converters.Add(converter); } // From JsonDefaults.PascalCase options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; }); foreach (Assembly pluginAssembly in pluginAssemblies) { mvcBuilder.AddApplicationPart(pluginAssembly); } return mvcBuilder.AddControllersAsServices(); } /// /// Adds Swagger to the service collection. /// /// The service collection. /// The updated service collection. public static IServiceCollection AddJellyfinApiSwagger(this IServiceCollection serviceCollection) { return serviceCollection.AddSwaggerGen(c => { var version = typeof(ApplicationHost).Assembly.GetName().Version?.ToString(3) ?? "0.0.1"; c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = version, Extensions = new Dictionary { { "x-jellyfin-version", new OpenApiString(version) } } }); c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, In = ParameterLocation.Header, Name = "Authorization", Description = "API key header parameter" }); // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( AppContext.BaseDirectory, "*.xml", SearchOption.TopDirectoryOnly); foreach (var xmlFile in xmlFiles) { c.IncludeXmlComments(xmlFile); } // Order actions by route path, then by http method. c.OrderActionsBy(description => $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}"); // Use method name as operationId c.CustomOperationIds( description => { description.TryGetMethodInfo(out MethodInfo methodInfo); // Attribute name, method name, none. return description?.ActionDescriptor.AttributeRouteInfo?.Name ?? methodInfo?.Name ?? null; }); // Allow parameters to properly be nullable. c.UseAllOfToExtendReferenceSchemas(); c.SupportNonNullableReferenceTypes(); // TODO - remove when all types are supported in System.Text.Json c.AddSwaggerTypeMappings(); c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); c.OperationFilter(); c.DocumentFilter(); }); } private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement) { authorizationOptions.AddPolicy(policyName, policy => { policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizationRequirement); }); } /// /// Sets up the proxy configuration based on the addresses/subnets in . /// /// The containing the config settings. /// The string array to parse. /// The instance. internal static void AddProxyAddresses(NetworkConfiguration config, string[] allowedProxies, ForwardedHeadersOptions options) { for (var i = 0; i < allowedProxies.Length; i++) { if (IPAddress.TryParse(allowedProxies[i], out var addr)) { AddIPAddress(config, options, addr, addr.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize); } else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet)) { if (subnet is not null) { AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) { foreach (var address in addresses) { AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize); } } } } private static void AddIPAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength) { if (addr.IsIPv4MappedToIPv6) { addr = addr.MapToIPv4(); } if ((!config.EnableIPv4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPv6 && addr.AddressFamily == AddressFamily.InterNetworkV6)) { return; } if (prefixLength == Network.MinimumIPv4PrefixSize) { options.KnownProxies.Add(addr); } else { options.KnownNetworks.Add(new IPNetwork(addr, prefixLength)); } } private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) { /* * TODO remove when System.Text.Json properly supports non-string keys. * Used in BaseItemDto.ImageBlurHashes */ options.MapType>(() => new OpenApiSchema { Type = "object", AdditionalProperties = new OpenApiSchema { Type = "string" } }); /* * Support BlurHash dictionary */ options.MapType>>(() => new OpenApiSchema { Type = "object", Properties = typeof(ImageType).GetEnumNames().ToDictionary( name => name, _ => new OpenApiSchema { Type = "object", AdditionalProperties = new OpenApiSchema { Type = "string" } }) }); // Support dictionary with nullable string value. options.MapType>(() => new OpenApiSchema { Type = "object", AdditionalProperties = new OpenApiSchema { Type = "string", Nullable = true } }); // Manually describe Flags enum. options.MapType(() => new OpenApiSchema { Type = "array", Items = new OpenApiSchema { Reference = new OpenApiReference { Id = nameof(TranscodeReason), Type = ReferenceType.Schema, } } }); // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it. options.MapType(() => new OpenApiSchema { Type = "string" }); } } }