ApiServiceCollectionExtensions.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Net.Sockets;
  8. using System.Reflection;
  9. using System.Runtime.CompilerServices;
  10. using System.Text;
  11. using Emby.Server.Implementations;
  12. using Jellyfin.Api.Auth;
  13. using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
  14. using Jellyfin.Api.Auth.DownloadPolicy;
  15. using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy;
  16. using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy;
  17. using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy;
  18. using Jellyfin.Api.Auth.IgnoreParentalControlPolicy;
  19. using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy;
  20. using Jellyfin.Api.Auth.LocalAccessPolicy;
  21. using Jellyfin.Api.Auth.RequiresElevationPolicy;
  22. using Jellyfin.Api.Auth.SyncPlayAccessPolicy;
  23. using Jellyfin.Api.Constants;
  24. using Jellyfin.Api.Controllers;
  25. using Jellyfin.Api.ModelBinders;
  26. using Jellyfin.Data.Enums;
  27. using Jellyfin.Networking.Configuration;
  28. using Jellyfin.Networking.Manager;
  29. using Jellyfin.Server.Configuration;
  30. using Jellyfin.Server.Filters;
  31. using Jellyfin.Server.Formatters;
  32. using MediaBrowser.Common.Json;
  33. using MediaBrowser.Common.Net;
  34. using MediaBrowser.Model.Entities;
  35. using Microsoft.AspNetCore.Authentication;
  36. using Microsoft.AspNetCore.Authorization;
  37. using Microsoft.AspNetCore.Builder;
  38. using Microsoft.AspNetCore.Cors.Infrastructure;
  39. using Microsoft.AspNetCore.HttpOverrides;
  40. using Microsoft.Extensions.DependencyInjection;
  41. using Microsoft.OpenApi.Any;
  42. using Microsoft.OpenApi.Interfaces;
  43. using Microsoft.OpenApi.Models;
  44. using Swashbuckle.AspNetCore.SwaggerGen;
  45. using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
  46. namespace Jellyfin.Server.Extensions
  47. {
  48. /// <summary>
  49. /// API specific extensions for the service collection.
  50. /// </summary>
  51. public static class ApiServiceCollectionExtensions
  52. {
  53. /// <summary>
  54. /// Adds jellyfin API authorization policies to the DI container.
  55. /// </summary>
  56. /// <param name="serviceCollection">The service collection.</param>
  57. /// <returns>The updated service collection.</returns>
  58. public static IServiceCollection AddJellyfinApiAuthorization(this IServiceCollection serviceCollection)
  59. {
  60. serviceCollection.AddSingleton<IAuthorizationHandler, DefaultAuthorizationHandler>();
  61. serviceCollection.AddSingleton<IAuthorizationHandler, DownloadHandler>();
  62. serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrDefaultHandler>();
  63. serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupOrElevatedHandler>();
  64. serviceCollection.AddSingleton<IAuthorizationHandler, IgnoreParentalControlHandler>();
  65. serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeOrIgnoreParentalControlSetupHandler>();
  66. serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessHandler>();
  67. serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
  68. serviceCollection.AddSingleton<IAuthorizationHandler, RequiresElevationHandler>();
  69. serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
  70. return serviceCollection.AddAuthorizationCore(options =>
  71. {
  72. options.AddPolicy(
  73. Policies.DefaultAuthorization,
  74. policy =>
  75. {
  76. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  77. policy.AddRequirements(new DefaultAuthorizationRequirement());
  78. });
  79. options.AddPolicy(
  80. Policies.Download,
  81. policy =>
  82. {
  83. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  84. policy.AddRequirements(new DownloadRequirement());
  85. });
  86. options.AddPolicy(
  87. Policies.FirstTimeSetupOrDefault,
  88. policy =>
  89. {
  90. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  91. policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement());
  92. });
  93. options.AddPolicy(
  94. Policies.FirstTimeSetupOrElevated,
  95. policy =>
  96. {
  97. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  98. policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement());
  99. });
  100. options.AddPolicy(
  101. Policies.IgnoreParentalControl,
  102. policy =>
  103. {
  104. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  105. policy.AddRequirements(new IgnoreParentalControlRequirement());
  106. });
  107. options.AddPolicy(
  108. Policies.FirstTimeSetupOrIgnoreParentalControl,
  109. policy =>
  110. {
  111. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  112. policy.AddRequirements(new FirstTimeOrIgnoreParentalControlSetupRequirement());
  113. });
  114. options.AddPolicy(
  115. Policies.LocalAccessOnly,
  116. policy =>
  117. {
  118. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  119. policy.AddRequirements(new LocalAccessRequirement());
  120. });
  121. options.AddPolicy(
  122. Policies.LocalAccessOrRequiresElevation,
  123. policy =>
  124. {
  125. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  126. policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement());
  127. });
  128. options.AddPolicy(
  129. Policies.RequiresElevation,
  130. policy =>
  131. {
  132. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  133. policy.AddRequirements(new RequiresElevationRequirement());
  134. });
  135. options.AddPolicy(
  136. Policies.SyncPlayHasAccess,
  137. policy =>
  138. {
  139. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  140. policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
  141. });
  142. options.AddPolicy(
  143. Policies.SyncPlayCreateGroup,
  144. policy =>
  145. {
  146. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  147. policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
  148. });
  149. options.AddPolicy(
  150. Policies.SyncPlayJoinGroup,
  151. policy =>
  152. {
  153. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  154. policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
  155. });
  156. options.AddPolicy(
  157. Policies.SyncPlayIsInGroup,
  158. policy =>
  159. {
  160. policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
  161. policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
  162. });
  163. });
  164. }
  165. /// <summary>
  166. /// Adds custom legacy authentication to the service collection.
  167. /// </summary>
  168. /// <param name="serviceCollection">The service collection.</param>
  169. /// <returns>The updated service collection.</returns>
  170. public static AuthenticationBuilder AddCustomAuthentication(this IServiceCollection serviceCollection)
  171. {
  172. return serviceCollection.AddAuthentication(AuthenticationSchemes.CustomAuthentication)
  173. .AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>(AuthenticationSchemes.CustomAuthentication, null);
  174. }
  175. /// <summary>
  176. /// Extension method for adding the jellyfin API to the service collection.
  177. /// </summary>
  178. /// <param name="serviceCollection">The service collection.</param>
  179. /// <param name="pluginAssemblies">An IEnumerable containing all plugin assemblies with API controllers.</param>
  180. /// <param name="config">The <see cref="NetworkConfiguration"/>.</param>
  181. /// <returns>The MVC builder.</returns>
  182. public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, IEnumerable<Assembly> pluginAssemblies, NetworkConfiguration config)
  183. {
  184. IMvcBuilder mvcBuilder = serviceCollection
  185. .AddCors()
  186. .AddTransient<ICorsPolicyProvider, CorsPolicyProvider>()
  187. .Configure<ForwardedHeadersOptions>(options =>
  188. {
  189. // https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/HttpOverrides/src/ForwardedHeadersMiddleware.cs
  190. // Enable debug logging on Microsoft.AspNetCore.HttpOverrides.ForwardedHeadersMiddleware to help investigate issues.
  191. options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
  192. if (config.KnownProxies.Length == 0)
  193. {
  194. options.KnownNetworks.Clear();
  195. options.KnownProxies.Clear();
  196. }
  197. else
  198. {
  199. ParseList(config, config.KnownProxies, options);
  200. }
  201. // Only set forward limit if we have some known proxies or some known networks.
  202. if (options.KnownProxies.Count != 0 || options.KnownNetworks.Count != 0)
  203. {
  204. options.ForwardLimit = null;
  205. }
  206. })
  207. .AddMvc(opts =>
  208. {
  209. // Allow requester to change between camelCase and PascalCase
  210. opts.RespectBrowserAcceptHeader = true;
  211. opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter());
  212. opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter());
  213. opts.OutputFormatters.Add(new CssOutputFormatter());
  214. opts.OutputFormatters.Add(new XmlOutputFormatter());
  215. opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider());
  216. })
  217. // Clear app parts to avoid other assemblies being picked up
  218. .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear())
  219. .AddApplicationPart(typeof(StartupController).Assembly)
  220. .AddJsonOptions(options =>
  221. {
  222. // Update all properties that are set in JsonDefaults
  223. var jsonOptions = JsonDefaults.GetPascalCaseOptions();
  224. // From JsonDefaults
  225. options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling;
  226. options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
  227. options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
  228. options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
  229. options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive;
  230. options.JsonSerializerOptions.Converters.Clear();
  231. foreach (var converter in jsonOptions.Converters)
  232. {
  233. options.JsonSerializerOptions.Converters.Add(converter);
  234. }
  235. // From JsonDefaults.PascalCase
  236. options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy;
  237. });
  238. foreach (Assembly pluginAssembly in pluginAssemblies)
  239. {
  240. mvcBuilder.AddApplicationPart(pluginAssembly);
  241. }
  242. return mvcBuilder.AddControllersAsServices();
  243. }
  244. /// <summary>
  245. /// Adds Swagger to the service collection.
  246. /// </summary>
  247. /// <param name="serviceCollection">The service collection.</param>
  248. /// <returns>The updated service collection.</returns>
  249. public static IServiceCollection AddJellyfinApiSwagger(this IServiceCollection serviceCollection)
  250. {
  251. return serviceCollection.AddSwaggerGen(c =>
  252. {
  253. c.SwaggerDoc("api-docs", new OpenApiInfo
  254. {
  255. Title = "Jellyfin API",
  256. Version = "v1",
  257. Extensions = new Dictionary<string, IOpenApiExtension>
  258. {
  259. {
  260. "x-jellyfin-version",
  261. new OpenApiString(typeof(ApplicationHost).Assembly.GetName().Version?.ToString())
  262. }
  263. }
  264. });
  265. c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
  266. {
  267. Type = SecuritySchemeType.ApiKey,
  268. In = ParameterLocation.Header,
  269. Name = "X-Emby-Authorization",
  270. Description = "API key header parameter"
  271. });
  272. // Add all xml doc files to swagger generator.
  273. var xmlFiles = Directory.GetFiles(
  274. AppContext.BaseDirectory,
  275. "*.xml",
  276. SearchOption.TopDirectoryOnly);
  277. foreach (var xmlFile in xmlFiles)
  278. {
  279. c.IncludeXmlComments(xmlFile);
  280. }
  281. // Order actions by route path, then by http method.
  282. c.OrderActionsBy(description =>
  283. $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}");
  284. // Use method name as operationId
  285. c.CustomOperationIds(
  286. description =>
  287. {
  288. description.TryGetMethodInfo(out MethodInfo methodInfo);
  289. // Attribute name, method name, none.
  290. return description?.ActionDescriptor?.AttributeRouteInfo?.Name
  291. ?? methodInfo?.Name
  292. ?? null;
  293. });
  294. // TODO - remove when all types are supported in System.Text.Json
  295. c.AddSwaggerTypeMappings();
  296. c.OperationFilter<SecurityRequirementsOperationFilter>();
  297. c.OperationFilter<FileResponseFilter>();
  298. c.DocumentFilter<WebsocketModelFilter>();
  299. });
  300. }
  301. /// <summary>
  302. /// Sets up the proxy configuration based on the addresses in <paramref name="userList"/>.
  303. /// </summary>
  304. /// <param name="config">The <see cref="NetworkConfiguration"/> containing the config settings.</param>
  305. /// <param name="userList">The string array to parse.</param>
  306. /// <param name="options">The <see cref="ForwardedHeadersOptions"/> instance.</param>
  307. internal static void ParseList(NetworkConfiguration config, string[] userList, ForwardedHeadersOptions options)
  308. {
  309. for (var i = 0; i < userList.Length; i++)
  310. {
  311. if (IPNetAddress.TryParse(userList[i], out var addr))
  312. {
  313. AddIpAddress(config, options, addr.Address, addr.PrefixLength);
  314. }
  315. else if (IPHost.TryParse(userList[i], out var host))
  316. {
  317. foreach (var address in host.GetAddresses())
  318. {
  319. AddIpAddress(config, options, addr.Address, addr.PrefixLength);
  320. }
  321. }
  322. }
  323. }
  324. private static void AddIpAddress(NetworkConfiguration config, ForwardedHeadersOptions options, IPAddress addr, int prefixLength)
  325. {
  326. if ((!config.EnableIPV4 && addr.AddressFamily == AddressFamily.InterNetwork) || (!config.EnableIPV6 && addr.AddressFamily == AddressFamily.InterNetworkV6))
  327. {
  328. return;
  329. }
  330. // In order for dual-mode sockets to be used, IP6 has to be enabled in JF and an interface has to have an IP6 address.
  331. if (NetworkManager.SystemIP6Enabled && addr.AddressFamily == AddressFamily.InterNetwork && config.EnableIPV6)
  332. {
  333. // If the server is using dual-mode sockets, IPv4 addresses are supplied in an IPv6 format.
  334. // https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-5.0 .
  335. addr = addr.MapToIPv6();
  336. }
  337. if (prefixLength == 32)
  338. {
  339. options.KnownProxies.Add(addr);
  340. }
  341. else
  342. {
  343. options.KnownNetworks.Add(new IPNetwork(addr, prefixLength));
  344. }
  345. }
  346. private static void AddSwaggerTypeMappings(this SwaggerGenOptions options)
  347. {
  348. /*
  349. * TODO remove when System.Text.Json properly supports non-string keys.
  350. * Used in BaseItemDto.ImageBlurHashes
  351. */
  352. options.MapType<Dictionary<ImageType, string>>(() =>
  353. new OpenApiSchema
  354. {
  355. Type = "object",
  356. AdditionalProperties = new OpenApiSchema
  357. {
  358. Type = "string"
  359. }
  360. });
  361. /*
  362. * Support BlurHash dictionary
  363. */
  364. options.MapType<Dictionary<ImageType, Dictionary<string, string>>>(() =>
  365. new OpenApiSchema
  366. {
  367. Type = "object",
  368. Properties = typeof(ImageType).GetEnumNames().ToDictionary(
  369. name => name,
  370. name => new OpenApiSchema
  371. {
  372. Type = "object",
  373. AdditionalProperties = new OpenApiSchema
  374. {
  375. Type = "string"
  376. }
  377. })
  378. });
  379. }
  380. }
  381. }