repo creation with initial code after cloning public repo

This commit is contained in:
2026-02-19 07:36:25 -05:00
commit 460884fea3
2860 changed files with 650825 additions and 0 deletions
@@ -0,0 +1,121 @@
using System.Collections.Generic;
using Jellyfin.Api.Middleware;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.OpenApi.Models;
namespace Jellyfin.Server.Extensions
{
/// <summary>
/// Extensions for adding API specific functionality to the application pipeline.
/// </summary>
public static class ApiApplicationBuilderExtensions
{
/// <summary>
/// Adds swagger and swagger UI to the application pipeline.
/// </summary>
/// <param name="applicationBuilder">The application builder.</param>
/// <param name="serverConfigurationManager">The server configuration.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseJellyfinApiSwagger(
this IApplicationBuilder applicationBuilder,
IServerConfigurationManager serverConfigurationManager)
{
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
var baseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl.Trim('/');
var apiDocBaseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
if (!string.IsNullOrEmpty(baseUrl))
{
baseUrl += '/';
}
return applicationBuilder
.UseSwagger(c =>
{
// Custom path requires {documentName}, SwaggerDoc documentName is 'api-docs'
c.RouteTemplate = "{documentName}/openapi.json";
c.PreSerializeFilters.Add((swagger, httpReq) =>
{
swagger.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}{apiDocBaseUrl}" } };
});
})
.UseSwaggerUI(c =>
{
c.DocumentTitle = "Jellyfin API";
c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API");
c.InjectStylesheet($"/{baseUrl}api-docs/swagger/custom.css");
c.RoutePrefix = "api-docs/swagger";
})
.UseReDoc(c =>
{
c.DocumentTitle = "Jellyfin API";
c.SpecUrl($"/{baseUrl}api-docs/openapi.json");
c.InjectStylesheet($"/{baseUrl}api-docs/redoc/custom.css");
c.RoutePrefix = "api-docs/redoc";
});
}
/// <summary>
/// Adds IP based access validation to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseIPBasedAccessValidation(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<IPBasedAccessValidationMiddleware>();
}
/// <summary>
/// Enables url decoding before binding to the application pipeline.
/// </summary>
/// <param name="appBuilder">The <see cref="IApplicationBuilder"/>.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseQueryStringDecoding(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<QueryStringDecodingMiddleware>();
}
/// <summary>
/// Adds base url redirection to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseBaseUrlRedirection(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<BaseUrlRedirectionMiddleware>();
}
/// <summary>
/// Adds a custom message during server startup to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseServerStartupMessage(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<ServerStartupMessageMiddleware>();
}
/// <summary>
/// Adds a WebSocket request handler to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseWebSocketHandler(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>();
}
/// <summary>
/// Adds robots.txt redirection to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseRobotsRedirection(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>();
}
}
}
@@ -0,0 +1,362 @@
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.Database.Implementations.Enums;
using Jellyfin.Extensions.Json;
using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Net;
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.Extensions.DependencyInjection.Extensions;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
namespace Jellyfin.Server.Extensions
{
/// <summary>
/// API specific extensions for the service collection.
/// </summary>
public static class ApiServiceCollectionExtensions
{
/// <summary>
/// Adds jellyfin API authorization policies to the DI container.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
{
// The default handler must be first so that it is evaluated first
serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, UserPermissionHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
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.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement));
options.AddPolicy(Policies.LyricManagement, new UserPermissionRequirement(PermissionKind.EnableLyricManagement));
options.AddPolicy(
Policies.RequiresElevation,
policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
.RequireClaim(ClaimTypes.Role, UserRoles.Administrator));
});
}
/// <summary>
/// Adds custom legacy authentication to the service collection.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <returns>The updated service collection.</returns>
public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection serviceCollection)
{
return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication)
.AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(AuthenticationSchemes.CustomAuthentication, null);
}
/// <summary>
/// Extension method for adding the Jellyfin API to the service collection.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
/// <param name="config">The <see cref="NetworkConfiguration"/>.</param>
/// <returns>The MVC builder.</returns>
public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies, NetworkConfiguration config)
{
IMvcBuilder mvcBuilder = serviceCollection
.AddCors()
.AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
.Configure<ForwardedHeadersOptions>(options =>
{
ConfigureForwardHeaders(config, options);
})
.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();
}
internal static void ConfigureForwardHeaders(NetworkConfiguration config, ForwardedHeadersOptions 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.
if (config.KnownProxies.Length == 0)
{
options.ForwardedHeaders = ForwardedHeaders.None;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
}
else
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
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.KnownIPNetworks.Count != 0)
{
options.ForwardLimit = null;
}
}
/// <summary>
/// Adds Swagger to the service collection.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <returns>The updated service collection.</returns>
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<string, IOpenApiExtension>
{
{
"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.EnumerateFiles(
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.SchemaFilter<IgnoreEnumSchemaFilter>();
c.SchemaFilter<FlagsEnumSchemaFilter>();
c.OperationFilter<RetryOnTemporarilyUnavailableFilter>();
c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<FileResponseFilter>();
c.OperationFilter<FileRequestFilter>();
c.OperationFilter<ParameterObsoleteFilter>();
c.DocumentFilter<AdditionalModelFilter>();
})
.Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
}
private static void AddPolicy(this AuthorizationOptions authorizationOptions, string policyName, IAuthorizationRequirement authorizationRequirement)
{
authorizationOptions.AddPolicy(policyName, policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication).AddRequirements(authorizationRequirement);
});
}
/// <summary>
/// Sets up the proxy configuration based on the addresses/subnets in <paramref name="allowedProxies"/>.
/// </summary>
/// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param>
/// <param name="allowedProxies">The string array to parse.</param>
/// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param>
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 ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize);
}
else if (NetworkUtils.TryParseToSubnet(allowedProxies[i], out var subnet))
{
AddIPAddress(config, options, subnet.Address, subnet.Subnet.PrefixLength);
}
else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
{
foreach (var address in addresses)
{
AddIPAddress(config, options, address, address.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.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 == NetworkConstants.MinimumIPv4PrefixSize)
{
options.KnownProxies.Add(addr);
}
else
{
options.KnownIPNetworks.Add(new System.Net.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<Dictionary<ImageType, string>>(() =>
new OpenApiSchema
{
Type = "object",
AdditionalProperties = new OpenApiSchema
{
Type = "string"
}
});
// Support dictionary with nullable string value.
options.MapType<Dictionary<string, string?>>(() =>
new OpenApiSchema
{
Type = "object",
AdditionalProperties = new OpenApiSchema
{
Type = "string",
Nullable = true
}
});
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
options.MapType<Version>(() => new OpenApiSchema
{
Type = "string"
});
}
}
}
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using Jellyfin.Server.Helpers;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Extensions;
/// <summary>
/// Extensions for configuring the web host builder.
/// </summary>
public static class WebHostBuilderExtensions
{
/// <summary>
/// Configure the web host builder.
/// </summary>
/// <param name="builder">The builder to configure.</param>
/// <param name="appHost">The application host.</param>
/// <param name="startupConfig">The application configuration.</param>
/// <param name="appPaths">The application paths.</param>
/// <param name="logger">The logger.</param>
/// <returns>The configured web host builder.</returns>
public static IWebHostBuilder ConfigureWebHostBuilder(
this IWebHostBuilder builder,
CoreAppHost appHost,
IConfiguration startupConfig,
IApplicationPaths appPaths,
ILogger logger)
{
return builder
.UseKestrel((builderContext, options) =>
{
SetupJellyfinWebServer(
appHost.NetManager.GetAllBindInterfaces(false),
appHost.HttpPort,
appHost.ListenWithHttps ? appHost.HttpsPort : null,
appHost.Certificate,
startupConfig,
appPaths,
logger,
builderContext,
options);
})
.UseStartup(context => new Startup(appHost, context.Configuration));
}
/// <summary>
/// Configures a Kestrel type webServer to bind to the specific arguments.
/// </summary>
/// <param name="addresses">The IP addresses that should be listend to.</param>
/// <param name="httpPort">The http port.</param>
/// <param name="httpsPort">If set the https port. If set you must also set the certificate.</param>
/// <param name="certificate">The certificate used for https port.</param>
/// <param name="startupConfig">The startup config.</param>
/// <param name="appPaths">The app paths.</param>
/// <param name="logger">A logger.</param>
/// <param name="builderContext">The kestrel build pipeline context.</param>
/// <param name="options">The kestrel server options.</param>
/// <exception cref="InvalidOperationException">Will be thrown when a https port is set but no or an invalid certificate is provided.</exception>
public static void SetupJellyfinWebServer(
IReadOnlyList<IPData> addresses,
int httpPort,
int? httpsPort,
X509Certificate2? certificate,
IConfiguration startupConfig,
IApplicationPaths appPaths,
ILogger logger,
WebHostBuilderContext builderContext,
KestrelServerOptions options)
{
bool flagged = false;
foreach (var netAdd in addresses)
{
var address = netAdd.Address;
logger.LogInformation("Kestrel is listening on {Address}", address.Equals(IPAddress.IPv6Any) ? "all interfaces" : address);
options.Listen(netAdd.Address, httpPort);
if (httpsPort.HasValue)
{
if (builderContext.HostingEnvironment.IsDevelopment())
{
try
{
options.Listen(
address,
httpsPort.Value,
listenOptions => listenOptions.UseHttps());
}
catch (InvalidOperationException)
{
if (!flagged)
{
logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted");
flagged = true;
}
}
}
else
{
if (certificate is null)
{
throw new InvalidOperationException("Cannot run jellyfin with https without setting a valid certificate.");
}
options.Listen(
address,
httpsPort.Value,
listenOptions => listenOptions.UseHttps(certificate));
}
}
}
// Bind to unix socket (only on unix systems)
if (startupConfig.UseUnixSocket() && Environment.OSVersion.Platform == PlatformID.Unix)
{
var socketPath = StartupHelpers.GetUnixSocketPath(startupConfig, appPaths);
// Workaround for https://github.com/aspnet/AspNetCore/issues/14134
if (File.Exists(socketPath))
{
File.Delete(socketPath);
}
options.ListenUnixSocket(socketPath);
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
}
}
}