From b16da095493bc207f4196b8b61cfc768a237a5bc Mon Sep 17 00:00:00 2001 From: David Date: Wed, 10 Jun 2020 15:18:13 +0200 Subject: Move /System Endpoint to Jellyfin.Api --- Jellyfin.Api/Controllers/SystemController.cs | 222 +++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 Jellyfin.Api/Controllers/SystemController.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs new file mode 100644 index 000000000..cab6f308f --- /dev/null +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The system controller. + /// + [Route("/System")] + public class SystemController : BaseJellyfinApiController + { + private readonly IServerApplicationHost _appHost; + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + private readonly INetworkManager _network; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public SystemController( + IServerConfigurationManager serverConfigurationManager, + IServerApplicationHost appHost, + IFileSystem fileSystem, + INetworkManager network, + ILogger logger) + { + _appPaths = serverConfigurationManager.ApplicationPaths; + _appHost = appHost; + _fileSystem = fileSystem; + _network = network; + _logger = logger; + } + + /// + /// Gets information about the server. + /// + /// Information retrieved. + /// A with info about the system. + [HttpGet("Info")] + // TODO: Authorize EscapeParentalControl + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetSystemInfo() + { + return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Gets public information about the server. + /// + /// Information retrieved. + /// A with public info about the system. + [HttpGet("Info/Public")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task> GetPublicSystemInfo() + { + return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false); + } + + /// + /// Pings the system. + /// + /// Information retrieved. + /// The server name. + [HttpGet("Ping")] + [HttpPost("Ping")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult PingSystem() + { + return _appHost.Name; + } + + /// + /// Restarts the application. + /// + /// Server restarted. + /// No content. Server restarted. + [HttpPost("Restart")] + // TODO: Authorize AllowLocal = true + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult RestartApplication() + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + _appHost.Restart(); + }); + return NoContent(); + } + + /// + /// Shuts down the application. + /// + /// Server shut down. + /// No content. Server shut down. + [HttpPost("Shutdown")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult ShutdownApplication() + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + await _appHost.Shutdown().ConfigureAwait(false); + }); + return NoContent(); + } + + /// + /// Gets a list of available server log files. + /// + /// Information retrieved. + /// An array of with the available log files. + [HttpGet("Logs")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetServerLogs() + { + IEnumerable files; + + try + { + files = _fileSystem.GetFiles(_appPaths.LogDirectoryPath, new[] { ".txt", ".log" }, true, false); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error getting logs"); + files = Enumerable.Empty(); + } + + var result = files.Select(i => new LogFile + { + DateCreated = _fileSystem.GetCreationTimeUtc(i), + DateModified = _fileSystem.GetLastWriteTimeUtc(i), + Name = i.Name, + Size = i.Length + }) + .OrderByDescending(i => i.DateModified) + .ThenByDescending(i => i.DateCreated) + .ThenBy(i => i.Name) + .ToArray(); + + return result; + } + + /// + /// Gets information about the request endpoint. + /// + /// Information retrieved. + /// with information about the endpoint. + [HttpGet("Endpoint")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetEndpointInfo() + { + return new EndPointInfo + { + IsLocal = Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress), + IsInNetwork = _network.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString()) + }; + } + + /// + /// Gets a log file. + /// + /// The name of the log file to get. + /// Log file retrieved. + /// The log file. + [HttpGet("Logs/Log")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetLogFile([FromQuery, Required] string name) + { + var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) + .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); + + // For older files, assume fully static + var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; + + FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare); + return File(stream, "text/plain"); + } + + /// + /// Gets wake on lan information. + /// + /// Information retrieved. + /// An with the WakeOnLan infos. + [HttpGet("WakeOnLanInfo")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetWakeOnLanInfo() + { + var result = _appHost.GetWakeOnLanInfo(); + return Ok(result); + } + } +} -- cgit v1.2.3 From b51b9653ac9ff015d34233099bdc744fa153f8ee Mon Sep 17 00:00:00 2001 From: David Date: Fri, 19 Jun 2020 14:29:32 +0200 Subject: Add missing authorization policies --- Jellyfin.Api/Controllers/SystemController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index cab6f308f..f4dae40ef 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -60,7 +60,7 @@ namespace Jellyfin.Api.Controllers /// Information retrieved. /// A with info about the system. [HttpGet("Info")] - // TODO: Authorize EscapeParentalControl + [Authorize(Policy = Policies.IgnoreSchedule)] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetSystemInfo() @@ -99,7 +99,7 @@ namespace Jellyfin.Api.Controllers /// Server restarted. /// No content. Server restarted. [HttpPost("Restart")] - // TODO: Authorize AllowLocal = true + [Authorize(Policy = Policies.LocalAccessOnly)] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RestartApplication() -- cgit v1.2.3 From 6b72fb86316786451be4fb84e4ba89d496b4ef2f Mon Sep 17 00:00:00 2001 From: David Date: Mon, 22 Jun 2020 15:49:15 +0200 Subject: Add missing default authorization policy --- Jellyfin.Api/Controllers/SystemController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index f4dae40ef..e33821b24 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers /// Information retrieved. /// with information about the endpoint. [HttpGet("Endpoint")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetEndpointInfo() { @@ -211,7 +211,7 @@ namespace Jellyfin.Api.Controllers /// Information retrieved. /// An with the WakeOnLan infos. [HttpGet("WakeOnLanInfo")] - [Authorize] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetWakeOnLanInfo() { -- cgit v1.2.3 From 73bcda7eac6d0785745179fe4b7f58b6bc4ec488 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 27 Jun 2020 10:50:44 -0600 Subject: Make all optional strings nullable --- Jellyfin.Api/Controllers/AlbumsController.cs | 4 +- Jellyfin.Api/Controllers/ApiKeyController.cs | 4 +- Jellyfin.Api/Controllers/CollectionController.cs | 8 ++-- .../Controllers/ConfigurationController.cs | 4 +- Jellyfin.Api/Controllers/DashboardController.cs | 2 +- Jellyfin.Api/Controllers/DevicesController.cs | 8 ++-- .../Controllers/DisplayPreferencesController.cs | 12 +++--- Jellyfin.Api/Controllers/ImageByNameController.cs | 12 +++--- Jellyfin.Api/Controllers/InstantMixController.cs | 16 ++++---- Jellyfin.Api/Controllers/ItemUpdateController.cs | 2 +- Jellyfin.Api/Controllers/LibraryController.cs | 16 ++++---- .../Controllers/LibraryStructureController.cs | 22 +++++------ .../Controllers/NotificationsController.cs | 4 +- Jellyfin.Api/Controllers/PackageController.cs | 8 ++-- Jellyfin.Api/Controllers/PlaylistsController.cs | 14 +++---- Jellyfin.Api/Controllers/PluginsController.cs | 4 +- Jellyfin.Api/Controllers/RemoteImageController.cs | 2 +- .../Controllers/ScheduledTasksController.cs | 8 ++-- Jellyfin.Api/Controllers/SearchController.cs | 10 ++--- Jellyfin.Api/Controllers/SessionController.cs | 46 +++++++++++----------- Jellyfin.Api/Controllers/StartupController.cs | 6 +-- Jellyfin.Api/Controllers/SubtitleController.cs | 14 +++---- Jellyfin.Api/Controllers/SystemController.cs | 2 +- Jellyfin.Api/Controllers/TvShowsController.cs | 8 ++-- Jellyfin.Api/Controllers/UserController.cs | 8 ++-- Jellyfin.Api/Controllers/UserLibraryController.cs | 6 +-- Jellyfin.Api/Controllers/UserViewsController.cs | 2 +- .../Controllers/VideoAttachmentsController.cs | 2 +- Jellyfin.Api/Controllers/VideosController.cs | 2 +- Jellyfin.Api/Controllers/YearsController.cs | 16 ++++---- Jellyfin.Api/Extensions/DtoExtensions.cs | 4 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 10 ++--- Jellyfin.Api/Helpers/SimilarItemsHelper.cs | 2 +- 33 files changed, 143 insertions(+), 145 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs index 622123873..70315b0a3 100644 --- a/Jellyfin.Api/Controllers/AlbumsController.cs +++ b/Jellyfin.Api/Controllers/AlbumsController.cs @@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers public ActionResult> GetSimilarAlbums( [FromRoute] string albumId, [FromQuery] Guid userId, - [FromQuery] string excludeArtistIds, + [FromQuery] string? excludeArtistIds, [FromQuery] int? limit) { var dtoOptions = new DtoOptions().AddClientFields(Request); @@ -85,7 +85,7 @@ namespace Jellyfin.Api.Controllers public ActionResult> GetSimilarArtists( [FromRoute] string artistId, [FromQuery] Guid userId, - [FromQuery] string excludeArtistIds, + [FromQuery] string? excludeArtistIds, [FromQuery] int? limit) { var dtoOptions = new DtoOptions().AddClientFields(Request); diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index ed521c1fc..fef4d7262 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Keys")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateKey([FromQuery, Required] string app) + public ActionResult CreateKey([FromQuery, Required] string? app) { _authRepo.Create(new AuthenticationInfo { @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Keys/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RevokeKey([FromRoute] string key) + public ActionResult RevokeKey([FromRoute] string? key) { _sessionManager.RevokeToken(key); return NoContent(); diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 29db0b178..7ff98b251 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -51,8 +51,8 @@ namespace Jellyfin.Api.Controllers [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult CreateCollection( - [FromQuery] string name, - [FromQuery] string ids, + [FromQuery] string? name, + [FromQuery] string? ids, [FromQuery] bool isLocked, [FromQuery] Guid? parentId) { @@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string itemIds) + public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds) { _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true)); return NoContent(); @@ -101,7 +101,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpDelete("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string itemIds) + public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds) { _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true)); return NoContent(); diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index d275ed2eb..13933cb33 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -70,7 +70,7 @@ namespace Jellyfin.Api.Controllers /// Configuration. [HttpGet("Configuration/{key}")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetNamedConfiguration([FromRoute] string key) + public ActionResult GetNamedConfiguration([FromRoute] string? key) { return _configurationManager.GetConfiguration(key); } @@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Configuration/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task UpdateNamedConfiguration([FromRoute] string key) + public async Task UpdateNamedConfiguration([FromRoute] string? key) { var configurationType = _configurationManager.GetConfigurationType(key); var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 6cfee2463..699ef6bf7 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("/web/ConfigurationPage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDashboardConfigurationPage([FromQuery] string name) + public ActionResult GetDashboardConfigurationPage([FromQuery] string? name) { IPlugin? plugin = null; Stream? stream = null; diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 55ca7b7c0..3cf7b3378 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceInfo([FromQuery, BindRequired] string id) + public ActionResult GetDeviceInfo([FromQuery, BindRequired] string? id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo == null) @@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) + public ActionResult GetDeviceOptions([FromQuery, BindRequired] string? id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo == null) @@ -111,7 +111,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateDeviceOptions( - [FromQuery, BindRequired] string id, + [FromQuery, BindRequired] string? id, [FromBody, BindRequired] DeviceOptions deviceOptions) { var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); @@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteDevice([FromQuery, BindRequired] string id) + public ActionResult DeleteDevice([FromQuery, BindRequired] string? id) { var existingDevice = _deviceManager.GetDevice(id); if (existingDevice == null) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 3f946d9d2..1255e6dab 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -39,9 +39,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("{displayPreferencesId}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDisplayPreferences( - [FromRoute] string displayPreferencesId, - [FromQuery] [Required] string userId, - [FromQuery] [Required] string client) + [FromRoute] string? displayPreferencesId, + [FromQuery] [Required] string? userId, + [FromQuery] [Required] string? client) { return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client); } @@ -59,9 +59,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] public ActionResult UpdateDisplayPreferences( - [FromRoute] string displayPreferencesId, - [FromQuery, BindRequired] string userId, - [FromQuery, BindRequired] string client, + [FromRoute] string? displayPreferencesId, + [FromQuery, BindRequired] string? userId, + [FromQuery, BindRequired] string? client, [FromBody, BindRequired] DisplayPreferences displayPreferences) { _displayPreferencesRepository.SaveDisplayPreferences( diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 4800c0608..5244c35b8 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetGeneralImage([FromRoute] string name, [FromRoute] string type) + public ActionResult GetGeneralImage([FromRoute] string? name, [FromRoute] string? type) { var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) ? "folder" @@ -110,8 +110,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetRatingImage( - [FromRoute] string theme, - [FromRoute] string name) + [FromRoute] string? theme, + [FromRoute] string? name) { return GetImageFile(_applicationPaths.RatingsPath, theme, name); } @@ -143,8 +143,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetMediaInfoImage( - [FromRoute] string theme, - [FromRoute] string name) + [FromRoute] string? theme, + [FromRoute] string? name) { return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); } @@ -156,7 +156,7 @@ namespace Jellyfin.Api.Controllers /// Theme to search. /// File name to search for. /// A containing the image contents on success, or a if the image could not be found. - private ActionResult GetImageFile(string basePath, string theme, string name) + private ActionResult GetImageFile(string basePath, string? theme, string? name) { var themeFolder = Path.Combine(basePath, theme); if (Directory.Exists(themeFolder)) diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index f1ff770a4..9d945fe2b 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] Guid id, [FromQuery] Guid userId, [FromQuery] int? limit, - [FromQuery] string fields, + [FromQuery] string? fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -100,7 +100,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] Guid id, [FromQuery] Guid userId, [FromQuery] int? limit, - [FromQuery] string fields, + [FromQuery] string? fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -135,7 +135,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] Guid id, [FromQuery] Guid userId, [FromQuery] int? limit, - [FromQuery] string fields, + [FromQuery] string? fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -167,10 +167,10 @@ namespace Jellyfin.Api.Controllers [HttpGet("/MusicGenres/{name}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromMusicGenre( - [FromRoute] string name, + [FromRoute] string? name, [FromQuery] Guid userId, [FromQuery] int? limit, - [FromQuery] string fields, + [FromQuery] string? fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] Guid id, [FromQuery] Guid userId, [FromQuery] int? limit, - [FromQuery] string fields, + [FromQuery] string? fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -239,7 +239,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] Guid id, [FromQuery] Guid userId, [FromQuery] int? limit, - [FromQuery] string fields, + [FromQuery] string? fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -274,7 +274,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] Guid id, [FromQuery] Guid userId, [FromQuery] int? limit, - [FromQuery] string fields, + [FromQuery] string? fields, [FromQuery] bool? enableImages, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 384f250ec..c9b2aafcc 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Items/{itemId}/ContentType")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType) + public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType) { var item = _libraryManager.GetItemById(itemId); if (item == null) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 1ecf2ac73..f1106cda6 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -524,7 +524,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Library/Series/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedSeries([FromQuery] string tvdbId) + public ActionResult PostUpdatedSeries([FromQuery] string? tvdbId) { var series = _libraryManager.GetItemList(new InternalItemsQuery { @@ -554,7 +554,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Library/Movies/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedMovies([FromRoute] string tmdbId, [FromRoute] string imdbId) + public ActionResult PostUpdatedMovies([FromRoute] string? tmdbId, [FromRoute] string? imdbId) { var movies = _libraryManager.GetItemList(new InternalItemsQuery { @@ -687,10 +687,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute] Guid itemId, - [FromQuery] string excludeArtistIds, + [FromQuery] string? excludeArtistIds, [FromQuery] Guid userId, [FromQuery] int? limit, - [FromQuery] string fields) + [FromQuery] string? fields) { var item = itemId.Equals(Guid.Empty) ? (!userId.Equals(Guid.Empty) @@ -737,7 +737,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("/Libraries/AvailableOptions")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetLibraryOptionsInfo([FromQuery] string libraryContentType, [FromQuery] bool isNewLibrary) + public ActionResult GetLibraryOptionsInfo([FromQuery] string? libraryContentType, [FromQuery] bool isNewLibrary) { var result = new LibraryOptionsResultDto(); @@ -877,10 +877,10 @@ namespace Jellyfin.Api.Controllers private QueryResult GetSimilarItemsResult( BaseItem item, - string excludeArtistIds, + string? excludeArtistIds, Guid userId, int? limit, - string fields, + string? fields, string[] includeItemTypes, bool isMovie) { @@ -942,7 +942,7 @@ namespace Jellyfin.Api.Controllers return result; } - private static string[] GetRepresentativeItemTypes(string contentType) + private static string[] GetRepresentativeItemTypes(string? contentType) { return contentType switch { diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index e4ac019c9..0c91f8447 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -72,8 +72,8 @@ namespace Jellyfin.Api.Controllers [HttpPost] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task AddVirtualFolder( - [FromQuery] string name, - [FromQuery] string collectionType, + [FromQuery] string? name, + [FromQuery] string? collectionType, [FromQuery] bool refreshLibrary, [FromQuery] string[] paths, [FromQuery] LibraryOptions libraryOptions) @@ -100,7 +100,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task RemoveVirtualFolder( - [FromQuery] string name, + [FromQuery] string? name, [FromQuery] bool refreshLibrary) { await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false); @@ -123,8 +123,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] public ActionResult RenameVirtualFolder( - [FromQuery] string name, - [FromQuery] string newName, + [FromQuery] string? name, + [FromQuery] string? newName, [FromQuery] bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) @@ -205,8 +205,8 @@ namespace Jellyfin.Api.Controllers [HttpPost("Paths")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddMediaPath( - [FromQuery] string name, - [FromQuery] string path, + [FromQuery] string? name, + [FromQuery] string? path, [FromQuery] MediaPathInfo pathInfo, [FromQuery] bool refreshLibrary) { @@ -256,7 +256,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Paths/Update")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateMediaPath( - [FromQuery] string name, + [FromQuery] string? name, [FromQuery] MediaPathInfo pathInfo) { if (string.IsNullOrWhiteSpace(name)) @@ -280,8 +280,8 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Paths")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveMediaPath( - [FromQuery] string name, - [FromQuery] string path, + [FromQuery] string? name, + [FromQuery] string? path, [FromQuery] bool refreshLibrary) { if (string.IsNullOrWhiteSpace(name)) @@ -327,7 +327,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("LibraryOptions")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateLibraryOptions( - [FromQuery] string id, + [FromQuery] string? id, [FromQuery] LibraryOptions libraryOptions) { var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id); diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index cfa7545c9..02aa39b24 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -93,8 +93,8 @@ namespace Jellyfin.Api.Controllers [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult CreateAdminNotification( - [FromQuery] string name, - [FromQuery] string description, + [FromQuery] string? name, + [FromQuery] string? description, [FromQuery] string? url, [FromQuery] NotificationLevel? level) { diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 486575d23..68ae05658 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("/{name}")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetPackageInfo( - [FromRoute] [Required] string name, + [FromRoute] [Required] string? name, [FromQuery] string? assemblyGuid) { var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); @@ -80,9 +80,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] [Authorize(Policy = Policies.RequiresElevation)] public async Task InstallPackage( - [FromRoute] [Required] string name, - [FromQuery] string assemblyGuid, - [FromQuery] string version) + [FromRoute] [Required] string? name, + [FromQuery] string? assemblyGuid, + [FromQuery] string? version) { var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); var package = _installationManager.GetCompatibleVersions( diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 2dc0d2dc7..d62404fc9 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -84,8 +84,8 @@ namespace Jellyfin.Api.Controllers [HttpPost("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddToPlaylist( - [FromRoute] string playlistId, - [FromQuery] string ids, + [FromRoute] string? playlistId, + [FromQuery] string? ids, [FromQuery] Guid userId) { _playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId); @@ -103,8 +103,8 @@ namespace Jellyfin.Api.Controllers [HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult MoveItem( - [FromRoute] string playlistId, - [FromRoute] string itemId, + [FromRoute] string? playlistId, + [FromRoute] string? itemId, [FromRoute] int newIndex) { _playlistManager.MoveItem(playlistId, itemId, newIndex); @@ -120,7 +120,7 @@ namespace Jellyfin.Api.Controllers /// An on success. [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds) + public ActionResult RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds) { _playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true)); return NoContent(); @@ -147,11 +147,11 @@ namespace Jellyfin.Api.Controllers [FromRoute] Guid userId, [FromRoute] int? startIndex, [FromRoute] int? limit, - [FromRoute] string fields, + [FromRoute] string? fields, [FromRoute] bool? enableImages, [FromRoute] bool? enableUserData, [FromRoute] int? imageTypeLimit, - [FromRoute] string enableImageTypes) + [FromRoute] string? enableImageTypes) { var playlist = (Playlist)_libraryManager.GetItemById(playlistId); if (playlist == null) diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index fd48983ea..056395a51 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -166,7 +166,7 @@ namespace Jellyfin.Api.Controllers [Obsolete("This endpoint should not be used.")] [HttpPost("RegistrationRecords/{name}")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetRegistrationStatus([FromRoute] string name) + public ActionResult GetRegistrationStatus([FromRoute] string? name) { return new MBRegistrationRecord { @@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers [Obsolete("Paid plugins are not supported")] [HttpGet("/Registrations/{name}")] [ProducesResponseType(StatusCodes.Status501NotImplemented)] - public ActionResult GetRegistration([FromRoute] string name) + public ActionResult GetRegistration([FromRoute] string? name) { // TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins, // delete all these registration endpoints. They are only kept for compatibility. diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index a0d14be7a..6fff30129 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers public async Task DownloadRemoteImage( [FromRoute] Guid itemId, [FromQuery, BindRequired] ImageType type, - [FromQuery] string imageUrl) + [FromQuery] string? imageUrl) { var item = _libraryManager.GetItemById(itemId); if (item == null) diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index bf5c3076e..3df325e3b 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("{taskId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetTask([FromRoute] string taskId) + public ActionResult GetTask([FromRoute] string? taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); @@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Running/{taskId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult StartTask([FromRoute] string taskId) + public ActionResult StartTask([FromRoute] string? taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); @@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Running/{taskId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult StopTask([FromRoute] string taskId) + public ActionResult StopTask([FromRoute] string? taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateTask( - [FromRoute] string taskId, + [FromRoute] string? taskId, [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index d971889db..14dc0815c 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -81,11 +81,11 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] Guid userId, - [FromQuery, Required] string searchTerm, - [FromQuery] string includeItemTypes, - [FromQuery] string excludeItemTypes, - [FromQuery] string mediaTypes, - [FromQuery] string parentId, + [FromQuery, Required] string? searchTerm, + [FromQuery] string? includeItemTypes, + [FromQuery] string? excludeItemTypes, + [FromQuery] string? mediaTypes, + [FromQuery] string? parentId, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, [FromQuery] bool? isNews, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 39da4178d..bd738aa38 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSessions( [FromQuery] Guid controllableByUserId, - [FromQuery] string deviceId, + [FromQuery] string? deviceId, [FromQuery] int? activeWithinSeconds) { var result = _sessionManager.Sessions; @@ -123,10 +123,10 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/{sessionId}/Viewing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DisplayContent( - [FromRoute] string sessionId, - [FromQuery] string itemType, - [FromQuery] string itemId, - [FromQuery] string itemName) + [FromRoute] string? sessionId, + [FromQuery] string? itemType, + [FromQuery] string? itemId, + [FromQuery] string? itemName) { var command = new BrowseRequest { @@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/{sessionId}/Playing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult Play( - [FromRoute] string sessionId, + [FromRoute] string? sessionId, [FromQuery] Guid[] itemIds, [FromQuery] long? startPositionTicks, [FromQuery] PlayCommand playCommand, @@ -191,7 +191,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/{sessionId}/Playing/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendPlaystateCommand( - [FromRoute] string sessionId, + [FromRoute] string? sessionId, [FromBody] PlaystateRequest playstateRequest) { _sessionManager.SendPlaystateCommand( @@ -213,8 +213,8 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/{sessionId}/System/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendSystemCommand( - [FromRoute] string sessionId, - [FromRoute] string command) + [FromRoute] string? sessionId, + [FromRoute] string? command) { var name = command; if (Enum.TryParse(name, true, out GeneralCommandType commandType)) @@ -244,8 +244,8 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/{sessionId}/Command/{Command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendGeneralCommand( - [FromRoute] string sessionId, - [FromRoute] string command) + [FromRoute] string? sessionId, + [FromRoute] string? command) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); @@ -270,7 +270,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/{sessionId}/Command")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendFullGeneralCommand( - [FromRoute] string sessionId, + [FromRoute] string? sessionId, [FromBody, Required] GeneralCommand command) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); @@ -303,9 +303,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/{sessionId}/Message")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendMessageCommand( - [FromRoute] string sessionId, - [FromQuery] string text, - [FromQuery] string header, + [FromRoute] string? sessionId, + [FromQuery] string? text, + [FromQuery] string? header, [FromQuery] long? timeoutMs) { var command = new MessageCommand @@ -330,7 +330,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/{sessionId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddUserToSession( - [FromRoute] string sessionId, + [FromRoute] string? sessionId, [FromRoute] Guid userId) { _sessionManager.AddAdditionalUser(sessionId, userId); @@ -347,7 +347,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("/Sessions/{sessionId}/User/{userId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveUserFromSession( - [FromRoute] string sessionId, + [FromRoute] string? sessionId, [FromRoute] Guid userId) { _sessionManager.RemoveAdditionalUser(sessionId, userId); @@ -368,9 +368,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/Capabilities")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostCapabilities( - [FromQuery] string id, - [FromQuery] string playableMediaTypes, - [FromQuery] string supportedCommands, + [FromQuery] string? id, + [FromQuery] string? playableMediaTypes, + [FromQuery] string? supportedCommands, [FromQuery] bool supportsMediaControl, [FromQuery] bool supportsSync, [FromQuery] bool supportsPersistentIdentifier = true) @@ -401,7 +401,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/Capabilities/Full")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostFullCapabilities( - [FromQuery] string id, + [FromQuery] string? id, [FromBody, Required] ClientCapabilities capabilities) { if (string.IsNullOrWhiteSpace(id)) @@ -424,8 +424,8 @@ namespace Jellyfin.Api.Controllers [HttpPost("/Sessions/Viewing")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportViewing( - [FromQuery] string sessionId, - [FromQuery] string itemId) + [FromQuery] string? sessionId, + [FromQuery] string? itemId) { string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id; diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index d96b0f993..cc1f797b1 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -75,9 +75,9 @@ namespace Jellyfin.Api.Controllers [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateInitialConfiguration( - [FromForm] string uiCulture, - [FromForm] string metadataCountryCode, - [FromForm] string preferredMetadataLanguage) + [FromForm] string? uiCulture, + [FromForm] string? metadataCountryCode, + [FromForm] string? preferredMetadataLanguage) { _config.Configuration.UICulture = uiCulture; _config.Configuration.MetadataCountryCode = metadataCountryCode; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 95cc39524..baedafaa6 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> SearchRemoteSubtitles( [FromRoute] Guid itemId, - [FromRoute] string language, + [FromRoute] string? language, [FromQuery] bool? isPerfectMatch) { var video = (Video)_libraryManager.GetItemById(itemId); @@ -132,7 +132,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DownloadRemoteSubtitles( [FromRoute] Guid itemId, - [FromRoute] string subtitleId) + [FromRoute] string? subtitleId) { var video = (Video)_libraryManager.GetItemById(itemId); @@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] - public async Task GetRemoteSubtitles([FromRoute] string id) + public async Task GetRemoteSubtitles([FromRoute] string? id) { var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); @@ -186,9 +186,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSubtitle( [FromRoute, Required] Guid itemId, - [FromRoute, Required] string mediaSourceId, + [FromRoute, Required] string? mediaSourceId, [FromRoute, Required] int index, - [FromRoute, Required] string format, + [FromRoute, Required] string? format, [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps, [FromQuery] bool addVttTimeMap, @@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers public async Task GetSubtitlePlaylist( [FromRoute] Guid itemId, [FromRoute] int index, - [FromRoute] string mediaSourceId, + [FromRoute] string? mediaSourceId, [FromQuery, Required] int segmentLength) { var item = (Video)_libraryManager.GetItemById(itemId); @@ -324,7 +324,7 @@ namespace Jellyfin.Api.Controllers /// A with the new subtitle file. private Task EncodeSubtitles( Guid id, - string mediaSourceId, + string? mediaSourceId, int index, string format, long startPositionTicks, diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index e33821b24..bc606f7aa 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Logs/Log")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetLogFile([FromQuery, Required] string name) + public ActionResult GetLogFile([FromQuery, Required] string? name) { var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 6738dd8c8..80b6a2488 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetEpisodes( - [FromRoute] string seriesId, + [FromRoute] string? seriesId, [FromQuery] Guid userId, [FromQuery] string? fields, [FromQuery] int? season, @@ -311,12 +311,12 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetSeasons( - [FromRoute] string seriesId, + [FromRoute] string? seriesId, [FromQuery] Guid userId, - [FromQuery] string fields, + [FromQuery] string? fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, - [FromQuery] string adjacentTo, + [FromQuery] string? adjacentTo, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery] string? enableImageTypes, diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 9f8d564a7..24194dcc2 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -164,8 +164,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> AuthenticateUser( [FromRoute, Required] Guid userId, - [FromQuery, BindRequired] string pw, - [FromQuery, BindRequired] string password) + [FromQuery, BindRequired] string? pw, + [FromQuery, BindRequired] string? password) { var user = _userManager.GetUserById(userId); @@ -483,7 +483,7 @@ namespace Jellyfin.Api.Controllers /// A containing a . [HttpPost("ForgotPassword")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ForgotPassword([FromBody] string enteredUsername) + public async Task> ForgotPassword([FromBody] string? enteredUsername) { var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress) || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()); @@ -501,7 +501,7 @@ namespace Jellyfin.Api.Controllers /// A containing a . [HttpPost("ForgotPassword/Pin")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> ForgotPasswordPin([FromBody] string pin) + public async Task> ForgotPasswordPin([FromBody] string? pin) { var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false); return result; diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 597e70469..ca804ebc9 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -265,12 +265,12 @@ namespace Jellyfin.Api.Controllers public ActionResult> GetLatestMedia( [FromRoute] Guid userId, [FromQuery] Guid parentId, - [FromQuery] string fields, - [FromQuery] string includeItemTypes, + [FromQuery] string? fields, + [FromQuery] string? includeItemTypes, [FromQuery] bool? isPlayed, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, - [FromQuery] string enableImageTypes, + [FromQuery] string? enableImageTypes, [FromQuery] bool? enableUserData, [FromQuery] int limit = 20, [FromQuery] bool groupItems = true) diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 38bf94087..ad8927262 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers [FromRoute] Guid userId, [FromQuery] bool? includeExternalContent, [FromQuery] bool includeHidden, - [FromQuery] string presetViews) + [FromQuery] string? presetViews) { var query = new UserViewQuery { diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 943ba8af3..eef0a93cd 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -50,7 +50,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetAttachment( [FromRoute] Guid videoId, - [FromRoute] string mediaSourceId, + [FromRoute] string? mediaSourceId, [FromRoute] int index) { try diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index effe630a9..fb1141984 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public ActionResult MergeVersions([FromQuery] string itemIds) + public ActionResult MergeVersions([FromQuery] string? itemIds) { var items = RequestHelpers.Split(itemIds, ',', true) .Select(i => _libraryManager.GetItemById(i)) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index a036c818c..a66a3951e 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -64,16 +64,16 @@ namespace Jellyfin.Api.Controllers public ActionResult> GetYears( [FromQuery] int? startIndex, [FromQuery] int? limit, - [FromQuery] string sortOrder, - [FromQuery] string parentId, - [FromQuery] string fields, - [FromQuery] string excludeItemTypes, - [FromQuery] string includeItemTypes, - [FromQuery] string mediaTypes, - [FromQuery] string sortBy, + [FromQuery] string? sortOrder, + [FromQuery] string? parentId, + [FromQuery] string? fields, + [FromQuery] string? excludeItemTypes, + [FromQuery] string? includeItemTypes, + [FromQuery] string? mediaTypes, + [FromQuery] string? sortBy, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, - [FromQuery] string enableImageTypes, + [FromQuery] string? enableImageTypes, [FromQuery] Guid userId, [FromQuery] bool recursive = true, [FromQuery] bool? enableImages = true) diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index ac248109d..e61e9c29d 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Api.Extensions /// DtoOptions object. /// Comma delimited string of fields. /// Modified DtoOptions object. - internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string fields) + internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string? fields) { if (string.IsNullOrEmpty(fields)) { @@ -126,7 +126,7 @@ namespace Jellyfin.Api.Extensions bool? enableImages, bool? enableUserData, int? imageTypeLimit, - string enableImageTypes) + string? enableImageTypes) { dtoOptions.EnableImages = enableImages ?? true; diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index a8ba98f1f..fd86feb8b 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Helpers /// The char that separates the substrings. /// Option to remove empty substrings from the array. /// An array of the substrings. - internal static string[] Split(string value, char separator, bool removeEmpty) + internal static string[] Split(string? value, char separator, bool removeEmpty) { if (string.IsNullOrWhiteSpace(value)) { @@ -99,16 +99,14 @@ namespace Jellyfin.Api.Helpers /// Sort by. /// Sort order. /// Resulting order by. - internal static ValueTuple[] GetOrderBy(string sortBy, string requestedSortOrder) + internal static ValueTuple[] GetOrderBy(string? sortBy, string? requestedSortOrder) { - var val = sortBy; - - if (string.IsNullOrEmpty(val)) + if (string.IsNullOrEmpty(sortBy)) { return Array.Empty>(); } - var vals = val.Split(','); + var vals = sortBy.Split(','); if (string.IsNullOrWhiteSpace(requestedSortOrder)) { requestedSortOrder = "Ascending"; diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs index 751e3c481..fd0c31504 100644 --- a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs +++ b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Api.Helpers IDtoService dtoService, Guid userId, string id, - string excludeArtistIds, + string? excludeArtistIds, int? limit, Type[] includeTypes, Func, List, BaseItem, int> getSimilarityScore) -- cgit v1.2.3 From 9e00aa3014c0044c0918a775c3394763666b30af Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 3 Aug 2020 14:38:51 -0600 Subject: fix openapi validation errors --- Jellyfin.Api/Controllers/AudioController.cs | 8 ++-- Jellyfin.Api/Controllers/BrandingController.cs | 2 +- Jellyfin.Api/Controllers/DlnaServerController.cs | 12 +++--- Jellyfin.Api/Controllers/DynamicHlsController.cs | 4 +- Jellyfin.Api/Controllers/HlsSegmentController.cs | 4 +- Jellyfin.Api/Controllers/ImageController.cs | 28 +++++++------- Jellyfin.Api/Controllers/ItemsController.cs | 2 +- Jellyfin.Api/Controllers/LibraryController.cs | 14 +++---- Jellyfin.Api/Controllers/LiveTvController.cs | 6 +-- Jellyfin.Api/Controllers/SessionController.cs | 2 +- Jellyfin.Api/Controllers/StartupController.cs | 4 +- Jellyfin.Api/Controllers/SubtitleController.cs | 2 +- Jellyfin.Api/Controllers/SyncPlayController.cs | 18 ++++----- Jellyfin.Api/Controllers/SystemController.cs | 4 +- .../Controllers/UniversalAudioController.cs | 6 +-- Jellyfin.Api/Controllers/VideosController.cs | 6 +-- .../Extensions/ApiServiceCollectionExtensions.cs | 11 +++++- tests/Jellyfin.Api.Tests/GetPathValueTests.cs | 45 ---------------------- 18 files changed, 70 insertions(+), 108 deletions(-) delete mode 100644 tests/Jellyfin.Api.Tests/GetPathValueTests.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index ebae1caa0..4de87616c 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -144,10 +144,10 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Audio stream returned. /// A containing the audio file. - [HttpGet("{itemId}/{stream=stream}.{container?}")] - [HttpGet("{itemId}/stream")] - [HttpHead("{itemId}/{stream=stream}.{container?}")] - [HttpHead("{itemId}/stream")] + [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetAudioStreamByContainer")] + [HttpGet("{itemId}/stream", Name = "GetAudioStream")] + [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadAudioStreamByContainer")] + [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetAudioStream( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs index 67790c0e4..1d4836f27 100644 --- a/Jellyfin.Api/Controllers/BrandingController.cs +++ b/Jellyfin.Api/Controllers/BrandingController.cs @@ -44,7 +44,7 @@ namespace Jellyfin.Api.Controllers /// or a if the css is not configured. /// [HttpGet("Css")] - [HttpGet("Css.css")] + [HttpGet("Css.css", Name = "GetBrandingCss_2")] [Produces("text/css")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 2f5561adb..ef507f2ed 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -42,8 +42,8 @@ namespace Jellyfin.Api.Controllers /// Server UUID. /// Description xml returned. /// An containing the description xml. - [HttpGet("{serverId}/description.xml")] [HttpGet("{serverId}/description")] + [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDescriptionXml([FromRoute] string serverId) @@ -60,8 +60,8 @@ namespace Jellyfin.Api.Controllers /// Server UUID. /// Dlna content directory returned. /// An containing the dlna content directory xml. - [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml")] [HttpGet("{serverId}/ContentDirectory/ContentDirectory")] + [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_2")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] @@ -75,8 +75,8 @@ namespace Jellyfin.Api.Controllers /// /// Server UUID. /// Dlna media receiver registrar xml. - [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")] + [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] @@ -90,8 +90,8 @@ namespace Jellyfin.Api.Controllers /// /// Server UUID. /// Dlna media receiver registrar xml. - [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager")] + [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_2")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] @@ -181,7 +181,7 @@ namespace Jellyfin.Api.Controllers /// Server UUID. /// The icon filename. /// Icon stream. - [HttpGet("{serverId}/icons/{filename}")] + [HttpGet("{serverId}/icons/{fileName}")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName) { @@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers /// /// The icon filename. /// Icon stream. - [HttpGet("icons/{filename}")] + [HttpGet("icons/{fileName}")] public ActionResult GetIcon([FromRoute] string fileName) { return GetIconInternal(fileName); diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b7e1837c9..c4f79ce95 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -165,7 +165,7 @@ namespace Jellyfin.Api.Controllers /// Video stream returned. /// A containing the playlist file. [HttpGet("/Videos/{itemId}/master.m3u8")] - [HttpHead("/Videos/{itemId}/master.m3u8")] + [HttpHead("/Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetMasterHlsVideoPlaylist( [FromRoute] Guid itemId, @@ -335,7 +335,7 @@ namespace Jellyfin.Api.Controllers /// Audio stream returned. /// A containing the playlist file. [HttpGet("/Audio/{itemId}/master.m3u8")] - [HttpHead("/Audio/{itemId}/master.m3u8")] + [HttpHead("/Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetMasterHlsAudioPlaylist( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index efdb6a369..7bf9326a7 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -50,8 +50,8 @@ namespace Jellyfin.Api.Controllers /// A containing the audio stream. // Can't require authentication just yet due to seeing some requests come from Chrome without full query string // [Authenticated] - [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3")] - [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac")] + [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")] + [HttpGet("/Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId) diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 18220c5f3..3a445b1b3 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers /// User does not have permission to delete the image. /// A . [HttpPost("/Users/{userId}/Images/{imageType}")] - [HttpPost("/Users/{userId}/Images/{imageType}/{index?}")] + [HttpPost("/Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] @@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers /// User does not have permission to delete the image. /// A . [HttpDelete("/Users/{userId}/Images/{itemType}")] - [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}")] + [HttpDelete("/Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -167,7 +167,7 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// A on success, or a if item not found. [HttpDelete("/Items/{itemId}/Images/{imageType}")] - [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [HttpDelete("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// A on success, or a if item not found. [HttpPost("/Items/{itemId}/Images/{imageType}")] - [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [HttpPost("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -342,9 +342,9 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Items/{itemId}/Images/{imageType}")] - [HttpHead("/Items/{itemId}/Images/{imageType}")] - [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")] + [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")] + [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetItemImage( @@ -422,7 +422,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] - [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}")] + [HttpHead("/Items/{itemId}/Images/{imageType}/{imageIndex}/{tag}/{format}/{maxWidth}/{maxHeight}/{percentPlayed}/{unplayedCount}", Name = "HeadItemImage2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetItemImage2( @@ -500,7 +500,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Artists/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetArtistImage( @@ -578,7 +578,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Genres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetGenreImage( @@ -656,7 +656,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetMusicGenreImage( @@ -734,7 +734,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Persons/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetPersonImage( @@ -812,7 +812,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Studios/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetStudioImage( @@ -890,7 +890,7 @@ namespace Jellyfin.Api.Controllers /// or a if item not found. /// [HttpGet("/Users/{userId}/Images/{imageType}/{imageIndex?}")] - [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}")] + [HttpHead("/Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetUserImage( diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 49fb9238f..354741ced 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers /// Optional, include image information in output. /// A with the items. [HttpGet("/Items")] - [HttpGet("/Users/{uId}/Items")] + [HttpGet("/Users/{uId}/Items", Name = "GetItems_2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetItems( [FromRoute] Guid? uId, diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 5ad466c55..0ec7e2b8c 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -521,7 +521,7 @@ namespace Jellyfin.Api.Controllers /// The tvdbId. /// Report success. /// A . - [HttpPost("/Library/Series/Added")] + [HttpPost("/Library/Series/Added", Name = "PostAddedSeries")] [HttpPost("/Library/Series/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -551,7 +551,7 @@ namespace Jellyfin.Api.Controllers /// The imdbId. /// Report success. /// A . - [HttpPost("/Library/Movies/Added")] + [HttpPost("/Library/Movies/Added", Name = "PostAddedMovies")] [HttpPost("/Library/Movies/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -679,12 +679,12 @@ namespace Jellyfin.Api.Controllers /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls. /// Similar items returned. /// A containing the similar items. - [HttpGet("/Artists/{itemId}/Similar")] + [HttpGet("/Artists/{itemId}/Similar", Name = "GetSimilarArtists2")] [HttpGet("/Items/{itemId}/Similar")] - [HttpGet("/Albums/{itemId}/Similar")] - [HttpGet("/Shows/{itemId}/Similar")] - [HttpGet("/Movies/{itemId}/Similar")] - [HttpGet("/Trailers/{itemId}/Similar")] + [HttpGet("/Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")] + [HttpGet("/Shows/{itemId}/Similar", Name = "GetSimilarShows2")] + [HttpGet("/Movies/{itemId}/Similar", Name = "GetSimilarMovies2")] + [HttpGet("/Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index bbe5544f9..89112eea7 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Channels")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] - public ActionResult> GetChannels( + public ActionResult> GetLiveTvChannels( [FromQuery] ChannelType? type, [FromQuery] Guid? userId, [FromQuery] int? startIndex, @@ -535,7 +535,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Programs")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] - public async Task>> GetPrograms( + public async Task>> GetLiveTvPrograms( [FromQuery] string? channelIds, [FromQuery] Guid? userId, [FromQuery] DateTime? minStartDate, @@ -933,7 +933,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status404NotFound)] [Obsolete("This endpoint is obsolete.")] - public ActionResult GetRecordingGroup([FromQuery] Guid? groupId) + public ActionResult GetRecordingGroup([FromRoute] Guid? groupId) { return NotFound(); } diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 0c98a8e71..1b300e0d8 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -241,7 +241,7 @@ namespace Jellyfin.Api.Controllers /// The command to send. /// General command sent to session. /// A . - [HttpPost("/Sessions/{sessionId}/Command/{Command}")] + [HttpPost("/Sessions/{sessionId}/Command/{command}")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendGeneralCommand( [FromRoute] string? sessionId, diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index f9e4e61b5..c8e3cc4f5 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -106,7 +106,7 @@ namespace Jellyfin.Api.Controllers /// Initial user retrieved. /// The first user. [HttpGet("User")] - [HttpGet("FirstUser")] + [HttpGet("FirstUser", Name = "GetFirstUser_2")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetFirstUser() { @@ -131,7 +131,7 @@ namespace Jellyfin.Api.Controllers /// [HttpPost("User")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) + public async Task UpdateStartupUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index b62ff80fc..f8c19d15c 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -182,7 +182,7 @@ namespace Jellyfin.Api.Controllers /// File returned. /// A with the subtitle file. [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] - [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}")] + [HttpGet("/Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetSubtitle( [FromRoute, Required] Guid itemId, diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 55ed42227..2b1b95b1b 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("New")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateNewGroup() + public ActionResult SyncPlayCreateGroup() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); _syncPlayManager.NewGroup(currentSession, CancellationToken.None); @@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Join")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult JoinGroup([FromQuery, Required] Guid groupId) + public ActionResult SyncPlayJoinGroup([FromQuery, Required] Guid groupId) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); @@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Leave")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult LeaveGroup() + public ActionResult SyncPlayLeaveGroup() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); @@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers /// An containing the available SyncPlay groups. [HttpGet("List")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetSyncPlayGroups([FromQuery] Guid? filterItemId) + public ActionResult> SyncPlayGetGroups([FromQuery] Guid? filterItemId) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); return Ok(_syncPlayManager.ListGroups(currentSession, filterItemId.HasValue ? filterItemId.Value : Guid.Empty)); @@ -110,7 +110,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Play")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Play() + public ActionResult SyncPlayPlay() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() @@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Pause")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Pause() + public ActionResult SyncPlayPause() { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() @@ -147,7 +147,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Seek")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Seek([FromQuery] long positionTicks) + public ActionResult SyncPlaySeek([FromQuery] long positionTicks) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() @@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Buffering")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Buffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone) + public ActionResult SyncPlayBuffering([FromQuery] DateTime when, [FromQuery] long positionTicks, [FromQuery] bool bufferingDone) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() @@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("Ping")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult Ping([FromQuery] double ping) + public ActionResult SyncPlayPing([FromQuery] double ping) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request); var syncPlayRequest = new PlaybackRequest() diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index bc606f7aa..e0bce3a41 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers /// /// Information retrieved. /// The server name. - [HttpGet("Ping")] - [HttpPost("Ping")] + [HttpGet("Ping", Name = "GetPingSystem")] + [HttpPost("Ping", Name = "PostPingSystem")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult PingSystem() { diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 50ab0ac05..5a9bec2b0 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -69,9 +69,9 @@ namespace Jellyfin.Api.Controllers /// Redirected to remote audio stream. /// A containing the audio file. [HttpGet("/Audio/{itemId}/universal")] - [HttpGet("/Audio/{itemId}/{universal=universal}.{container?}")] - [HttpHead("/Audio/{itemId}/universal")] - [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}")] + [HttpGet("/Audio/{itemId}/{universal=universal}.{container?}", Name = "GetUniversalAudioStream_2")] + [HttpHead("/Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] + [HttpHead("/Audio/{itemId}/{universal=universal}.{container?}", Name = "HeadUniversalAudioStream_2")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status302Found)] diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index d1ef817eb..ebe88a9c0 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -316,10 +316,10 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Video stream returned. /// A containing the audio file. - [HttpGet("{itemId}/{stream=stream}.{container?}")] + [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStream_2")] [HttpGet("{itemId}/stream")] - [HttpHead("{itemId}/{stream=stream}.{container?}")] - [HttpHead("{itemId}/stream")] + [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")] + [HttpHead("{itemId}/stream", Name = "HeadVideoStream")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetVideoStream( [FromRoute] Guid itemId, diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index cfbabf795..6e91042df 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -198,8 +198,15 @@ namespace Jellyfin.Server.Extensions $"{description.ActionDescriptor.RouteValues["controller"]}_{description.RelativePath}"); // Use method name as operationId - c.CustomOperationIds(description => - description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); + c.CustomOperationIds( + description => + { + description.TryGetMethodInfo(out MethodInfo methodInfo); + // Attribute name, method name, none. + return description?.ActionDescriptor?.AttributeRouteInfo?.Name + ?? methodInfo?.Name + ?? null; + }); // TODO - remove when all types are supported in System.Text.Json c.AddSwaggerTypeMappings(); diff --git a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs b/tests/Jellyfin.Api.Tests/GetPathValueTests.cs deleted file mode 100644 index 397eb2edc..000000000 --- a/tests/Jellyfin.Api.Tests/GetPathValueTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using MediaBrowser.Api; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using Xunit; - -namespace Jellyfin.Api.Tests -{ - public class GetPathValueTests - { - [Theory] - [InlineData("https://localhost:8096/ScheduledTasks/1234/Triggers", "", 1, "1234")] - [InlineData("https://localhost:8096/emby/ScheduledTasks/1234/Triggers", "", 1, "1234")] - [InlineData("https://localhost:8096/mediabrowser/ScheduledTasks/1234/Triggers", "", 1, "1234")] - [InlineData("https://localhost:8096/jellyfin/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/jellyfin/2/emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/jellyfin/2/mediabrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/JELLYFIN/2/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/JELLYFIN/2/Emby/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - [InlineData("https://localhost:8096/JELLYFIN/2/MediaBrowser/ScheduledTasks/1234/Triggers", "jellyfin/2", 1, "1234")] - public void GetPathValueTest(string path, string baseUrl, int index, string value) - { - var reqMock = Mock.Of(x => x.PathInfo == path); - var conf = new ServerConfiguration() - { - BaseUrl = baseUrl - }; - - var confManagerMock = Mock.Of(x => x.Configuration == conf); - - var service = new TestService( - new NullLogger(), - confManagerMock, - Mock.Of()) - { - Request = reqMock - }; - - Assert.Equal(value, service.GetPathValue(index).ToString()); - } - } -} -- cgit v1.2.3 From 8f6c2e767906c1d4c62d51ae2e66af1a78edde9a Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 4 Aug 2020 08:27:54 -0600 Subject: Remove leading slash from route attributes --- .../Services/SwaggerService.cs | 287 --------------------- Jellyfin.Api/Controllers/ActivityLogController.cs | 2 +- Jellyfin.Api/Controllers/ApiKeyController.cs | 2 +- Jellyfin.Api/Controllers/ArtistsController.cs | 2 +- Jellyfin.Api/Controllers/CollectionController.cs | 2 +- Jellyfin.Api/Controllers/ItemRefreshController.cs | 3 +- .../Controllers/LibraryStructureController.cs | 2 +- Jellyfin.Api/Controllers/SearchController.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 2 +- Jellyfin.Api/Controllers/TimeSyncController.cs | 2 +- Jellyfin.Api/Controllers/TvShowsController.cs | 2 +- Jellyfin.Api/Controllers/UserController.cs | 2 +- 12 files changed, 11 insertions(+), 299 deletions(-) delete mode 100644 Emby.Server.Implementations/Services/SwaggerService.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs deleted file mode 100644 index 4f011a678..000000000 --- a/Emby.Server.Implementations/Services/SwaggerService.cs +++ /dev/null @@ -1,287 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - [Route("/swagger", "GET", Summary = "Gets the swagger specifications")] - [Route("/swagger.json", "GET", Summary = "Gets the swagger specifications")] - public class GetSwaggerSpec : IReturn - { - } - - public class SwaggerSpec - { - public string swagger { get; set; } - - public string[] schemes { get; set; } - - public SwaggerInfo info { get; set; } - - public string host { get; set; } - - public string basePath { get; set; } - - public SwaggerTag[] tags { get; set; } - - public IDictionary> paths { get; set; } - - public Dictionary definitions { get; set; } - - public SwaggerComponents components { get; set; } - } - - public class SwaggerComponents - { - public Dictionary securitySchemes { get; set; } - } - - public class SwaggerSecurityScheme - { - public string name { get; set; } - - public string type { get; set; } - - public string @in { get; set; } - } - - public class SwaggerInfo - { - public string description { get; set; } - - public string version { get; set; } - - public string title { get; set; } - - public string termsOfService { get; set; } - - public SwaggerConcactInfo contact { get; set; } - } - - public class SwaggerConcactInfo - { - public string email { get; set; } - - public string name { get; set; } - - public string url { get; set; } - } - - public class SwaggerTag - { - public string description { get; set; } - - public string name { get; set; } - } - - public class SwaggerMethod - { - public string summary { get; set; } - - public string description { get; set; } - - public string[] tags { get; set; } - - public string operationId { get; set; } - - public string[] consumes { get; set; } - - public string[] produces { get; set; } - - public SwaggerParam[] parameters { get; set; } - - public Dictionary responses { get; set; } - - public Dictionary[] security { get; set; } - } - - public class SwaggerParam - { - public string @in { get; set; } - - public string name { get; set; } - - public string description { get; set; } - - public bool required { get; set; } - - public string type { get; set; } - - public string collectionFormat { get; set; } - } - - public class SwaggerResponse - { - public string description { get; set; } - - // ex. "$ref":"#/definitions/Pet" - public Dictionary schema { get; set; } - } - - public class SwaggerDefinition - { - public string type { get; set; } - - public Dictionary properties { get; set; } - } - - public class SwaggerProperty - { - public string type { get; set; } - - public string format { get; set; } - - public string description { get; set; } - - public string[] @enum { get; set; } - - public string @default { get; set; } - } - - public class SwaggerService : IService, IRequiresRequest - { - private readonly IHttpServer _httpServer; - private SwaggerSpec _spec; - - public IRequest Request { get; set; } - - public SwaggerService(IHttpServer httpServer) - { - _httpServer = httpServer; - } - - public object Get(GetSwaggerSpec request) - { - return _spec ?? (_spec = GetSpec()); - } - - private SwaggerSpec GetSpec() - { - string host = null; - Uri uri; - if (Uri.TryCreate(Request.RawUrl, UriKind.Absolute, out uri)) - { - host = uri.Host; - } - - var securitySchemes = new Dictionary(); - - securitySchemes["api_key"] = new SwaggerSecurityScheme - { - name = "api_key", - type = "apiKey", - @in = "query" - }; - - var spec = new SwaggerSpec - { - schemes = new[] { "http" }, - tags = GetTags(), - swagger = "2.0", - info = new SwaggerInfo - { - title = "Jellyfin Server API", - version = "1.0.0", - description = "Explore the Jellyfin Server API", - contact = new SwaggerConcactInfo - { - name = "Jellyfin Community", - url = "https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help/" - } - }, - paths = GetPaths(), - definitions = GetDefinitions(), - basePath = "/jellyfin", - host = host, - - components = new SwaggerComponents - { - securitySchemes = securitySchemes - } - }; - - return spec; - } - - - private SwaggerTag[] GetTags() - { - return Array.Empty(); - } - - private Dictionary GetDefinitions() - { - return new Dictionary(); - } - - private IDictionary> GetPaths() - { - var paths = new SortedDictionary>(); - - // REVIEW: this can be done better - var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList(); - - foreach (var current in all) - { - foreach (var info in current.Value) - { - if (info.IsHidden) - { - continue; - } - - if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase) - || info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - paths[info.Path] = GetPathInfo(info); - } - } - - return paths; - } - - private Dictionary GetPathInfo(RestPath info) - { - var result = new Dictionary(); - - foreach (var verb in info.Verbs) - { - var responses = new Dictionary - { - { "200", new SwaggerResponse { description = "OK" } } - }; - - var apiKeySecurity = new Dictionary - { - { "api_key", Array.Empty() } - }; - - result[verb.ToLowerInvariant()] = new SwaggerMethod - { - summary = info.Summary, - description = info.Description, - produces = new[] { "application/json" }, - consumes = new[] { "application/json" }, - operationId = info.RequestType.Name, - tags = Array.Empty(), - - parameters = Array.Empty(), - - responses = responses, - - security = new[] { apiKeySecurity } - }; - } - - return result; - } - } -} diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs index 12ea24973..a07cea9c0 100644 --- a/Jellyfin.Api/Controllers/ActivityLogController.cs +++ b/Jellyfin.Api/Controllers/ActivityLogController.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Api.Controllers /// /// Activity log controller. /// - [Route("/System/ActivityLog")] + [Route("System/ActivityLog")] [Authorize(Policy = Policies.RequiresElevation)] public class ActivityLogController : BaseJellyfinApiController { diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index fef4d7262..ccb7f47f0 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers /// /// Authentication controller. /// - [Route("/Auth")] + [Route("Auth")] public class ApiKeyController : BaseJellyfinApiController { private readonly ISessionManager _sessionManager; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index d39021446..9d1010803 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Api.Controllers /// The artists controller. /// [Authorize(Policy = Policies.DefaultAuthorization)] - [Route("/Artists")] + [Route("Artists")] public class ArtistsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 6f78a7d84..1e8426df4 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -16,7 +16,7 @@ namespace Jellyfin.Api.Controllers /// The collection controller. /// [Authorize(Policy = Policies.DefaultAuthorization)] - [Route("/Collections")] + [Route("Collections")] public class CollectionController : BaseJellyfinApiController { private readonly ICollectionManager _collectionManager; diff --git a/Jellyfin.Api/Controllers/ItemRefreshController.cs b/Jellyfin.Api/Controllers/ItemRefreshController.cs index 4697d869d..3f5d305c1 100644 --- a/Jellyfin.Api/Controllers/ItemRefreshController.cs +++ b/Jellyfin.Api/Controllers/ItemRefreshController.cs @@ -13,8 +13,7 @@ namespace Jellyfin.Api.Controllers /// /// Item Refresh Controller. /// - /// [Authenticated] - [Route("/Items")] + [Route("Items")] [Authorize(Policy = Policies.DefaultAuthorization)] public class ItemRefreshController : BaseJellyfinApiController { diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index 827879e0a..ca150f3f2 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -24,7 +24,7 @@ namespace Jellyfin.Api.Controllers /// /// The library structure controller. /// - [Route("/Library/VirtualFolders")] + [Route("Library/VirtualFolders")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] public class LibraryStructureController : BaseJellyfinApiController { diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 2cbd32d2f..e159a9666 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers /// /// Search controller. /// - [Route("/Search/Hints")] + [Route("Search/Hints")] [Authorize(Policy = Policies.DefaultAuthorization)] public class SearchController : BaseJellyfinApiController { diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index e0bce3a41..6f9a75e2f 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Api.Controllers /// /// The system controller. /// - [Route("/System")] + [Route("System")] public class SystemController : BaseJellyfinApiController { private readonly IServerApplicationHost _appHost; diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs index 57a720b26..bbabcd6e6 100644 --- a/Jellyfin.Api/Controllers/TimeSyncController.cs +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Api.Controllers /// /// The time sync controller. /// - [Route("/GetUtcTime")] + [Route("GetUtcTime")] public class TimeSyncController : BaseJellyfinApiController { /// diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 508b5e24e..d4560dfa2 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers /// /// The tv shows controller. /// - [Route("/Shows")] + [Route("Shows")] [Authorize(Policy = Policies.DefaultAuthorization)] public class TvShowsController : BaseJellyfinApiController { diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index ce0c9281b..482baf641 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Api.Controllers /// /// User controller. /// - [Route("/Users")] + [Route("Users")] public class UserController : BaseJellyfinApiController { private readonly IUserManager _userManager; -- cgit v1.2.3 From fffa94fc33b923863e7cfe0d57d85ae86206975e Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 6 Aug 2020 08:17:45 -0600 Subject: Apply fixes from review --- .../FirstTimeSetupOrDefaultHandler.cs | 56 ++++++++++++++++++++++ .../FirstTimeSetupOrDefaultRequirement.cs | 11 +++++ ...IgnoreParentalControlOrFirstTimeSetupHandler.cs | 51 ++++++++++++++++++++ ...reParentalControlOrFirstTimeSetupRequirement.cs | 11 +++++ .../IgnoreParentalControlHandler.cs | 42 ++++++++++++++++ .../IgnoreParentalControlRequirement.cs | 11 +++++ .../IgnoreSchedulePolicy/IgnoreScheduleHandler.cs | 42 ---------------- .../IgnoreScheduleRequirement.cs | 11 ----- .../LocalAccessOrRequiresElevationHandler.cs | 46 ++++++++++++++++++ .../LocalAccessOrRequiresElevationRequirement.cs | 11 +++++ Jellyfin.Api/Constants/Policies.cs | 19 +++++++- Jellyfin.Api/Controllers/ApiKeyController.cs | 2 +- Jellyfin.Api/Controllers/CollectionController.cs | 5 +- .../Controllers/ConfigurationController.cs | 6 +-- Jellyfin.Api/Controllers/DevicesController.cs | 14 +++--- .../Controllers/DisplayPreferencesController.cs | 7 ++- Jellyfin.Api/Controllers/EnvironmentController.cs | 8 ++-- Jellyfin.Api/Controllers/ImageByNameController.cs | 11 +++-- Jellyfin.Api/Controllers/ImageController.cs | 2 + Jellyfin.Api/Controllers/InstantMixController.cs | 3 +- Jellyfin.Api/Controllers/ItemLookupController.cs | 21 ++++---- Jellyfin.Api/Controllers/ItemUpdateController.cs | 6 +-- Jellyfin.Api/Controllers/LibraryController.cs | 9 ++-- .../Controllers/LibraryStructureController.cs | 4 +- Jellyfin.Api/Controllers/LocalizationController.cs | 2 +- Jellyfin.Api/Controllers/MediaInfoController.cs | 5 +- .../Controllers/NotificationsController.cs | 14 ++++-- Jellyfin.Api/Controllers/PackageController.cs | 1 - Jellyfin.Api/Controllers/PersonsController.cs | 3 ++ Jellyfin.Api/Controllers/PlaylistsController.cs | 4 +- Jellyfin.Api/Controllers/PluginsController.cs | 4 +- Jellyfin.Api/Controllers/RemoteImageController.cs | 6 +-- .../Controllers/ScheduledTasksController.cs | 10 ++-- Jellyfin.Api/Controllers/SessionController.cs | 49 ++++++++++++------- Jellyfin.Api/Controllers/SubtitleController.cs | 6 +-- Jellyfin.Api/Controllers/SyncPlayController.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 7 +-- Jellyfin.Api/Controllers/TimeSyncController.cs | 4 +- Jellyfin.Api/Controllers/TvShowsController.cs | 13 ++--- Jellyfin.Api/Controllers/UserController.cs | 11 ++--- .../Controllers/VideoAttachmentsController.cs | 10 ++-- Jellyfin.Api/Controllers/VideosController.cs | 4 +- Jellyfin.Api/Controllers/YearsController.cs | 3 ++ .../Models/StartupDtos/StartupConfigurationDto.cs | 2 +- .../Extensions/ApiServiceCollectionExtensions.cs | 35 ++++++++++++-- .../IgnoreScheduleHandlerTests.cs | 8 ++-- 46 files changed, 432 insertions(+), 180 deletions(-) create mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs create mode 100644 Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs create mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs create mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs create mode 100644 Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs new file mode 100644 index 000000000..67fb2b79a --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy +{ + /// + /// Authorization handler for requiring first time setup or elevated privileges. + /// + public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler + { + private readonly IConfigurationManager _configurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public FirstTimeSetupOrDefaultHandler( + IConfigurationManager configurationManager, + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + _configurationManager = configurationManager; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrElevatedRequirement) + { + if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) + { + context.Succeed(firstTimeSetupOrElevatedRequirement); + return Task.CompletedTask; + } + + var validated = ValidateClaims(context.User); + if (validated) + { + context.Succeed(firstTimeSetupOrElevatedRequirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs new file mode 100644 index 000000000..23d7ee01f --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy +{ + /// + /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler. + /// + public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs new file mode 100644 index 000000000..6c9258b3d --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs @@ -0,0 +1,51 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy +{ + /// + /// Escape schedule controls handler. + /// + public class IgnoreParentalControlOrFirstTimeSetupHandler : BaseAuthorizationHandler + { + private readonly IConfigurationManager _configurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public IgnoreParentalControlOrFirstTimeSetupHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor, + IConfigurationManager configurationManager) + : base(userManager, networkManager, httpContextAccessor) + { + _configurationManager = configurationManager; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) + { + var validated = ValidateClaims(context.User, ignoreSchedule: true); + if (validated || !_configurationManager.CommonConfiguration.IsStartupWizardCompleted) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs new file mode 100644 index 000000000..36ded0625 --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy +{ + /// + /// Escape schedule controls requirement. + /// + public class IgnoreParentalControlOrFirstTimeSetupRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs new file mode 100644 index 000000000..5213bc4cb --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy +{ + /// + /// Escape schedule controls handler. + /// + public class IgnoreParentalControlHandler : BaseAuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public IgnoreParentalControlHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) + { + var validated = ValidateClaims(context.User, ignoreSchedule: true); + if (!validated) + { + context.Fail(); + return Task.CompletedTask; + } + + context.Succeed(requirement); + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs new file mode 100644 index 000000000..cdad74270 --- /dev/null +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy +{ + /// + /// Escape schedule controls requirement. + /// + public class IgnoreParentalControlRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs deleted file mode 100644 index 9afa0b28f..000000000 --- a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy -{ - /// - /// Escape schedule controls handler. - /// - public class IgnoreScheduleHandler : BaseAuthorizationHandler - { - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public IgnoreScheduleHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor) - : base(userManager, networkManager, httpContextAccessor) - { - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement) - { - var validated = ValidateClaims(context.User, ignoreSchedule: true); - if (!validated) - { - context.Fail(); - return Task.CompletedTask; - } - - context.Succeed(requirement); - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs b/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs deleted file mode 100644 index d5bb61ce6..000000000 --- a/Jellyfin.Api/Auth/IgnoreSchedulePolicy/IgnoreScheduleRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy -{ - /// - /// Escape schedule controls requirement. - /// - public class IgnoreScheduleRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs new file mode 100644 index 000000000..d9ab8aa68 --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Constants; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy +{ + /// + /// Local access handler. + /// + public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler + { + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public LocalAccessOrRequiresElevationHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor) + : base(userManager, networkManager, httpContextAccessor) + { + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) + { + var validated = ValidateClaims(context.User, localAccessOnly: true); + + if (validated || context.User.IsInRole(UserRoles.Administrator)) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs new file mode 100644 index 000000000..ad96caa81 --- /dev/null +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy +{ + /// + /// The local access authorization requirement. + /// + public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 851b56d73..8de637c4e 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -13,7 +13,7 @@ namespace Jellyfin.Api.Constants /// /// Policy name for requiring first time setup or elevated privileges. /// - public const string FirstTimeSetupOrElevated = "FirstTimeOrElevated"; + public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated"; /// /// Policy name for requiring elevated privileges. @@ -28,11 +28,26 @@ namespace Jellyfin.Api.Constants /// /// Policy name for escaping schedule controls. /// - public const string IgnoreSchedule = "IgnoreSchedule"; + public const string IgnoreParentalControl = "IgnoreParentalControl"; /// /// Policy name for requiring download permission. /// public const string Download = "Download"; + + /// + /// Policy name for requiring first time setup or default permissions. + /// + public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault"; + + /// + /// Policy name for requiring local access or elevated privileges. + /// + public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation"; + + /// + /// Policy name for escaping schedule controls or requiring first time setup. + /// + public const string IgnoreParentalControlOrFirstTimeSetup = "IgnoreParentalControlOrFirstTimeSetup"; } } diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index ccb7f47f0..0e28d4c47 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Keys/{key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RevokeKey([FromRoute] string? key) + public ActionResult RevokeKey([FromRoute, Required] string? key) { _sessionManager.RevokeToken(key); return NoContent(); diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index b63fc5ab1..53821a188 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; @@ -86,7 +87,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds) + public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds) { _collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true)); return NoContent(); @@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpDelete("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds) + public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds) { _collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true)); return NoContent(); diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 7d262ed59..019703dae 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -9,7 +10,6 @@ using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Configuration")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) + public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration) { _configurationManager.ReplaceConfiguration(configuration); return NoContent(); @@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) + public ActionResult UpdateMediaEncoderPath([FromForm, Required] MediaEncoderPathDto mediaEncoderPath) { _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); return NoContent(); diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 3cf7b3378..23d10e215 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Security; @@ -8,7 +9,6 @@ using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -48,7 +48,7 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; return _deviceManager.GetDevices(deviceQuery); @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceInfo([FromQuery, BindRequired] string? id) + public ActionResult GetDeviceInfo([FromQuery, Required] string? id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo == null) @@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, BindRequired] string? id) + public ActionResult GetDeviceOptions([FromQuery, Required] string? id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo == null) @@ -111,8 +111,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateDeviceOptions( - [FromQuery, BindRequired] string? id, - [FromBody, BindRequired] DeviceOptions deviceOptions) + [FromQuery, Required] string? id, + [FromBody, Required] DeviceOptions deviceOptions) { var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); if (existingDeviceOptions == null) @@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteDevice([FromQuery, BindRequired] string? id) + public ActionResult DeleteDevice([FromQuery, Required] string? id) { var existingDevice = _deviceManager.GetDevice(id); if (existingDevice == null) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 62f6097f3..c547d0cde 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -11,7 +11,6 @@ using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -99,9 +98,9 @@ namespace Jellyfin.Api.Controllers [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")] public ActionResult UpdateDisplayPreferences( [FromRoute] string? displayPreferencesId, - [FromQuery, BindRequired] Guid userId, - [FromQuery, BindRequired] string? client, - [FromBody, BindRequired] DisplayPreferencesDto displayPreferences) + [FromQuery, Required] Guid userId, + [FromQuery, Required] string? client, + [FromBody, Required] DisplayPreferencesDto displayPreferences) { HomeSectionType[] defaults = { diff --git a/Jellyfin.Api/Controllers/EnvironmentController.cs b/Jellyfin.Api/Controllers/EnvironmentController.cs index 719bb7d86..64670f7d8 100644 --- a/Jellyfin.Api/Controllers/EnvironmentController.cs +++ b/Jellyfin.Api/Controllers/EnvironmentController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using Jellyfin.Api.Constants; @@ -8,7 +9,6 @@ using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers @@ -47,7 +47,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("DirectoryContents")] [ProducesResponseType(StatusCodes.Status200OK)] public IEnumerable GetDirectoryContents( - [FromQuery, BindRequired] string path, + [FromQuery, Required] string path, [FromQuery] bool includeFiles = false, [FromQuery] bool includeDirectories = false) { @@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("ValidatePath")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult ValidatePath([FromBody, BindRequired] ValidatePathDto validatePathDto) + public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto) { if (validatePathDto.IsFile.HasValue) { @@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers /// Parent path. [HttpGet("ParentPath")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetParentPath([FromQuery, BindRequired] string path) + public ActionResult GetParentPath([FromQuery, Required] string path) { string? parent = Path.GetDirectoryName(path); if (string.IsNullOrEmpty(parent)) diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 5244c35b8..528590536 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; @@ -64,7 +65,7 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetGeneralImage([FromRoute] string? name, [FromRoute] string? type) + public ActionResult GetGeneralImage([FromRoute, Required] string? name, [FromRoute, Required] string? type) { var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase) ? "folder" @@ -110,8 +111,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetRatingImage( - [FromRoute] string? theme, - [FromRoute] string? name) + [FromRoute, Required] string? theme, + [FromRoute, Required] string? name) { return GetImageFile(_applicationPaths.RatingsPath, theme, name); } @@ -143,8 +144,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetMediaInfoImage( - [FromRoute] string? theme, - [FromRoute] string? name) + [FromRoute, Required] string? theme, + [FromRoute, Required] string? name) { return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name); } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 360164ad4..410456a25 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -84,6 +84,7 @@ namespace Jellyfin.Api.Controllers /// A . [HttpPost("Users/{userId}/Images/{imageType}")] [HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] @@ -259,6 +260,7 @@ namespace Jellyfin.Api.Controllers /// Item not found. /// The list of image infos on success, or if item not found. [HttpGet("Items/{itemId}/Images")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetItemImageInfos([FromRoute] Guid itemId) diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 8ca232cef..73bd30c4d 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; @@ -174,7 +175,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("MusicGenres/{name}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromMusicGenre( - [FromRoute] string? name, + [FromRoute, Required] string? name, [FromQuery] Guid? userId, [FromQuery] int? limit, [FromQuery] string? fields, diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index 0d9dffbfe..c9ad15bab 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -22,7 +22,6 @@ using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; namespace Jellyfin.Api.Controllers @@ -94,7 +93,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Movie")] - public async Task>> GetMovieRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -111,7 +110,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Trailer")] - public async Task>> GetTrailerRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -128,7 +127,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicVideo")] - public async Task>> GetMusicVideoRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -145,7 +144,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Series")] - public async Task>> GetSeriesRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -162,7 +161,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/BoxSet")] - public async Task>> GetBoxSetRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -179,7 +178,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicArtist")] - public async Task>> GetMusicArtistRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -196,7 +195,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/MusicAlbum")] - public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -214,7 +213,7 @@ namespace Jellyfin.Api.Controllers /// [HttpPost("Items/RemoteSearch/Person")] [Authorize(Policy = Policies.RequiresElevation)] - public async Task>> GetPersonRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -231,7 +230,7 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the list of remote search results. /// [HttpPost("Items/RemoteSearch/Book")] - public async Task>> GetBookRemoteSearchResults([FromBody, BindRequired] RemoteSearchQuery query) + public async Task>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery query) { var results = await _providerManager.GetRemoteSearchResults(query, CancellationToken.None) .ConfigureAwait(false); @@ -296,7 +295,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] public async Task ApplySearchCriteria( [FromRoute] Guid itemId, - [FromBody, BindRequired] RemoteSearchResult searchResult, + [FromBody, Required] RemoteSearchResult searchResult, [FromQuery] bool replaceAllImages = true) { var item = _libraryManager.GetItemById(itemId); diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index a5d9d36a3..4b40c6ada 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using Jellyfin.Api.Constants; @@ -17,7 +18,6 @@ using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -67,7 +67,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Items/{itemId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, BindRequired] BaseItemDto request) + public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Items/{itemId}/ContentType")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType) + public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, Required] string? contentType) { var item = _libraryManager.GetItemById(itemId); if (item == null) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 4731a5c8b..4548e202a 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; @@ -32,7 +33,6 @@ using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; using Movie = Jellyfin.Data.Entities.Movie; @@ -597,7 +597,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Library/Media/Updated")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult PostUpdatedMedia([FromBody, BindRequired] MediaUpdateInfoDto[] updates) + public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates) { foreach (var item in updates) { @@ -685,6 +685,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")] [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")] [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute] Guid itemId, @@ -736,11 +737,11 @@ namespace Jellyfin.Api.Controllers /// Library options info returned. /// Library options info. [HttpGet("Libraries/AvailableOptions")] - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetLibraryOptionsInfo( [FromQuery] string? libraryContentType, - [FromQuery] bool isNewLibrary = false) + [FromQuery] bool isNewLibrary) { var result = new LibraryOptionsResultDto(); diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index ca150f3f2..cdab4f356 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.IO; using System.Linq; @@ -17,7 +18,6 @@ using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Paths")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddMediaPath( - [FromBody, BindRequired] MediaPathDto mediaPathDto, + [FromBody, Required] MediaPathDto mediaPathDto, [FromQuery] bool refreshLibrary = false) { _libraryMonitor.Stop(); diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs index 1466dd3ec..ef2e7e8b1 100644 --- a/Jellyfin.Api/Controllers/LocalizationController.cs +++ b/Jellyfin.Api/Controllers/LocalizationController.cs @@ -11,7 +11,7 @@ namespace Jellyfin.Api.Controllers /// /// Localization controller. /// - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [Authorize(Policy = Policies.FirstTimeSetupOrDefault)] public class LocalizationController : BaseJellyfinApiController { private readonly ILocalizationManager _localization; diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 242cbf191..517113074 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -1,5 +1,6 @@ using System; using System.Buffers; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Net.Mime; @@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers /// A containing a with the playback information. [HttpGet("Items/{itemId}/PlaybackInfo")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid? userId) + public async Task> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery, Required] Guid? userId) { return await GetPlaybackInfoInternal(itemId, userId).ConfigureAwait(false); } @@ -281,7 +282,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("LiveStreams/Close")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CloseLiveStream([FromQuery] string? liveStreamId) + public ActionResult CloseLiveStream([FromQuery, Required] string? liveStreamId) { _mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult(); return NoContent(); diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 1bb39b5f7..47ce48b2d 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; +using Jellyfin.Api.Constants; using Jellyfin.Api.Models.NotificationDtos; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Notifications; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// The notification controller. /// + [Authorize(Policy = Policies.DefaultAuthorization)] public class NotificationsController : BaseJellyfinApiController { private readonly INotificationManager _notificationManager; @@ -83,19 +87,19 @@ namespace Jellyfin.Api.Controllers /// /// Sends a notification to all admins. /// - /// The name of the notification. - /// The description of the notification. /// The URL of the notification. /// The level of the notification. + /// The name of the notification. + /// The description of the notification. /// Notification sent. /// A . [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult CreateAdminNotification( - [FromQuery] string? name, - [FromQuery] string? description, [FromQuery] string? url, - [FromQuery] NotificationLevel? level) + [FromQuery] NotificationLevel? level, + [FromQuery, Required] string name = "", + [FromQuery, Required] string description = "") { var notification = new NotificationRequest { diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index 06c4213fb..3d6a87909 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -127,7 +127,6 @@ namespace Jellyfin.Api.Controllers /// Package repositories returned. /// An containing the list of package repositories. [HttpGet("Repositories")] - [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetRepositories() { diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 23cc23ce7..b6ccec666 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; @@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// Persons controller. /// + [Authorize(Policy = Policies.DefaultAuthorization)] public class PersonsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index cf4660494..12c87d7c3 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; @@ -14,7 +15,6 @@ using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreatePlaylist( - [FromBody, BindRequired] CreatePlaylistDto createPlaylistRequest) + [FromBody, Required] CreatePlaylistDto createPlaylistRequest) { Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids); var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index fe10f0f1b..b2f34680b 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -13,7 +14,6 @@ using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("SecurityInfo")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo) + public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo) { return NoContent(); } diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index 50a161ef6..baa3d80ac 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; using System.Net.Mime; @@ -18,7 +19,6 @@ using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -154,7 +154,7 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task GetRemoteImage([FromQuery, BindRequired] string imageUrl) + public async Task GetRemoteImage([FromQuery, Required] string imageUrl) { var urlHash = imageUrl.GetMD5(); var pointerCachePath = GetFullCachePath(urlHash.ToString()); @@ -209,7 +209,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DownloadRemoteImage( [FromRoute] Guid itemId, - [FromQuery, BindRequired] ImageType type, + [FromQuery, Required] ImageType type, [FromQuery] string? imageUrl) { var item = _libraryManager.GetItemById(itemId); diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs index 3df325e3b..e672070c0 100644 --- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs +++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using MediaBrowser.Model.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("{taskId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetTask([FromRoute] string? taskId) + public ActionResult GetTask([FromRoute, Required] string? taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(i => string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase)); @@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete("Running/{taskId}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult StopTask([FromRoute] string? taskId) + public ActionResult StopTask([FromRoute, Required] string? taskId) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); @@ -144,8 +144,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateTask( - [FromRoute] string? taskId, - [FromBody, BindRequired] TaskTriggerInfo[] triggerInfos) + [FromRoute, Required] string? taskId, + [FromBody, Required] TaskTriggerInfo[] triggerInfos) { var task = _taskManager.ScheduledTasks.FirstOrDefault(o => o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase)); diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 3e6f577f1..48b57bdb7 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -122,12 +122,13 @@ namespace Jellyfin.Api.Controllers /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Viewing")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult DisplayContent( - [FromRoute] string? sessionId, - [FromQuery] string? itemType, - [FromQuery] string? itemId, - [FromQuery] string? itemName) + [FromRoute, Required] string? sessionId, + [FromQuery, Required] string? itemType, + [FromQuery, Required] string? itemId, + [FromQuery, Required] string? itemName) { var command = new BrowseRequest { @@ -156,9 +157,10 @@ namespace Jellyfin.Api.Controllers /// Instruction sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult Play( - [FromRoute] string? sessionId, + [FromRoute, Required] string? sessionId, [FromQuery] Guid[] itemIds, [FromQuery] long? startPositionTicks, [FromQuery] PlayCommand playCommand, @@ -190,9 +192,10 @@ namespace Jellyfin.Api.Controllers /// Playstate command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Playing/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendPlaystateCommand( - [FromRoute] string? sessionId, + [FromRoute, Required] string? sessionId, [FromBody] PlaystateRequest playstateRequest) { _sessionManager.SendPlaystateCommand( @@ -212,10 +215,11 @@ namespace Jellyfin.Api.Controllers /// System command sent to session. /// A . [HttpPost("Sessions/{sessionId}/System/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendSystemCommand( - [FromRoute] string? sessionId, - [FromRoute] string? command) + [FromRoute, Required] string? sessionId, + [FromRoute, Required] string? command) { var name = command; if (Enum.TryParse(name, true, out GeneralCommandType commandType)) @@ -243,10 +247,11 @@ namespace Jellyfin.Api.Controllers /// General command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Command/{command}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendGeneralCommand( - [FromRoute] string? sessionId, - [FromRoute] string? command) + [FromRoute, Required] string? sessionId, + [FromRoute, Required] string? command) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); @@ -269,9 +274,10 @@ namespace Jellyfin.Api.Controllers /// Full general command sent to session. /// A . [HttpPost("Sessions/{sessionId}/Command")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendFullGeneralCommand( - [FromRoute] string? sessionId, + [FromRoute, Required] string? sessionId, [FromBody, Required] GeneralCommand command) { var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request); @@ -302,11 +308,12 @@ namespace Jellyfin.Api.Controllers /// Message sent. /// A . [HttpPost("Sessions/{sessionId}/Message")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SendMessageCommand( - [FromRoute] string? sessionId, - [FromQuery] string? text, - [FromQuery] string? header, + [FromRoute, Required] string? sessionId, + [FromQuery, Required] string? text, + [FromQuery, Required] string? header, [FromQuery] long? timeoutMs) { var command = new MessageCommand @@ -329,9 +336,10 @@ namespace Jellyfin.Api.Controllers /// User added to session. /// A . [HttpPost("Sessions/{sessionId}/User/{userId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult AddUserToSession( - [FromRoute] string? sessionId, + [FromRoute, Required] string? sessionId, [FromRoute] Guid userId) { _sessionManager.AddAdditionalUser(sessionId, userId); @@ -346,6 +354,7 @@ namespace Jellyfin.Api.Controllers /// User removed from session. /// A . [HttpDelete("Sessions/{sessionId}/User/{userId}")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RemoveUserFromSession( [FromRoute] string? sessionId, @@ -367,9 +376,10 @@ namespace Jellyfin.Api.Controllers /// Capabilities posted. /// A . [HttpPost("Sessions/Capabilities")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostCapabilities( - [FromQuery] string? id, + [FromQuery, Required] string? id, [FromQuery] string? playableMediaTypes, [FromQuery] string? supportedCommands, [FromQuery] bool supportsMediaControl = false, @@ -400,9 +410,10 @@ namespace Jellyfin.Api.Controllers /// Capabilities updated. /// A . [HttpPost("Sessions/Capabilities/Full")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostFullCapabilities( - [FromQuery] string? id, + [FromQuery, Required] string? id, [FromBody, Required] ClientCapabilities capabilities) { if (string.IsNullOrWhiteSpace(id)) @@ -423,6 +434,7 @@ namespace Jellyfin.Api.Controllers /// Session reported to server. /// A . [HttpPost("Sessions/Viewing")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportViewing( [FromQuery] string? sessionId, @@ -440,6 +452,7 @@ namespace Jellyfin.Api.Controllers /// Session end reported to server. /// A . [HttpPost("Sessions/Logout")] + [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult ReportSessionEnded() { @@ -455,6 +468,7 @@ namespace Jellyfin.Api.Controllers /// Auth providers retrieved. /// An with the auth providers. [HttpGet("Auth/Providers")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAuthProviders() { @@ -468,6 +482,7 @@ namespace Jellyfin.Api.Controllers /// An with the password reset providers. [HttpGet("Auto/PasswordResetProviders")] [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = Policies.RequiresElevation)] public ActionResult> GetPasswordResetProviders() { return _userManager.GetPasswordResetProviders(); diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index d5633fba5..988acccc3 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -113,7 +113,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> SearchRemoteSubtitles( [FromRoute] Guid itemId, - [FromRoute] string? language, + [FromRoute, Required] string? language, [FromQuery] bool? isPerfectMatch) { var video = (Video)_libraryManager.GetItemById(itemId); @@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task DownloadRemoteSubtitles( [FromRoute] Guid itemId, - [FromRoute] string? subtitleId) + [FromRoute, Required] string? subtitleId) { var video = (Video)_libraryManager.GetItemById(itemId); @@ -162,7 +162,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] - public async Task GetRemoteSubtitles([FromRoute] string? id) + public async Task GetRemoteSubtitles([FromRoute, Required] string? id) { var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 2b1b95b1b..e16a10ba4 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers /// /// Optional. Filter by item id. /// Groups returned. - /// An containing the available SyncPlay groups. + /// An containing the available SyncPlay groups. [HttpGet("List")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> SyncPlayGetGroups([FromQuery] Guid? filterItemId) diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 6f9a75e2f..08f1b421d 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -23,7 +23,6 @@ namespace Jellyfin.Api.Controllers /// /// The system controller. /// - [Route("System")] public class SystemController : BaseJellyfinApiController { private readonly IServerApplicationHost _appHost; @@ -60,8 +59,7 @@ namespace Jellyfin.Api.Controllers /// Information retrieved. /// A with info about the system. [HttpGet("Info")] - [Authorize(Policy = Policies.IgnoreSchedule)] - [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] + [Authorize(Policy = Policies.IgnoreParentalControlOrFirstTimeSetup)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetSystemInfo() { @@ -99,8 +97,7 @@ namespace Jellyfin.Api.Controllers /// Server restarted. /// No content. Server restarted. [HttpPost("Restart")] - [Authorize(Policy = Policies.LocalAccessOnly)] - [Authorize(Policy = Policies.RequiresElevation)] + [Authorize(Policy = Policies.LocalAccessOrRequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult RestartApplication() { diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs index bbabcd6e6..2dc744e7c 100644 --- a/Jellyfin.Api/Controllers/TimeSyncController.cs +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -9,7 +9,7 @@ namespace Jellyfin.Api.Controllers /// /// The time sync controller. /// - [Route("GetUtcTime")] + [Route("")] public class TimeSyncController : BaseJellyfinApiController { /// @@ -17,7 +17,7 @@ namespace Jellyfin.Api.Controllers /// /// Time returned. /// An to sync the client and server time. - [HttpGet] + [HttpGet("GetUtcTime")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public ActionResult GetUtcTime() { diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index d4560dfa2..f463ab889 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; @@ -68,7 +69,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNextUp( - [FromQuery] Guid? userId, + [FromQuery, Required] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? fields, @@ -126,7 +127,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Upcoming")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUpcomingEpisodes( - [FromQuery] Guid? userId, + [FromQuery, Required] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? fields, @@ -193,8 +194,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetEpisodes( - [FromRoute] string? seriesId, - [FromQuery] Guid? userId, + [FromRoute, Required] string? seriesId, + [FromQuery, Required] Guid? userId, [FromQuery] string? fields, [FromQuery] int? season, [FromQuery] string? seasonId, @@ -316,8 +317,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult> GetSeasons( - [FromRoute] string? seriesId, - [FromQuery] Guid? userId, + [FromRoute, Required] string? seriesId, + [FromQuery, Required] Guid? userId, [FromQuery] string? fields, [FromQuery] bool? isSpecialSeason, [FromQuery] bool? isMissing, diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 2ce5c7e56..d897f07b7 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -20,7 +20,6 @@ using MediaBrowser.Model.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Jellyfin.Api.Controllers { @@ -106,7 +105,7 @@ namespace Jellyfin.Api.Controllers /// User not found. /// An with information about the user or a if the user was not found. [HttpGet("{userId}")] - [Authorize(Policy = Policies.IgnoreSchedule)] + [Authorize(Policy = Policies.IgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetUserById([FromRoute] Guid userId) @@ -157,8 +156,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> AuthenticateUser( [FromRoute, Required] Guid userId, - [FromQuery, BindRequired] string? pw, - [FromQuery, BindRequired] string? password) + [FromQuery, Required] string? pw, + [FromQuery] string? password) { var user = _userManager.GetUserById(userId); @@ -190,7 +189,7 @@ namespace Jellyfin.Api.Controllers /// A containing an with information about the new session. [HttpPost("AuthenticateByName")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> AuthenticateUserByName([FromBody, BindRequired] AuthenticateUserByName request) + public async Task> AuthenticateUserByName([FromBody, Required] AuthenticateUserByName request) { var auth = _authContext.GetAuthorizationInfo(Request); @@ -371,7 +370,7 @@ namespace Jellyfin.Api.Controllers /// User policy update forbidden. /// A indicating success or a or a on failure.. [HttpPost("{userId}/Policy")] - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index eef0a93cd..09a1c93e6 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -1,12 +1,11 @@ using System; +using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,7 +15,6 @@ namespace Jellyfin.Api.Controllers /// Attachments controller. /// [Route("Videos")] - [Authorize(Policy = Policies.DefaultAuthorization)] public class VideoAttachmentsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -49,9 +47,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetAttachment( - [FromRoute] Guid videoId, - [FromRoute] string? mediaSourceId, - [FromRoute] int index) + [FromRoute, Required] Guid videoId, + [FromRoute, Required] string mediaSourceId, + [FromRoute, Required] int index) { try { diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index ebe88a9c0..fe065c76f 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Net.Http; @@ -35,7 +36,6 @@ namespace Jellyfin.Api.Controllers /// /// The videos controller. /// - [Route("Videos")] public class VideosController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public ActionResult MergeVersions([FromQuery] string? itemIds) + public ActionResult MergeVersions([FromQuery, Required] string? itemIds) { var items = RequestHelpers.Split(itemIds, ',', true) .Select(i => _libraryManager.GetItemById(i)) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index d09b016a9..eb91ac23e 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Data.Entities; @@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -17,6 +19,7 @@ namespace Jellyfin.Api.Controllers /// /// Years controller. /// + [Authorize(Policy = Policies.DefaultAuthorization)] public class YearsController : BaseJellyfinApiController { private readonly ILibraryManager _libraryManager; diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs index a5f012245..66e797699 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs @@ -8,7 +8,7 @@ namespace Jellyfin.Api.Models.StartupDtos /// /// Gets or sets UI language culture. /// - public string? UICulture { get; set; } + public string UICulture { get; set; } = null!; /// /// Gets or sets the metadata country code. diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 6e91042df..586746430 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -7,8 +7,11 @@ using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.DownloadPolicy; +using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; -using Jellyfin.Api.Auth.IgnoreSchedulePolicy; +using Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy; +using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; @@ -41,9 +44,12 @@ namespace Jellyfin.Server.Extensions { serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); return serviceCollection.AddAuthorizationCore(options => { @@ -61,6 +67,13 @@ namespace Jellyfin.Server.Extensions policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddRequirements(new DownloadRequirement()); }); + options.AddPolicy( + Policies.FirstTimeSetupOrDefault, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new FirstTimeSetupOrDefaultRequirement()); + }); options.AddPolicy( Policies.FirstTimeSetupOrElevated, policy => @@ -69,11 +82,18 @@ namespace Jellyfin.Server.Extensions policy.AddRequirements(new FirstTimeSetupOrElevatedRequirement()); }); options.AddPolicy( - Policies.IgnoreSchedule, + Policies.IgnoreParentalControl, policy => { policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new IgnoreScheduleRequirement()); + policy.AddRequirements(new IgnoreParentalControlRequirement()); + }); + options.AddPolicy( + Policies.IgnoreParentalControlOrFirstTimeSetup, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new IgnoreParentalControlOrFirstTimeSetupRequirement()); }); options.AddPolicy( Policies.LocalAccessOnly, @@ -82,6 +102,13 @@ namespace Jellyfin.Server.Extensions policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); policy.AddRequirements(new LocalAccessRequirement()); }); + options.AddPolicy( + Policies.LocalAccessOrRequiresElevation, + policy => + { + policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); + policy.AddRequirements(new LocalAccessOrRequiresElevationRequirement()); + }); options.AddPolicy( Policies.RequiresElevation, policy => diff --git a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs index b65d45aa0..7150c90bb 100644 --- a/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/IgnoreSchedulePolicy/IgnoreScheduleHandlerTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Jellyfin.Api.Auth.IgnoreSchedulePolicy; +using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; using Jellyfin.Api.Constants; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -20,7 +20,7 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy { private readonly Mock _configurationManagerMock; private readonly List _requirements; - private readonly IgnoreScheduleHandler _sut; + private readonly IgnoreParentalControlHandler _sut; private readonly Mock _userManagerMock; private readonly Mock _httpContextAccessor; @@ -33,11 +33,11 @@ namespace Jellyfin.Api.Tests.Auth.IgnoreSchedulePolicy { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze>(); - _requirements = new List { new IgnoreScheduleRequirement() }; + _requirements = new List { new IgnoreParentalControlRequirement() }; _userManagerMock = fixture.Freeze>(); _httpContextAccessor = fixture.Freeze>(); - _sut = fixture.Create(); + _sut = fixture.Create(); } [Theory] -- cgit v1.2.3 From 05f9473544dd40c6ffdc8512d675256468c09cb3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 6 Aug 2020 17:59:48 -0600 Subject: Apply fixes from review --- ...FirstTimeOrIgnoreParentalControlSetupHandler.cs | 57 ++++++++++++++++++++++ ...tTimeOrIgnoreParentalControlSetupRequirement.cs | 11 +++++ .../FirstTimeSetupOrDefaultHandler.cs | 8 +-- .../FirstTimeSetupOrDefaultRequirement.cs | 2 +- ...IgnoreParentalControlOrFirstTimeSetupHandler.cs | 51 ------------------- ...reParentalControlOrFirstTimeSetupRequirement.cs | 11 ----- .../LocalAccessOrRequiresElevationHandler.cs | 3 +- .../LocalAccessOrRequiresElevationRequirement.cs | 2 +- Jellyfin.Api/Constants/Policies.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 1 + Jellyfin.Api/Controllers/SystemController.cs | 2 +- .../Models/StartupDtos/StartupConfigurationDto.cs | 2 +- .../Extensions/ApiServiceCollectionExtensions.cs | 8 +-- 13 files changed, 83 insertions(+), 77 deletions(-) create mode 100644 Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs create mode 100644 Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs delete mode 100644 Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs new file mode 100644 index 000000000..2a02f8bc7 --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -0,0 +1,57 @@ +using System.Threading.Tasks; +using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Library; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy +{ + /// + /// Ignore parental control schedule and allow before startup wizard has been completed. + /// + public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler + { + private readonly IConfigurationManager _configurationManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public FirstTimeOrIgnoreParentalControlSetupHandler( + IUserManager userManager, + INetworkManager networkManager, + IHttpContextAccessor httpContextAccessor, + IConfigurationManager configurationManager) + : base(userManager, networkManager, httpContextAccessor) + { + _configurationManager = configurationManager; + } + + /// + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) + { + if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) + { + context.Succeed(requirement); + return Task.CompletedTask; + } + + var validated = ValidateClaims(context.User, ignoreSchedule: true); + if (validated) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + } +} diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs new file mode 100644 index 000000000..00aaec334 --- /dev/null +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupRequirement.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authorization; + +namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy +{ + /// + /// First time setup or ignore parental controls requirement. + /// + public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement + { + } +} diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs index 67fb2b79a..9815e252e 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy { /// - /// Authorization handler for requiring first time setup or elevated privileges. + /// Authorization handler for requiring first time setup or default privileges. /// public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler { @@ -32,18 +32,18 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy } /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrElevatedRequirement) + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement) { if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted) { - context.Succeed(firstTimeSetupOrElevatedRequirement); + context.Succeed(firstTimeSetupOrDefaultRequirement); return Task.CompletedTask; } var validated = ValidateClaims(context.User); if (validated) { - context.Succeed(firstTimeSetupOrElevatedRequirement); + context.Succeed(firstTimeSetupOrDefaultRequirement); } else { diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs index 23d7ee01f..f7366bd7a 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultRequirement.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Authorization; namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy { /// - /// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler. + /// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler. /// public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement { diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs deleted file mode 100644 index 6c9258b3d..000000000 --- a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupHandler.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Threading.Tasks; -using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Library; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; - -namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy -{ - /// - /// Escape schedule controls handler. - /// - public class IgnoreParentalControlOrFirstTimeSetupHandler : BaseAuthorizationHandler - { - private readonly IConfigurationManager _configurationManager; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public IgnoreParentalControlOrFirstTimeSetupHandler( - IUserManager userManager, - INetworkManager networkManager, - IHttpContextAccessor httpContextAccessor, - IConfigurationManager configurationManager) - : base(userManager, networkManager, httpContextAccessor) - { - _configurationManager = configurationManager; - } - - /// - protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) - { - var validated = ValidateClaims(context.User, ignoreSchedule: true); - if (validated || !_configurationManager.CommonConfiguration.IsStartupWizardCompleted) - { - context.Succeed(requirement); - } - else - { - context.Fail(); - } - - return Task.CompletedTask; - } - } -} diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs b/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs deleted file mode 100644 index 36ded0625..000000000 --- a/Jellyfin.Api/Auth/IgnoreParentalControlOrFirstTimeSetupPolicy/IgnoreParentalControlOrFirstTimeSetupRequirement.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Authorization; - -namespace Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy -{ - /// - /// Escape schedule controls requirement. - /// - public class IgnoreParentalControlOrFirstTimeSetupRequirement : IAuthorizationRequirement - { - } -} diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index d9ab8aa68..14722aa57 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http; namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy { /// - /// Local access handler. + /// Local access or require elevated privileges handler. /// public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler { @@ -30,7 +30,6 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement) { var validated = ValidateClaims(context.User, localAccessOnly: true); - if (validated || context.User.IsInRole(UserRoles.Administrator)) { context.Succeed(requirement); diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs index ad96caa81..d9c64d01c 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationRequirement.cs @@ -3,7 +3,7 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy { /// - /// The local access authorization requirement. + /// The local access or elevated privileges authorization requirement. /// public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement { diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs index 8de637c4e..7d7767470 100644 --- a/Jellyfin.Api/Constants/Policies.cs +++ b/Jellyfin.Api/Constants/Policies.cs @@ -48,6 +48,6 @@ namespace Jellyfin.Api.Constants /// /// Policy name for escaping schedule controls or requiring first time setup. /// - public const string IgnoreParentalControlOrFirstTimeSetup = "IgnoreParentalControlOrFirstTimeSetup"; + public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl"; } } diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 410456a25..45447ae0c 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -131,6 +131,7 @@ namespace Jellyfin.Api.Controllers /// A . [HttpDelete("Users/{userId}/Images/{itemType}")] [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")] + [Authorize(Policy = Policies.DefaultAuthorization)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 08f1b421d..bbfd163de 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers /// Information retrieved. /// A with info about the system. [HttpGet("Info")] - [Authorize(Policy = Policies.IgnoreParentalControlOrFirstTimeSetup)] + [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> GetSystemInfo() { diff --git a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs index 66e797699..a5f012245 100644 --- a/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs +++ b/Jellyfin.Api/Models/StartupDtos/StartupConfigurationDto.cs @@ -8,7 +8,7 @@ namespace Jellyfin.Api.Models.StartupDtos /// /// Gets or sets UI language culture. /// - public string UICulture { get; set; } = null!; + public string? UICulture { get; set; } /// /// Gets or sets the metadata country code. diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 586746430..83d8fac5b 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -7,9 +7,9 @@ using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Auth.DownloadPolicy; +using Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; -using Jellyfin.Api.Auth.IgnoreParentalControlOrFirstTimeSetupPolicy; using Jellyfin.Api.Auth.IgnoreParentalControlPolicy; using Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy; using Jellyfin.Api.Auth.LocalAccessPolicy; @@ -47,7 +47,7 @@ namespace Jellyfin.Server.Extensions serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -89,11 +89,11 @@ namespace Jellyfin.Server.Extensions policy.AddRequirements(new IgnoreParentalControlRequirement()); }); options.AddPolicy( - Policies.IgnoreParentalControlOrFirstTimeSetup, + Policies.FirstTimeSetupOrIgnoreParentalControl, policy => { policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication); - policy.AddRequirements(new IgnoreParentalControlOrFirstTimeSetupRequirement()); + policy.AddRequirements(new FirstTimeOrIgnoreParentalControlSetupRequirement()); }); options.AddPolicy( Policies.LocalAccessOnly, -- cgit v1.2.3 From 2f33bee2a94f7175050f83d568f8bd65a01a86f8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 1 Sep 2020 17:26:49 -0600 Subject: Set openapi schema type to file where possible --- .../Attributes/ProducesAudioFileAttribute.cs | 18 ++++++++ Jellyfin.Api/Attributes/ProducesFileAttribute.cs | 28 ++++++++++++ .../Attributes/ProducesImageFileAttribute.cs | 18 ++++++++ .../Attributes/ProducesPlaylistFileAttribute.cs | 18 ++++++++ .../Attributes/ProducesVideoFileAttribute.cs | 18 ++++++++ Jellyfin.Api/Controllers/AudioController.cs | 2 + .../Controllers/ConfigurationController.cs | 3 ++ Jellyfin.Api/Controllers/DashboardController.cs | 3 ++ Jellyfin.Api/Controllers/DlnaServerController.cs | 25 +++++++---- Jellyfin.Api/Controllers/DynamicHlsController.cs | 7 +++ Jellyfin.Api/Controllers/HlsSegmentController.cs | 4 ++ Jellyfin.Api/Controllers/ImageController.cs | 11 ++++- Jellyfin.Api/Controllers/ItemLookupController.cs | 10 +++-- Jellyfin.Api/Controllers/LibraryController.cs | 3 ++ Jellyfin.Api/Controllers/LiveTvController.cs | 4 ++ Jellyfin.Api/Controllers/MediaInfoController.cs | 2 + Jellyfin.Api/Controllers/SubtitleController.cs | 4 ++ Jellyfin.Api/Controllers/SystemController.cs | 9 ++-- .../Controllers/UniversalAudioController.cs | 2 + Jellyfin.Api/Controllers/UserController.cs | 6 +-- Jellyfin.Api/Controllers/VideoHlsController.cs | 2 + Jellyfin.Api/Controllers/VideosController.cs | 4 +- .../Extensions/ApiServiceCollectionExtensions.cs | 3 ++ Jellyfin.Server/Filters/FileResponseFilter.cs | 52 ++++++++++++++++++++++ 24 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs create mode 100644 Jellyfin.Api/Attributes/ProducesFileAttribute.cs create mode 100644 Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs create mode 100644 Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs create mode 100644 Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs create mode 100644 Jellyfin.Server/Filters/FileResponseFilter.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs new file mode 100644 index 000000000..3adb700eb --- /dev/null +++ b/Jellyfin.Api/Attributes/ProducesAudioFileAttribute.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Attributes +{ + /// + /// Produces file attribute of "image/*". + /// + public class ProducesAudioFileAttribute : ProducesFileAttribute + { + private const string ContentType = "audio/*"; + + /// + /// Initializes a new instance of the class. + /// + public ProducesAudioFileAttribute() + : base(ContentType) + { + } + } +} diff --git a/Jellyfin.Api/Attributes/ProducesFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs new file mode 100644 index 000000000..62a576ede --- /dev/null +++ b/Jellyfin.Api/Attributes/ProducesFileAttribute.cs @@ -0,0 +1,28 @@ +using System; + +namespace Jellyfin.Api.Attributes +{ + /// + /// Internal produces image attribute. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ProducesFileAttribute : Attribute + { + private readonly string[] _contentTypes; + + /// + /// Initializes a new instance of the class. + /// + /// Content types this endpoint produces. + public ProducesFileAttribute(params string[] contentTypes) + { + _contentTypes = contentTypes; + } + + /// + /// Gets the configured content types. + /// + /// the configured content types. + public string[] GetContentTypes() => _contentTypes; + } +} diff --git a/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs new file mode 100644 index 000000000..e15813676 --- /dev/null +++ b/Jellyfin.Api/Attributes/ProducesImageFileAttribute.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Attributes +{ + /// + /// Produces file attribute of "image/*". + /// + public class ProducesImageFileAttribute : ProducesFileAttribute + { + private const string ContentType = "image/*"; + + /// + /// Initializes a new instance of the class. + /// + public ProducesImageFileAttribute() + : base(ContentType) + { + } + } +} diff --git a/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs new file mode 100644 index 000000000..5d928ab91 --- /dev/null +++ b/Jellyfin.Api/Attributes/ProducesPlaylistFileAttribute.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Attributes +{ + /// + /// Produces file attribute of "image/*". + /// + public class ProducesPlaylistFileAttribute : ProducesFileAttribute + { + private const string ContentType = "application/x-mpegURL"; + + /// + /// Initializes a new instance of the class. + /// + public ProducesPlaylistFileAttribute() + : base(ContentType) + { + } + } +} diff --git a/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs new file mode 100644 index 000000000..d8b2856dc --- /dev/null +++ b/Jellyfin.Api/Attributes/ProducesVideoFileAttribute.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Attributes +{ + /// + /// Produces file attribute of "video/*". + /// + public class ProducesVideoFileAttribute : ProducesFileAttribute + { + private const string ContentType = "video/*"; + + /// + /// Initializes a new instance of the class. + /// + public ProducesVideoFileAttribute() + : base(ContentType) + { + } + } +} diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 802cd026e..48fd198b5 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Controller.MediaEncoding; @@ -88,6 +89,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] public async Task GetAudioStream( [FromRoute] Guid itemId, [FromRoute] string? container, diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 20fb0ec87..606e27970 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; +using System.Net.Mime; using System.Text.Json; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using MediaBrowser.Common.Json; @@ -73,6 +75,7 @@ namespace Jellyfin.Api.Controllers /// Configuration. [HttpGet("Configuration/{key}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile(MediaTypeNames.Application.Json)] public ActionResult GetNamedConfiguration([FromRoute] string? key) { return _configurationManager.GetConfiguration(key); diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index 33abe3ccd..c1bcc71e3 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Mime; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Models; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller; @@ -123,6 +125,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("web/ConfigurationPage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")] public ActionResult GetDashboardConfigurationPage([FromQuery] string? name) { IPlugin? plugin = null; diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index ee87d917b..dd91c7058 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Net.Mime; using System.Threading.Tasks; using Emby.Dlna; using Emby.Dlna.Main; @@ -17,8 +19,6 @@ namespace Jellyfin.Api.Controllers [Route("Dlna")] public class DlnaServerController : BaseJellyfinApiController { - private const string XMLContentType = "text/xml; charset=UTF-8"; - private readonly IDlnaManager _dlnaManager; private readonly IContentDirectory _contentDirectory; private readonly IConnectionManager _connectionManager; @@ -44,7 +44,8 @@ namespace Jellyfin.Api.Controllers /// An containing the description xml. [HttpGet("{serverId}/description")] [HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")] - [Produces(XMLContentType)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDescriptionXml([FromRoute] string serverId) { @@ -62,7 +63,8 @@ namespace Jellyfin.Api.Controllers /// An containing the dlna content directory xml. [HttpGet("{serverId}/ContentDirectory")] [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")] - [Produces(XMLContentType)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetContentDirectory([FromRoute] string serverId) @@ -77,7 +79,8 @@ namespace Jellyfin.Api.Controllers /// Dlna media receiver registrar xml. [HttpGet("{serverId}/MediaReceiverRegistrar")] [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")] - [Produces(XMLContentType)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId) @@ -92,7 +95,8 @@ namespace Jellyfin.Api.Controllers /// Dlna media receiver registrar xml. [HttpGet("{serverId}/ConnectionManager")] [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")] - [Produces(XMLContentType)] + [Produces(MediaTypeNames.Text.Xml)] + [ProducesFile(MediaTypeNames.Text.Xml)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] public ActionResult GetConnectionManager([FromRoute] string serverId) @@ -183,7 +187,9 @@ namespace Jellyfin.Api.Controllers /// Icon stream. [HttpGet("{serverId}/icons/{fileName}")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] - public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesImageFile] + public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName) { return GetIconInternal(fileName); } @@ -194,12 +200,13 @@ namespace Jellyfin.Api.Controllers /// The icon filename. /// Icon stream. [HttpGet("icons/{fileName}")] - public ActionResult GetIcon([FromRoute] string fileName) + [ProducesImageFile] + public ActionResult GetIcon([FromRoute] string fileName) { return GetIconInternal(fileName); } - private ActionResult GetIconInternal(string fileName) + private ActionResult GetIconInternal(string fileName) { var icon = _dlnaManager.GetIcon(fileName); if (icon == null) diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b115ac6cd..230bb8295 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; @@ -166,6 +167,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Videos/{itemId}/master.m3u8")] [HttpHead("Videos/{itemId}/master.m3u8", Name = "HeadMasterHlsVideoPlaylist")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] public async Task GetMasterHlsVideoPlaylist( [FromRoute] Guid itemId, [FromRoute] string? container, @@ -333,6 +335,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Audio/{itemId}/master.m3u8")] [HttpHead("Audio/{itemId}/master.m3u8", Name = "HeadMasterHlsAudioPlaylist")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] public async Task GetMasterHlsAudioPlaylist( [FromRoute] Guid itemId, [FromRoute] string? container, @@ -498,6 +501,7 @@ namespace Jellyfin.Api.Controllers /// A containing the audio file. [HttpGet("Videos/{itemId}/main.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] public async Task GetVariantHlsVideoPlaylist( [FromRoute] Guid itemId, [FromRoute] string? container, @@ -663,6 +667,7 @@ namespace Jellyfin.Api.Controllers /// A containing the audio file. [HttpGet("Audio/{itemId}/main.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] public async Task GetVariantHlsAudioPlaylist( [FromRoute] Guid itemId, [FromRoute] string? container, @@ -830,6 +835,7 @@ namespace Jellyfin.Api.Controllers /// A containing the audio file. [HttpGet("Videos/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesVideoFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] public async Task GetHlsVideoSegment( [FromRoute] Guid itemId, @@ -999,6 +1005,7 @@ namespace Jellyfin.Api.Controllers /// A containing the audio file. [HttpGet("Audio/{itemId}/hls1/{playlistId}/{segmentId}.{container}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "playlistId", Justification = "Imported from ServiceStack")] public async Task GetHlsAudioSegment( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 816252f80..e534be50a 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Configuration; @@ -54,6 +55,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")] [HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId) { @@ -74,6 +76,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsPlaylistLegacy([FromRoute] string itemId, [FromRoute] string playlistId) { @@ -112,6 +115,7 @@ namespace Jellyfin.Api.Controllers // [Authenticated] [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesVideoFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] public ActionResult GetHlsVideoSegmentLegacy( [FromRoute] string itemId, diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index a204fe35c..dbd569e07 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Controller.Configuration; @@ -351,6 +352,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] public async Task GetItemImage( [FromRoute] Guid itemId, [FromRoute] ImageType imageType, @@ -507,6 +509,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] public async Task GetArtistImage( [FromRoute] string name, [FromRoute] ImageType imageType, @@ -585,6 +588,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] public async Task GetGenreImage( [FromRoute] string name, [FromRoute] ImageType imageType, @@ -663,6 +667,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] public async Task GetMusicGenreImage( [FromRoute] string name, [FromRoute] ImageType imageType, @@ -741,6 +746,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] public async Task GetPersonImage( [FromRoute] string name, [FromRoute] ImageType imageType, @@ -819,6 +825,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] public async Task GetStudioImage( [FromRoute] string name, [FromRoute] ImageType imageType, @@ -897,6 +904,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] public async Task GetUserImage( [FromRoute] Guid userId, [FromRoute] ImageType imageType, @@ -1297,8 +1305,7 @@ namespace Jellyfin.Api.Controllers return NoContent(); } - var stream = new FileStream(imagePath, FileMode.Open, FileAccess.Read); - return File(stream, imageContentType); + return PhysicalFile(imagePath, imageContentType); } } } diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index afde4a433..2bde3443a 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -7,6 +7,7 @@ using System.Net.Mime; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; @@ -18,6 +19,7 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -248,6 +250,8 @@ namespace Jellyfin.Api.Controllers /// The task result contains an containing the images file stream. /// [HttpGet("Items/RemoteSearch/Image")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesImageFile] public async Task GetRemoteSearchImage( [FromQuery, Required] string imageUrl, [FromQuery, Required] string providerName) @@ -274,10 +278,7 @@ namespace Jellyfin.Api.Controllers } await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false); - - // Read the pointer file again - await using var fileStream = System.IO.File.OpenRead(pointerCachePath); - return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet); + return PhysicalFile(pointerCachePath, MimeTypes.GetMimeType(pointerCachePath)); } /// @@ -293,6 +294,7 @@ namespace Jellyfin.Api.Controllers /// [HttpPost("Items/RemoteSearch/Apply/{id}")] [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task ApplySearchCriteria( [FromRoute] Guid itemId, [FromBody, Required] RemoteSearchResult searchResult, diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index a30873e9e..8c727cc59 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -8,6 +8,7 @@ using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; @@ -104,6 +105,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesFile("video/*", "audio/*")] public ActionResult GetFile([FromRoute] Guid itemId) { var item = _libraryManager.GetItemById(itemId); @@ -618,6 +620,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.Download)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesFile("video/*", "audio/*")] public async Task GetDownload([FromRoute] Guid itemId) { var item = _libraryManager.GetItemById(itemId); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 3ccfc826d..07a176edd 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -9,6 +9,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; @@ -1067,6 +1068,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("ListingProviders/SchedulesDirect/Countries")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile(MediaTypeNames.Application.Json)] public async Task GetSchedulesDirectCountries() { var client = _httpClientFactory.CreateClient(); @@ -1175,6 +1177,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("LiveRecordings/{recordingId}/stream")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesVideoFile] public async Task GetLiveRecordingFile([FromRoute] string recordingId) { var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId); @@ -1205,6 +1208,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("LiveStreamFiles/{streamId}/stream.{container}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesVideoFile] public async Task GetLiveStreamFile([FromRoute] string streamId, [FromRoute] string container) { var liveStreamInfo = await _mediaSourceManager.GetDirectStreamProviderByUniqueId(streamId, CancellationToken.None).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index 1e154a039..2a204e650 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net.Mime; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.MediaInfoDtos; @@ -286,6 +287,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [Produces(MediaTypeNames.Application.Octet)] + [ProducesFile(MediaTypeNames.Application.Octet)] public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400) { const int MaxSize = 10_000_000; diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index 988acccc3..a82431623 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -9,6 +9,7 @@ using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -162,6 +163,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [Produces(MediaTypeNames.Application.Octet)] + [ProducesFile("text/*")] public async Task GetRemoteSubtitles([FromRoute, Required] string? id) { var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false); @@ -185,6 +187,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")] [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile("text/*")] public async Task GetSubtitle( [FromRoute, Required] Guid itemId, [FromRoute, Required] string? mediaSourceId, @@ -251,6 +254,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task GetSubtitlePlaylist( [FromRoute] Guid itemId, diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index bbfd163de..256f39d0c 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; +using System.Net.Mime; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -190,16 +192,13 @@ namespace Jellyfin.Api.Controllers [HttpGet("Logs/Log")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile(MediaTypeNames.Text.Plain)] public ActionResult GetLogFile([FromQuery, Required] string? name) { var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); - // For older files, assume fully static - var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; - - FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare); - return File(stream, "text/plain"); + return File(file.FullName, MediaTypeNames.Text.Plain); } /// diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index b13cf9fa5..b00653d9b 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.StreamingDtos; @@ -91,6 +92,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status302Found)] + [ProducesAudioFile] public async Task GetUniversalAudioStream( [FromRoute] Guid itemId, [FromRoute] string? container, diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index d67f82219..746710475 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers /// Deletes a user. /// /// The user id. - /// User deleted. + /// User deleted. /// User not found. /// A indicating success or a if the user was not found. [HttpDelete("{userId}")] @@ -255,7 +255,7 @@ namespace Jellyfin.Api.Controllers /// /// The user id. /// The request. - /// Password successfully reset. + /// Password successfully reset. /// User is not allowed to update the password. /// User not found. /// A indicating success or a or a on failure. @@ -313,7 +313,7 @@ namespace Jellyfin.Api.Controllers /// /// The user id. /// The request. - /// Password successfully reset. + /// Password successfully reset. /// User is not allowed to update the password. /// User not found. /// A indicating success or a or a on failure. diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 76188f46d..92e82727e 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; @@ -161,6 +162,7 @@ namespace Jellyfin.Api.Controllers /// A containing the hls file. [HttpGet("Videos/{itemId}/live.m3u8")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesPlaylistFile] public async Task GetLiveHlsStream( [FromRoute] Guid itemId, [FromQuery] string? container, diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index f42810c94..18023664b 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; @@ -159,7 +160,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success, or a if the video doesn't exist. [HttpDelete("{itemId}/AlternateSources")] [Authorize(Policy = Policies.RequiresElevation)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteAlternateSources([FromRoute] Guid itemId) { @@ -326,6 +327,7 @@ namespace Jellyfin.Api.Controllers [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStream_2")] [HttpHead("{itemId}/stream", Name = "HeadVideoStream")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesVideoFile] public async Task GetVideoStream( [FromRoute] Guid itemId, [FromRoute] string? container, diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 0160a05f9..04b611123 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -16,6 +16,7 @@ using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Server.Filters; using Jellyfin.Server.Formatters; using Jellyfin.Server.Models; using MediaBrowser.Common; @@ -249,6 +250,8 @@ namespace Jellyfin.Server.Extensions // TODO - remove when all types are supported in System.Text.Json c.AddSwaggerTypeMappings(); + + c.OperationFilter(); }); } diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs new file mode 100644 index 000000000..8ea35c281 --- /dev/null +++ b/Jellyfin.Server/Filters/FileResponseFilter.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using Jellyfin.Api.Attributes; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters +{ + /// + public class FileResponseFilter : IOperationFilter + { + private const string SuccessCode = "200"; + private static readonly OpenApiMediaType _openApiMediaType = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "file" + } + }; + + /// + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata) + { + if (attribute is ProducesFileAttribute producesFileAttribute) + { + // Get operation response values. + var (_, value) = operation.Responses + .FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal)); + + // Operation doesn't have a response. + if (value == null) + { + continue; + } + + // Clear existing responses. + value.Content.Clear(); + + // Add all content-types as file. + foreach (var contentType in producesFileAttribute.GetContentTypes()) + { + value.Content.Add(contentType, _openApiMediaType); + } + + break; + } + } + } + } +} -- cgit v1.2.3 From 25ac778a79c7ff39433db2acd92788e26228879f Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 9 Sep 2020 12:26:18 -0600 Subject: revert changes --- Jellyfin.Api/Controllers/SystemController.cs | 5 ++++- Jellyfin.Api/Controllers/VideoAttachmentsController.cs | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 256f39d0c..e878e8f36 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -198,7 +198,10 @@ namespace Jellyfin.Api.Controllers var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); - return File(file.FullName, MediaTypeNames.Text.Plain); + // For older files, assume fully static + var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; + FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare); + return File(stream, "text/plain"); } /// diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 658e63955..09a1c93e6 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using System.IO; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; -- cgit v1.2.3 From 63ebae2f9eb015b1b7a834417c2958c81e82bbf8 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 9 Sep 2020 14:28:30 -0600 Subject: Remove nullable from required --- Jellyfin.Api/Controllers/ApiKeyController.cs | 2 +- Jellyfin.Api/Controllers/CollectionController.cs | 4 ++-- Jellyfin.Api/Controllers/DevicesController.cs | 8 ++++---- Jellyfin.Api/Controllers/DisplayPreferencesController.cs | 4 ++-- Jellyfin.Api/Controllers/DynamicHlsController.cs | 2 +- Jellyfin.Api/Controllers/ImageController.cs | 8 ++++---- Jellyfin.Api/Controllers/ItemUpdateController.cs | 2 +- Jellyfin.Api/Controllers/MediaInfoController.cs | 4 ++-- Jellyfin.Api/Controllers/SearchController.cs | 2 +- Jellyfin.Api/Controllers/SessionController.cs | 4 ++-- Jellyfin.Api/Controllers/SystemController.cs | 2 +- Jellyfin.Api/Controllers/TvShowsController.cs | 4 ++-- Jellyfin.Api/Controllers/UserController.cs | 2 +- Jellyfin.Api/Controllers/VideosController.cs | 2 +- 14 files changed, 25 insertions(+), 25 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs index bf973b8dd..e8d6ccdf2 100644 --- a/Jellyfin.Api/Controllers/ApiKeyController.cs +++ b/Jellyfin.Api/Controllers/ApiKeyController.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Keys")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] - public ActionResult CreateKey([FromQuery, Required] string? app) + public ActionResult CreateKey([FromQuery, Required] string app) { _authRepo.Create(new AuthenticationInfo { diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index f78690b06..2fc697a6a 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string? itemIds) + public async Task AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds) { await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true); return NoContent(); @@ -103,7 +103,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpDelete("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string? itemIds) + public async Task RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds) { await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false); return NoContent(); diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 1aed20ade..74380c2ef 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceInfo([FromQuery, Required] string? id) + public ActionResult GetDeviceInfo([FromQuery, Required] string id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo == null) @@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, Required] string? id) + public ActionResult GetDeviceOptions([FromQuery, Required] string id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo == null) @@ -111,7 +111,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateDeviceOptions( - [FromQuery, Required] string? id, + [FromQuery, Required] string id, [FromBody, Required] DeviceOptions deviceOptions) { var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); @@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult DeleteDevice([FromQuery, Required] string? id) + public ActionResult DeleteDevice([FromQuery, Required] string id) { var existingDevice = _deviceManager.GetDevice(id); if (existingDevice == null) diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs index 86aa52d05..874467c75 100644 --- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs +++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs @@ -45,7 +45,7 @@ namespace Jellyfin.Api.Controllers public ActionResult GetDisplayPreferences( [FromRoute, Required] string displayPreferencesId, [FromQuery, Required] Guid userId, - [FromQuery, Required] string? client) + [FromQuery, Required] string client) { var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client); var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client); @@ -99,7 +99,7 @@ namespace Jellyfin.Api.Controllers public ActionResult UpdateDisplayPreferences( [FromRoute, Required] string displayPreferencesId, [FromQuery, Required] Guid userId, - [FromQuery, Required] string? client, + [FromQuery, Required] string client, [FromBody, Required] DisplayPreferencesDto displayPreferences) { HomeSectionType[] defaults = diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 778c18560..670b41611 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -179,7 +179,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, - [FromQuery, Required] string? mediaSourceId, + [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, [FromQuery] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index ab4bf3d88..7afec1219 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -833,10 +833,10 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] ImageType imageType, [FromRoute, Required] string tag, [FromRoute, Required] string format, - [FromRoute, Required] int? maxWidth, - [FromRoute, Required] int? maxHeight, - [FromRoute, Required] double? percentPlayed, - [FromRoute, Required] int? unplayedCount, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, [FromQuery] int? width, [FromQuery] int? height, [FromQuery] int? quality, diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 4308a434d..0a6ed31ae 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("Items/{itemId}/ContentType")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery, Required] string? contentType) + public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType) { var item = _libraryManager.GetItemById(itemId); if (item == null) diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index ee171e856..a984afc96 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers /// A containing a with the playback information. [HttpGet("Items/{itemId}/PlaybackInfo")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid? userId) + public async Task> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId) { return await _mediaInfoHelper.GetPlaybackInfo( itemId, @@ -270,7 +270,7 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("LiveStreams/Close")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task CloseLiveStream([FromQuery, Required] string? liveStreamId) + public async Task CloseLiveStream([FromQuery, Required] string liveStreamId) { await _mediaSourceManager.CloseLiveStream(liveStreamId).ConfigureAwait(false); return NoContent(); diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index e159a9666..62c870cb1 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -81,7 +81,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] Guid? userId, - [FromQuery, Required] string? searchTerm, + [FromQuery, Required] string searchTerm, [FromQuery] string? includeItemTypes, [FromQuery] string? excludeItemTypes, [FromQuery] string? mediaTypes, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index 9da8b3ed4..b00675d67 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers public ActionResult SendMessageCommand( [FromRoute, Required] string sessionId, [FromQuery, Required] string text, - [FromQuery, Required] string? header, + [FromQuery] string? header, [FromQuery] long? timeoutMs) { var command = new MessageCommand @@ -375,7 +375,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostCapabilities( - [FromQuery, Required] string? id, + [FromQuery] string? id, [FromQuery] string? playableMediaTypes, [FromQuery] string? supportedCommands, [FromQuery] bool supportsMediaControl = false, diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index e878e8f36..a5dc123dd 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile(MediaTypeNames.Text.Plain)] - public ActionResult GetLogFile([FromQuery, Required] string? name) + public ActionResult GetLogFile([FromQuery, Required] string name) { var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath) .First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase)); diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index b9c3bd1e7..d158f6c34 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("NextUp")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNextUp( - [FromQuery, Required] Guid? userId, + [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? fields, @@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("Upcoming")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetUpcomingEpisodes( - [FromQuery, Required] Guid? userId, + [FromQuery] Guid? userId, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] string? fields, diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 435588146..f3e4b1bea 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -156,7 +156,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> AuthenticateUser( [FromRoute, Required] Guid userId, - [FromQuery, Required] string? pw, + [FromQuery, Required] string pw, [FromQuery] string? password) { var user = _userManager.GetUserById(userId); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 2608ba916..cce4cfbe3 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -203,7 +203,7 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task MergeVersions([FromQuery, Required] string? itemIds) + public async Task MergeVersions([FromQuery, Required] string itemIds) { var items = RequestHelpers.Split(itemIds, ',', true) .Select(i => _libraryManager.GetItemById(i)) -- cgit v1.2.3 From 7576824cee0dc0d8e1729ae0a7e8e4f256b71efd Mon Sep 17 00:00:00 2001 From: cvium Date: Thu, 10 Sep 2020 14:16:41 +0200 Subject: Standardize use of IsLocal and RemoteIp --- .../HttpServer/Security/SessionContext.cs | 2 +- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 3 +- Jellyfin.Api/Controllers/MediaInfoController.cs | 3 +- Jellyfin.Api/Controllers/SystemController.cs | 5 ++- .../Controllers/UniversalAudioController.cs | 3 +- Jellyfin.Api/Controllers/UserController.cs | 21 +++++----- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 10 ++--- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 3 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 3 +- .../IpBasedAccessValidationMiddleware.cs | 4 +- .../Middleware/ResponseTimeMiddleware.cs | 3 +- .../Extensions/HttpContextExtensions.cs | 46 ++++++---------------- 12 files changed, 45 insertions(+), 61 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 8777c59b7..86914dea2 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security var authorization = _authContext.GetAuthorizationInfo(requestContext); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user); + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user); } public SessionInfo GetSession(object requestContext) diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index aa366f567..d732b6bc6 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; @@ -69,7 +70,7 @@ namespace Jellyfin.Api.Auth return false; } - var ip = RequestHelpers.NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString(); + var ip = _httpContextAccessor.HttpContext.GetNormalizedRemoteIp(); var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip); // User cannot access remotely and user is remote if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork) diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index cc6eba4ae..f32bdb161 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.MediaInfoDtos; using Jellyfin.Api.Models.VideoDtos; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -164,7 +165,7 @@ namespace Jellyfin.Api.Controllers enableTranscoding, allowVideoStreamCopy, allowAudioStreamCopy, - Request.HttpContext.Connection.RemoteIpAddress.ToString()); + Request.HttpContext.GetNormalizedRemoteIp()); } _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index bbfd163de..34c7bb18d 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Constants; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; @@ -176,8 +177,8 @@ namespace Jellyfin.Api.Controllers { return new EndPointInfo { - IsLocal = Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress), - IsInNetwork = _network.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString()) + IsLocal = HttpContext.IsLocal(), + IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()) }; } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index f7f2d0174..e3e3166b0 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -158,7 +159,7 @@ namespace Jellyfin.Api.Controllers true, true, true, - Request.HttpContext.Connection.RemoteIpAddress.ToString()); + Request.HttpContext.GetNormalizedRemoteIp()); } _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate); diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 95067bc17..85430e63f 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -7,6 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; @@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers return NotFound("User not found"); } - var result = _userManager.GetUserDto(user, HttpContext.Connection.RemoteIpAddress.ToString()); + var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp()); return result; } @@ -203,7 +204,7 @@ namespace Jellyfin.Api.Controllers DeviceName = auth.Device, Password = request.Pw, PasswordSha1 = request.Password, - RemoteEndPoint = HttpContext.Connection.RemoteIpAddress.ToString(), + RemoteEndPoint = HttpContext.GetNormalizedRemoteIp(), Username = request.Username }).ConfigureAwait(false); @@ -212,7 +213,7 @@ namespace Jellyfin.Api.Controllers catch (SecurityException e) { // rethrow adding IP address to message - throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e); + throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); } } @@ -246,7 +247,7 @@ namespace Jellyfin.Api.Controllers catch (SecurityException e) { // rethrow adding IP address to message - throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e); + throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e); } } @@ -290,7 +291,7 @@ namespace Jellyfin.Api.Controllers user.Username, request.CurrentPw, request.CurrentPw, - HttpContext.Connection.RemoteIpAddress.ToString(), + HttpContext.GetNormalizedRemoteIp(), false).ConfigureAwait(false); if (success == null) @@ -496,7 +497,7 @@ namespace Jellyfin.Api.Controllers await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false); } - var result = _userManager.GetUserDto(newUser, HttpContext.Connection.RemoteIpAddress.ToString()); + var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp()); return result; } @@ -511,8 +512,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ForgotPassword([FromBody] string? enteredUsername) { - var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress) - || _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString()); + var isLocal = HttpContext.IsLocal() + || _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()); var result = await _userManager.StartForgotPasswordProcess(enteredUsername, isLocal).ConfigureAwait(false); @@ -559,7 +560,7 @@ namespace Jellyfin.Api.Controllers if (filterByNetwork) { - if (!_networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString())) + if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())) { users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess)); } @@ -567,7 +568,7 @@ namespace Jellyfin.Api.Controllers var result = users .OrderBy(u => u.Username) - .Select(i => _userManager.GetUserDto(i, HttpContext.Connection.RemoteIpAddress.ToString())); + .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp())); return result; } diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 6a8829d46..af0519ffa 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; @@ -198,12 +199,12 @@ namespace Jellyfin.Api.Helpers if (!string.IsNullOrWhiteSpace(subtitleGroup)) { - AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.Request.HttpContext.User); + AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); } AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup); - if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.Request.HttpContext.Connection.RemoteIpAddress)) + if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp())) { var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0; @@ -334,11 +335,10 @@ namespace Jellyfin.Api.Helpers } } - private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, IPAddress ipAddress) + private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream, bool enableAdaptiveBitrateStreaming, string ipAddress) { // Within the local network this will likely do more harm than good. - var ip = RequestHelpers.NormalizeIp(ipAddress).ToString(); - if (_networkManager.IsInLocalNetwork(ip)) + if (_networkManager.IsInLocalNetwork(ipAddress)) { return false; } diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 3a736d1e8..1207fb513 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; @@ -498,7 +499,7 @@ namespace Jellyfin.Api.Helpers true, true, true, - httpRequest.HttpContext.Connection.RemoteIpAddress.ToString()); + httpRequest.HttpContext.GetNormalizedRemoteIp()); } else { diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index fbaa69270..d15b5603e 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using Jellyfin.Data.Enums; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Querying; @@ -119,7 +120,7 @@ namespace Jellyfin.Api.Helpers authorization.Version, authorization.DeviceId, authorization.Device, - request.HttpContext.Connection.RemoteIpAddress.ToString(), + request.HttpContext.GetNormalizedRemoteIp(), user); if (session == null) diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 59b5fb1ed..4bda8f273 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -32,13 +32,13 @@ namespace Jellyfin.Server.Middleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - if (httpContext.Request.IsLocal()) + if (httpContext.IsLocal()) { await _next(httpContext).ConfigureAwait(false); return; } - var remoteIp = httpContext.Request.RemoteIp(); + var remoteIp = httpContext.GetNormalizedRemoteIp(); if (serverConfigurationManager.Configuration.EnableRemoteAccess) { diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs index 3122d92cb..74874da1b 100644 --- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs +++ b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Globalization; using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -69,7 +70,7 @@ namespace Jellyfin.Server.Middleware _logger.LogWarning( "Slow HTTP Response from {url} to {remoteIp} in {elapsed:g} with Status Code {statusCode}", context.Request.GetDisplayUrl(), - context.Connection.RemoteIpAddress, + context.GetNormalizedRemoteIp(), watch.Elapsed, context.Response.StatusCode); } diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs index e0cf3f9ac..8d2908882 100644 --- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -1,5 +1,3 @@ -using System.Net; -using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Common.Extensions @@ -10,54 +8,32 @@ namespace MediaBrowser.Common.Extensions public static class HttpContextExtensions { /// - /// Checks the origin of the HTTP request. + /// Checks the origin of the HTTP context. /// - /// The incoming HTTP request. + /// The incoming HTTP context. /// true if the request is coming from LAN, false otherwise. - public static bool IsLocal(this HttpRequest request) + public static bool IsLocal(this HttpContext context) { - return (request.HttpContext.Connection.LocalIpAddress == null - && request.HttpContext.Connection.RemoteIpAddress == null) - || request.HttpContext.Connection.LocalIpAddress.Equals(request.HttpContext.Connection.RemoteIpAddress); + return (context.Connection.LocalIpAddress == null + && context.Connection.RemoteIpAddress == null) + || context.Connection.LocalIpAddress.Equals(context.Connection.RemoteIpAddress); } /// - /// Extracts the remote IP address of the caller of the HTTP request. + /// Extracts the remote IP address of the caller of the HTTP context. /// - /// The HTTP request. + /// The HTTP context. /// The remote caller IP address. - public static string RemoteIp(this HttpRequest request) + public static string GetNormalizedRemoteIp(this HttpContext context) { - var cachedRemoteIp = request.HttpContext.Items["RemoteIp"]?.ToString(); - if (!string.IsNullOrEmpty(cachedRemoteIp)) - { - return cachedRemoteIp; - } - - IPAddress ip; - - // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip - // (if the server is behind a reverse proxy for example) - if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XForwardedFor].ToString(), out ip)) - { - if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XRealIP].ToString(), out ip)) - { - ip = request.HttpContext.Connection.RemoteIpAddress; - - // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) - ip ??= IPAddress.Loopback; - } - } + var ip = context.Connection.RemoteIpAddress; if (ip.IsIPv4MappedToIPv6) { ip = ip.MapToIPv4(); } - var normalizedIp = ip.ToString(); - - request.HttpContext.Items["RemoteIp"] = normalizedIp; - return normalizedIp; + return ip.ToString(); } } } -- cgit v1.2.3 From 9ef79d190b2490a03c566bfaaf963fbba7d124a9 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Sat, 12 Sep 2020 16:41:37 +0100 Subject: Large number of files --- Emby.Dlna/Main/DlnaEntryPoint.cs | 42 +- Emby.Dlna/PlayTo/PlayToManager.cs | 10 +- Emby.Server.Implementations/ApplicationHost.cs | 218 +--- .../Emby.Server.Implementations.csproj | 2 +- .../LiveTv/LiveTvMediaSourceProvider.cs | 2 +- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 1 + .../TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 3 +- .../LiveTv/TunerHosts/M3UTunerHost.cs | 1 + .../Networking/NetworkManager.cs | 556 --------- Emby.Server.Implementations/Udp/UdpServer.cs | 2 +- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 3 +- .../DefaultAuthorizationHandler.cs | 3 +- .../Auth/DownloadPolicy/DownloadHandler.cs | 3 +- ...FirstTimeOrIgnoreParentalControlSetupHandler.cs | 3 +- .../FirstTimeSetupOrDefaultHandler.cs | 1 + .../FirstTimeSetupOrElevatedHandler.cs | 1 + .../IgnoreParentalControlHandler.cs | 3 +- .../LocalAccessOrRequiresElevationHandler.cs | 3 +- .../Auth/LocalAccessPolicy/LocalAccessHandler.cs | 3 +- .../RequiresElevationHandler.cs | 1 + Jellyfin.Api/Controllers/SystemController.cs | 11 +- Jellyfin.Api/Controllers/UserController.cs | 3 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 3 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 3 +- Jellyfin.Networking/Jellyfin.Networking.csproj | 39 + Jellyfin.Networking/Manager/INetworkManager.cs | 189 +++ Jellyfin.Networking/Manager/NetworkManager.cs | 1203 ++++++++++++++++++++ .../Jellyfin.Server.Implementations.csproj | 1 + .../Users/UserManager.cs | 4 +- Jellyfin.Server/CoreAppHost.cs | 4 +- .../IpBasedAccessValidationMiddleware.cs | 37 +- .../Middleware/LanFilteringMiddleware.cs | 24 +- Jellyfin.Server/Program.cs | 67 +- MediaBrowser.Common/Net/INetworkManager.cs | 97 -- MediaBrowser.Controller/IServerApplicationHost.cs | 30 +- .../Configuration/PathSubstitution.cs | 19 + .../Configuration/ServerConfiguration.cs | 388 ++++--- MediaBrowser.sln | 34 +- RSSDP/RSSDP.csproj | 1 + RSSDP/SsdpCommunicationsServer.cs | 5 +- RSSDP/SsdpDevicePublisher.cs | 11 +- .../LocalAccessPolicy/LocalAccessHandlerTests.cs | 3 +- .../JellyfinApplicationFactory.cs | 3 - 43 files changed, 1836 insertions(+), 1204 deletions(-) delete mode 100644 Emby.Server.Implementations/Networking/NetworkManager.cs create mode 100644 Jellyfin.Networking/Jellyfin.Networking.csproj create mode 100644 Jellyfin.Networking/Manager/INetworkManager.cs create mode 100644 Jellyfin.Networking/Manager/NetworkManager.cs delete mode 100644 MediaBrowser.Common/Net/INetworkManager.cs create mode 100644 MediaBrowser.Model/Configuration/PathSubstitution.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 40c2cc0e0..98f50c09a 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -2,12 +2,14 @@ using System; using System.Globalization; +using System.Linq; using System.Net.Http; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -25,6 +27,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Net; using MediaBrowser.Model.System; using Microsoft.Extensions.Logging; +using NetworkCollection; using Rssdp; using Rssdp.Infrastructure; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; @@ -134,20 +137,20 @@ namespace Emby.Dlna.Main { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); - await ReloadComponents().ConfigureAwait(false); + ReloadComponents(); _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; } - private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) { if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) { - await ReloadComponents().ConfigureAwait(false); + ReloadComponents(); } } - private async Task ReloadComponents() + private void ReloadComponents() { var options = _config.GetDlnaConfiguration(); @@ -155,7 +158,7 @@ namespace Emby.Dlna.Main if (options.EnableServer) { - await StartDevicePublisher(options).ConfigureAwait(false); + StartDevicePublisher(options); } else { @@ -225,7 +228,7 @@ namespace Emby.Dlna.Main } } - public async Task StartDevicePublisher(Configuration.DlnaOptions options) + public void StartDevicePublisher(Configuration.DlnaOptions options) { if (!options.BlastAliveMessages) { @@ -245,7 +248,7 @@ namespace Emby.Dlna.Main SupportPnpRootDevice = false }; - await RegisterServerEndpoints().ConfigureAwait(false); + RegisterServerEndpoints(); _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); } @@ -255,39 +258,46 @@ namespace Emby.Dlna.Main } } - private async Task RegisterServerEndpoints() + private void RegisterServerEndpoints() { - var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false); + var bindAddresses = _networkManager.GetInternalBindAddresses() + .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)); var udn = CreateUuid(_appHost.SystemId); - foreach (var address in addresses) + if (!bindAddresses.Any()) { - if (address.AddressFamily == AddressFamily.InterNetworkV6) + // No interfaces returned, so use loopback. + bindAddresses = _networkManager.GetLoopbacks(); + } + + foreach (var addr in bindAddresses) + { + if (addr.AddressFamily == AddressFamily.InterNetworkV6) { // Not supporting IPv6 right now continue; } // Limit to LAN addresses only - if (!_networkManager.IsAddressInSubnets(address, true, true)) + if (!_networkManager.IsInLocalNetwork(addr)) { continue; } var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); + _logger.LogInformation("Registering publisher for {0} on {1}", fullService, addr); var descriptorUri = "/dlna/" + udn + "/description.xml"; - var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); + var uri = new Uri(_appHost.GetSmartApiUrl(addr.Address) + descriptorUri); var device = new SsdpRootDevice { CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. Location = uri, // Must point to the URL that serves your devices UPnP description document. - Address = address, - SubnetMask = _networkManager.GetLocalIpSubnetMask(address), + Address = addr.Address, + SubnetMask = ((IPNetAddress)addr).Mask, // MIGRATION: This fields is going. FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index 21877f121..10887bf60 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -177,15 +177,7 @@ namespace Emby.Dlna.PlayTo _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); - string serverAddress; - if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any)) - { - serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - } - else - { - serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress); - } + string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress); controller = new PlayToController( sessionInfo, diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 642e2fdbe..cc04cb03f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -46,6 +46,7 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; @@ -97,6 +98,7 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -117,7 +119,6 @@ namespace Emby.Server.Implementations private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; private readonly IFileSystem _fileSystemManager; - private readonly INetworkManager _networkManager; private readonly IXmlSerializer _xmlSerializer; private readonly IStartupOptions _startupOptions; @@ -188,6 +189,11 @@ namespace Emby.Server.Implementations /// The plugins. public IReadOnlyList Plugins => _plugins; + /// + /// Gets the NetworkManager object. + /// + private readonly INetworkManager _networkManager; + /// /// Gets the logger factory. /// @@ -211,7 +217,7 @@ namespace Emby.Server.Implementations private readonly List _disposableParts = new List(); /// - /// Gets the configuration manager. + /// Gets or sets the configuration manager. /// /// The configuration manager. protected IConfigurationManager ConfigurationManager { get; set; } @@ -244,28 +250,25 @@ namespace Emby.Server.Implementations /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. /// Instance of the interface. public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager, IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); ServiceCollection = serviceCollection; - _networkManager = networkManager; - networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; - ApplicationPaths = applicationPaths; LoggerFactory = loggerFactory; _fileSystemManager = fileSystem; ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + _networkManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); + Logger = LoggerFactory.CreateLogger(); _startupOptions = options; @@ -278,8 +281,6 @@ namespace Emby.Server.Implementations fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); - _networkManager.NetworkChanged += OnNetworkChanged; - CertificateInfo = new CertificateInfo { Path = ServerConfigurationManager.Configuration.CertificatePath, @@ -308,16 +309,6 @@ namespace Emby.Server.Implementations .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); } - private string[] GetConfiguredLocalSubnets() - { - return ServerConfigurationManager.Configuration.LocalNetworkSubnets; - } - - private void OnNetworkChanged(object sender, EventArgs e) - { - _validAddressResults.Clear(); - } - /// public Version ApplicationVersion { get; } @@ -398,7 +389,7 @@ namespace Emby.Server.Implementations /// /// Resolves this instance. /// - /// The type + /// The type. /// ``0. public T Resolve() => ServiceProvider.GetService(); @@ -1091,13 +1082,10 @@ namespace Emby.Server.Implementations /// /// Gets the system status. /// - /// The cancellation token. + /// Where this request originated. /// SystemInfo. - public async Task GetSystemInfo(CancellationToken cancellationToken) + public SystemInfo GetSystemInfo(IPAddress source) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - var transcodingTempPath = ConfigurationManager.GetTranscodePath(); - return new SystemInfo { HasPendingRestart = HasPendingRestart, @@ -1117,9 +1105,9 @@ namespace Emby.Server.Implementations CanSelfRestart = CanSelfRestart, CanLaunchWebBrowser = CanLaunchWebBrowser, HasUpdateAvailable = HasUpdateAvailable, - TranscodingTempPath = transcodingTempPath, + TranscodingTempPath = ConfigurationManager.GetTranscodePath(), ServerName = FriendlyName, - LocalAddress = localAddress, + LocalAddress = GetSmartApiUrl(source), SupportsLibraryMonitor = true, EncoderLocation = _mediaEncoder.EncoderLocation, SystemArchitecture = RuntimeInformation.OSArchitecture, @@ -1132,10 +1120,8 @@ namespace Emby.Server.Implementations .Select(i => new WakeOnLanInfo(i)) .ToList(); - public async Task GetPublicSystemInfo(CancellationToken cancellationToken) + public PublicSystemInfo GetPublicSystemInfo(IPAddress source) { - var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - return new PublicSystemInfo { Version = ApplicationVersionString, @@ -1143,8 +1129,8 @@ namespace Emby.Server.Implementations Id = SystemId, OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, - LocalAddress = localAddress, - StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted + LocalAddress = GetSmartApiUrl(source), + StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted }; } @@ -1152,186 +1138,51 @@ namespace Emby.Server.Implementations public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; /// - public async Task GetLocalApiUrl(CancellationToken cancellationToken) + public string GetSmartApiUrl(object source) { - try - { - // Return the first matched address, if found, or the first known local address - var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false); - if (addresses.Count == 0) - { - return null; - } - - return GetLocalApiUrl(addresses[0]); - } - catch (Exception ex) + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) { - Logger.LogError(ex, "Error getting local Ip address information"); + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - return null; - } + string smart = _networkManager.GetBindInterface(source, out int? port); - /// - /// Removes the scope id from IPv6 addresses. - /// - /// The IPv6 address. - /// The IPv6 address without the scope id. - private ReadOnlySpan RemoveScopeId(ReadOnlySpan address) - { - var index = address.IndexOf('%'); - if (index == -1) + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return address; + return smart.Trim('/'); } - return address.Slice(0, index); + return GetLocalApiUrl(smart.Trim('/'), source is HttpRequest request ? request.Scheme : null, port); } - /// - public string GetLocalApiUrl(IPAddress ipAddress) + /// + public string GetLoopbackHttpApiUrl() { - if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + if (NetworkManager.IsIP6Enabled) { - var str = RemoveScopeId(ipAddress.ToString()); - Span span = new char[str.Length + 2]; - span[0] = '['; - str.CopyTo(span.Slice(1)); - span[^1] = ']'; - - return GetLocalApiUrl(span); + return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); } - return GetLocalApiUrl(ipAddress.ToString()); - } - - /// - public string GetLoopbackHttpApiUrl() - { return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort); } /// - public string GetLocalApiUrl(ReadOnlySpan host, string scheme = null, int? port = null) + public string GetLocalApiUrl(string host, string scheme = null, int? port = null) { // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does // not. For consistency, always trim the trailing slash. return new UriBuilder { Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), - Host = host.ToString(), + Host = host, Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), Path = ServerConfigurationManager.Configuration.BaseUrl }.ToString().TrimEnd('/'); } - public Task> GetLocalIpAddresses(CancellationToken cancellationToken) - { - return GetLocalIpAddressesInternal(true, 0, cancellationToken); - } - - private async Task> GetLocalIpAddressesInternal(bool allowLoopback, int limit, CancellationToken cancellationToken) - { - var addresses = ServerConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(x => NormalizeConfiguredLocalAddress(x)) - .Where(i => i != null) - .ToList(); - - if (addresses.Count == 0) - { - addresses.AddRange(_networkManager.GetLocalIpAddresses()); - } - - var resultList = new List(); - - foreach (var address in addresses) - { - if (!allowLoopback) - { - if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback)) - { - continue; - } - } - - if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false)) - { - resultList.Add(address); - - if (limit > 0 && resultList.Count >= limit) - { - return resultList; - } - } - } - - return resultList; - } - - public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan address) - { - var index = address.Trim('/').IndexOf('/'); - if (index != -1) - { - address = address.Slice(index + 1); - } - - if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) - { - return result; - } - - return null; - } - - private readonly ConcurrentDictionary _validAddressResults = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - - private async Task IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) - { - if (address.Equals(IPAddress.Loopback) - || address.Equals(IPAddress.IPv6Loopback)) - { - return true; - } - - var apiUrl = GetLocalApiUrl(address) + "/system/ping"; - - if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult)) - { - return cachedResult; - } - - try - { - using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - - await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); - var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); - - _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); - return valid; - } - catch (OperationCanceledException) - { - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, "Cancelled"); - throw; - } - catch (Exception ex) - { - Logger.LogDebug(ex, "Ping test result to {0}. Success: {1}", apiUrl, false); - - _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false); - return false; - } - } - public string FriendlyName => string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName) ? Environment.MachineName @@ -1486,6 +1337,7 @@ namespace Emby.Server.Implementations _disposed = true; } + } internal class CertificateInfo diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 0a348f0d0..db8116743 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -36,7 +36,7 @@ - + diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 8a0c0043a..598cf0af7 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -76,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv } var list = sources.ToList(); - var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + var serverUrl = _appHost.GetSmartApiUrl(string.Empty); foreach (var source in list) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 28e30fac8..1cf129ad2 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -10,6 +10,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 6730751d5..02ee302d0 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; @@ -57,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var mediaSource = OriginalMediaSource; var uri = new Uri(mediaSource.Path); - var localPort = _networkManager.GetRandomUnusedUdpPort(); + var localPort = 50000; // Will return to random after next PR. Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 8107bc427..f297ecd5d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs deleted file mode 100644 index 089ec30e6..000000000 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ /dev/null @@ -1,556 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Networking -{ - /// - /// Class to take care of network interface management. - /// - public class NetworkManager : INetworkManager - { - private readonly ILogger _logger; - - private IPAddress[] _localIpAddresses; - private readonly object _localIpAddressSyncLock = new object(); - - private readonly object _subnetLookupLock = new object(); - private readonly Dictionary> _subnetLookup = new Dictionary>(StringComparer.Ordinal); - - private List _macAddresses; - - /// - /// Initializes a new instance of the class. - /// - /// Logger to use for messages. - public NetworkManager(ILogger logger) - { - _logger = logger; - - NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; - NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - } - - /// - public event EventHandler NetworkChanged; - - /// - public Func LocalSubnetsFn { get; set; } - - private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e) - { - _logger.LogDebug("NetworkAvailabilityChanged"); - OnNetworkChanged(); - } - - private void OnNetworkAddressChanged(object sender, EventArgs e) - { - _logger.LogDebug("NetworkAddressChanged"); - OnNetworkChanged(); - } - - private void OnNetworkChanged() - { - lock (_localIpAddressSyncLock) - { - _localIpAddresses = null; - _macAddresses = null; - } - - NetworkChanged?.Invoke(this, EventArgs.Empty); - } - - /// - public IPAddress[] GetLocalIpAddresses() - { - lock (_localIpAddressSyncLock) - { - if (_localIpAddresses == null) - { - var addresses = GetLocalIpAddressesInternal().ToArray(); - - _localIpAddresses = addresses; - } - - return _localIpAddresses; - } - } - - private List GetLocalIpAddressesInternal() - { - var list = GetIPsDefault().ToList(); - - if (list.Count == 0) - { - list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList(); - } - - var listClone = new List(); - - var subnets = LocalSubnetsFn(); - - foreach (var i in list) - { - if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (Array.IndexOf(subnets, $"[{i}]") == -1) - { - listClone.Add(i); - } - } - - return listClone - .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1) - // .ThenBy(i => listClone.IndexOf(i)) - .GroupBy(i => i.ToString()) - .Select(x => x.First()) - .ToList(); - } - - /// - public bool IsInPrivateAddressSpace(string endpoint) - { - return IsInPrivateAddressSpace(endpoint, true); - } - - // Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address - private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets) - { - if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // IPV6 - if (endpoint.Split('.').Length > 4) - { - // Handle ipv4 mapped to ipv6 - var originalEndpoint = endpoint; - endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase); - - if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - - // Private address space: - - if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (!IPAddress.TryParse(endpoint, out var ipAddress)) - { - return false; - } - - byte[] octet = ipAddress.GetAddressBytes(); - - if ((octet[0] == 10) || - (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 - (octet[0] == 192 && octet[1] == 168) || // RFC1918 - (octet[0] == 127) || // RFC1122 - (octet[0] == 169 && octet[1] == 254)) // RFC3927 - { - return true; - } - - if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) - { - return true; - } - - return false; - } - - /// - public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint) - { - if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase)) - { - var endpointFirstPart = endpoint.Split('.')[0]; - - var subnets = GetSubnets(endpointFirstPart); - - foreach (var subnet_Match in subnets) - { - // logger.LogDebug("subnet_Match:" + subnet_Match); - - if (endpoint.StartsWith(subnet_Match + ".", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - } - - return false; - } - - // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart - private List GetSubnets(string endpointFirstPart) - { - lock (_subnetLookupLock) - { - if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets)) - { - return subnets; - } - - subnets = new List(); - - foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces()) - { - foreach (var unicastIPAddressInformation in adapter.GetIPProperties().UnicastAddresses) - { - if (unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetwork && endpointFirstPart == unicastIPAddressInformation.Address.ToString().Split('.')[0]) - { - int subnet_Test = 0; - foreach (string part in unicastIPAddressInformation.IPv4Mask.ToString().Split('.')) - { - if (part.Equals("0", StringComparison.Ordinal)) - { - break; - } - - subnet_Test++; - } - - var subnet_Match = string.Join(".", unicastIPAddressInformation.Address.ToString().Split('.').Take(subnet_Test).ToArray()); - - // TODO: Is this check necessary? - if (adapter.OperationalStatus == OperationalStatus.Up) - { - subnets.Add(subnet_Match); - } - } - } - } - - _subnetLookup[endpointFirstPart] = subnets; - - return subnets; - } - } - - /// - public bool IsInLocalNetwork(string endpoint) - { - return IsInLocalNetworkInternal(endpoint, true); - } - - /// - public bool IsAddressInSubnets(string addressString, string[] subnets) - { - return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets); - } - - /// - public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) - { - byte[] octet = address.GetAddressBytes(); - - if ((octet[0] == 127) || // RFC1122 - (octet[0] == 169 && octet[1] == 254)) // RFC3927 - { - // don't use on loopback or 169 interfaces - return false; - } - - string addressString = address.ToString(); - string excludeAddress = "[" + addressString + "]"; - var subnets = LocalSubnetsFn(); - - // Include any address if LAN subnets aren't specified - if (subnets.Length == 0) - { - return true; - } - - // Exclude any addresses if they appear in the LAN list in [ ] - if (Array.IndexOf(subnets, excludeAddress) != -1) - { - return false; - } - - return IsAddressInSubnets(address, addressString, subnets); - } - - /// - /// Checks if the give address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format. - /// - /// IPAddress version of the address. - /// The address to check. - /// If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address. - /// falseif the address isn't in the subnets, true otherwise. - private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets) - { - foreach (var subnet in subnets) - { - var normalizedSubnet = subnet.Trim(); - // Is the subnet a host address and does it match the address being passes? - if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - // Parse CIDR subnets and see if address falls within it. - if (normalizedSubnet.Contains('/', StringComparison.Ordinal)) - { - try - { - var ipNetwork = IPNetwork.Parse(normalizedSubnet); - if (ipNetwork.Contains(address)) - { - return true; - } - } - catch - { - // Ignoring - invalid subnet passed encountered. - } - } - } - - return false; - } - - private bool IsInLocalNetworkInternal(string endpoint, bool resolveHost) - { - if (string.IsNullOrEmpty(endpoint)) - { - throw new ArgumentNullException(nameof(endpoint)); - } - - if (IPAddress.TryParse(endpoint, out var address)) - { - var addressString = address.ToString(); - - var localSubnetsFn = LocalSubnetsFn; - if (localSubnetsFn != null) - { - var localSubnets = localSubnetsFn(); - foreach (var subnet in localSubnets) - { - // Only validate if there's at least one valid entry. - if (!string.IsNullOrWhiteSpace(subnet)) - { - return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false); - } - } - } - - int lengthMatch = 100; - if (address.AddressFamily == AddressFamily.InterNetwork) - { - lengthMatch = 4; - if (IsInPrivateAddressSpace(addressString, true)) - { - return true; - } - } - else if (address.AddressFamily == AddressFamily.InterNetworkV6) - { - lengthMatch = 9; - if (IsInPrivateAddressSpace(endpoint, true)) - { - return true; - } - } - - // Should be even be doing this with ipv6? - if (addressString.Length >= lengthMatch) - { - var prefix = addressString.Substring(0, lengthMatch); - - if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) - { - return true; - } - } - } - else if (resolveHost) - { - if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out var uri)) - { - try - { - var host = uri.DnsSafeHost; - _logger.LogDebug("Resolving host {0}", host); - - address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault(); - - if (address != null) - { - _logger.LogDebug("{0} resolved to {1}", host, address); - - return IsInLocalNetworkInternal(address.ToString(), false); - } - } - catch (InvalidOperationException) - { - // Can happen with reverse proxy or IIS url rewriting? - } - catch (Exception ex) - { - _logger.LogError(ex, "Error resolving hostname"); - } - } - } - - return false; - } - - private static Task GetIpAddresses(string hostName) - { - return Dns.GetHostAddressesAsync(hostName); - } - - private IEnumerable GetIPsDefault() - { - IEnumerable interfaces; - - try - { - interfaces = NetworkInterface.GetAllNetworkInterfaces() - .Where(x => x.OperationalStatus == OperationalStatus.Up - || x.OperationalStatus == OperationalStatus.Unknown); - } - catch (NetworkInformationException ex) - { - _logger.LogError(ex, "Error in GetAllNetworkInterfaces"); - return Enumerable.Empty(); - } - - return interfaces.SelectMany(network => - { - var ipProperties = network.GetIPProperties(); - - // Exclude any addresses if they appear in the LAN list in [ ] - - return ipProperties.UnicastAddresses - .Select(i => i.Address) - .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6); - }).GroupBy(i => i.ToString()) - .Select(x => x.First()); - } - - private static async Task> GetLocalIpAddressesFallback() - { - var host = await Dns.GetHostEntryAsync(Dns.GetHostName()).ConfigureAwait(false); - - // Reverse them because the last one is usually the correct one - // It's not fool-proof so ultimately the consumer will have to examine them and decide - return host.AddressList - .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6) - .Reverse(); - } - - /// - /// Gets a random port number that is currently available. - /// - /// System.Int32. - public int GetRandomUnusedTcpPort() - { - var listener = new TcpListener(IPAddress.Any, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } - - /// - public int GetRandomUnusedUdpPort() - { - var localEndPoint = new IPEndPoint(IPAddress.Any, 0); - using (var udpClient = new UdpClient(localEndPoint)) - { - return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; - } - } - - /// - public List GetMacAddresses() - { - return _macAddresses ??= GetMacAddressesInternal().ToList(); - } - - private static IEnumerable GetMacAddressesInternal() - => NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .Select(x => x.GetPhysicalAddress()) - .Where(x => !x.Equals(PhysicalAddress.None)); - - /// - public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask) - { - IPAddress network1 = GetNetworkAddress(address1, subnetMask); - IPAddress network2 = GetNetworkAddress(address2, subnetMask); - return network1.Equals(network2); - } - - private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask) - { - byte[] ipAdressBytes = address.GetAddressBytes(); - byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); - - if (ipAdressBytes.Length != subnetMaskBytes.Length) - { - throw new ArgumentException("Lengths of IP address and subnet mask do not match."); - } - - byte[] broadcastAddress = new byte[ipAdressBytes.Length]; - for (int i = 0; i < broadcastAddress.Length; i++) - { - broadcastAddress[i] = (byte)(ipAdressBytes[i] & subnetMaskBytes[i]); - } - - return new IPAddress(broadcastAddress); - } - - /// - public IPAddress GetLocalIpSubnetMask(IPAddress address) - { - NetworkInterface[] interfaces; - - try - { - var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown }; - - interfaces = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => validStatuses.Contains(i.OperationalStatus)) - .ToArray(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in GetAllNetworkInterfaces"); - return null; - } - - foreach (NetworkInterface ni in interfaces) - { - foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) - { - if (ip.Address.Equals(address) && ip.IPv4Mask != null) - { - return ip.IPv4Mask; - } - } - } - - return null; - } - } -} diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index b7a59cee2..3dc34da5c 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Udp { string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey]) ? _config[AddressOverrideConfigKey] - : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false); + : _appHost.GetSmartApiUrl(string.Empty); // MIGRATION: Temp value. if (!string.IsNullOrEmpty(localUrl)) { diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index d732b6bc6..08746b346 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,6 +1,7 @@ -using System.Security.Claims; +using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index b5913daab..69e6a8fb2 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs index b61680ab1..d1297119c 100644 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs index 31482a930..53b5d4778 100644 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs index 9815e252e..abdf2858d 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index decbe0c03..ada8a0d4e 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs index 5213bc4cb..475e3cdac 100644 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index 14722aa57..d022c9067 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,5 +1,6 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs index af73352bc..418d63de6 100644 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs index b235c4b63..a1cddbca3 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 4cb1984a2..6876b47b4 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -64,9 +65,9 @@ namespace Jellyfin.Api.Controllers [HttpGet("Info")] [Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetSystemInfo() + public ActionResult GetSystemInfo() { - return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false); + return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); } /// @@ -76,9 +77,9 @@ namespace Jellyfin.Api.Controllers /// A with public info about the system. [HttpGet("Info/Public")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> GetPublicSystemInfo() + public ActionResult GetPublicSystemInfo() { - return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false); + return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); } /// diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 630e9df6a..152e650bc 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -7,6 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index af0519ffa..3be8734b9 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 1207fb513..d63e3ab11 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Linq; using System.Text.Json; @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj new file mode 100644 index 000000000..b6834cb0e --- /dev/null +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -0,0 +1,39 @@ + + + Jellyfin.Networking + Library + netstandard2.1 + false + true + true + enable + + + + + + + + + + + + + + + ../jellyfin.ruleset + + + + + + + + + + + + + + + diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/Jellyfin.Networking/Manager/INetworkManager.cs new file mode 100644 index 000000000..ba571750b --- /dev/null +++ b/Jellyfin.Networking/Manager/INetworkManager.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using MediaBrowser.Model.Configuration; +using NetworkCollection; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Interface for the NetworkManager class. + /// + public interface INetworkManager + { + /// + /// Event triggered on network changes. + /// + event EventHandler NetworkChanged; + + /// + /// Gets the Published server override list. + /// + Dictionary PublishedServerOverrides { get; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; } + + /// + /// Gets returns the remote address filter. + /// + NetCollection RemoteAddressFilter { get; } + + /// + /// Calculates the list of interfaces to use for Kestrel. + /// + /// A NetCollection object containing all the interfaces to bind. + /// If all the interfaces are specified, and none are excluded, it returns zero items + /// to represent any address. + NetCollection GetAllBindInterfaces(); + + /// + /// Returns a collection containing the loopback interfaces. + /// + /// Netcollection. + public NetCollection GetLoopbacks(); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// The priority of selection is as follows:- + /// + /// The value contained in the startup parameter --published-server-url. + /// + /// If the user specified custom subnet overrides, the correct subnet for the source address. + /// + /// If the user specified bind interfaces to use:- + /// The bind interface that contains the source subnet. + /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. + /// + /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- + /// The first public interface that isn't a loopback and contains the source subnet. + /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. + /// An internal interface if there are no public ip addresses. + /// + /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- + /// The first private interface that contains the source subnet. + /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. + /// + /// If no interfaces meet any of these criteria, then a loopback address is returned. + /// + /// Interface that have been specifically excluded from binding are not used in any of the calculations. + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(object? source, out int? port); + + /// + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// + /// IP address to check. + /// True if it is. + bool IsExcludedInterface(IPAddress address); + + /// + /// Get a list of all the MAC addresses associated with active interfaces. + /// + /// List of MAC addresses. + List GetMacAddresses(); + + /// + /// Checks to see if the IP Address provided matches an interface that has a gateway. + /// + /// IP to check. Can be an IPAddress or an IPObject. + /// Result of the check. + public bool IsGatewayInterface(object? addressObj); + + /// + /// Returns true if the address is a private address. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// Address to check. + /// True or False. + bool IsPrivateAddressRange(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(string address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPAddress address); + + /// + /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. + /// eg. "eth1", or "TP-LINK Wireless USB Adapter". + /// + /// Token to parse. + /// Resultant object if successful. + /// Success of the operation. + bool TryParseInterface(string token, out IPNetAddress result); + + /// + /// Parses an array of strings into a NetCollection. + /// + /// Values to parse. + /// When true, only include values in []. When false, ignore bracketed values. + /// IPCollection object containing the value strings. + NetCollection CreateIPCollection(string[] values, bool bracketed = false); + + /// + /// Returns all the internal Bind interface addresses. + /// + /// An internal list of interfaces addresses. + NetCollection GetInternalBindAddresses(); + + /// + /// Checks to see if an IP address is still a valid interface address. + /// + /// IP address to check. + /// True if it is. + bool IsValidInterfaceAddress(IPAddress address); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(IPAddress ip); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(EndPoint ip); + + /// + /// Gets the filtered LAN ip addresses. + /// + /// Optional filter for the list. + /// Returns a filtered list of LAN addresses. + NetCollection GetFilteredLANSubnets(NetCollection? filter = null); + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// to use. + public void UpdateSettings(ServerConfiguration config); + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs new file mode 100644 index 000000000..36a0a94a0 --- /dev/null +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -0,0 +1,1203 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using NetworkCollection; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Class to take care of network interface management. + /// + public class NetworkManager : INetworkManager, IDisposable + { + private static NetworkManager? _instance; + + /// + /// Contains the description of the interface along with its index. + /// + private readonly SortedList _interfaceNames; + + /// + /// Threading lock for network interfaces. + /// + private readonly object _intLock = new object(); + + /// + /// List of all interface addresses and masks. + /// + private readonly NetCollection _interfaceAddresses; + + /// + /// List of all interface MAC addresses. + /// + private readonly List _macAddresses; + + private readonly ILogger _logger; + + private readonly IConfigurationManager _configurationManager; + + /// + /// Holds the bind address overrides. + /// + private readonly Dictionary _overrideUrls; + + /// + /// Used to stop "event-racing conditions". + /// + private bool _eventfire; + + /// + /// Unfiltered user defined LAN subnets. (Configuration.LocalNetworkSubnets). + /// or internal interface network subnets if undefined by user. + /// + private NetCollection _lanSubnets; + + /// + /// User defined list of subnets to excluded from the LAN. + /// + private NetCollection _excludedSubnets; + + /// + /// List of interface addresses to bind the WS. + /// + private NetCollection _bindAddresses; + + /// + /// List of interface addresses to exclude from bind. + /// + private NetCollection _bindExclusions; + + /// + /// Caches list of all internal filtered interface addresses and masks. + /// + private NetCollection _internalInterfaces; + + /// + /// Flag set when no custom LAN has been defined in the config. + /// + private bool _usingPrivateAddresses; + + /// + /// True if this object is disposed. + /// + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// IServerConfigurationManager instance. + /// Logger to use for messages. +#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in InitialiseLAN function. Compiler doesn't yet recognise this. + public NetworkManager(IConfigurationManager configurationManager, ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager)); + + _interfaceAddresses = new NetCollection(unique: false); + _macAddresses = new List(); + _interfaceNames = new SortedList(); + _overrideUrls = new Dictionary(); + + UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); + if (!IsIP6Enabled && !IsIP4Enabled) + { + throw new ApplicationException("IPv4 and IPv6 cannot both be disabled."); + } + + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + _configurationManager.ConfigurationUpdated += ConfigurationUpdated; + + Instance = this; + } +#pragma warning restore CS8618 // Non-nullable field is uninitialized. + + /// + /// Event triggered on network changes. + /// + public event EventHandler? NetworkChanged; + + /// + /// Gets the singleton of this object. + /// + public static NetworkManager Instance + { + get => GetInstance(); + + internal set + { + _instance = value; + } + } + + /// + /// Gets the unique network location signature, which is updated on every network change. + /// + public static string NetworkLocationSignature { get; internal set; } = Guid.NewGuid().ToString(); + + /// + /// Gets a value indicating whether IP6 is enabled. + /// + public static bool IsIP6Enabled { get; internal set; } + + /// + /// Gets a value indicating whether IP4 is enabled. + /// + public static bool IsIP4Enabled { get; internal set; } = true; + + /// + /// Gets a value indicating whether is multi-socket binding available. + /// + public static bool EnableMultiSocketBinding { get; internal set; } = true; + + /// + /// Gets the number of times the network address has changed. + /// + public static int NetworkChangeCount { get; internal set; } = 1; + + /// + public NetCollection RemoteAddressFilter { get; private set; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; internal set; } + + /// + /// Gets the Published server override list. + /// + public Dictionary PublishedServerOverrides => _overrideUrls; + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public List GetMacAddresses() + { + // Populated in construction - so always has values. + lock (_intLock) + { + return _macAddresses.ToList(); + } + } + + /// + public bool IsGatewayInterface(object? addressObj) + { + var address = (addressObj is IPAddress addressIP) ? + addressIP : (addressObj is IPObject addressIPObj) ? + addressIPObj.Address : IPAddress.None; + + lock (_intLock) + { + return _internalInterfaces.Where(i => i.Address.Equals(address) && (i.Tag < 0)).Any(); + } + } + + /// + public NetCollection GetLoopbacks() + { + NetCollection nc = new NetCollection(); + if (IsIP4Enabled) + { + nc.Add(IPAddress.Loopback); + } + + if (IsIP6Enabled) + { + nc.Add(IPAddress.IPv6Loopback); + } + + return nc; + } + + /// + public bool IsExcluded(IPAddress ip) + { + return _excludedSubnets.Contains(ip); + } + + /// + public bool IsExcluded(EndPoint ip) + { + if (ip != null) + { + return _excludedSubnets.Contains(((IPEndPoint)ip).Address); + } + + return false; + } + + /// + public NetCollection CreateIPCollection(string[] values, bool bracketed = false) + { + NetCollection col = new NetCollection(); + if (values != null) + { + for (int a = 0; a < values.Length; a++) + { + string v = values[a].Trim(); + + try + { + if (v.StartsWith("[", StringComparison.OrdinalIgnoreCase) && v.EndsWith("]", StringComparison.OrdinalIgnoreCase)) + { + if (bracketed) + { + AddToCollection(col, v.Remove(v.Length - 1).Substring(1)); + } + } + else if (v.StartsWith("!", StringComparison.OrdinalIgnoreCase)) + { + if (bracketed) + { + AddToCollection(col, v.Substring(1)); + } + } + else if (!bracketed) + { + AddToCollection(col, v); + } + } + catch (ArgumentException e) + { + _logger.LogInformation("Ignoring LAN value {value}. Reason : {reason}", v, e.Message); + } + } + } + + return col; + } + + /// + public NetCollection GetAllBindInterfaces() + { + lock (_intLock) + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the interfaces except the ones specifically excluded. + return _interfaceAddresses.Exclude(_bindExclusions); + } + + // No bind address and no exclusions, so listen on all interfaces. + NetCollection result = new NetCollection(); + + if (IsIP4Enabled) + { + result.Add(IPAddress.Any); + } + + if (IsIP6Enabled) + { + result.Add(IPAddress.IPv6Any); + } + + return result; + } + + // Remove any excluded bind interfaces. + return _bindAddresses.Exclude(_bindExclusions); + } + } + + /// + public string GetBindInterface(object? source, out int? port) + { + bool chromeCast = false; + port = null; + // Parse the source object in an attempt to discover where the request originated. + IPObject sourceAddr; + if (source is HttpRequest sourceReq) + { + port = sourceReq.Host.Port; + if (IPHost.TryParse(sourceReq.Host.Host, out IPHost host)) + { + sourceAddr = host; + } + else + { + // Assume it's external, as we cannot resolve the host. + sourceAddr = IPHost.None; + } + } + else if (source is string sourceStr && !string.IsNullOrEmpty(sourceStr)) + { + if (string.Equals(sourceStr, "chromecast", StringComparison.OrdinalIgnoreCase)) + { + chromeCast = true; + // Just assign a variable so has source = true; + sourceAddr = IPNetAddress.IP4Loopback; + } + + if (IPHost.TryParse(sourceStr, out IPHost host)) + { + sourceAddr = host; + } + else + { + // Assume it's external, as we cannot resolve the host. + sourceAddr = IPHost.None; + } + } + else if (source is IPAddress sourceIP) + { + sourceAddr = new IPNetAddress(sourceIP); + } + else + { + // If we have no idea, then assume it came from an external address. + sourceAddr = IPHost.None; + } + + // Do we have a source? + bool haveSource = !sourceAddr.Address.Equals(IPAddress.None); + + if (haveSource) + { + if (!IsIP6Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetworkV6) + { + _logger.LogWarning("IPv6 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); + } + + if (!IsIP4Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetwork) + { + _logger.LogWarning("IPv4 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); + } + } + + bool isExternal = haveSource && !IsInLocalNetwork(sourceAddr); + + string bindPreference = string.Empty; + if (haveSource) + { + // Check for user override. + foreach (var addr in _overrideUrls) + { + // Remaining. Match anything. + if (addr.Key.Equals(IPAddress.Broadcast)) + { + bindPreference = addr.Value; + break; + } + else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || chromeCast)) + { + // External. + bindPreference = addr.Value; + break; + } + else if (addr.Key.Contains(sourceAddr)) + { + // Match ip address. + bindPreference = addr.Value; + break; + } + } + } + + _logger.LogDebug("GetBindInterface: Souce: {0}, External: {1}:", haveSource, isExternal); + + if (!string.IsNullOrEmpty(bindPreference)) + { + // Has it got a port defined? + var parts = bindPreference.Split(':'); + if (parts.Length > 1) + { + if (int.TryParse(parts[1], out int p)) + { + bindPreference = parts[0]; + port = p; + } + } + + _logger.LogInformation("{0}: Using BindAddress {1}:{2}", sourceAddr, bindPreference, port); + return bindPreference; + } + + string ipresult; + + // No preference given, so move on to bind addresses. + lock (_intLock) + { + var nc = _bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback()); + + int count = nc.Count(); + if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses.Equals(IPAddress.IPv6Any))) + { + // Ignore IPAny addresses. + count = 0; + } + + if (count != 0) + { + // Check to see if any of the bind interfaces are in the same subnet. + + IEnumerable bindResult; + IPAddress? defaultGateway = null; + + if (isExternal) + { + // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. + bindResult = nc.Where(p => !IsInLocalNetwork(p)).OrderBy(p => p.Tag); + defaultGateway = bindResult.FirstOrDefault()?.Address; + bindResult = bindResult.Where(p => p.Contains(sourceAddr)).OrderBy(p => p.Tag); + } + else + { + // Look for the best internal address. + bindResult = nc.Where(p => IsInLocalNetwork(p) && p.Contains(sourceAddr)).OrderBy(p => p.Tag); + } + + if (bindResult.Any()) + { + ipresult = FormatIP6String(bindResult.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", sourceAddr, ipresult); + return ipresult; + } + + if (isExternal && defaultGateway != null) + { + ipresult = FormatIP6String(defaultGateway); + _logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", sourceAddr, ipresult); + return ipresult; + } + + ipresult = FormatIP6String(nc.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", sourceAddr, ipresult); + + if (isExternal) + { + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", sourceAddr); + } + + return ipresult; + } + + if (isExternal) + { + // Get the first WAN interface address that isn't a loopback. + var extResult = _interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => !IsInLocalNetwork(p)) + .OrderBy(p => p.Tag); + + if (extResult.Any()) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in extResult) + { + if (!IsInLocalNetwork(intf) && intf.Contains(sourceAddr)) + { + ipresult = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", sourceAddr, ipresult); + return ipresult; + } + } + + ipresult = FormatIP6String(extResult.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", sourceAddr, ipresult); + return ipresult; + } + + // Have to return something, so return an internal address + + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, no WAN interface found.", sourceAddr); + } + + // Get the first LAN interface address that isn't a loopback. + var result = _interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => IsInLocalNetwork(p)) + .OrderBy(p => p.Tag); + + if (result.Any()) + { + if (haveSource) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in result) + { + if (intf.Contains(sourceAddr)) + { + ipresult = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", sourceAddr, ipresult); + return ipresult; + } + } + } + + ipresult = FormatIP6String(result.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", sourceAddr, ipresult); + return ipresult; + } + + // There isn't any others, so we'll use the loopback. + ipresult = IsIP6Enabled ? "::" : "127.0.0.1"; + _logger.LogWarning("{0}: GetBindInterface: Loopback return.", sourceAddr, ipresult); + return ipresult; + } + } + + /// + public NetCollection GetInternalBindAddresses() + { + lock (_intLock) + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the internal interfaces except the ones excluded. + return new NetCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); + } + + // No bind address, so return all internal interfaces. + return new NetCollection(_internalInterfaces.Where(p => !p.IsLoopback())); + } + + return new NetCollection(_bindAddresses.Where(p => !p.IsLoopback())); + } + } + + /// + public bool IsInLocalNetwork(IPObject address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.Equals(IPAddress.None)) + { + return false; + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + lock (_intLock) + { + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + } + } + + /// + public bool IsInLocalNetwork(string address) + { + if (IPHost.TryParse(address, out IPHost ep)) + { + lock (_intLock) + { + return _lanSubnets.Contains(ep) && !_excludedSubnets.Contains(ep); + } + } + + return false; + } + + /// + public bool IsInLocalNetwork(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + + lock (_intLock) + { + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + } + } + + /// + public bool IsPrivateAddressRange(IPObject address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + { + return true; + } + else + { + return address.IsPrivateAddressRange(); + } + } + + /// + public bool IsExcludedInterface(IPAddress address) + { + lock (_intLock) + { + if (_bindExclusions.Count > 0) + { + return _bindExclusions.Contains(address); + } + + return false; + } + } + + /// + public NetCollection GetFilteredLANSubnets(NetCollection? filter = null) + { + lock (_intLock) + { + if (filter == null) + { + return NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets)); + } + + return _lanSubnets.Exclude(filter); + } + } + + /// + public bool IsValidInterfaceAddress(IPAddress address) + { + lock (_intLock) + { + return _interfaceAddresses.Contains(address); + } + } + + /// + public bool TryParseInterface(string token, out IPNetAddress result) + { + if (string.IsNullOrEmpty(token)) + { + result = IPNetAddress.None; + return false; + } + + if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + { + _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); + + // Replace interface tags with the interface IP's. + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (Math.Abs(iface.Tag) == index && + ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || + (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) + { + result = iface; + return true; + } + } + } + + return IPNetAddress.TryParse(token, out result); + } + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// to use. + public void UpdateSettings(ServerConfiguration config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + IsIP4Enabled = Socket.OSSupportsIPv6 && config.EnableIPV4; + IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; + TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; + EnableMultiSocketBinding = config.EnableMultiSocketBinding; + + InitialiseInterfaces(); + InitialiseLAN(config); + InitialiseBind(config); + InitialiseRemote(config); + InitialiseOverrides(config); + } + + /// + /// Protected implementation of Dispose pattern. + /// + /// True to dispose the managed state. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _configurationManager.ConfigurationUpdated -= ConfigurationUpdated; + NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; + } + + _disposed = true; + } + } + + private static NetworkManager GetInstance() + { + if (_instance == null) + { + throw new ApplicationException("NetworkManager is not initialised."); + } + + return _instance; + } + + private void ConfigurationUpdated(object? sender, EventArgs args) + { + UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); + } + + /// + /// Converts an IPAddress into a string. + /// Ipv6 addresses are returned in [ ], with their scope removed. + /// + /// Address to convert. + /// URI save conversion of the address. + private string FormatIP6String(IPAddress address) + { + var str = address.ToString(); + if (address.AddressFamily == AddressFamily.InterNetworkV6) + { + int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase); + + if (i != -1) + { + str = str.Substring(0, i); + } + + return $"[{str}]"; + } + + return str; + } + + /// + /// Parses strings into the collection, replacing any interface references. + /// + /// Collection. + /// String to parse. + private void AddToCollection(NetCollection col, string token) + { + // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1. + // Null check required here for automated testing. + if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + { + _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); + + // Replace interface tags with the interface IP's. + foreach (IPNetAddress iface in _interfaceAddresses) + { + if (Math.Abs(iface.Tag) == index && + ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || + (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) + { + col.Add(iface); + } + } + } + else if (NetCollection.TryParse(token, out IPObject obj)) + { + if (!IsIP6Enabled) + { + // Remove IP6 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetworkV6); + if (!obj.IsIP6()) + { + col.Add(obj); + } + } + else if (!IsIP4Enabled) + { + // Remove IP4 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetwork); + if (obj.IsIP6()) + { + col.Add(obj); + } + } + else + { + col.Add(obj); + } + } + else + { + _logger.LogDebug("Invalid or unknown network {0}.", token); + } + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// Network availablity information. + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + _logger.LogDebug("Network availability changed."); + OnNetworkChanged(); + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// Event arguments. + private void OnNetworkAddressChanged(object? sender, EventArgs e) + { + _logger.LogDebug("Network address change detected."); + OnNetworkChanged(); + } + + /// + /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession. + /// + /// The network change async. + private async Task OnNetworkChangeAsync() + { + try + { + await Task.Delay(2000).ConfigureAwait(false); + InitialiseInterfaces(); + // Recalculate LAN caches. + InitialiseLAN((ServerConfiguration)_configurationManager.CommonConfiguration); + + NetworkChanged?.Invoke(this, EventArgs.Empty); + } + finally + { + _eventfire = false; + } + } + + /// + /// Triggers our event, and re-loads interface information. + /// + private void OnNetworkChanged() + { + // As per UPnP Device Architecture v1.0 Annex A - IPv6 Support. + NetworkLocationSignature = Guid.NewGuid().ToString(); + NetworkChangeCount++; + if (NetworkChangeCount > 99) + { + NetworkChangeCount = 1; + } + + if (!_eventfire) + { + _logger.LogDebug("Network Address Change Event."); + // As network events tend to fire one after the other only fire once every second. + _eventfire = true; + _ = OnNetworkChangeAsync(); + } + } + + /// + /// Parses the user defined overrides into the dictionary object. + /// Overrides are the equivalent of localised publishedServerUrl, enabling + /// different addresses to be advertised over different subnets. + /// format is subnet=ipaddress|host|uri + /// when subnet = 0.0.0.0, any external address matches. + /// + private void InitialiseOverrides(ServerConfiguration config) + { + string[] overrides = config.PublishedServerUriBySubnet; + if (overrides == null) + { + lock (_intLock) + { + _overrideUrls.Clear(); + } + + return; + } + + lock (_intLock) + { + _overrideUrls.Clear(); + + foreach (var entry in overrides) + { + var parts = entry.Split('='); + if (parts.Length != 2) + { + _logger.LogError("Unable to parse bind override. {0}", entry); + } + else + { + var replacement = parts[1].Trim(); + if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) + { + _overrideUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; + } + else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) + { + _overrideUrls[new IPNetAddress(IPAddress.Any)] = replacement; + } + else if (TryParseInterface(parts[0], out IPNetAddress address)) + { + _overrideUrls[address] = replacement; + } + else + { + _logger.LogError("Unable to parse bind ip address. {0}", parts[1]); + } + } + } + } + } + + private void InitialiseBind(ServerConfiguration config) + { + string[] ba = config.LocalNetworkAddresses; + + // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334 + + if (ba.Length == 1 && ba[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1) + { + ba = ba[0].Split(','); + } + + // TODO: end fix. + + // Read and parse bind addresses and exclusions, removing ones that don't exist. + _bindAddresses = CreateIPCollection(ba).Union(_interfaceAddresses); + _bindExclusions = CreateIPCollection(ba, true).Union(_interfaceAddresses); + _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); + _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); + } + + private void InitialiseRemote(ServerConfiguration config) + { + RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); + } + + /// + /// Initialises internal LAN cache settings. + /// + private void InitialiseLAN(ServerConfiguration config) + { + lock (_intLock) + { + _logger.LogDebug("Refreshing LAN information."); + + // Get config options. + string[] subnets = config.LocalNetworkSubnets; + + // Create lists from user settings. + + _lanSubnets = CreateIPCollection(subnets); + _excludedSubnets = NetCollection.AsNetworks(CreateIPCollection(subnets, true)); + + // If no LAN addresses are specified - all private subnets are deemed to be the LAN + _usingPrivateAddresses = _lanSubnets.Count == 0; + + // NOTE: The order of the commands in this statement matters. + if (_usingPrivateAddresses) + { + _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); + // Internal interfaces must be private and not excluded. + _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i))); + + // Subnets are the same as the calculated internal interface. + _lanSubnets = new NetCollection(); + + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.Add(IPNetAddress.IP6Loopback); + _lanSubnets.Add(IPNetAddress.Parse("fc00::/7")); // ULA + _lanSubnets.Add(IPNetAddress.Parse("fe80::/10")); // Site local + } + + if (IsIP4Enabled) + { + _lanSubnets.Add(IPNetAddress.IP4Loopback); + _lanSubnets.Add(IPNetAddress.Parse("10.0.0.0/8")); + _lanSubnets.Add(IPNetAddress.Parse("172.16.0.0/12")); + _lanSubnets.Add(IPNetAddress.Parse("192.168.0.0/16")); + } + } + else + { + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.Add(IPNetAddress.IP6Loopback); + } + + if (IsIP4Enabled) + { + _lanSubnets.Add(IPNetAddress.IP4Loopback); + } + + // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. + _internalInterfaces = new NetCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i))); + } + + _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); + _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets); + _logger.LogInformation("Using LAN addresses: {0}", NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets))); + } + } + + /// + /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. + /// Generate a list of all active mac addresses that aren't loopback addreses. + /// + private void InitialiseInterfaces() + { + lock (_intLock) + { + _logger.LogDebug("Refreshing interfaces."); + + _interfaceNames.Clear(); + _interfaceAddresses.Clear(); + + try + { + IEnumerable nics = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); + + foreach (NetworkInterface adapter in nics) + { + try + { + IPInterfaceProperties ipProperties = adapter.GetIPProperties(); + PhysicalAddress mac = adapter.GetPhysicalAddress(); + + // populate mac list + if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && mac != null && mac != PhysicalAddress.None) + { + _macAddresses.Add(mac); + } + + // populate interface address list + foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses) + { + if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) + { + IPNetAddress nw = new IPNetAddress(info.Address, info.IPv4Mask) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv4Properties().Index + }; + + int tag = nw.Tag; + /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ + if ((ipProperties.GatewayAddresses.Count > 0 || ipProperties.DnsAddresses.Count > 0) && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.Add(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + } + else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) + { + IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv6Properties().Index + }; + + int tag = nw.Tag; + /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ + if ((ipProperties.GatewayAddresses.Count > 0 || ipProperties.DnsAddresses.Count > 0) && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.Add(nw); + + // Store interface name so we can use the name in Collections. + _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; + _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag; + } + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch + { + // Ignore error, and attempt to continue. + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); + _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses); + + // If for some reason we don't have an interface info, resolve our DNS name. + if (_interfaceAddresses.Count == 0) + { + _logger.LogWarning("No interfaces information available. Using loopback."); + + IPHost host = new IPHost(Dns.GetHostName()); + foreach (var a in host.GetAddresses()) + { + _interfaceAddresses.Add(a); + } + + if (_interfaceAddresses.Count == 0) + { + _logger.LogError("No interfaces information available. Resolving DNS name."); + // Last ditch attempt - use loopback address. + _interfaceAddresses.Add(IPNetAddress.IP4Loopback); + if (IsIP6Enabled) + { + _interfaceAddresses.Add(IPNetAddress.IP6Loopback); + } + } + } + } + catch (NetworkInformationException ex) + { + _logger.LogError(ex, "Error in InitialiseInterfaces."); + } + } + } + } +} diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 30ed3e6af..335a1a915 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -36,6 +36,7 @@ + diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8f04baa08..8c19665cc 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable #pragma warning disable CA1307 using System; @@ -12,10 +12,10 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; +using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 8d569a779..566ba0ad8 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -5,6 +5,7 @@ using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; +using Jellyfin.Networking.Manager; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Events; @@ -34,21 +35,18 @@ namespace Jellyfin.Server /// The to be used by the . /// The to be used by the . /// The to be used by the . - /// The to be used by the . /// The to be used by the . public CoreAppHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager, IServiceCollection collection) : base( applicationPaths, loggerFactory, options, fileSystem, - networkManager, collection) { } diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 4bda8f273..110290027 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,9 +1,11 @@ using System.Linq; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; +using NetworkCollection; namespace Jellyfin.Server.Middleware { @@ -38,36 +40,35 @@ namespace Jellyfin.Server.Middleware return; } - var remoteIp = httpContext.GetNormalizedRemoteIp(); + var remoteIp = httpContext.Connection.RemoteIpAddress; if (serverConfigurationManager.Configuration.EnableRemoteAccess) { - var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. + // If left blank, all remote addresses will be allowed. + NetCollection remoteAddressFilter = networkManager.RemoteAddressFilter; - if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp)) + if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp)) { - if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) + // remoteAddressFilter is a whitelist or blacklist. + bool isListed = remoteAddressFilter.Contains(remoteIp); + if (!serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) { - if (networkManager.IsAddressInSubnets(remoteIp, addressFilter)) - { - return; - } + // Black list, so flip over. + isListed = !isListed; } - else + + if (!isListed) { - if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter)) - { - return; - } + // If your name isn't on the list, you arn't coming in. + return; } } } - else + else if (!networkManager.IsInLocalNetwork(remoteIp)) { - if (!networkManager.IsInLocalNetwork(remoteIp)) - { - return; - } + // Remote not enabled. So everyone should be LAN. + return; } await _next(httpContext).ConfigureAwait(false); diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 9d795145a..2ff6f8a76 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; @@ -32,32 +33,13 @@ namespace Jellyfin.Server.Middleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - var currentHost = httpContext.Request.Host.ToString(); - var hosts = serverConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) - .ToList(); + var host = httpContext.Connection.RemoteIpAddress; - if (hosts.Count == 0) + if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.Configuration.EnableRemoteAccess) { - await _next(httpContext).ConfigureAwait(false); return; } - currentHost ??= string.Empty; - - if (networkManager.IsInPrivateAddressSpace(currentHost)) - { - hosts.Add("localhost"); - hosts.Add("127.0.0.1"); - - if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1)) - { - return; - } - } - await _next(httpContext).ConfigureAwait(false); } diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index c933d679f..8549e39db 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -12,8 +12,8 @@ using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; -using Emby.Server.Implementations.Networking; using Jellyfin.Api.Controllers; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; @@ -24,6 +24,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using NetworkCollection; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; @@ -161,7 +162,6 @@ namespace Jellyfin.Server _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger(), appPaths), - new NetworkManager(_loggerFactory.CreateLogger()), serviceCollection); try @@ -272,57 +272,16 @@ namespace Jellyfin.Server return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.ServerConfigurationManager - .Configuration - .LocalNetworkAddresses - .Select(x => appHost.NormalizeConfiguredLocalAddress(x)) - .Where(i => i != null) - .ToHashSet(); - if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any)) - { - if (!addresses.Contains(IPAddress.Loopback)) - { - // we must listen on loopback for LiveTV to function regardless of the settings - addresses.Add(IPAddress.Loopback); - } + NetCollection addresses = NetworkManager.Instance.GetAllBindInterfaces(); - foreach (var address in addresses) - { - _logger.LogInformation("Kestrel listening on {IpAddress}", address); - options.Listen(address, appHost.HttpPort); - if (appHost.ListenWithHttps) - { - options.Listen(address, appHost.HttpsPort, listenOptions => - { - listenOptions.UseHttps(appHost.Certificate); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - } - else if (builderContext.HostingEnvironment.IsDevelopment()) - { - try - { - options.Listen(address, appHost.HttpsPort, listenOptions => - { - listenOptions.UseHttps(); - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - } - catch (InvalidOperationException ex) - { - _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); - } - } - } - } - else + bool flagged = false; + foreach (IPObject netAdd in addresses) { - _logger.LogInformation("Kestrel listening on all interfaces"); - options.ListenAnyIP(appHost.HttpPort); - + _logger.LogInformation("Kestrel listening on {0}", netAdd); + options.Listen(netAdd.Address, appHost.HttpPort); if (appHost.ListenWithHttps) { - options.ListenAnyIP(appHost.HttpsPort, listenOptions => + options.Listen(netAdd.Address, appHost.HttpsPort, listenOptions => { listenOptions.UseHttps(appHost.Certificate); listenOptions.Protocols = HttpProtocols.Http1AndHttp2; @@ -332,15 +291,19 @@ namespace Jellyfin.Server { try { - options.ListenAnyIP(appHost.HttpsPort, listenOptions => + options.Listen(netAdd.Address, appHost.HttpsPort, listenOptions => { listenOptions.UseHttps(); listenOptions.Protocols = HttpProtocols.Http1AndHttp2; }); } - catch (InvalidOperationException ex) + catch (InvalidOperationException) { - _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted."); + 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; + } } } } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs deleted file mode 100644 index a0330afef..000000000 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ /dev/null @@ -1,97 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.NetworkInformation; - -namespace MediaBrowser.Common.Net -{ - public interface INetworkManager - { - event EventHandler NetworkChanged; - - /// - /// Gets or sets a function to return the list of user defined LAN addresses. - /// - Func LocalSubnetsFn { get; set; } - - /// - /// Gets a random port TCP number that is currently available. - /// - /// System.Int32. - int GetRandomUnusedTcpPort(); - - /// - /// Gets a random port UDP number that is currently available. - /// - /// System.Int32. - int GetRandomUnusedUdpPort(); - - /// - /// Returns the MAC Address from first Network Card in Computer. - /// - /// The MAC Address. - List GetMacAddresses(); - - /// - /// Determines whether [is in private address space] [the specified endpoint]. - /// - /// The endpoint. - /// true if [is in private address space] [the specified endpoint]; otherwise, false. - bool IsInPrivateAddressSpace(string endpoint); - - /// - /// Determines whether [is in private address space 10.x.x.x] [the specified endpoint] and exists in the subnets returned by GetSubnets(). - /// - /// The endpoint. - /// true if [is in private address space 10.x.x.x] [the specified endpoint]; otherwise, false. - bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint); - - /// - /// Determines whether [is in local network] [the specified endpoint]. - /// - /// The endpoint. - /// true if [is in local network] [the specified endpoint]; otherwise, false. - bool IsInLocalNetwork(string endpoint); - - /// - /// Investigates an caches a list of interface addresses, excluding local link and LAN excluded addresses. - /// - /// The list of ipaddresses. - IPAddress[] GetLocalIpAddresses(); - - /// - /// Checks if the given address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format. - /// - /// The address to check. - /// If true, check against addresses in the LAN settings surrounded by brackets ([]). - /// trueif the address is in at least one of the given subnets, false otherwise. - bool IsAddressInSubnets(string addressString, string[] subnets); - - /// - /// Returns true if address is in the LAN list in the config file. - /// - /// The address to check. - /// If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address. - /// If true, returns false if address is in the 127.x.x.x or 169.128.x.x range. - /// falseif the address isn't in the LAN list, true if the address has been defined as a LAN address. - bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC); - - /// - /// Checks if address is in the LAN list in the config file. - /// - /// Source address to check. - /// Destination address to check against. - /// Destination subnet to check against. - /// true/falsedepending on whether address1 is in the same subnet as IPAddress2 with subnetMask. - bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask); - - /// - /// Returns the subnet mask of an interface with the given address. - /// - /// The address to check. - /// Returns the subnet mask of an interface with the given address, or null if an interface match cannot be found. - IPAddress GetLocalIpSubnetMask(IPAddress address); - } -} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index cfad17fb7..f147e6a86 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -56,41 +56,27 @@ namespace MediaBrowser.Controller /// /// Gets the system info. /// + /// The originator of the request. /// SystemInfo. - Task GetSystemInfo(CancellationToken cancellationToken); + SystemInfo GetSystemInfo(IPAddress source); - Task GetPublicSystemInfo(CancellationToken cancellationToken); - - /// - /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request - /// to the API that should exist at the address. - /// - /// A cancellation token that can be used to cancel the task. - /// A list containing all the local IP addresses of the server. - Task> GetLocalIpAddresses(CancellationToken cancellationToken); + PublicSystemInfo GetPublicSystemInfo(IPAddress address); /// /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured - /// IP address that can be found via . HTTPS will be preferred when available. + /// HTTPS will be preferred when available. /// - /// A cancellation token that can be used to cancel the task. + /// The source of the request. /// The server URL. - Task GetLocalApiUrl(CancellationToken cancellationToken); + string GetSmartApiUrl(object source); /// - /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1) + /// Gets a localhost URL that can be used to access the API using the loop-back IP address. /// over HTTP (not HTTPS). /// /// The API URL. string GetLoopbackHttpApiUrl(); - /// - /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available. - /// - /// The IP address to use as the hostname in the URL. - /// The API URL. - string GetLocalApiUrl(IPAddress address); - /// /// Gets a local (LAN) URL that can be used to access the API. /// Note: if passing non-null scheme or port it is up to the caller to ensure they form the correct pair. @@ -105,7 +91,7 @@ namespace MediaBrowser.Controller /// preferring the HTTPS port, if available. /// /// The API URL. - string GetLocalApiUrl(ReadOnlySpan hostname, string scheme = null, int? port = null); + string GetLocalApiUrl(string hostname, string scheme = null, int? port = null); /// /// Open a URL in an external browser window. diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs new file mode 100644 index 000000000..40eb36c2e --- /dev/null +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -0,0 +1,19 @@ +#nullable enable +#pragma warning disable CS1591 +#pragma warning disable CA1819 + +using System; +using System.Collections.Generic; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Model.Configuration +{ + + public class PathSubstitution + { + public string From { get; set; } = string.Empty; + + public string To { get; set; } = string.Empty; + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 48d1a7346..073a62982 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,5 +1,6 @@ #nullable disable #pragma warning disable CS1591 +#pragma warning disable CA1819 using System; using System.Collections.Generic; @@ -15,41 +16,174 @@ namespace MediaBrowser.Model.Configuration { public const int DefaultHttpPort = 8096; public const int DefaultHttpsPort = 8920; - private string _baseUrl; + private string _baseUrl = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + public ServerConfiguration() + { + MetadataOptions = new[] + { + new MetadataOptions() + { + ItemType = "Book" + }, + new MetadataOptions() + { + ItemType = "Movie" + }, + new MetadataOptions + { + ItemType = "MusicVideo", + DisabledMetadataFetchers = new[] { "The Open Movie Database" }, + DisabledImageFetchers = new[] { "The Open Movie Database" } + }, + new MetadataOptions + { + ItemType = "Series", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + DisabledImageFetchers = new[] { "TheMovieDb" } + }, + new MetadataOptions + { + ItemType = "MusicAlbum", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "MusicArtist", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "BoxSet" + }, + new MetadataOptions + { + ItemType = "Season", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + }, + new MetadataOptions + { + ItemType = "Episode", + DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, + DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } + } + }; + } /// /// Gets or sets a value indicating whether to enable automatic port forwarding. /// - public bool EnableUPnP { get; set; } + public bool EnableUPnP { get; set; } = false; /// /// Gets or sets a value indicating whether to enable prometheus metrics exporting. /// - public bool EnableMetrics { get; set; } + public bool EnableMetrics { get; set; } = false; /// /// Gets or sets the public mapped port. /// /// The public mapped port. - public int PublicPort { get; set; } + public int PublicPort { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding. + /// + public bool UPnPCreateHttpPortMap { get; set; } = false; + + /// + /// Gets or sets client udp port range. + /// + public string UDPPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether gets or sets IPV6 capability. + /// + public bool EnableIPV6 { get; set; } = false; + + /// + /// Gets or sets a value indicating whether gets or sets IPV4 capability. + /// + public bool EnableIPV4 { get; set; } = true; + + /// + /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public bool EnableSSDPTracing { get; set; } = false; + + /// + /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log. + /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work. + /// + public string SSDPTracingFilter { get; set; } = string.Empty; + + /// + /// Gets or sets the number of times SSDP UDP messages are sent. + /// + public int UDPSendCount { get; set; } = 2; + + /// + /// Gets or sets the delay between each groups of SSDP messages (in ms). + /// + public int UDPSendDelay { get; set; } = 100; + + /// + /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor. + /// + public int GatewayMonitorPeriod { get; set; } = 60; + + /// + /// Gets a value indicating whether is multi-socket binding available. + /// + public bool EnableMultiSocketBinding { get; } = true; + + /// + /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network. + /// Depending on the address range implemented ULA ranges might not be used. + /// + public bool TrustAllIP6Interfaces { get; set; } = false; + + /// + /// Gets or sets the ports that HDHomerun uses. + /// + public string HDHomerunPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets PublishedServerUri to advertise for specific subnets. + /// + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether gets or sets Autodiscovery tracing. + /// + public bool AutoDiscoveryTracing { get; set; } = false; + + /// + /// Gets or sets a value indicating whether Autodiscovery is enabled. + /// + public bool AutoDiscovery { get; set; } = true; /// /// Gets or sets the public HTTPS port. /// /// The public HTTPS port. - public int PublicHttpsPort { get; set; } + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; /// /// Gets or sets the HTTP server port number. /// /// The HTTP server port number. - public int HttpServerPortNumber { get; set; } + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; /// /// Gets or sets the HTTPS server port number. /// /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; /// /// Gets or sets a value indicating whether to use HTTPS. @@ -58,19 +192,19 @@ namespace MediaBrowser.Model.Configuration /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be /// provided for and . /// - public bool EnableHttps { get; set; } + public bool EnableHttps { get; set; } = false; - public bool EnableNormalizedItemByNameIds { get; set; } + public bool EnableNormalizedItemByNameIds { get; set; } = true; /// /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. /// - public string CertificatePath { get; set; } + public string CertificatePath { get; set; } = string.Empty; /// /// Gets or sets the password required to access the X.509 certificate data in the file specified by . /// - public string CertificatePassword { get; set; } + public string CertificatePassword { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether this instance is port authorized. @@ -79,92 +213,95 @@ namespace MediaBrowser.Model.Configuration public bool IsPortAuthorized { get; set; } /// - /// Gets or sets if quick connect is available for use on this server. + /// Gets or sets a value indicating whether quick connect is available for use on this server. /// - public bool QuickConnectAvailable { get; set; } + public bool QuickConnectAvailable { get; set; } = false; - public bool AutoRunWebApp { get; set; } + public bool AutoRunWebApp { get; set; } = true; - public bool EnableRemoteAccess { get; set; } + /// + /// Gets or sets a value indicating whether access outside of the LAN is permitted. + /// + public bool EnableRemoteAccess { get; set; } = true; /// /// Gets or sets a value indicating whether [enable case sensitive item ids]. /// /// true if [enable case sensitive item ids]; otherwise, false. - public bool EnableCaseSensitiveItemIds { get; set; } + public bool EnableCaseSensitiveItemIds { get; set; } = true; - public bool DisableLiveTvChannelUserDataName { get; set; } + public bool DisableLiveTvChannelUserDataName { get; set; } = true; /// /// Gets or sets the metadata path. /// /// The metadata path. - public string MetadataPath { get; set; } + public string MetadataPath { get; set; } = string.Empty; - public string MetadataNetworkPath { get; set; } + public string MetadataNetworkPath { get; set; } = string.Empty; /// /// Gets or sets the preferred metadata language. /// /// The preferred metadata language. - public string PreferredMetadataLanguage { get; set; } + public string PreferredMetadataLanguage { get; set; } = string.Empty; /// /// Gets or sets the metadata country code. /// /// The metadata country code. - public string MetadataCountryCode { get; set; } + public string MetadataCountryCode { get; set; } = "US"; /// - /// Characters to be replaced with a ' ' in strings to create a sort name. + /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. /// /// The sort replace characters. - public string[] SortReplaceCharacters { get; set; } + public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; /// - /// Characters to be removed from strings to create a sort name. + /// Gets or sets characters to be removed from strings to create a sort name. /// /// The sort remove characters. - public string[] SortRemoveCharacters { get; set; } + public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; /// - /// Words to be removed from strings to create a sort name. + /// Gets or sets words to be removed from strings to create a sort name. /// /// The sort remove words. - public string[] SortRemoveWords { get; set; } + public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; /// /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. /// /// The min resume PCT. - public int MinResumePct { get; set; } + public int MinResumePct { get; set; } = 5; /// /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. /// /// The max resume PCT. - public int MaxResumePct { get; set; } + public int MaxResumePct { get; set; } = 90; /// /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. /// /// The min resume duration seconds. - public int MinResumeDurationSeconds { get; set; } + public int MinResumeDurationSeconds { get; set; } = 300; /// - /// The delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several /// different directories and files. /// /// The file watcher delay. - public int LibraryMonitorDelay { get; set; } + public int LibraryMonitorDelay { get; set; } = 60; /// /// Gets or sets a value indicating whether [enable dashboard response caching]. /// Allows potential contributors without visual studio to modify production dashboard code and test changes. /// /// true if [enable dashboard response caching]; otherwise, false. - public bool EnableDashboardResponseCaching { get; set; } + public bool EnableDashboardResponseCaching { get; set; } = true; /// /// Gets or sets the image saving convention. @@ -174,9 +311,9 @@ namespace MediaBrowser.Model.Configuration public MetadataOptions[] MetadataOptions { get; set; } - public bool SkipDeserializationForBasicTypes { get; set; } + public bool SkipDeserializationForBasicTypes { get; set; } = true; - public string ServerName { get; set; } + public string ServerName { get; set; } = string.Empty; public string BaseUrl { @@ -208,189 +345,80 @@ namespace MediaBrowser.Model.Configuration } } - public string UICulture { get; set; } - - public bool SaveMetadataHidden { get; set; } + public string UICulture { get; set; } = "en-US"; - public NameValuePair[] ContentTypes { get; set; } + public bool SaveMetadataHidden { get; set; } = false; - public int RemoteClientBitrateLimit { get; set; } + public NameValuePair[] ContentTypes { get; set; } = Array.Empty(); - public bool EnableFolderView { get; set; } + public int RemoteClientBitrateLimit { get; set; } = 0; - public bool EnableGroupingIntoCollections { get; set; } + public bool EnableFolderView { get; set; } = false; - public bool DisplaySpecialsWithinSeasons { get; set; } + public bool EnableGroupingIntoCollections { get; set; } = false; - public string[] LocalNetworkSubnets { get; set; } + public bool DisplaySpecialsWithinSeasons { get; set; } = true; - public string[] LocalNetworkAddresses { get; set; } + /// + /// Gets or sets the subnets that are deemed to make up the LAN. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); - public string[] CodecsUsed { get; set; } + /// + /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. + /// + public string[] LocalNetworkAddresses { get; set; } = Array.Empty(); - public List PluginRepositories { get; set; } + public string[] CodecsUsed { get; set; } = Array.Empty(); - public bool IgnoreVirtualInterfaces { get; set; } + public List PluginRepositories { get; set; } = new List(); - public bool EnableExternalContentInSuggestions { get; set; } + public bool EnableExternalContentInSuggestions { get; set; } = true; /// /// Gets or sets a value indicating whether the server should force connections over HTTPS. /// - public bool RequireHttps { get; set; } + public bool RequireHttps { get; set; } = false; - public bool EnableNewOmdbSupport { get; set; } + public bool EnableNewOmdbSupport { get; set; } = true; - public string[] RemoteIPFilter { get; set; } + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); - public bool IsRemoteIPFilterBlacklist { get; set; } + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } = false; - public int ImageExtractionTimeoutMs { get; set; } + public int ImageExtractionTimeoutMs { get; set; } = 0; - public PathSubstitution[] PathSubstitutions { get; set; } + public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); - public bool EnableSimpleArtistDetection { get; set; } + public bool EnableSimpleArtistDetection { get; set; } = false; - public string[] UninstalledPlugins { get; set; } + public string[] UninstalledPlugins { get; set; } = Array.Empty(); /// /// Gets or sets a value indicating whether slow server responses should be logged as a warning. /// - public bool EnableSlowResponseWarning { get; set; } + public bool EnableSlowResponseWarning { get; set; } = true; /// /// Gets or sets the threshold for the slow response time warning in ms. /// - public long SlowResponseThresholdMs { get; set; } + public long SlowResponseThresholdMs { get; set; } = 500; /// /// Gets or sets the cors hosts. /// - public string[] CorsHosts { get; set; } + public string[] CorsHosts { get; set; } = new[] { "*" }; /// /// Gets or sets the known proxies. /// - public string[] KnownProxies { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public ServerConfiguration() - { - UninstalledPlugins = Array.Empty(); - RemoteIPFilter = Array.Empty(); - LocalNetworkSubnets = Array.Empty(); - LocalNetworkAddresses = Array.Empty(); - CodecsUsed = Array.Empty(); - PathSubstitutions = Array.Empty(); - IgnoreVirtualInterfaces = false; - EnableSimpleArtistDetection = false; - SkipDeserializationForBasicTypes = true; - - PluginRepositories = new List(); - - DisplaySpecialsWithinSeasons = true; - EnableExternalContentInSuggestions = true; - - ImageSavingConvention = ImageSavingConvention.Compatible; - PublicPort = DefaultHttpPort; - PublicHttpsPort = DefaultHttpsPort; - HttpServerPortNumber = DefaultHttpPort; - HttpsPortNumber = DefaultHttpsPort; - EnableMetrics = false; - EnableHttps = false; - EnableDashboardResponseCaching = true; - EnableCaseSensitiveItemIds = true; - EnableNormalizedItemByNameIds = true; - DisableLiveTvChannelUserDataName = true; - EnableNewOmdbSupport = true; - - AutoRunWebApp = true; - EnableRemoteAccess = true; - QuickConnectAvailable = false; - - EnableUPnP = false; - MinResumePct = 5; - MaxResumePct = 90; - - // 5 minutes - MinResumeDurationSeconds = 300; - - LibraryMonitorDelay = 60; - - ContentTypes = Array.Empty(); - - PreferredMetadataLanguage = "en"; - MetadataCountryCode = "US"; - - SortReplaceCharacters = new[] { ".", "+", "%" }; - SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" }; - SortRemoveWords = new[] { "the", "a", "an" }; - - BaseUrl = string.Empty; - UICulture = "en-US"; - - MetadataOptions = new[] - { - new MetadataOptions() - { - ItemType = "Book" - }, - new MetadataOptions() - { - ItemType = "Movie" - }, - new MetadataOptions - { - ItemType = "MusicVideo", - DisabledMetadataFetchers = new[] { "The Open Movie Database" }, - DisabledImageFetchers = new[] { "The Open Movie Database" } - }, - new MetadataOptions - { - ItemType = "Series", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - DisabledImageFetchers = new[] { "TheMovieDb" } - }, - new MetadataOptions - { - ItemType = "MusicAlbum", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "MusicArtist", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "BoxSet" - }, - new MetadataOptions - { - ItemType = "Season", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - }, - new MetadataOptions - { - ItemType = "Episode", - DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, - DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } - } - }; - - EnableSlowResponseWarning = true; - SlowResponseThresholdMs = 500; - CorsHosts = new[] { "*" }; - KnownProxies = Array.Empty(); - } - } - - public class PathSubstitution - { - public string From { get; set; } + public string[] KnownProxies { get; set; } = Array.Empty(); - public string To { get; set; } } } diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 25402aee1..d460c0ab0 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.3 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" EndProject @@ -66,12 +66,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -132,10 +138,6 @@ Global {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.Build.0 = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {07E39F42-A2C6-4B32-AF8C-725F957A73FF}.Release|Any CPU.Build.0 = Release|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -176,10 +178,22 @@ Global {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} EndGlobalSection @@ -201,12 +215,4 @@ Global $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - {462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} - EndGlobalSection EndGlobal diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index 664663bd7..3266a05cb 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -6,6 +6,7 @@ + diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index a4be32e7d..e28a2f48d 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -7,6 +7,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; @@ -352,7 +353,7 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetLocalIpAddresses()) + foreach (var address in _networkManager.GetAllBindInterfaces()) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -362,7 +363,7 @@ namespace Rssdp.Infrastructure try { - sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address, _LocalPort)); + sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address.Address, _LocalPort)); } catch (Exception ex) { diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 1a8577d8d..43fccdad4 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Net; +using NetworkCollection; namespace Rssdp.Infrastructure { @@ -300,17 +302,14 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { - if (!_sendOnlyMatchedHost || - _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.Address, device.ToRootDevice().SubnetMask)) + var ip1 = new IPNetAddress(device.ToRootDevice().Address, device.ToRootDevice().SubnetMask); + var ip2 = new IPNetAddress(remoteEndPoint.Address, device.ToRootDevice().SubnetMask); + if (!_sendOnlyMatchedHost || ip1.NetworkAddress.Equals(ip2.NetworkAddress)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); } } } - else - { - // WriteTrace(String.Format("Sending 0 search responses.")); - } }); } diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 09ffa8468..2c7f0c4f9 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Constants; +using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs index 77f1640fa..939023a95 100644 --- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs +++ b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs @@ -3,8 +3,6 @@ using System.Collections.Concurrent; using System.IO; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; -using Emby.Server.Implementations.Networking; -using Jellyfin.Drawing.Skia; using Jellyfin.Server; using MediaBrowser.Common; using Microsoft.AspNetCore.Hosting; @@ -81,7 +79,6 @@ namespace Jellyfin.Api.Tests loggerFactory, commandLineOpts, new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths), - new NetworkManager(loggerFactory.CreateLogger()), serviceCollection); _disposableComponents.Add(appHost); appHost.Init(); -- cgit v1.2.3 From b44455ad0d6e78b5baed535c06a7f7d49116d1ee Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Mon, 14 Sep 2020 15:46:38 +0100 Subject: Update based on PR1 changes. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- Emby.Server.Implementations/ApplicationHost.cs | 62 ++- .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 2 +- .../TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 2 +- .../LiveTv/TunerHosts/M3UTunerHost.cs | 2 +- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 2 +- .../DefaultAuthorizationHandler.cs | 2 +- .../Auth/DownloadPolicy/DownloadHandler.cs | 2 +- ...FirstTimeOrIgnoreParentalControlSetupHandler.cs | 2 +- .../FirstTimeSetupOrDefaultHandler.cs | 2 +- .../FirstTimeSetupOrElevatedHandler.cs | 2 +- .../IgnoreParentalControlHandler.cs | 2 +- .../LocalAccessOrRequiresElevationHandler.cs | 2 +- .../Auth/LocalAccessPolicy/LocalAccessHandler.cs | 2 +- .../RequiresElevationHandler.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 2 +- Jellyfin.Api/Controllers/UserController.cs | 2 +- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- Jellyfin.Networking/Manager/INetworkManager.cs | 189 --------- Jellyfin.Networking/Manager/NetworkManager.cs | 463 +++++++++++---------- .../Users/UserManager.cs | 3 +- Jellyfin.Server/CoreAppHost.cs | 2 +- .../IpBasedAccessValidationMiddleware.cs | 2 +- .../Middleware/LanFilteringMiddleware.cs | 2 +- Jellyfin.Server/Program.cs | 4 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 3 +- MediaBrowser.Common/Net/INetworkManager.cs | 221 ++++++++++ MediaBrowser.Controller/IServerApplicationHost.cs | 26 +- RSSDP/SsdpCommunicationsServer.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 2 +- .../LocalAccessPolicy/LocalAccessHandlerTests.cs | 2 +- 32 files changed, 572 insertions(+), 447 deletions(-) delete mode 100644 Jellyfin.Networking/Manager/INetworkManager.cs create mode 100644 MediaBrowser.Common/Net/INetworkManager.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 98f50c09a..a5da2fc5c 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index cc04cb03f..67a352fc9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -160,6 +160,11 @@ namespace Emby.Server.Implementations } } + /// + /// Gets the singleton instance. + /// + public INetworkManager NetManager { get; internal set; } + /// /// Occurs when [has pending restart changed]. /// @@ -189,11 +194,6 @@ namespace Emby.Server.Implementations /// The plugins. public IReadOnlyList Plugins => _plugins; - /// - /// Gets the NetworkManager object. - /// - private readonly INetworkManager _networkManager; - /// /// Gets the logger factory. /// @@ -267,7 +267,7 @@ namespace Emby.Server.Implementations ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); - _networkManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); + NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); Logger = LoggerFactory.CreateLogger(); @@ -524,7 +524,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(); - ServiceCollection.AddSingleton(_networkManager); + ServiceCollection.AddSingleton(NetManager); ServiceCollection.AddSingleton(); @@ -1116,7 +1116,7 @@ namespace Emby.Server.Implementations } public IEnumerable GetWakeOnLanInfo() - => _networkManager.GetMacAddresses() + => NetManager.GetMacAddresses() .Select(i => new WakeOnLanInfo(i)) .ToList(); @@ -1138,7 +1138,47 @@ namespace Emby.Server.Implementations public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; /// - public string GetSmartApiUrl(object source) + public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) + { + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) + { + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + } + + string smart = NetManager.GetBindInterface(ipAddress, out port); + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return smart.Trim('/'); + } + + return GetLocalApiUrl(smart.Trim('/'), null, port); + } + + /// + public string GetSmartApiUrl(HttpRequest request, int? port = null) + { + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) + { + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + } + + string smart = NetManager.GetBindInterface(request, out port); + // If the smartAPI doesn't start with http then treat it as a host or ip. + if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return smart.Trim('/'); + } + + return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port); + } + + /// + public string GetSmartApiUrl(string hostname, int? port = null) { // Published server ends with a / if (_startupOptions.PublishedServerUrl != null) @@ -1147,7 +1187,7 @@ namespace Emby.Server.Implementations return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - string smart = _networkManager.GetBindInterface(source, out int? port); + string smart = NetManager.GetBindInterface(hostname, out port); // If the smartAPI doesn't start with http then treat it as a host or ip. if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase)) @@ -1155,7 +1195,7 @@ namespace Emby.Server.Implementations return smart.Trim('/'); } - return GetLocalApiUrl(smart.Trim('/'), source is HttpRequest request ? request.Scheme : null, port); + return GetLocalApiUrl(smart.Trim('/'), null, port); } /// diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 1cf129ad2..27937d2f8 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -10,7 +10,7 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 02ee302d0..efb6d162a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -7,7 +7,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index f297ecd5d..531a785a0 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index 08746b346..bd76b93bf 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,7 +1,7 @@ using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index 69e6a8fb2..dfa366796 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs index d1297119c..3183d1318 100644 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs index 53b5d4778..1cee962e9 100644 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs index abdf2858d..214198e00 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs index ada8a0d4e..9867ea4ca 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs index 475e3cdac..affd95551 100644 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index d022c9067..fab464b50 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs index 418d63de6..801ee2f4c 100644 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs index a1cddbca3..7adf72c3d 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 6876b47b4..b87e275d0 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -8,7 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 152e650bc..c60497e30 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -7,7 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 3be8734b9..f81fb88fb 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -8,7 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index d63e3ab11..9e4def774 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/Jellyfin.Networking/Manager/INetworkManager.cs deleted file mode 100644 index ba571750b..000000000 --- a/Jellyfin.Networking/Manager/INetworkManager.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.NetworkInformation; -using MediaBrowser.Model.Configuration; -using NetworkCollection; - -namespace Jellyfin.Networking.Manager -{ - /// - /// Interface for the NetworkManager class. - /// - public interface INetworkManager - { - /// - /// Event triggered on network changes. - /// - event EventHandler NetworkChanged; - - /// - /// Gets the Published server override list. - /// - Dictionary PublishedServerOverrides { get; } - - /// - /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. - /// - public bool TrustAllIP6Interfaces { get; } - - /// - /// Gets returns the remote address filter. - /// - NetCollection RemoteAddressFilter { get; } - - /// - /// Calculates the list of interfaces to use for Kestrel. - /// - /// A NetCollection object containing all the interfaces to bind. - /// If all the interfaces are specified, and none are excluded, it returns zero items - /// to represent any address. - NetCollection GetAllBindInterfaces(); - - /// - /// Returns a collection containing the loopback interfaces. - /// - /// Netcollection. - public NetCollection GetLoopbacks(); - - /// - /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) - /// If no bind addresses are specified, an internal interface address is selected. - /// The priority of selection is as follows:- - /// - /// The value contained in the startup parameter --published-server-url. - /// - /// If the user specified custom subnet overrides, the correct subnet for the source address. - /// - /// If the user specified bind interfaces to use:- - /// The bind interface that contains the source subnet. - /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. - /// - /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- - /// The first public interface that isn't a loopback and contains the source subnet. - /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. - /// An internal interface if there are no public ip addresses. - /// - /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- - /// The first private interface that contains the source subnet. - /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. - /// - /// If no interfaces meet any of these criteria, then a loopback address is returned. - /// - /// Interface that have been specifically excluded from binding are not used in any of the calculations. - /// - /// Source of the request. - /// Optional port returned, if it's part of an override. - /// IP Address to use, or loopback address if all else fails. - string GetBindInterface(object? source, out int? port); - - /// - /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. - /// - /// IP address to check. - /// True if it is. - bool IsExcludedInterface(IPAddress address); - - /// - /// Get a list of all the MAC addresses associated with active interfaces. - /// - /// List of MAC addresses. - List GetMacAddresses(); - - /// - /// Checks to see if the IP Address provided matches an interface that has a gateway. - /// - /// IP to check. Can be an IPAddress or an IPObject. - /// Result of the check. - public bool IsGatewayInterface(object? addressObj); - - /// - /// Returns true if the address is a private address. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// - /// Address to check. - /// True or False. - bool IsPrivateAddressRange(IPObject address); - - /// - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// - /// IP to check. - /// True if endpoint is within the LAN range. - bool IsInLocalNetwork(string address); - - /// - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// - /// IP to check. - /// True if endpoint is within the LAN range. - bool IsInLocalNetwork(IPObject address); - - /// - /// Returns true if the address is part of the user defined LAN. - /// The config option TrustIP6Interfaces overrides this functions behaviour. - /// - /// IP to check. - /// True if endpoint is within the LAN range. - bool IsInLocalNetwork(IPAddress address); - - /// - /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. - /// eg. "eth1", or "TP-LINK Wireless USB Adapter". - /// - /// Token to parse. - /// Resultant object if successful. - /// Success of the operation. - bool TryParseInterface(string token, out IPNetAddress result); - - /// - /// Parses an array of strings into a NetCollection. - /// - /// Values to parse. - /// When true, only include values in []. When false, ignore bracketed values. - /// IPCollection object containing the value strings. - NetCollection CreateIPCollection(string[] values, bool bracketed = false); - - /// - /// Returns all the internal Bind interface addresses. - /// - /// An internal list of interfaces addresses. - NetCollection GetInternalBindAddresses(); - - /// - /// Checks to see if an IP address is still a valid interface address. - /// - /// IP address to check. - /// True if it is. - bool IsValidInterfaceAddress(IPAddress address); - - /// - /// Returns true if the IP address is in the excluded list. - /// - /// IP to check. - /// True if excluded. - bool IsExcluded(IPAddress ip); - - /// - /// Returns true if the IP address is in the excluded list. - /// - /// IP to check. - /// True if excluded. - bool IsExcluded(EndPoint ip); - - /// - /// Gets the filtered LAN ip addresses. - /// - /// Optional filter for the list. - /// Returns a filtered list of LAN addresses. - NetCollection GetFilteredLANSubnets(NetCollection? filter = null); - - /// - /// Reloads all settings and re-initialises the instance. - /// - /// to use. - public void UpdateSettings(ServerConfiguration config); - } -} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 36a0a94a0..760938c40 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -7,6 +7,7 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -19,8 +20,6 @@ namespace Jellyfin.Networking.Manager /// public class NetworkManager : INetworkManager, IDisposable { - private static NetworkManager? _instance; - /// /// Contains the description of the interface along with its index. /// @@ -48,7 +47,7 @@ namespace Jellyfin.Networking.Manager /// /// Holds the bind address overrides. /// - private readonly Dictionary _overrideUrls; + private readonly Dictionary _publishedServerUrls; /// /// Used to stop "event-racing conditions". @@ -105,7 +104,7 @@ namespace Jellyfin.Networking.Manager _interfaceAddresses = new NetCollection(unique: false); _macAddresses = new List(); _interfaceNames = new SortedList(); - _overrideUrls = new Dictionary(); + _publishedServerUrls = new Dictionary(); UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); if (!IsIP6Enabled && !IsIP4Enabled) @@ -117,8 +116,6 @@ namespace Jellyfin.Networking.Manager NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; _configurationManager.ConfigurationUpdated += ConfigurationUpdated; - - Instance = this; } #pragma warning restore CS8618 // Non-nullable field is uninitialized. @@ -127,19 +124,6 @@ namespace Jellyfin.Networking.Manager /// public event EventHandler? NetworkChanged; - /// - /// Gets the singleton of this object. - /// - public static NetworkManager Instance - { - get => GetInstance(); - - internal set - { - _instance = value; - } - } - /// /// Gets the unique network location signature, which is updated on every network change. /// @@ -176,7 +160,7 @@ namespace Jellyfin.Networking.Manager /// /// Gets the Published server override list. /// - public Dictionary PublishedServerOverrides => _overrideUrls; + public Dictionary PublishedServerUrls => _publishedServerUrls; /// public void Dispose() @@ -198,9 +182,12 @@ namespace Jellyfin.Networking.Manager /// public bool IsGatewayInterface(object? addressObj) { - var address = (addressObj is IPAddress addressIP) ? - addressIP : (addressObj is IPObject addressIPObj) ? - addressIPObj.Address : IPAddress.None; + var address = addressObj switch + { + IPAddress addressIp => addressIp, + IPObject addressIpObj => addressIpObj.Address, + _ => IPAddress.None + }; lock (_intLock) { @@ -320,243 +307,127 @@ namespace Jellyfin.Networking.Manager } /// - public string GetBindInterface(object? source, out int? port) + public string GetBindInterface(string source, out int? port) { - bool chromeCast = false; - port = null; - // Parse the source object in an attempt to discover where the request originated. - IPObject sourceAddr; - if (source is HttpRequest sourceReq) + if (!string.IsNullOrEmpty(source)) { - port = sourceReq.Host.Port; - if (IPHost.TryParse(sourceReq.Host.Host, out IPHost host)) + if (string.Equals(source, "chromecast", StringComparison.OrdinalIgnoreCase)) { - sourceAddr = host; - } - else - { - // Assume it's external, as we cannot resolve the host. - sourceAddr = IPHost.None; - } - } - else if (source is string sourceStr && !string.IsNullOrEmpty(sourceStr)) - { - if (string.Equals(sourceStr, "chromecast", StringComparison.OrdinalIgnoreCase)) - { - chromeCast = true; // Just assign a variable so has source = true; - sourceAddr = IPNetAddress.IP4Loopback; + return GetBindInterface(IPNetAddress.IP4Loopback, out port); } - if (IPHost.TryParse(sourceStr, out IPHost host)) + if (IPHost.TryParse(source, out IPHost host)) { - sourceAddr = host; - } - else - { - // Assume it's external, as we cannot resolve the host. - sourceAddr = IPHost.None; + return GetBindInterface(host, out port); } } - else if (source is IPAddress sourceIP) + + return GetBindInterface(IPHost.None, out port); + } + + /// + public string GetBindInterface(IPAddress source, out int? port) + { + return GetBindInterface(new IPNetAddress(source), out port); + } + + /// + public string GetBindInterface(HttpRequest source, out int? port) + { + string result; + + if (source != null && IPHost.TryParse(source.Host.Host, out IPHost host)) { - sourceAddr = new IPNetAddress(sourceIP); + result = GetBindInterface(host, out port); + port ??= source.Host.Port; } else { - // If we have no idea, then assume it came from an external address. - sourceAddr = IPHost.None; + result = GetBindInterface(IPNetAddress.None, out port); + port ??= source?.Host.Port; } + return result; + } + + /// + public string GetBindInterface(IPObject source, out int? port) + { + port = null; + bool isChromeCast = source == IPNetAddress.IP4Loopback; // Do we have a source? - bool haveSource = !sourceAddr.Address.Equals(IPAddress.None); + bool haveSource = !source.Address.Equals(IPAddress.None); + bool isExternal = false; if (haveSource) { - if (!IsIP6Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetworkV6) + if (!IsIP6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6) { _logger.LogWarning("IPv6 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); } - if (!IsIP4Enabled && sourceAddr.AddressFamily == AddressFamily.InterNetwork) + if (!IsIP4Enabled && source.AddressFamily == AddressFamily.InterNetwork) { _logger.LogWarning("IPv4 is disabled in JellyFin, but enabled in the OS. This may affect how the interface is selected."); } - } - bool isExternal = haveSource && !IsInLocalNetwork(sourceAddr); + isExternal = !IsInLocalNetwork(source); - string bindPreference = string.Empty; - if (haveSource) - { - // Check for user override. - foreach (var addr in _overrideUrls) + if (MatchesPublishedServerUrl(source, isExternal, isChromeCast, out string result, out port)) { - // Remaining. Match anything. - if (addr.Key.Equals(IPAddress.Broadcast)) - { - bindPreference = addr.Value; - break; - } - else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || chromeCast)) - { - // External. - bindPreference = addr.Value; - break; - } - else if (addr.Key.Contains(sourceAddr)) - { - // Match ip address. - bindPreference = addr.Value; - break; - } + _logger.LogInformation("{0}: Using BindAddress {1}:{2}", source, result, port); + return result; } } _logger.LogDebug("GetBindInterface: Souce: {0}, External: {1}:", haveSource, isExternal); - if (!string.IsNullOrEmpty(bindPreference)) - { - // Has it got a port defined? - var parts = bindPreference.Split(':'); - if (parts.Length > 1) - { - if (int.TryParse(parts[1], out int p)) - { - bindPreference = parts[0]; - port = p; - } - } - - _logger.LogInformation("{0}: Using BindAddress {1}:{2}", sourceAddr, bindPreference, port); - return bindPreference; - } - - string ipresult; - // No preference given, so move on to bind addresses. lock (_intLock) { - var nc = _bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback()); - - int count = nc.Count(); - if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses.Equals(IPAddress.IPv6Any))) - { - // Ignore IPAny addresses. - count = 0; - } - - if (count != 0) + if (MatchesBindInterface(source, isExternal, out string result)) { - // Check to see if any of the bind interfaces are in the same subnet. - - IEnumerable bindResult; - IPAddress? defaultGateway = null; - - if (isExternal) - { - // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. - bindResult = nc.Where(p => !IsInLocalNetwork(p)).OrderBy(p => p.Tag); - defaultGateway = bindResult.FirstOrDefault()?.Address; - bindResult = bindResult.Where(p => p.Contains(sourceAddr)).OrderBy(p => p.Tag); - } - else - { - // Look for the best internal address. - bindResult = nc.Where(p => IsInLocalNetwork(p) && p.Contains(sourceAddr)).OrderBy(p => p.Tag); - } - - if (bindResult.Any()) - { - ipresult = FormatIP6String(bindResult.First().Address); - _logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", sourceAddr, ipresult); - return ipresult; - } - - if (isExternal && defaultGateway != null) - { - ipresult = FormatIP6String(defaultGateway); - _logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", sourceAddr, ipresult); - return ipresult; - } - - ipresult = FormatIP6String(nc.First().Address); - _logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", sourceAddr, ipresult); - - if (isExternal) - { - // TODO: remove this after testing. - _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", sourceAddr); - } - - return ipresult; + return result; } - if (isExternal) + if (isExternal && MatchesExternalInterface(source, out result)) { - // Get the first WAN interface address that isn't a loopback. - var extResult = _interfaceAddresses - .Exclude(_bindExclusions) - .Where(p => !IsInLocalNetwork(p)) - .OrderBy(p => p.Tag); - - if (extResult.Any()) - { - // Does the request originate in one of the interface subnets? - // (For systems with multiple internal network cards, and multiple subnets) - foreach (var intf in extResult) - { - if (!IsInLocalNetwork(intf) && intf.Contains(sourceAddr)) - { - ipresult = FormatIP6String(intf.Address); - _logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", sourceAddr, ipresult); - return ipresult; - } - } - - ipresult = FormatIP6String(extResult.First().Address); - _logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", sourceAddr, ipresult); - return ipresult; - } - - // Have to return something, so return an internal address - - // TODO: remove this after testing. - _logger.LogWarning("{0}: External request received, however, no WAN interface found.", sourceAddr); + return result; } // Get the first LAN interface address that isn't a loopback. - var result = _interfaceAddresses + var interfaces = new NetCollection(_interfaceAddresses .Exclude(_bindExclusions) .Where(p => IsInLocalNetwork(p)) - .OrderBy(p => p.Tag); + .OrderBy(p => p.Tag)); - if (result.Any()) + if (interfaces.Count > 0) { if (haveSource) { // Does the request originate in one of the interface subnets? // (For systems with multiple internal network cards, and multiple subnets) - foreach (var intf in result) + foreach (var intf in interfaces) { - if (intf.Contains(sourceAddr)) + if (intf.Contains(source)) { - ipresult = FormatIP6String(intf.Address); - _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", sourceAddr, ipresult); - return ipresult; + result = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", source, result); + return result; } } } - ipresult = FormatIP6String(result.First().Address); - _logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", sourceAddr, ipresult); - return ipresult; + result = FormatIP6String(interfaces.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Matched first internal interface. {1}", source, result); + return result; } // There isn't any others, so we'll use the loopback. - ipresult = IsIP6Enabled ? "::" : "127.0.0.1"; - _logger.LogWarning("{0}: GetBindInterface: Loopback return.", sourceAddr, ipresult); - return ipresult; + result = IsIP6Enabled ? "::" : "127.0.0.1"; + _logger.LogWarning("{0}: GetBindInterface: Loopback return.", source, result); + return result; } } @@ -771,16 +642,6 @@ namespace Jellyfin.Networking.Manager } } - private static NetworkManager GetInstance() - { - if (_instance == null) - { - throw new ApplicationException("NetworkManager is not initialised."); - } - - return _instance; - } - private void ConfigurationUpdated(object? sender, EventArgs args) { UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); @@ -944,7 +805,7 @@ namespace Jellyfin.Networking.Manager { lock (_intLock) { - _overrideUrls.Clear(); + _publishedServerUrls.Clear(); } return; @@ -952,7 +813,7 @@ namespace Jellyfin.Networking.Manager lock (_intLock) { - _overrideUrls.Clear(); + _publishedServerUrls.Clear(); foreach (var entry in overrides) { @@ -966,15 +827,15 @@ namespace Jellyfin.Networking.Manager var replacement = parts[1].Trim(); if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) { - _overrideUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; + _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; } else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) { - _overrideUrls[new IPNetAddress(IPAddress.Any)] = replacement; + _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; } else if (TryParseInterface(parts[0], out IPNetAddress address)) { - _overrideUrls[address] = replacement; + _publishedServerUrls[address] = replacement; } else { @@ -1199,5 +1060,179 @@ namespace Jellyfin.Networking.Manager } } } + + /// + /// Attempts to match the source against a user defined bind interface. + /// + /// IP source address to use. + /// True if the source is in the external subnet. + /// True if the request is for a chromecast device. + /// The published server url that matches the source address. + /// The resultant port, if one exists. + /// True if a match is found. + private bool MatchesPublishedServerUrl(IPObject source, bool isExternal, bool isChromeCast, out string bindPreference, out int? port) + { + bindPreference = string.Empty; + port = null; + + // Check for user override. + foreach (var addr in _publishedServerUrls) + { + // Remaining. Match anything. + if (addr.Key.Equals(IPAddress.Broadcast)) + { + bindPreference = addr.Value; + break; + } + else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || isChromeCast)) + { + // External. + bindPreference = addr.Value; + break; + } + else if (addr.Key.Contains(source)) + { + // Match ip address. + bindPreference = addr.Value; + break; + } + } + + if (!string.IsNullOrEmpty(bindPreference)) + { + // Has it got a port defined? + var parts = bindPreference.Split(':'); + if (parts.Length > 1) + { + if (int.TryParse(parts[1], out int p)) + { + bindPreference = parts[0]; + port = p; + } + } + + return true; + } + + return false; + } + + /// + /// Attempts to match the source against a user defined bind interface. + /// + /// IP source address to use. + /// True if the source is in the external subnet. + /// The result, if a match is found. + /// True if a match is found. + private bool MatchesBindInterface(IPObject source, bool isExternal, out string result) + { + result = string.Empty; + var nc = new NetCollection(_bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback())); + + int count = nc.Count; + if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any))) + { + // Ignore IPAny addresses. + count = 0; + } + + if (count != 0) + { + // Check to see if any of the bind interfaces are in the same subnet. + + NetCollection bindResult; + IPAddress? defaultGateway = null; + IPAddress? bindAddress; + + if (isExternal) + { + // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. + bindResult = new NetCollection(nc + .Where(p => !IsInLocalNetwork(p)) + .OrderBy(p => p.Tag)); + defaultGateway = bindResult.FirstOrDefault()?.Address; + bindAddress = bindResult + .Where(p => p.Contains(source)) + .OrderBy(p => p.Tag) + .FirstOrDefault()?.Address; + } + else + { + // Look for the best internal address. + bindAddress = nc + .Where(p => IsInLocalNetwork(p) && (p.Contains(source) || p.Equals(IPAddress.None))) + .OrderBy(p => p.Tag) + .FirstOrDefault()?.Address; + } + + if (bindAddress != null) + { + result = FormatIP6String(bindAddress); + _logger.LogDebug("{0}: GetBindInterface: Has source, found a match bind interface subnets. {1}", source, result); + return true; + } + + if (isExternal && defaultGateway != null) + { + result = FormatIP6String(defaultGateway); + _logger.LogDebug("{0}: GetBindInterface: Using first user defined external interface. {1}", source, result); + return true; + } + + result = FormatIP6String(nc.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first user defined interface. {1}", source, result); + + if (isExternal) + { + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", source); + } + + return true; + } + + return false; + } + + /// + /// Attempts to match the source against am external interface. + /// + /// IP source address to use. + /// The result, if a match is found. + /// True if a match is found. + private bool MatchesExternalInterface(IPObject source, out string result) + { + result = string.Empty; + // Get the first WAN interface address that isn't a loopback. + var extResult = new NetCollection(_interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => !IsInLocalNetwork(p)) + .OrderBy(p => p.Tag)); + + if (extResult.Count > 0) + { + // Does the request originate in one of the interface subnets? + // (For systems with multiple internal network cards, and multiple subnets) + foreach (var intf in extResult) + { + if (!IsInLocalNetwork(intf) && intf.Contains(source)) + { + result = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Selected best external on interface on range. {1}", source, result); + return true; + } + } + + result = FormatIP6String(extResult.First().Address); + _logger.LogDebug("{0}: GetBindInterface: Selected first external interface. {0}", source, result); + return true; + } + + // Have to return something, so return an internal address + + // TODO: remove this after testing. + _logger.LogWarning("{0}: External request received, however, no WAN interface found.", source); + return false; + } } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 8c19665cc..f73c917a0 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -12,10 +12,11 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Events; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index 566ba0ad8..e2d7305af 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -5,7 +5,7 @@ using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; -using Jellyfin.Networking.Manager; + using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Events; diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index ff82fe6cc..e927a147a 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,6 +1,6 @@ using System.Linq; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 87c82bf58..fa34a167b 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -2,7 +2,7 @@ using System; using System.Linq; using System.Net; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 8549e39db..939f61656 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -13,7 +13,7 @@ using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; using Jellyfin.Api.Controllers; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; @@ -272,7 +272,7 @@ namespace Jellyfin.Server return builder .UseKestrel((builderContext, options) => { - NetCollection addresses = NetworkManager.Instance.GetAllBindInterfaces(); + NetCollection addresses = appHost.NetManager.GetAllBindInterfaces(); bool flagged = false; foreach (IPObject netAdd in addresses) diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 70dcc2397..0fda47788 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -20,8 +20,9 @@ - + + diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs new file mode 100644 index 000000000..32c017aee --- /dev/null +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -0,0 +1,221 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.NetworkInformation; +using MediaBrowser.Model.Configuration; +using Microsoft.AspNetCore.Http; +using NetworkCollection; + +namespace MediaBrowser.Common.Net +{ + /// + /// Interface for the NetworkManager class. + /// + public interface INetworkManager + { + /// + /// Event triggered on network changes. + /// + event EventHandler NetworkChanged; + + /// + /// Gets the published server urls list. + /// + Dictionary PublishedServerUrls { get; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + public bool TrustAllIP6Interfaces { get; } + + /// + /// Gets the remote address filter. + /// + NetCollection RemoteAddressFilter { get; } + + /// + /// Calculates the list of interfaces to use for Kestrel. + /// + /// A NetCollection object containing all the interfaces to bind. + /// If all the interfaces are specified, and none are excluded, it returns zero items + /// to represent any address. + NetCollection GetAllBindInterfaces(); + + /// + /// Returns a collection containing the loopback interfaces. + /// + /// Netcollection. + public NetCollection GetLoopbacks(); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// The priority of selection is as follows:- + /// + /// The value contained in the startup parameter --published-server-url. + /// + /// If the user specified custom subnet overrides, the correct subnet for the source address. + /// + /// If the user specified bind interfaces to use:- + /// The bind interface that contains the source subnet. + /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. + /// + /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- + /// The first public interface that isn't a loopback and contains the source subnet. + /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. + /// An internal interface if there are no public ip addresses. + /// + /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- + /// The first private interface that contains the source subnet. + /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. + /// + /// If no interfaces meet any of these criteria, then a loopback address is returned. + /// + /// Interface that have been specifically excluded from binding are not used in any of the calculations. + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(IPObject source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See above). + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(HttpRequest source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See above). + /// + /// IP address of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(IPAddress source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See above). + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(string source, out int? port); + + /// + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// + /// IP address to check. + /// True if it is. + bool IsExcludedInterface(IPAddress address); + + /// + /// Get a list of all the MAC addresses associated with active interfaces. + /// + /// List of MAC addresses. + List GetMacAddresses(); + + /// + /// Checks to see if the IP Address provided matches an interface that has a gateway. + /// + /// IP to check. Can be an IPAddress or an IPObject. + /// Result of the check. + public bool IsGatewayInterface(object? addressObj); + + /// + /// Returns true if the address is a private address. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// Address to check. + /// True or False. + bool IsPrivateAddressRange(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(string address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPObject address); + + /// + /// Returns true if the address is part of the user defined LAN. + /// The config option TrustIP6Interfaces overrides this functions behaviour. + /// + /// IP to check. + /// True if endpoint is within the LAN range. + bool IsInLocalNetwork(IPAddress address); + + /// + /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes. + /// eg. "eth1", or "TP-LINK Wireless USB Adapter". + /// + /// Token to parse. + /// Resultant object if successful. + /// Success of the operation. + bool TryParseInterface(string token, out IPNetAddress result); + + /// + /// Parses an array of strings into a NetCollection. + /// + /// Values to parse. + /// When true, only include values in []. When false, ignore bracketed values. + /// IPCollection object containing the value strings. + NetCollection CreateIPCollection(string[] values, bool bracketed = false); + + /// + /// Returns all the internal Bind interface addresses. + /// + /// An internal list of interfaces addresses. + NetCollection GetInternalBindAddresses(); + + /// + /// Checks to see if an IP address is still a valid interface address. + /// + /// IP address to check. + /// True if it is. + bool IsValidInterfaceAddress(IPAddress address); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(IPAddress ip); + + /// + /// Returns true if the IP address is in the excluded list. + /// + /// IP to check. + /// True if excluded. + bool IsExcluded(EndPoint ip); + + /// + /// Gets the filtered LAN ip addresses. + /// + /// Optional filter for the list. + /// Returns a filtered list of LAN addresses. + NetCollection GetFilteredLANSubnets(NetCollection? filter = null); + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// to use. + public void UpdateSettings(ServerConfiguration config); + } +} diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index f147e6a86..7bc1006fc 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -63,12 +63,28 @@ namespace MediaBrowser.Controller PublicSystemInfo GetPublicSystemInfo(IPAddress address); /// - /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured - /// HTTPS will be preferred when available. + /// Gets a URL specific for the request. /// - /// The source of the request. - /// The server URL. - string GetSmartApiUrl(object source); + /// The instance. + /// Optional port number. + /// An accessible URL. + string GetSmartApiUrl(HttpRequest request, int? port = null); + + /// + /// Gets a URL specific for the request. + /// + /// The remote of the connection. + /// Optional port number. + /// An accessible URL. + string GetSmartApiUrl(IPAddress remoteAddr, int? port = null); + + /// + /// Gets a URL specific for the request. + /// + /// The hostname used in the connection. + /// Optional port number. + /// An accessible URL. + string GetSmartApiUrl(string hostname, int? port = null); /// /// Gets a localhost URL that can be used to access the API using the loop-back IP address. diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index e28a2f48d..932f6bbb4 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -7,7 +7,7 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 43fccdad4..84456f7dc 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Net; using NetworkCollection; diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 2c7f0c4f9..05dd8f325 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -4,7 +4,7 @@ using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Constants; -using Jellyfin.Networking.Manager; + using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; -- cgit v1.2.3 From d617c0b8b0bd5cac78e9ba9ce968e890a8d5d43a Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 24 Sep 2020 15:47:35 +0100 Subject: Update SystemController.cs --- Jellyfin.Api/Controllers/SystemController.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index b87e275d0..7784e8a11 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; -- cgit v1.2.3 From e113a5059794b84090ff3dfb37f52b28ce7651f0 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Tue, 22 Dec 2020 16:14:06 +0000 Subject: Possible null reference fix. --- Jellyfin.Api/Controllers/SystemController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index 7784e8a11..d79bea985 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; using System.Linq; +using System.Net; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; @@ -66,7 +67,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetSystemInfo() { - return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); + return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); } /// @@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetPublicSystemInfo() { - return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress); + return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); } /// -- cgit v1.2.3 From 4c291da45c57f1999868182490b55fb7872c26ba Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Wed, 30 Dec 2020 17:31:26 +0000 Subject: Encoding fix for System Logs. (#4564) --- Emby.Dlna/Service/BaseControlHandler.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 2 +- Jellyfin.Server/Program.cs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 198852ec1..8d2486fee 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -49,7 +49,7 @@ namespace Emby.Dlna.Service { ControlRequestInfo requestInfo = null; - using (var streamReader = new StreamReader(request.InputXml)) + using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8)) { var readerSettings = new XmlReaderSettings() { diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index d79bea985..e67a27ae3 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -203,7 +203,7 @@ namespace Jellyfin.Api.Controllers // For older files, assume fully static var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare); - return File(stream, "text/plain"); + return File(stream, "text/plain; charset=utf-8"); } /// diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index a1a7a3053..f05cdfe9b 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -598,7 +598,8 @@ namespace Jellyfin.Server .WriteTo.Async(x => x.File( Path.Combine(appPaths.LogDirectoryPath, "log_.log"), rollingInterval: RollingInterval.Day, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}")) + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", + encoding: Encoding.UTF8)) .Enrich.FromLogContext() .Enrich.WithThreadId() .CreateLogger(); -- cgit v1.2.3 From 223b42aed3395f7d01ea513bf352cdf4fd3e7002 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 10 Feb 2021 17:09:23 -0700 Subject: Create BaseItemKind enum --- Emby.Server.Implementations/Dto/DtoService.cs | 4 +- Jellyfin.Api/Controllers/ArtistsController.cs | 18 +- Jellyfin.Api/Controllers/CollectionController.cs | 1 - Jellyfin.Api/Controllers/DashboardController.cs | 6 - Jellyfin.Api/Controllers/DynamicHlsController.cs | 1 - Jellyfin.Api/Controllers/FilterController.cs | 37 ++--- Jellyfin.Api/Controllers/GenresController.cs | 9 +- Jellyfin.Api/Controllers/HlsSegmentController.cs | 2 - Jellyfin.Api/Controllers/InstantMixController.cs | 1 - Jellyfin.Api/Controllers/ItemLookupController.cs | 2 - Jellyfin.Api/Controllers/ItemsController.cs | 27 ++- Jellyfin.Api/Controllers/LibraryController.cs | 1 - Jellyfin.Api/Controllers/MusicGenresController.cs | 9 +- Jellyfin.Api/Controllers/PackageController.cs | 1 - Jellyfin.Api/Controllers/PersonsController.cs | 1 - Jellyfin.Api/Controllers/PlaylistsController.cs | 1 - Jellyfin.Api/Controllers/PluginsController.cs | 2 - Jellyfin.Api/Controllers/SearchController.cs | 9 +- Jellyfin.Api/Controllers/StudiosController.cs | 9 +- Jellyfin.Api/Controllers/SuggestionsController.cs | 1 - Jellyfin.Api/Controllers/SyncPlayController.cs | 1 - Jellyfin.Api/Controllers/SystemController.cs | 1 - Jellyfin.Api/Controllers/TimeSyncController.cs | 1 - Jellyfin.Api/Controllers/TrailersController.cs | 4 +- Jellyfin.Api/Controllers/TvShowsController.cs | 1 - Jellyfin.Api/Controllers/UserLibraryController.cs | 5 +- Jellyfin.Api/Controllers/UserViewsController.cs | 1 - Jellyfin.Api/Controllers/VideoHlsController.cs | 1 - Jellyfin.Api/Controllers/YearsController.cs | 15 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 16 ++ Jellyfin.Data/Enums/BaseItemKind.cs | 190 ++++++++++++++++++++++ MediaBrowser.Controller/Entities/BaseItem.cs | 5 + MediaBrowser.Model/Dto/BaseItemDto.cs | 3 +- 33 files changed, 289 insertions(+), 97 deletions(-) create mode 100644 Jellyfin.Data/Enums/BaseItemKind.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index e3ab0d6ea..54b18a8c8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -249,7 +249,7 @@ namespace Emby.Server.Implementations.Dto var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path); if (activeRecording != null) { - dto.Type = "Recording"; + dto.Type = BaseItemKind.Recording; dto.CanDownload = false; dto.RunTimeTicks = null; @@ -904,7 +904,7 @@ namespace Emby.Server.Implementations.Dto } } - dto.Type = item.GetClientTypeName(); + dto.Type = item.GetBaseItemKind(); if ((item.CommunityRating ?? 0) > 0) { dto.CommunityRating = item.CommunityRating; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index fed7ed3e5..4b2e5e7ea 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -3,8 +3,10 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -88,8 +90,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, @@ -127,8 +129,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, @@ -287,8 +289,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, @@ -326,8 +328,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index 2a7b2b5c6..852d1e9cb 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/DashboardController.cs b/Jellyfin.Api/Controllers/DashboardController.cs index ff7895373..b2baa9cea 100644 --- a/Jellyfin.Api/Controllers/DashboardController.cs +++ b/Jellyfin.Api/Controllers/DashboardController.cs @@ -7,17 +7,11 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Models; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Jellyfin.Api.Controllers { diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 6e85737d2..e375645cf 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -15,7 +15,6 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 9220b988f..223b2a2b6 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,13 +1,12 @@ using System; using System.Linq; using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; @@ -51,7 +50,7 @@ namespace Jellyfin.Api.Controllers public ActionResult GetQueryFiltersLegacy( [FromQuery] Guid? userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) { var user = userId.HasValue && !userId.Equals(Guid.Empty) @@ -60,10 +59,10 @@ namespace Jellyfin.Api.Controllers BaseItem? item = null; if (includeItemTypes.Length != 1 - || !(string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase))) + || !(includeItemTypes[0] == BaseItemKind.BoxSet + || includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.Trailer + || includeItemTypes[0] == BaseItemKind.Program)) { item = _libraryManager.GetParentItem(parentId, user?.Id); } @@ -72,7 +71,7 @@ namespace Jellyfin.Api.Controllers { User = user, MediaTypes = mediaTypes, - IncludeItemTypes = includeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), Recursive = true, EnableTotalRecordCount = false, DtoOptions = new DtoOptions @@ -137,7 +136,7 @@ namespace Jellyfin.Api.Controllers public ActionResult GetQueryFilters( [FromQuery] Guid? userId, [FromQuery] Guid? parentId, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isAiring, [FromQuery] bool? isMovie, [FromQuery] bool? isSports, @@ -152,10 +151,10 @@ namespace Jellyfin.Api.Controllers BaseItem? parentItem = null; if (includeItemTypes.Length == 1 - && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase))) + && (includeItemTypes[0] == BaseItemKind.BoxSet + || includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.Trailer + || includeItemTypes[0] == BaseItemKind.Program)) { parentItem = null; } @@ -167,7 +166,7 @@ namespace Jellyfin.Api.Controllers var filters = new QueryFilters(); var genreQuery = new InternalItemsQuery(user) { - IncludeItemTypes = includeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), DtoOptions = new DtoOptions { Fields = Array.Empty(), @@ -192,10 +191,10 @@ namespace Jellyfin.Api.Controllers } if (includeItemTypes.Length == 1 - && (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase))) + && (includeItemTypes[0] == BaseItemKind.MusicAlbum + || includeItemTypes[0] == BaseItemKind.MusicVideo + || includeItemTypes[0] == BaseItemKind.MusicArtist + || includeItemTypes[0] == BaseItemKind.Audio)) { filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair { diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index b6755ed5e..7bcf4674c 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -74,8 +75,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -96,8 +97,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index f51987732..25abe73ed 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -2,13 +2,11 @@ using System; using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.IO; diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 244625752..f061755c3 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs index 6c38f77ce..dfc68ffce 100644 --- a/Jellyfin.Api/Controllers/ItemLookupController.cs +++ b/Jellyfin.Api/Controllers/ItemLookupController.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; -using System.Linq; -using System.Net.Mime; using System.Text.Json; using System.Threading; using System.Threading.Tasks; diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 7d7747495..2c9760f6d 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -1,6 +1,5 @@ using System; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; @@ -178,8 +177,8 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, @@ -233,8 +232,8 @@ namespace Jellyfin.Api.Controllers .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); if (includeItemTypes.Length == 1 - && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase) - || includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase))) + && (includeItemTypes[0] == BaseItemKind.Playlist + || includeItemTypes[0] == BaseItemKind.BoxSet)) { parentId = null; } @@ -251,7 +250,7 @@ namespace Jellyfin.Api.Controllers && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { recursive = true; - includeItemTypes = new[] { "Playlist" }; + includeItemTypes = new[] { BaseItemKind.Playlist }; } var enabledChannels = user!.GetPreferenceValues(PreferenceKind.EnabledChannels); @@ -286,8 +285,8 @@ namespace Jellyfin.Api.Controllers { IsPlayed = isPlayed, MediaTypes = mediaTypes, - IncludeItemTypes = includeItemTypes, - ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), Recursive = recursive ?? false, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), IsFavorite = isFavorite, @@ -611,8 +610,8 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, @@ -773,8 +772,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { @@ -810,8 +809,8 @@ namespace Jellyfin.Api.Controllers CollapseBoxSetItems = false, EnableTotalRecordCount = enableTotalRecordCount, AncestorIds = ancestorIds, - IncludeItemTypes = includeItemTypes, - ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), SearchTerm = searchTerm }); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 28d359ac3..db4aa9668 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 2608a9cd0..7f7058b5e 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -74,8 +75,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -96,8 +97,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index c589f54ac..5dd49ef2f 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; -using MediaBrowser.Common.Json; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Updates; diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 17e631197..70a94e27c 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index a55e4ad2f..1667d6ede 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index b73611c97..b2e8bee91 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.IO; using System.Linq; -using System.Net.Mime; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Attributes; diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 08255ff8f..6c22050a7 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -6,6 +6,7 @@ using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -83,8 +84,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] Guid? userId, [FromQuery, Required] string searchTerm, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery] Guid? parentId, [FromQuery] bool? isMovie, @@ -109,8 +110,8 @@ namespace Jellyfin.Api.Controllers IncludeStudios = includeStudios, StartIndex = startIndex, UserId = userId ?? Guid.Empty, - IncludeItemTypes = includeItemTypes, - ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), MediaTypes = mediaTypes, ParentId = parentId, diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index bb54c59f6..da8f8b199 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -73,8 +74,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -96,8 +97,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 9f1dec712..a55f13e66 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs index 82cbe58df..f878f2329 100644 --- a/Jellyfin.Api/Controllers/SyncPlayController.cs +++ b/Jellyfin.Api/Controllers/SyncPlayController.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading; diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index e67a27ae3..bbbe5fb8d 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Mime; -using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; diff --git a/Jellyfin.Api/Controllers/TimeSyncController.cs b/Jellyfin.Api/Controllers/TimeSyncController.cs index c730ac12b..7df51c7af 100644 --- a/Jellyfin.Api/Controllers/TimeSyncController.cs +++ b/Jellyfin.Api/Controllers/TimeSyncController.cs @@ -1,5 +1,4 @@ using System; -using System.Globalization; using MediaBrowser.Model.SyncPlay; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 242b8f068..dd3836551 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -148,7 +148,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, @@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var includeItemTypes = new[] { "Trailer" }; + var includeItemTypes = new[] { BaseItemKind.Trailer }; return _itemsController .GetItems( diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 223f58859..e1c67f830 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 0e65591cc..1d70406ac 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -269,7 +270,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery] bool? isPlayed, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, @@ -296,7 +297,7 @@ namespace Jellyfin.Api.Controllers new LatestItemsQuery { GroupItems = groupItems, - IncludeItemTypes = includeItemTypes, + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), IsPlayed = isPlayed, Limit = limit, ParentId = parentId ?? Guid.Empty, diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index e1483ce9d..7bc5ecdf1 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.UserViewDtos; using MediaBrowser.Controller.Dto; diff --git a/Jellyfin.Api/Controllers/VideoHlsController.cs b/Jellyfin.Api/Controllers/VideoHlsController.cs index 7e743ee0c..ba51aa43e 100644 --- a/Jellyfin.Api/Controllers/VideoHlsController.cs +++ b/Jellyfin.Api/Controllers/VideoHlsController.cs @@ -12,7 +12,6 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 7c27752f7..d6dc6650c 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, - [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] bool? enableUserData, @@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypes, - IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes), + IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes), MediaTypes = mediaTypes, DtoOptions = dtoOptions }; @@ -193,16 +193,17 @@ namespace Jellyfin.Api.Controllers return _dtoService.GetBaseItemDto(item, dtoOptions); } - private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes) + private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes) { + var baseItemKind = f.GetBaseItemKind(); // Exclude item types - if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) + if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(baseItemKind)) { return false; } // Include item types - if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) + if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(baseItemKind)) { return false; } diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index db0ccc657..94856e03e 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -129,5 +129,21 @@ namespace Jellyfin.Api.Helpers TotalRecordCount = result.TotalRecordCount }; } + + internal static string[] GetItemTypeStrings(IReadOnlyList itemKinds) + { + if (itemKinds.Count == 0) + { + return Array.Empty(); + } + + var itemTypes = new string[itemKinds.Count]; + for (var i = 0; i < itemKinds.Count; i++) + { + itemTypes[i] = itemKinds[i].ToString(); + } + + return itemTypes; + } } } diff --git a/Jellyfin.Data/Enums/BaseItemKind.cs b/Jellyfin.Data/Enums/BaseItemKind.cs new file mode 100644 index 000000000..aac30279e --- /dev/null +++ b/Jellyfin.Data/Enums/BaseItemKind.cs @@ -0,0 +1,190 @@ +namespace Jellyfin.Data.Enums +{ + /// + /// The base item kind. + /// + /// + /// This enum is generated from all classes that inherit from BaseItem. + /// + public enum BaseItemKind + { + /// + /// Item is aggregate folder. + /// + AggregateFolder, + + /// + /// Item is audio. + /// + Audio, + + /// + /// Item is audio book. + /// + AudioBook, + + /// + /// Item is base plugin folder. + /// + BasePluginFolder, + + /// + /// Item is book. + /// + Book, + + /// + /// Item is box set. + /// + BoxSet, + + /// + /// Item is channel. + /// + Channel, + + /// + /// Item is channel folder item. + /// + ChannelFolderItem, + + /// + /// Item is collection folder. + /// + CollectionFolder, + + /// + /// Item is episode. + /// + Episode, + + /// + /// Item is folder. + /// + Folder, + + /// + /// Item is genre. + /// + Genre, + + /// + /// Item is manual playlists folder. + /// + ManualPlaylistsFolder, + + /// + /// Item is movie. + /// + Movie, + + /// + /// Item is music album. + /// + MusicAlbum, + + /// + /// Item is music artist. + /// + MusicArtist, + + /// + /// Item is music genre. + /// + MusicGenre, + + /// + /// Item is music video. + /// + MusicVideo, + + /// + /// Item is person. + /// + Person, + + /// + /// Item is photo. + /// + Photo, + + /// + /// Item is photo album. + /// + PhotoAlbum, + + /// + /// Item is playlist. + /// + Playlist, + + /// + /// Item is program + /// + Program, + + /// + /// Item is recording. + /// + /// + /// Manually added. + /// + Recording, + + /// + /// Item is season. + /// + Season, + + /// + /// Item is series. + /// + Series, + + /// + /// Item is studio. + /// + Studio, + + /// + /// Item is trailer. + /// + Trailer, + + /// + /// Item is live tv channel. + /// + /// + /// Type is overridden. + /// + TvChannel, + + /// + /// Item is live tv program. + /// + /// + /// Type is overridden. + /// + TvProgram, + + /// + /// Item is user root folder. + /// + UserRootFolder, + + /// + /// Item is user view. + /// + UserView, + + /// + /// Item is video. + /// + Video, + + /// + /// Item is year. + /// + Year + } +} \ No newline at end of file diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index cbb02aabd..7598b29e6 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1998,6 +1998,11 @@ namespace MediaBrowser.Controller.Entities return GetType().Name; } + public BaseItemKind GetBaseItemKind() + { + return Enum.Parse(GetClientTypeName()); + } + /// /// Gets the linked child. /// diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index 3f7aac9cd..2f9f9d3cd 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Jellyfin.Data.Enums; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Library; @@ -276,7 +277,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the type. /// /// The type. - public string Type { get; set; } + public BaseItemKind Type { get; set; } /// /// Gets or sets the people. -- cgit v1.2.3 From e3dac4fda2033801085eb7086a3a534c473a00a0 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 12 Jun 2021 22:20:35 +0200 Subject: Use async FileStreams where it makes sense --- Emby.Dlna/DlnaManager.cs | 2 +- Emby.Drawing/ImageProcessor.cs | 2 +- .../Channels/ChannelManager.cs | 4 +- .../IO/ManagedFileSystem.cs | 2 +- .../Library/LiveStreamHelper.cs | 5 +- .../Library/MediaSourceManager.cs | 2 +- .../LiveTv/EmbyTV/DirectRecorder.cs | 4 +- .../LiveTv/EmbyTV/EncodedRecorder.cs | 2 +- .../LiveTv/Listings/XmlTvListingsProvider.cs | 2 +- .../LiveTv/TunerHosts/BaseTunerHost.cs | 4 +- .../LiveTv/TunerHosts/LiveStream.cs | 4 +- .../LiveTv/TunerHosts/M3uParser.cs | 3 +- .../LiveTv/TunerHosts/SharedHttpStream.cs | 62 +++++++++++----------- .../Plugins/PluginManager.cs | 3 +- .../Serialization/MyXmlSerializer.cs | 2 +- Jellyfin.Api/Controllers/ImageByNameController.cs | 2 +- Jellyfin.Api/Controllers/RemoteImageController.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 2 +- Jellyfin.Api/Helpers/HlsHelpers.cs | 2 +- Jellyfin.Api/Helpers/ProgressiveFileCopier.cs | 3 +- Jellyfin.Api/Helpers/ProgressiveFileStream.cs | 2 +- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../Users/DefaultPasswordResetProvider.cs | 5 +- Jellyfin.Server/Program.cs | 3 +- .../Attachments/AttachmentExtractor.cs | 2 +- .../BdInfo/BdInfoFileInfo.cs | 6 +-- .../Probing/ProbeResultNormalizer.cs | 2 +- .../Subtitles/SubtitleEncoder.cs | 8 +-- MediaBrowser.Model/IO/AsyncFile.cs | 34 ++++++++++++ MediaBrowser.Providers/Manager/ImageSaver.cs | 2 +- .../Manager/ItemImageProvider.cs | 2 +- MediaBrowser.Providers/Manager/ProviderManager.cs | 2 +- .../Plugins/AudioDb/AudioDbAlbumImageProvider.cs | 3 +- .../Plugins/AudioDb/AudioDbAlbumProvider.cs | 4 +- .../Plugins/AudioDb/AudioDbArtistImageProvider.cs | 3 +- .../Plugins/AudioDb/AudioDbArtistProvider.cs | 4 +- .../Plugins/Omdb/OmdbProvider.cs | 22 ++------ .../Studios/StudiosImageProvider.cs | 2 +- .../Subtitles/SubtitleManager.cs | 2 +- .../FFprobeParserTests.cs | 3 +- 40 files changed, 127 insertions(+), 100 deletions(-) create mode 100644 MediaBrowser.Model/IO/AsyncFile.cs (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index af70793cc..68fc80c0a 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -366,7 +366,7 @@ namespace Emby.Dlna Directory.CreateDirectory(systemProfilesPath); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 7d952aa23..0ad8bca31 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -102,7 +102,7 @@ namespace Emby.Drawing { var file = await ProcessImage(options).ConfigureAwait(false); - using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true)) + using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) { await fileStream.CopyToAsync(toStream).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index aa54510a7..1478e9313 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -815,7 +815,7 @@ namespace Emby.Server.Implementations.Channels { if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - await using FileStream jsonStream = File.OpenRead(cachePath); + await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); var cachedResult = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (cachedResult != null) { @@ -838,7 +838,7 @@ namespace Emby.Server.Implementations.Channels { if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - await using FileStream jsonStream = File.OpenRead(cachePath); + await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); var cachedResult = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (cachedResult != null) { diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 7c3c7da23..af0d88ea5 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -248,7 +248,7 @@ namespace Emby.Server.Implementations.IO { try { - using (Stream thisFileStream = File.OpenRead(fileInfo.FullName)) + using (Stream thisFileStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1)) { result.Length = thisFileStream.Length; } diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 806269182..16b45161f 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -17,6 +17,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; @@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Library { try { - await using FileStream jsonStream = File.OpenRead(cacheFilePath); + await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); @@ -86,7 +87,7 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath != null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - await using FileStream createStream = File.OpenWrite(cacheFilePath); + await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Saved media info to {0}", cacheFilePath); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 91c9e61cf..4e0434b92 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -638,7 +638,7 @@ namespace Emby.Server.Implementations.Library { try { - await using FileStream jsonStream = File.OpenRead(cacheFilePath); + await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index bb3d635d1..c5a9a92ec 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None)) + using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) { onStarted(); @@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None); + await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO); onStarted(); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index e10bc7647..d806a0295 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false); await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index ebad4eddf..8202fab86 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew)) + await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO)) { await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 5941613cf..f87212cf3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts try { Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile)); - await using var writeStream = File.OpenWrite(channelCacheFile); + await using var writeStream = AsyncFile.OpenWrite(channelCacheFile); await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false); } catch (IOException) @@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { try { - await using var readStream = File.OpenRead(channelCacheFile); + await using var readStream = AsyncFile.OpenRead(channelCacheFile); var channels = await JsonSerializer.DeserializeAsync>(readStream, cancellationToken: cancellationToken) .ConfigureAwait(false); list.AddRange(channels); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 96a678c1d..2c21a4a89 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -155,15 +155,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token); cancellationToken = linkedCancellationTokenSource.Token; - // use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039 - var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT; - bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; var nextFileInfo = GetNextFile(null); var nextFile = nextFileInfo.file; var isLastFile = nextFileInfo.isLastFile; + var allowAsync = AsyncFile.UseAsyncIO; while (!string.IsNullOrEmpty(nextFile)) { var emptyReadLimit = isLastFile ? EmptyReadLimit : 1; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index d28c39e21..23071a430 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -14,6 +14,7 @@ using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; @@ -50,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return File.OpenRead(info.Url); + return AsyncFile.OpenRead(info.Url); } using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index f572151b8..862993877 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -129,37 +129,39 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) { - return Task.Run(async () => - { - try - { - Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath); - using var message = response; - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException ex) - { - Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath); - openTaskCompletionSource.TrySetException(ex); - } - catch (Exception ex) + return Task.Run( + async () => { - Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath); - openTaskCompletionSource.TrySetException(ex); - } - - openTaskCompletionSource.TrySetResult(false); - - EnableStreamSharing = false; - await DeleteTempFiles(new List { TempFilePath }).ConfigureAwait(false); - }, CancellationToken.None); + try + { + Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath); + using var message = response; + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await StreamHelper.CopyToAsync( + stream, + fileStream, + IODefaults.CopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException ex) + { + Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath); + openTaskCompletionSource.TrySetException(ex); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath); + openTaskCompletionSource.TrySetException(ex); + } + + openTaskCompletionSource.TrySetResult(false); + + EnableStreamSharing = false; + await DeleteTempFiles(new List { TempFilePath }).ConfigureAwait(false); + }, + CancellationToken.None); } private void Resolve(TaskCompletionSource openTaskCompletionSource) diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index fc0920edf..b8e1dc2c0 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -15,6 +15,7 @@ using Jellyfin.Extensions.Json.Converters; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Updates; using Microsoft.Extensions.DependencyInjection; @@ -371,7 +372,7 @@ namespace Emby.Server.Implementations.Plugins var url = new Uri(packageInfo.ImageUrl); imagePath = Path.Join(path, url.Segments[^1]); - await using var fileStream = File.OpenWrite(imagePath); + await using var fileStream = AsyncFile.OpenWrite(imagePath); try { diff --git a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs index 5ff73de81..059211a0b 100644 --- a/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs +++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs @@ -72,7 +72,7 @@ namespace Emby.Server.Implementations.Serialization /// The file. public void SerializeToFile(object obj, string file) { - using (var stream = new FileStream(file, FileMode.Create)) + using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write)) { SerializeToStream(obj, stream); } diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index e1b808098..99ab7f232 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers } var contentType = MimeTypes.GetMimeType(path); - return File(System.IO.File.OpenRead(path), contentType); + return File(AsyncFile.OpenRead(path), contentType); } /// diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index ec836f43e..8fec7d6df 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); Directory.CreateDirectory(fullCacheDirectory); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); + await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index bbbe5fb8d..e6584f0fe 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers // For older files, assume fully static var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; - FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare); + FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); return File(stream, "text/plain; charset=utf-8"); } diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index d1cdaf867..0c226f429 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -39,7 +39,7 @@ namespace Jellyfin.Api.Helpers FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, - FileOptions.SequentialScan); + (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan); await using (fileStream.ConfigureAwait(false)) { using var reader = new StreamReader(fileStream); diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index 963e17724..1fb4798ee 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -85,8 +85,7 @@ namespace Jellyfin.Api.Helpers var fileOptions = FileOptions.SequentialScan; var allowAsyncFileRead = false; - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (AsyncFile.UseAsyncIO) { fileOptions |= FileOptions.Asynchronous; allowAsyncFileRead = true; diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs index 499dbe84d..82f35fc35 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Api.Helpers _allowAsyncFileRead = false; // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (AsyncFile.UseAsyncIO) { fileOptions |= FileOptions.Asynchronous; _allowAsyncFileRead = true; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 05fa5b135..b168e6d00 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -557,7 +557,7 @@ namespace Jellyfin.Api.Helpers $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log"); // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index c99c5e4ef..6e98ad863 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Users; namespace Jellyfin.Server.Implementations.Users @@ -53,7 +54,7 @@ namespace Jellyfin.Server.Implementations.Users foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) { SerializablePasswordReset spr; - await using (var str = File.OpenRead(resetFile)) + await using (var str = AsyncFile.OpenRead(resetFile)) { spr = await JsonSerializer.DeserializeAsync(str).ConfigureAwait(false) ?? throw new ResourceNotFoundException($"Provided path ({resetFile}) is not valid."); @@ -110,7 +111,7 @@ namespace Jellyfin.Server.Implementations.Users UserName = user.Username }; - await using (FileStream fileStream = File.OpenWrite(filePath)) + await using (FileStream fileStream = AsyncFile.OpenWrite(filePath)) { await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false); await fileStream.FlushAsync().ConfigureAwait(false); diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 7018d537f..6e87c8939 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -15,6 +15,7 @@ using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -546,7 +547,7 @@ namespace Jellyfin.Server ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); // Copy the resource contents to the expected file path for the config file - await using Stream dst = File.Open(configPath, FileMode.CreateNew); + await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await resource.CopyToAsync(dst).ConfigureAwait(false); } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index a0ec3bd90..a524aeaa9 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -89,7 +89,7 @@ namespace MediaBrowser.MediaEncoding.Attachments CancellationToken cancellationToken) { var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false); - return File.OpenRead(attachmentPath); + return AsyncFile.OpenRead(attachmentPath); } private async Task GetReadableFile( diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs index 41143c259..d55688e3d 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo public bool IsDir => _impl.IsDirectory; - public System.IO.Stream OpenRead() + public Stream OpenRead() { return new FileStream( FullName, @@ -33,9 +33,9 @@ namespace MediaBrowser.MediaEncoding.BdInfo FileShare.Read); } - public System.IO.StreamReader OpenText() + public StreamReader OpenText() { - return new System.IO.StreamReader(OpenRead()); + return new StreamReader(OpenRead()); } } } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 93093bb18..8510a2f54 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1498,7 +1498,7 @@ namespace MediaBrowser.MediaEncoding.Probing { var packetBuffer = new byte[197]; - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1)) { fs.Read(packetBuffer); } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 608ebf443..6f6178af2 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -192,7 +192,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - return File.OpenRead(fileInfo.Path); + return AsyncFile.OpenRead(fileInfo.Path); } private async Task GetReadableFile( @@ -671,7 +671,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles string text; Encoding encoding; - using (var fileStream = File.OpenRead(file)) + using (var fileStream = AsyncFile.OpenRead(file)) using (var reader = new StreamReader(fileStream, true)) { encoding = reader.CurrentEncoding; @@ -684,7 +684,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!string.Equals(text, newText, StringComparison.Ordinal)) { // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None)) + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) using (var writer = new StreamWriter(fileStream, encoding)) { await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false); @@ -750,7 +750,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } case MediaProtocol.File: - return File.OpenRead(path); + return AsyncFile.OpenRead(path); default: throw new ArgumentOutOfRangeException(nameof(protocol)); } diff --git a/MediaBrowser.Model/IO/AsyncFile.cs b/MediaBrowser.Model/IO/AsyncFile.cs new file mode 100644 index 000000000..b888a4163 --- /dev/null +++ b/MediaBrowser.Model/IO/AsyncFile.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; + +namespace MediaBrowser.Model.IO +{ + /// + /// Helper class to create async s. + /// + public static class AsyncFile + { + /// + /// Gets a value indicating whether we should use async IO on this platform. + /// . + /// + /// Returns false on Windows; otherwise true. + public static bool UseAsyncIO => !OperatingSystem.IsWindows(); + + /// + /// Opens an existing file for reading. + /// + /// The file to be opened for reading. + /// A read-only on the specified path. + public static FileStream OpenRead(string path) + => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, UseAsyncIO); + + /// + /// Opens an existing file for writing. + /// + /// The file to be opened for writing. + /// An unshared object on the specified path with Write access. + public static FileStream OpenWrite(string path) + => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, UseAsyncIO); + } +} diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index fb1d4f490..3763c6d14 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -264,7 +264,7 @@ namespace MediaBrowser.Providers.Manager _fileSystem.SetAttributes(path, false, false); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) + await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) { await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 607fd127b..ce3ce66fa 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -164,7 +164,7 @@ namespace MediaBrowser.Providers.Manager { var mimeType = MimeTypes.GetMimeType(response.Path); - var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true); + var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 2dfaa372c..84d71f0b1 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -209,7 +209,7 @@ namespace MediaBrowser.Providers.Manager throw new ArgumentNullException(nameof(source)); } - var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, true); + var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs index 36d8eeb40..81bbc26b8 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs @@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.AudioDb @@ -57,7 +58,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id); - await using FileStream jsonStream = File.OpenRead(path); + await using FileStream jsonStream = AsyncFile.OpenRead(path); var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (obj != null && obj.album != null && obj.album.Count > 0) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index 9f2f7fc11..c1226febf 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetAlbumInfoPath(_config.ApplicationPaths, id); - await using FileStream jsonStream = File.OpenRead(path); + await using FileStream jsonStream = AsyncFile.OpenRead(path); var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (obj != null && obj.album != null && obj.album.Count > 0) @@ -173,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index aa61a56f6..3ffdcdbeb 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; namespace MediaBrowser.Providers.Plugins.AudioDb @@ -59,7 +60,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id); - await using FileStream jsonStream = File.OpenRead(path); + await using FileStream jsonStream = AsyncFile.OpenRead(path); var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (obj != null && obj.artists != null && obj.artists.Count > 0) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 2857c6c13..8572b3413 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -65,7 +65,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb var path = GetArtistInfoPath(_config.ApplicationPaths, id); - await using FileStream jsonStream = File.OpenRead(path); + await using FileStream jsonStream = AsyncFile.OpenRead(path); var obj = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); if (obj != null && obj.artists != null && obj.artists.Count > 0) @@ -155,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true); + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 1ae712e9e..1dea3dece 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -236,31 +236,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb internal async Task GetRootObject(string imdbId, CancellationToken cancellationToken) { var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false); - await using var stream = File.OpenRead(path); + await using var stream = AsyncFile.OpenRead(path); return await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); } internal async Task GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken) { var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false); - await using var stream = File.OpenRead(path); + await using var stream = AsyncFile.OpenRead(path); return await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); } - internal static bool IsValidSeries(Dictionary seriesProviderIds) - { - if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string id)) - { - // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet. - if (!string.IsNullOrWhiteSpace(id)) - { - return true; - } - } - - return false; - } - /// Gets OMDB URL. /// Appends query string to URL. /// OMDB URL with optional query string. @@ -309,7 +295,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb imdbParam)); var rootObject = await GetDeserializedOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); - await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); return path; @@ -349,7 +335,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb seasonId)); var rootObject = await GetDeserializedOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); - await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); return path; diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index 63e78d15e..7a057c065 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -146,7 +146,7 @@ namespace MediaBrowser.Providers.Studios Directory.CreateDirectory(Path.GetDirectoryName(file)); await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(file, FileMode.Create); + await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 0c791a2fe..d6c346ba1 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(savePath)); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true); + using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, AsyncFile.UseAsyncIO); await stream.CopyToAsync(fs).ConfigureAwait(false); return; diff --git a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs index 2955104a2..97dbb3be0 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/FFprobeParserTests.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Extensions.Json; using MediaBrowser.MediaEncoding.Probing; +using MediaBrowser.Model.IO; using Xunit; namespace Jellyfin.MediaEncoding.Tests @@ -14,7 +15,7 @@ namespace Jellyfin.MediaEncoding.Tests public async Task Test(string fileName) { var path = Path.Join("Test Data", fileName); - await using (var stream = File.OpenRead(path)) + await using (var stream = AsyncFile.OpenRead(path)) { var res = await JsonSerializer.DeserializeAsync(stream, JsonDefaults.Options).ConfigureAwait(false); Assert.NotNull(res); -- cgit v1.2.3 From 90174f68e2b477e613d756d78cbfd89c2a33f30a Mon Sep 17 00:00:00 2001 From: Fredrik Lindberg Date: Tue, 31 Aug 2021 22:22:55 +0200 Subject: Dynamically populate LocalAddress based on HTTP request Support populating the LocalAddress field in the system info endpoint based on the x-forwarded-host and x-forwarded-proto header. The x-forwarded-host header must contain both the host and port for the url to be properly constructed. Behind network configuration option that is disabled by default. --- Emby.Server.Implementations/ApplicationHost.cs | 22 +++++++++++++++++----- Jellyfin.Api/Controllers/SystemController.cs | 4 ++-- .../Configuration/NetworkConfiguration.cs | 5 +++++ .../Extensions/ApiServiceCollectionExtensions.cs | 3 ++- MediaBrowser.Controller/IServerApplicationHost.cs | 6 +++--- 5 files changed, 29 insertions(+), 11 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index bf7ddace2..64ec5833f 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1073,9 +1073,9 @@ namespace Emby.Server.Implementations /// /// Gets the system status. /// - /// Where this request originated. + /// Where this request originated. /// SystemInfo. - public SystemInfo GetSystemInfo(IPAddress source) + public SystemInfo GetSystemInfo(HttpRequest request) { return new SystemInfo { @@ -1097,7 +1097,7 @@ namespace Emby.Server.Implementations CanLaunchWebBrowser = CanLaunchWebBrowser, TranscodingTempPath = ConfigurationManager.GetTranscodePath(), ServerName = FriendlyName, - LocalAddress = GetSmartApiUrl(source), + LocalAddress = GetSmartApiUrl(request), SupportsLibraryMonitor = true, EncoderLocation = _mediaEncoder.EncoderLocation, SystemArchitecture = RuntimeInformation.OSArchitecture, @@ -1110,7 +1110,7 @@ namespace Emby.Server.Implementations .Select(i => new WakeOnLanInfo(i)) .ToList(); - public PublicSystemInfo GetPublicSystemInfo(IPAddress address) + public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) { return new PublicSystemInfo { @@ -1119,7 +1119,7 @@ namespace Emby.Server.Implementations Id = SystemId, OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(), ServerName = FriendlyName, - LocalAddress = GetSmartApiUrl(address), + LocalAddress = GetSmartApiUrl(request), StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted }; } @@ -1150,6 +1150,18 @@ namespace Emby.Server.Implementations /// public string GetSmartApiUrl(HttpRequest request, int? port = null) { + // Return the host in the HTTP request as the API url + if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) + { + int? requestPort = request.Host.Port; + if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) + { + requestPort = -1; + } + + return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort); + } + // Published server ends with a / if (!string.IsNullOrEmpty(PublishedServerUrl)) { diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index bbbe5fb8d..ff81cc53b 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -66,7 +66,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetSystemInfo() { - return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); + return _appHost.GetSystemInfo(Request); } /// @@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetPublicSystemInfo() { - return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback); + return _appHost.GetPublicSystemInfo(Request); } /// diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index faf814c06..61db223d9 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -226,5 +226,10 @@ namespace Jellyfin.Networking.Configuration /// Gets or sets the known proxies. If the proxy is a network, it's added to the KnownNetworks. /// public string[] KnownProxies { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests. + /// + public bool EnablePublishedServerUriByRequest { get; set; } = false; } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index f19e87aba..266dfef69 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -188,7 +188,8 @@ namespace Jellyfin.Server.Extensions // 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; + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost; + if (config.KnownProxies.Length == 0) { options.KnownNetworks.Clear(); diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 753c18bc7..07aea6ad1 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -51,11 +51,11 @@ namespace MediaBrowser.Controller /// /// Gets the system info. /// - /// The originator of the request. + /// The HTTP request. /// SystemInfo. - SystemInfo GetSystemInfo(IPAddress source); + SystemInfo GetSystemInfo(HttpRequest request); - PublicSystemInfo GetPublicSystemInfo(IPAddress address); + PublicSystemInfo GetPublicSystemInfo(HttpRequest request); /// /// Gets a URL specific for the request. -- cgit v1.2.3 From f31224fa8f1dccb730703c048c26e14b5d14fa55 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 25 Sep 2021 19:44:40 +0200 Subject: Remove sync FileStream hack --- Emby.Dlna/DlnaManager.cs | 2 +- Emby.Drawing/ImageProcessor.cs | 2 +- .../LiveTv/EmbyTV/DirectRecorder.cs | 4 ++-- .../LiveTv/EmbyTV/EncodedRecorder.cs | 2 +- .../LiveTv/Listings/XmlTvListingsProvider.cs | 2 +- .../LiveTv/TunerHosts/LiveStream.cs | 6 +++--- .../LiveTv/TunerHosts/SharedHttpStream.cs | 2 +- Jellyfin.Api/Controllers/RemoteImageController.cs | 2 +- Jellyfin.Api/Controllers/SystemController.cs | 2 +- Jellyfin.Api/Helpers/HlsHelpers.cs | 2 +- Jellyfin.Api/Helpers/ProgressiveFileStream.cs | 24 ++-------------------- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 2 +- .../SymlinkFollowingPhysicalFileResultExecutor.cs | 2 +- Jellyfin.Server/Program.cs | 2 +- .../Subtitles/SubtitleEncoder.cs | 2 +- MediaBrowser.Model/IO/AsyncFile.cs | 11 ++-------- MediaBrowser.Providers/Manager/ImageSaver.cs | 2 +- .../Manager/ItemImageProvider.cs | 2 +- .../Plugins/AudioDb/AudioDbAlbumProvider.cs | 2 +- .../Plugins/AudioDb/AudioDbArtistProvider.cs | 2 +- .../Plugins/Omdb/OmdbProvider.cs | 4 ++-- .../Studios/StudiosImageProvider.cs | 2 +- .../Subtitles/SubtitleManager.cs | 2 +- 23 files changed, 29 insertions(+), 56 deletions(-) (limited to 'Jellyfin.Api/Controllers/SystemController.cs') diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 305e43a3c..8fe9d484e 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -366,7 +366,7 @@ namespace Emby.Dlna Directory.CreateDirectory(systemProfilesPath); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { await stream.CopyToAsync(fileStream).ConfigureAwait(false); } diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 0ad8bca31..9b130fdfd 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -102,7 +102,7 @@ namespace Emby.Drawing { var file = await ProcessImage(options).ConfigureAwait(false); - using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) + using (var fileStream = new FileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { await fileStream.CopyToAsync(toStream).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index f6e0111b6..41381d55b 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) + using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { onStarted(); @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO); + await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous); onStarted(); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index a943efcc7..3633fa3e1 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + _logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false); await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index cb9801c17..2e51ac807 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, AsyncFile.UseAsyncIO)) + await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.CopyToBufferSize, FileOptions.Asynchronous)) { await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 817b1f804..20a9a705b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public Stream GetStream() { - var stream = GetInputStream(TempFilePath, AsyncFile.UseAsyncIO); + var stream = GetInputStream(TempFilePath); bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; if (seekFile) { @@ -107,14 +107,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return stream; } - protected FileStream GetInputStream(string path, bool allowAsyncFileRead) + protected FileStream GetInputStream(string path) => new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, - allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan); + FileOptions.SequentialScan | FileOptions.Asynchronous); protected async Task DeleteTempFiles(string path, int retryCount = 0) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 84a878e51..3b69e55b0 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); using var message = response; await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await StreamHelper.CopyToAsync( stream, fileStream, diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index bcb2b50c7..8a33b12f4 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); Directory.CreateDirectory(fullCacheDirectory); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath)); diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index e6584f0fe..741bdfee9 100644 --- a/Jellyfin.Api/Controllers/SystemController.cs +++ b/Jellyfin.Api/Controllers/SystemController.cs @@ -201,7 +201,7 @@ namespace Jellyfin.Api.Controllers // For older files, assume fully static var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; - FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); return File(stream, "text/plain; charset=utf-8"); } diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index f36769dc2..456762147 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -38,7 +38,7 @@ namespace Jellyfin.Api.Helpers FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, - (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan); + FileOptions.Asynchronous | FileOptions.SequentialScan); await using (fileStream.ConfigureAwait(false)) { using var reader = new StreamReader(fileStream); diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs index c57018351..61e18220a 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -17,7 +17,6 @@ namespace Jellyfin.Api.Helpers private readonly TranscodingJobDto? _job; private readonly TranscodingJobHelper? _transcodingJobHelper; private readonly int _timeoutMs; - private readonly bool _allowAsyncFileRead; private int _bytesWritten; private bool _disposed; @@ -34,17 +33,7 @@ namespace Jellyfin.Api.Helpers _transcodingJobHelper = transcodingJobHelper; _timeoutMs = timeoutMs; - var fileOptions = FileOptions.SequentialScan; - _allowAsyncFileRead = false; - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (AsyncFile.UseAsyncIO) - { - fileOptions |= FileOptions.Asynchronous; - _allowAsyncFileRead = true; - } - - _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions); + _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan); } /// @@ -57,7 +46,6 @@ namespace Jellyfin.Api.Helpers _job = null; _transcodingJobHelper = null; _timeoutMs = timeoutMs; - _allowAsyncFileRead = AsyncFile.UseAsyncIO; _stream = stream; } @@ -103,15 +91,7 @@ namespace Jellyfin.Api.Helpers while (remainingBytesToRead > 0) { cancellationToken.ThrowIfCancellationRequested(); - int bytesRead; - if (_allowAsyncFileRead) - { - bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false); - } - else - { - bytesRead = _stream.Read(buffer, newOffset, remainingBytesToRead); - } + int bytesRead = await _stream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false); remainingBytesToRead -= bytesRead; newOffset += bytesRead; diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 4e1e98df0..14f287aef 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -557,7 +557,7 @@ namespace Jellyfin.Api.Helpers $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log"); // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); diff --git a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs index 4abd5b36d..f3fbab77e 100644 --- a/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs +++ b/Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs @@ -132,7 +132,7 @@ namespace Jellyfin.Server.Infrastructure FileAccess.Read, FileShare.ReadWrite, bufferSize: BufferSize, - options: (AsyncFile.UseAsyncIO ? FileOptions.Asynchronous : FileOptions.None) | FileOptions.SequentialScan); + options: FileOptions.Asynchronous | FileOptions.SequentialScan); fileStream.Seek(offset, SeekOrigin.Begin); await StreamCopyOperation diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 1300ce3b6..f36675b95 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -547,7 +547,7 @@ namespace Jellyfin.Server ?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'"); // Copy the resource contents to the expected file path for the config file - await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await using Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await resource.CopyToAsync(dst).ConfigureAwait(false); } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index f8451e92c..022cdbe9d 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -680,7 +680,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!string.Equals(text, newText, StringComparison.Ordinal)) { // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) using (var writer = new StreamWriter(fileStream, encoding)) { await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Model/IO/AsyncFile.cs b/MediaBrowser.Model/IO/AsyncFile.cs index b888a4163..f38ed9ae3 100644 --- a/MediaBrowser.Model/IO/AsyncFile.cs +++ b/MediaBrowser.Model/IO/AsyncFile.cs @@ -8,20 +8,13 @@ namespace MediaBrowser.Model.IO /// public static class AsyncFile { - /// - /// Gets a value indicating whether we should use async IO on this platform. - /// . - /// - /// Returns false on Windows; otherwise true. - public static bool UseAsyncIO => !OperatingSystem.IsWindows(); - /// /// Opens an existing file for reading. /// /// The file to be opened for reading. /// A read-only on the specified path. public static FileStream OpenRead(string path) - => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, UseAsyncIO); + => new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); /// /// Opens an existing file for writing. @@ -29,6 +22,6 @@ namespace MediaBrowser.Model.IO /// The file to be opened for writing. /// An unshared object on the specified path with Write access. public static FileStream OpenWrite(string path) - => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, UseAsyncIO); + => new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); } } diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 6c14c8de1..7d259a9d3 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -264,7 +264,7 @@ namespace MediaBrowser.Providers.Manager _fileSystem.SetAttributes(path, false, false); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO)) + await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 81529e984..73bca5aa5 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -163,7 +163,7 @@ namespace MediaBrowser.Providers.Manager { var mimeType = MimeTypes.GetMimeType(response.Path); - var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index c1226febf..3e0b0014e 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -173,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 8572b3413..e0b2f9c58 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -155,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 1dea3dece..479ae0f39 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -295,7 +295,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb imdbParam)); var rootObject = await GetDeserializedOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); - await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); return path; @@ -335,7 +335,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb seasonId)); var rootObject = await GetDeserializedOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false); - await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false); return path; diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index 7a057c065..7b2454efc 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -146,7 +146,7 @@ namespace MediaBrowser.Providers.Studios Directory.CreateDirectory(Path.GetDirectoryName(file)); await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, AsyncFile.UseAsyncIO); + await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index d6c346ba1..772e617ab 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(savePath)); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . - using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, AsyncFile.UseAsyncIO); + using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, FileOptions.Asynchronous); await stream.CopyToAsync(fs).ConfigureAwait(false); return; -- cgit v1.2.3