ApiServiceCollectionExtensions.cs 20 KB

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