From 82ff3fa75d7da1d160ee5b774bd2238b5727d5ea Mon Sep 17 00:00:00 2001 From: Erik Rigtorp Date: Sun, 3 May 2020 20:44:18 -0700 Subject: [PATCH 001/257] Add additional resolver tests --- .../Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs | 1 + .../Jellyfin.Naming.Tests/Video/VideoResolverTests.cs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index 917d8fb3a9..49820df629 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -47,6 +47,7 @@ namespace Jellyfin.Naming.Tests.Video // FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)] [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again [InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)] + [InlineData(@"Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)] public void CleanDateTimeTest(string input, string expectedName, int? expectedYear) { input = Path.GetFileName(input); diff --git a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs index 99828b2eb7..096ebb836a 100644 --- a/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/VideoResolverTests.cs @@ -174,6 +174,16 @@ namespace Jellyfin.Naming.Tests.Video Year = 2006, } }; + yield return new object[] + { + new VideoFileInfo() + { + Path = @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", + Container = "mp4", + Name = "Rain Man", + Year = 1988, + } + }; } [Theory] From 75efb8f52d4234bfdefa2a0ac48f7261aa9ef58b Mon Sep 17 00:00:00 2001 From: Erik Rigtorp Date: Sun, 3 May 2020 21:29:52 -0700 Subject: [PATCH 002/257] Use additional unicode chars for year extraction --- Emby.Naming/Common/NamingOptions.cs | 4 +++- Emby.Naming/Video/CleanDateTimeParser.cs | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 1b343790e8..c727681a65 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -137,7 +137,9 @@ namespace Emby.Naming.Common CleanDateTimes = new[] { @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*", - @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*" + @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*", + @"(.+\w)\W+\p{Ps}(19[0-9]{2}|20[0-9]{2})\p{Pe}", // Prefer year enclosed in parenthesis (){}[] + @"(.+\w)\W+(19[0-9]{2}|20[0-9]{2})(\W|$)", // Secondary year surrounded by non-word chars }; CleanStrings = new[] diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs index 579c9e91e1..7c18ab039c 100644 --- a/Emby.Naming/Video/CleanDateTimeParser.cs +++ b/Emby.Naming/Video/CleanDateTimeParser.cs @@ -32,7 +32,6 @@ namespace Emby.Naming.Video var match = expression.Match(name); if (match.Success - && match.Groups.Count == 5 && match.Groups[1].Success && match.Groups[2].Success && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) From 9ef79d190b2490a03c566bfaaf963fbba7d124a9 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Sat, 12 Sep 2020 16:41:37 +0100 Subject: [PATCH 003/257] Large number of files --- Emby.Dlna/Main/DlnaEntryPoint.cs | 42 +- Emby.Dlna/PlayTo/PlayToManager.cs | 10 +- .../ApplicationHost.cs | 220 +-- .../Emby.Server.Implementations.csproj | 2 +- .../LiveTv/LiveTvMediaSourceProvider.cs | 2 +- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 1 + .../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 +- ...TimeOrIgnoreParentalControlSetupHandler.cs | 3 +- .../FirstTimeSetupOrDefaultHandler.cs | 1 + .../FirstTimeSetupOrElevatedHandler.cs | 1 + .../IgnoreParentalControlHandler.cs | 3 +- .../LocalAccessOrRequiresElevationHandler.cs | 3 +- .../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.csproj | 39 + .../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 -- .../IServerApplicationHost.cs | 30 +- .../Configuration/PathSubstitution.cs | 19 + .../Configuration/ServerConfiguration.cs | 668 ++++----- MediaBrowser.sln | 34 +- RSSDP/RSSDP.csproj | 1 + RSSDP/SsdpCommunicationsServer.cs | 5 +- RSSDP/SsdpDevicePublisher.cs | 11 +- .../LocalAccessHandlerTests.cs | 3 +- .../JellyfinApplicationFactory.cs | 3 - 43 files changed, 1977 insertions(+), 1345 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 diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 40c2cc0e0a..98f50c09af 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 21877f121f..10887bf60d 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 642e2fdbe9..cc04cb03f5 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 + // Published server ends with a / + if (_startupOptions.PublishedServerUrl != null) { - // 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) - { - 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); - } - - /// - public string GetLocalApiUrl(IPAddress ipAddress) - { - if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) - { - 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(ipAddress.ToString()); + return GetLocalApiUrl(smart.Trim('/'), source is HttpRequest request ? request.Scheme : null, port); } /// public string GetLoopbackHttpApiUrl() { + if (NetworkManager.IsIP6Enabled) + { + return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); + } + 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 0a348f0d00..db81167433 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 8a0c0043a9..598cf0af7c 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 28e30fac8b..1cf129ad2d 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 6730751d50..02ee302d0d 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 8107bc427b..f297ecd5df 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 089ec30e6b..0000000000 --- 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 b7a59cee2d..3dc34da5cf 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 d732b6bc6a..08746b346b 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 b5913daab9..69e6a8fb2d 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 b61680ab1a..d1297119cd 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 31482a930f..53b5d47787 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 9815e252ee..abdf2858d0 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 decbe0c035..ada8a0d4ec 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 5213bc4cb7..475e3cdac4 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 14722aa57e..d022c9067f 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 af73352bcc..418d63de68 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 b235c4b63b..a1cddbca3e 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 4cb1984a2f..6876b47b44 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 630e9df6ac..152e650bc0 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 af0519ffa8..3be8734b94 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 1207fb5134..d63e3ab118 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 0000000000..b6834cb0ee --- /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 0000000000..ba571750b9 --- /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 0000000000..36a0a94a09 --- /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 30ed3e6af3..335a1a9156 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 8f04baa089..8c19665cc1 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 8d569a779a..566ba0ad85 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 4bda8f2737..110290027b 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 9d795145aa..2ff6f8a765 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 c933d679f4..8549e39dbc 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 a0330afeff..0000000000 --- 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 cfad17fb72..f147e6a86c 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 0000000000..40eb36c2ea --- /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 48d1a7346a..073a629829 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,322 +16,13 @@ namespace MediaBrowser.Model.Configuration { public const int DefaultHttpPort = 8096; public const int DefaultHttpsPort = 8920; - private string _baseUrl; - - /// - /// Gets or sets a value indicating whether to enable automatic port forwarding. - /// - public bool EnableUPnP { get; set; } - - /// - /// Gets or sets a value indicating whether to enable prometheus metrics exporting. - /// - public bool EnableMetrics { get; set; } - - /// - /// Gets or sets the public mapped port. - /// - /// The public mapped port. - public int PublicPort { get; set; } - - /// - /// Gets or sets the public HTTPS port. - /// - /// The public HTTPS port. - public int PublicHttpsPort { get; set; } - - /// - /// Gets or sets the HTTP server port number. - /// - /// The HTTP server port number. - public int HttpServerPortNumber { get; set; } - - /// - /// Gets or sets the HTTPS server port number. - /// - /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } - - /// - /// Gets or sets a value indicating whether to use HTTPS. - /// - /// - /// 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 EnableNormalizedItemByNameIds { get; set; } - - /// - /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. - /// - public string CertificatePath { get; set; } - - /// - /// Gets or sets the password required to access the X.509 certificate data in the file specified by . - /// - public string CertificatePassword { get; set; } - - /// - /// Gets or sets a value indicating whether this instance is port authorized. - /// - /// true if this instance is port authorized; otherwise, false. - public bool IsPortAuthorized { get; set; } - - /// - /// Gets or sets if quick connect is available for use on this server. - /// - public bool QuickConnectAvailable { get; set; } - - public bool AutoRunWebApp { get; set; } - - public bool EnableRemoteAccess { get; set; } - - /// - /// 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 DisableLiveTvChannelUserDataName { get; set; } - - /// - /// Gets or sets the metadata path. - /// - /// The metadata path. - public string MetadataPath { get; set; } - - public string MetadataNetworkPath { get; set; } - - /// - /// Gets or sets the preferred metadata language. - /// - /// The preferred metadata language. - public string PreferredMetadataLanguage { get; set; } - - /// - /// Gets or sets the metadata country code. - /// - /// The metadata country code. - public string MetadataCountryCode { get; set; } - - /// - /// Characters to be replaced with a ' ' in strings to create a sort name. - /// - /// The sort replace characters. - public string[] SortReplaceCharacters { get; set; } - - /// - /// Characters to be removed from strings to create a sort name. - /// - /// The sort remove characters. - public string[] SortRemoveCharacters { get; set; } - - /// - /// Words to be removed from strings to create a sort name. - /// - /// The sort remove words. - public string[] SortRemoveWords { get; set; } - - /// - /// 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; } - - /// - /// 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; } - - /// - /// 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; } - - /// - /// 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; } - - /// - /// 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; } - - /// - /// Gets or sets the image saving convention. - /// - /// The image saving convention. - public ImageSavingConvention ImageSavingConvention { get; set; } - - public MetadataOptions[] MetadataOptions { get; set; } - - public bool SkipDeserializationForBasicTypes { get; set; } - - public string ServerName { get; set; } - - public string BaseUrl - { - get => _baseUrl; - set - { - // Normalize the start of the string - if (string.IsNullOrWhiteSpace(value)) - { - // If baseUrl is empty, set an empty prefix string - _baseUrl = string.Empty; - return; - } - - if (value[0] != '/') - { - // If baseUrl was not configured with a leading slash, append one for consistency - value = "/" + value; - } - - // Normalize the end of the string - if (value[value.Length - 1] == '/') - { - // If baseUrl was configured with a trailing slash, remove it for consistency - value = value.Remove(value.Length - 1); - } - - _baseUrl = value; - } - } - - public string UICulture { get; set; } - - public bool SaveMetadataHidden { get; set; } - - public NameValuePair[] ContentTypes { get; set; } - - public int RemoteClientBitrateLimit { get; set; } - - public bool EnableFolderView { get; set; } - - public bool EnableGroupingIntoCollections { get; set; } - - public bool DisplaySpecialsWithinSeasons { get; set; } - - public string[] LocalNetworkSubnets { get; set; } - - public string[] LocalNetworkAddresses { get; set; } - - public string[] CodecsUsed { get; set; } - - public List PluginRepositories { get; set; } - - public bool IgnoreVirtualInterfaces { get; set; } - - public bool EnableExternalContentInSuggestions { get; set; } - - /// - /// Gets or sets a value indicating whether the server should force connections over HTTPS. - /// - public bool RequireHttps { get; set; } - - public bool EnableNewOmdbSupport { get; set; } - - public string[] RemoteIPFilter { get; set; } - - public bool IsRemoteIPFilterBlacklist { get; set; } - - public int ImageExtractionTimeoutMs { get; set; } - - public PathSubstitution[] PathSubstitutions { get; set; } - - public bool EnableSimpleArtistDetection { get; set; } - - public string[] UninstalledPlugins { get; set; } - - /// - /// Gets or sets a value indicating whether slow server responses should be logged as a warning. - /// - public bool EnableSlowResponseWarning { get; set; } - - /// - /// Gets or sets the threshold for the slow response time warning in ms. - /// - public long SlowResponseThresholdMs { get; set; } - - /// - /// Gets or sets the cors hosts. - /// - public string[] CorsHosts { get; set; } - - /// - /// Gets or sets the known proxies. - /// - public string[] KnownProxies { get; set; } + private string _baseUrl = string.Empty; /// /// 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() @@ -379,18 +71,354 @@ namespace MediaBrowser.Model.Configuration DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } } }; - - EnableSlowResponseWarning = true; - SlowResponseThresholdMs = 500; - CorsHosts = new[] { "*" }; - KnownProxies = Array.Empty(); } - } - public class PathSubstitution - { - public string From { get; set; } + /// + /// Gets or sets a value indicating whether to enable automatic port forwarding. + /// + public bool EnableUPnP { get; set; } = false; + + /// + /// Gets or sets a value indicating whether to enable prometheus metrics exporting. + /// + public bool EnableMetrics { get; set; } = false; + + /// + /// Gets or sets the public mapped port. + /// + /// The public mapped port. + 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; } = DefaultHttpsPort; + + /// + /// Gets or sets the HTTP server port number. + /// + /// The HTTP server port number. + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets the HTTPS server port number. + /// + /// The HTTPS server port number. + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; + + /// + /// Gets or sets a value indicating whether to use HTTPS. + /// + /// + /// 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; } = false; + + 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; } = 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; } = string.Empty; + + /// + /// Gets or sets a value indicating whether this instance is port authorized. + /// + /// true if this instance is port authorized; otherwise, false. + public bool IsPortAuthorized { get; set; } + + /// + /// Gets or sets a value indicating whether quick connect is available for use on this server. + /// + public bool QuickConnectAvailable { get; set; } = false; + + public bool AutoRunWebApp { get; set; } = true; + + /// + /// 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; } = true; + + public bool DisableLiveTvChannelUserDataName { get; set; } = true; + + /// + /// Gets or sets the metadata path. + /// + /// The metadata path. + public string MetadataPath { get; set; } = string.Empty; + + public string MetadataNetworkPath { get; set; } = string.Empty; + + /// + /// Gets or sets the preferred metadata language. + /// + /// The preferred metadata language. + public string PreferredMetadataLanguage { get; set; } = string.Empty; + + /// + /// Gets or sets the metadata country code. + /// + /// The metadata country code. + public string MetadataCountryCode { get; set; } = "US"; + + /// + /// 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; } = new[] { ".", "+", "%" }; + + /// + /// Gets or sets characters to be removed from strings to create a sort name. + /// + /// The sort remove characters. + public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; + + /// + /// Gets or sets words to be removed from strings to create a sort name. + /// + /// The sort remove words. + 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; } = 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; } = 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; } = 300; + + /// + /// 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; } = 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; } = true; + + /// + /// Gets or sets the image saving convention. + /// + /// The image saving convention. + public ImageSavingConvention ImageSavingConvention { get; set; } + + public MetadataOptions[] MetadataOptions { get; set; } + + public bool SkipDeserializationForBasicTypes { get; set; } = true; + + public string ServerName { get; set; } = string.Empty; + + public string BaseUrl + { + get => _baseUrl; + set + { + // Normalize the start of the string + if (string.IsNullOrWhiteSpace(value)) + { + // If baseUrl is empty, set an empty prefix string + _baseUrl = string.Empty; + return; + } + + if (value[0] != '/') + { + // If baseUrl was not configured with a leading slash, append one for consistency + value = "/" + value; + } + + // Normalize the end of the string + if (value[value.Length - 1] == '/') + { + // If baseUrl was configured with a trailing slash, remove it for consistency + value = value.Remove(value.Length - 1); + } + + _baseUrl = value; + } + } + + public string UICulture { get; set; } = "en-US"; + + public bool SaveMetadataHidden { get; set; } = false; + + public NameValuePair[] ContentTypes { get; set; } = Array.Empty(); + + public int RemoteClientBitrateLimit { get; set; } = 0; + + public bool EnableFolderView { get; set; } = false; + + public bool EnableGroupingIntoCollections { get; set; } = false; + + public bool DisplaySpecialsWithinSeasons { get; set; } = true; + + /// + /// Gets or sets the subnets that are deemed to make up the LAN. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); + + /// + /// 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 string[] CodecsUsed { get; set; } = Array.Empty(); + + public List PluginRepositories { get; set; } = new List(); + + 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; } = false; + + public bool EnableNewOmdbSupport { get; set; } = true; + + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); + + /// + /// 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; } = 0; + + public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); + + public bool EnableSimpleArtistDetection { get; set; } = false; + + 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; } = true; + + /// + /// Gets or sets the threshold for the slow response time warning in ms. + /// + public long SlowResponseThresholdMs { get; set; } = 500; + + /// + /// Gets or sets the cors hosts. + /// + public string[] CorsHosts { get; set; } = new[] { "*" }; + + /// + /// Gets or sets the known proxies. + /// + public string[] KnownProxies { get; set; } = Array.Empty(); - public string To { get; set; } } } diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 25402aee13..d460c0ab0c 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 664663bd76..3266a05cbf 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -6,6 +6,7 @@ + diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index a4be32e7d2..e28a2f48d4 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 1a8577d8da..43fccdad42 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 09ffa84689..2c7f0c4f9b 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 77f1640fa3..939023a95b 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(); From 288d89493e76b8881c719fe549859b2c0b5f7e29 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Sat, 12 Sep 2020 17:21:03 +0100 Subject: [PATCH 004/257] Fixed testing units. --- .../Middleware/IpBasedAccessValidationMiddleware.cs | 3 ++- Jellyfin.Server/Middleware/LanFilteringMiddleware.cs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 110290027b..ff82fe6cc6 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -34,8 +34,9 @@ namespace Jellyfin.Server.Middleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - if (httpContext.IsLocal()) + if (httpContext.Connection.RemoteIpAddress == null) { + // Running locally. await _next(httpContext).ConfigureAwait(false); return; } diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 2ff6f8a765..87c82bf583 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -1,10 +1,13 @@ 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; using Microsoft.AspNetCore.Http; +using NetworkCollection; namespace Jellyfin.Server.Middleware { @@ -33,7 +36,7 @@ namespace Jellyfin.Server.Middleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - var host = httpContext.Connection.RemoteIpAddress; + var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.Configuration.EnableRemoteAccess) { From b44455ad0d6e78b5baed535c06a7f7d49116d1ee Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Mon, 14 Sep 2020 15:46:38 +0100 Subject: [PATCH 005/257] Update based on PR1 changes. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- .../ApplicationHost.cs | 62 ++- .../TunerHosts/HdHomerun/HdHomerunHost.cs | 2 +- .../HdHomerun/HdHomerunUdpStream.cs | 2 +- .../LiveTv/TunerHosts/M3UTunerHost.cs | 2 +- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 2 +- .../DefaultAuthorizationHandler.cs | 2 +- .../Auth/DownloadPolicy/DownloadHandler.cs | 2 +- ...TimeOrIgnoreParentalControlSetupHandler.cs | 2 +- .../FirstTimeSetupOrDefaultHandler.cs | 2 +- .../FirstTimeSetupOrElevatedHandler.cs | 2 +- .../IgnoreParentalControlHandler.cs | 2 +- .../LocalAccessOrRequiresElevationHandler.cs | 2 +- .../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/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.csproj | 3 +- .../Net}/INetworkManager.cs | 42 +- .../IServerApplicationHost.cs | 26 +- RSSDP/SsdpCommunicationsServer.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 2 +- .../LocalAccessHandlerTests.cs | 2 +- 31 files changed, 388 insertions(+), 263 deletions(-) rename {Jellyfin.Networking/Manager => MediaBrowser.Common/Net}/INetworkManager.cs (79%) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 98f50c09af..a5da2fc5c8 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 cc04cb03f5..67a352fc93 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,7 @@ 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) @@ -1147,7 +1147,47 @@ namespace Emby.Server.Implementations return _startupOptions.PublishedServerUrl.ToString().Trim('/'); } - string smart = _networkManager.GetBindInterface(source, out int? port); + 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) + { + // Published server ends with a '/', so we need to remove it. + return _startupOptions.PublishedServerUrl.ToString().Trim('/'); + } + + 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 1cf129ad2d..27937d2f84 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 02ee302d0d..efb6d162ac 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 f297ecd5df..531a785a07 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 08746b346b..bd76b93bf4 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 69e6a8fb2d..dfa366796f 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 d1297119cd..3183d13186 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 53b5d47787..1cee962e9f 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 abdf2858d0..214198e001 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 ada8a0d4ec..9867ea4ca6 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 475e3cdac4..affd955515 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 d022c9067f..fab464b503 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 418d63de68..801ee2f4c6 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 a1cddbca3e..7adf72c3d3 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 6876b47b44..b87e275d0f 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 152e650bc0..c60497e304 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 3be8734b94..f81fb88fb1 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 d63e3ab118..9e4def774a 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/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 36a0a94a09..760938c40f 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))) + if (MatchesBindInterface(source, isExternal, out string result)) { - // Ignore IPAny addresses. - count = 0; + return result; } - if (count != 0) + if (isExternal && MatchesExternalInterface(source, out 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; - } - - 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); + 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 8c19665cc1..f73c917a0b 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 566ba0ad85..e2d7305af4 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 ff82fe6cc6..e927a147aa 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 87c82bf583..fa34a167b3 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 8549e39dbc..939f61656f 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 70dcc2397c..0fda47788b 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -20,8 +20,9 @@ - + + diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs similarity index 79% rename from Jellyfin.Networking/Manager/INetworkManager.cs rename to MediaBrowser.Common/Net/INetworkManager.cs index ba571750b9..32c017aee6 100644 --- a/Jellyfin.Networking/Manager/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -1,11 +1,13 @@ +#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 Jellyfin.Networking.Manager +namespace MediaBrowser.Common.Net { /// /// Interface for the NetworkManager class. @@ -18,9 +20,9 @@ namespace Jellyfin.Networking.Manager event EventHandler NetworkChanged; /// - /// Gets the Published server override list. + /// Gets the published server urls list. /// - Dictionary PublishedServerOverrides { get; } + Dictionary PublishedServerUrls { get; } /// /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. @@ -28,7 +30,7 @@ namespace Jellyfin.Networking.Manager public bool TrustAllIP6Interfaces { get; } /// - /// Gets returns the remote address filter. + /// Gets the remote address filter. /// NetCollection RemoteAddressFilter { get; } @@ -75,7 +77,37 @@ namespace Jellyfin.Networking.Manager /// 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); + 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. diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index f147e6a86c..7bc1006fc0 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 e28a2f48d4..932f6bbb40 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 43fccdad42..84456f7dcd 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 2c7f0c4f9b..05dd8f325e 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; From 38b8110a3ec33267bb2b9e0b75ed0e2a704f41ca Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Mon, 14 Sep 2020 15:55:25 +0100 Subject: [PATCH 006/257] Removing blank lines. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 1 - .../LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs | 1 - .../LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 1 - Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs | 1 - Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 1 - .../DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs | 1 - Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs | 1 - .../FirstTimeOrIgnoreParentalControlSetupHandler.cs | 1 - .../FirstTimeSetupOrDefaultHandler.cs | 1 - .../FirstTimeSetupOrElevatedHandler.cs | 1 - .../IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs | 1 - .../LocalAccessOrRequiresElevationHandler.cs | 1 - Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs | 1 - .../Auth/RequiresElevationPolicy/RequiresElevationHandler.cs | 1 - Jellyfin.Api/Controllers/UserController.cs | 1 - Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 1 - Jellyfin.Api/Helpers/MediaInfoHelper.cs | 1 - Jellyfin.Server.Implementations/Users/UserManager.cs | 2 -- Jellyfin.Server/CoreAppHost.cs | 1 - Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs | 1 - Jellyfin.Server/Middleware/LanFilteringMiddleware.cs | 1 - Jellyfin.Server/Program.cs | 1 - RSSDP/SsdpCommunicationsServer.cs | 1 - RSSDP/SsdpDevicePublisher.cs | 1 - .../Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs | 1 - 25 files changed, 26 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index a5da2fc5c8..35ec15623c 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; using Emby.Dlna.Ssdp; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 27937d2f84..28e30fac8b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -10,7 +10,6 @@ using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; - 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 efb6d162ac..a8d2a27f78 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -7,7 +7,6 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; - 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 531a785a07..8107bc427b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; - 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 bd76b93bf4..e245b57681 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,7 +1,6 @@ using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; - 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 dfa366796f..e988ca6cd1 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - 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 3183d13186..cea5ac4734 100644 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - 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 1cee962e9f..96cf4db4b5 100644 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - 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 214198e001..9815e252ee 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrDefaultPolicy/FirstTimeSetupOrDefaultHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - 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 9867ea4ca6..decbe0c035 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandler.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; - 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 affd955515..3eb3ac6cdc 100644 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - 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 fab464b503..7bede142e0 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; - 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 801ee2f4c6..2999c6c8a8 100644 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; - 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 7adf72c3d3..b235c4b63b 100644 --- a/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/RequiresElevationPolicy/RequiresElevationHandler.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; - using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index c60497e304..11cf41b838 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -7,7 +7,6 @@ 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; diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index f81fb88fb1..1d330a3357 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -8,7 +8,6 @@ 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; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 9e4def774a..b4b6e2d35c 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -6,7 +6,6 @@ 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; diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index f73c917a0b..ffd0066ee3 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CA1307 using System; @@ -12,7 +11,6 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; - using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Extensions; diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index e2d7305af4..c2cb677eb5 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -5,7 +5,6 @@ using System.Reflection; using Emby.Drawing; using Emby.Server.Implementations; using Jellyfin.Drawing.Skia; - 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 e927a147aa..7f6b6bcce9 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,6 +1,5 @@ using System.Linq; using System.Threading.Tasks; - 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 fa34a167b3..7963d0d8c6 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Net; using System.Threading.Tasks; - 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 939f61656f..989cc439e6 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -13,7 +13,6 @@ using CommandLine; using Emby.Server.Implementations; using Emby.Server.Implementations.IO; using Jellyfin.Api.Controllers; - using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 932f6bbb40..ea9e9a6fb5 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -7,7 +7,6 @@ using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 84456f7dcd..c70294b38e 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; - 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 05dd8f325e..553e6a9cab 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -4,7 +4,6 @@ using AutoFixture; using AutoFixture.AutoMoq; using Jellyfin.Api.Auth.LocalAccessPolicy; using Jellyfin.Api.Constants; - using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; From c1bfba90813b6780d43f82d4056496f5c776a186 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Mon, 14 Sep 2020 15:57:00 +0100 Subject: [PATCH 007/257] Nullable added --- Jellyfin.Server.Implementations/Users/UserManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index ffd0066ee3..bd853f92f7 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,3 +1,4 @@ +#nullable enable #pragma warning disable CA1307 using System; From 40464a6fddf3b522c57a72e16bd036fbe8ac4580 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Tue, 15 Sep 2020 13:47:01 +0100 Subject: [PATCH 008/257] Update LanFilteringMiddleware.cs --- .../Middleware/LanFilteringMiddleware.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 7963d0d8c6..a1d21f39e6 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -43,18 +43,6 @@ namespace Jellyfin.Server.Middleware } await _next(httpContext).ConfigureAwait(false); - } - - private static string NormalizeConfiguredLocalAddress(string address) - { - var add = address.AsSpan().Trim('/'); - int index = add.IndexOf('/'); - if (index != -1) - { - add = add.Slice(index + 1); - } - - return add.TrimStart('/').ToString(); - } + } } } From 96c3c4af4237c1fd1955fe5840904015ae592bef Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Tue, 15 Sep 2020 13:48:40 +0100 Subject: [PATCH 009/257] Update IpBasedAccessValidationMiddleware.cs --- .../Middleware/IpBasedAccessValidationMiddleware.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 7f6b6bcce9..0713d97d65 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Net; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -33,14 +33,14 @@ namespace Jellyfin.Server.Middleware /// The async task. public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) { - if (httpContext.Connection.RemoteIpAddress == null) + if (httpContext.IsLocal()) { // Running locally. await _next(httpContext).ConfigureAwait(false); return; } - var remoteIp = httpContext.Connection.RemoteIpAddress; + var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; if (serverConfigurationManager.Configuration.EnableRemoteAccess) { From e6a3518220f159325cdf941fbfbbcd8ba871c28c Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Tue, 15 Sep 2020 18:05:42 +0100 Subject: [PATCH 010/257] Uploading latest --- Jellyfin.Networking/Manager/NetworkManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 760938c40f..83661d6b31 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -450,7 +450,7 @@ namespace Jellyfin.Networking.Manager return new NetCollection(_internalInterfaces.Where(p => !p.IsLoopback())); } - return new NetCollection(_bindAddresses.Where(p => !p.IsLoopback())); + return new NetCollection(_bindAddresses); } } @@ -1127,7 +1127,7 @@ namespace Jellyfin.Networking.Manager private bool MatchesBindInterface(IPObject source, bool isExternal, out string result) { result = string.Empty; - var nc = new NetCollection(_bindAddresses.Exclude(_bindExclusions).Where(p => !p.IsLoopback())); + var nc = _bindAddresses.Exclude(_bindExclusions); int count = nc.Count; if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any))) From 0ec3633f9e3d2d3cf78dbcc0fe7d084ac3ccbf3d Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Tue, 15 Sep 2020 18:09:43 +0100 Subject: [PATCH 011/257] removing whitespace & upgraded library version --- Jellyfin.Networking/Jellyfin.Networking.csproj | 2 +- Jellyfin.Server/Middleware/LanFilteringMiddleware.cs | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index b6834cb0ee..454dd9b7df 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -29,7 +29,7 @@ - + diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index a1d21f39e6..1ef460bd76 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -43,6 +43,6 @@ namespace Jellyfin.Server.Middleware } await _next(httpContext).ConfigureAwait(false); - } + } } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 0fda47788b..8de2ab8d8d 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -1,4 +1,4 @@ - + @@ -22,7 +22,7 @@ - + From 0496e1817923149f3d99d8e588efcaa2fbc5be1d Mon Sep 17 00:00:00 2001 From: Ryan Petris Date: Sat, 19 Sep 2020 05:13:24 +0000 Subject: [PATCH 012/257] Fix HD Home Run streaming. * Use LocalEndPoint instead of RemoteEndPoint when determining local address. * HdHomerunUdpStream.StartStreaming is meant to run until stream is closed, however HdHomerunUdpStream.Open needs to return as soon as stream is open to send stream url back to client. Therefore, StartStreaming should not be awaited on. * TcpClient(IPEndPoint) treats endpoint as the local endpoint; use TcpClient(string, int) instead as it treats endpoint as the remote endpoint. --- .../TunerHosts/HdHomerun/HdHomerunManager.cs | 6 +-- .../HdHomerun/HdHomerunUdpStream.cs | 51 ++++++++++--------- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index d4a88e299f..895a13690e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -111,7 +111,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) { - using (var client = new TcpClient(new IPEndPoint(remoteIp, HdHomeRunPort))) + using (var client = new TcpClient(remoteIp.ToString(), HdHomeRunPort)) using (var stream = client.GetStream()) { return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); @@ -142,7 +142,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); - _tcpClient = new TcpClient(_remoteEndPoint); + _tcpClient = new TcpClient(_remoteEndPoint.Address.ToString(), _remoteEndPoint.Port); if (!_lockkey.HasValue) { @@ -221,7 +221,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return; } - using (var tcpClient = new TcpClient(_remoteEndPoint)) + using (var tcpClient = new TcpClient(_remoteEndPoint.Address.ToString(), _remoteEndPoint.Port)) using (var stream = tcpClient.GetStream()) { var commandList = commands.GetCommands(); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 6730751d50..5a95b28b5b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false); - localAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address; + localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address; tcpClient.Close(); } catch (Exception ex) @@ -80,6 +80,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } + if (localAddress.IsIPv4MappedToIPv6) { + localAddress = localAddress.MapToIPv4(); + } + var udpClient = new UdpClient(localPort, AddressFamily.InterNetwork); var hdHomerunManager = new HdHomerunManager(); @@ -110,12 +114,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var taskCompletionSource = new TaskCompletionSource(); - await StartStreaming( + StartStreaming( udpClient, hdHomerunManager, remoteAddress, taskCompletionSource, - LiveStreamCancellationTokenSource.Token).ConfigureAwait(false); + LiveStreamCancellationTokenSource.Token); // OpenedMediaSource.Protocol = MediaProtocol.File; // OpenedMediaSource.Path = tempFile; @@ -131,33 +135,30 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await taskCompletionSource.Task.ConfigureAwait(false); } - private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) + private async void StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) { - return Task.Run(async () => + using (udpClient) + using (hdHomerunManager) { - using (udpClient) - using (hdHomerunManager) + try { - try - { - await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException ex) - { - Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress); - openTaskCompletionSource.TrySetException(ex); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error opening live stream:"); - openTaskCompletionSource.TrySetException(ex); - } - - EnableStreamSharing = false; + await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException ex) + { + Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress); + openTaskCompletionSource.TrySetException(ex); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error opening live stream:"); + openTaskCompletionSource.TrySetException(ex); } - await DeleteTempFiles(new List { TempFilePath }).ConfigureAwait(false); - }); + EnableStreamSharing = false; + } + + await DeleteTempFiles(new List { TempFilePath }).ConfigureAwait(false); } private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) From 361f51ac944184cbde3768818bc68e5cfee8e857 Mon Sep 17 00:00:00 2001 From: Ryan Petris Date: Sat, 19 Sep 2020 22:22:48 +0000 Subject: [PATCH 013/257] Use TcpClient.Connect(). --- .../TunerHosts/HdHomerun/HdHomerunManager.cs | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 895a13690e..cdc8c6870a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -111,11 +111,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) { - using (var client = new TcpClient(remoteIp.ToString(), HdHomeRunPort)) - using (var stream = client.GetStream()) - { - return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); - } + using var client = new TcpClient(); + client.Connect(remoteIp, HdHomeRunPort); + + using var stream = client.GetStream(); + return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); } private static async Task CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken) @@ -142,7 +142,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); - _tcpClient = new TcpClient(_remoteEndPoint.Address.ToString(), _remoteEndPoint.Port); + _tcpClient = new TcpClient(); + _tcpClient.Connect(_remoteEndPoint); if (!_lockkey.HasValue) { @@ -221,30 +222,30 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return; } - using (var tcpClient = new TcpClient(_remoteEndPoint.Address.ToString(), _remoteEndPoint.Port)) - using (var stream = tcpClient.GetStream()) - { - var commandList = commands.GetCommands(); - byte[] buffer = ArrayPool.Shared.Rent(8192); - try - { - foreach (var command in commandList) - { - var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); - await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); - int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + using var tcpClient = new TcpClient(); + tcpClient.Connect(_remoteEndPoint); - // parse response to make sure it worked - if (!ParseReturnMessage(buffer, receivedBytes, out _)) - { - return; - } + using var stream = tcpClient.GetStream(); + var commandList = commands.GetCommands(); + byte[] buffer = ArrayPool.Shared.Rent(8192); + try + { + foreach (var command in commandList) + { + var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); + await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); + int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + + // parse response to make sure it worked + if (!ParseReturnMessage(buffer, receivedBytes, out _)) + { + return; } } - finally - { - ArrayPool.Shared.Return(buffer); - } + } + finally + { + ArrayPool.Shared.Return(buffer); } } From 37cb4dbca6dd42d004fa389dd2b6fbc07d56b7d4 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Thu, 24 Sep 2020 15:34:49 +0100 Subject: [PATCH 014/257] Removed package reference. --- Emby.Server.Implementations/Emby.Server.Implementations.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 0793526271..4d7919d8b2 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,6 @@ - From 28634d3b366c814192a14970e5f790d6cae149bf Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 24 Sep 2020 15:43:06 +0100 Subject: [PATCH 015/257] Update DlnaEntryPoint.cs --- Emby.Dlna/Main/DlnaEntryPoint.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 35ec15623c..6f626711a2 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -259,15 +259,16 @@ namespace Emby.Dlna.Main private void RegisterServerEndpoints() { - var bindAddresses = _networkManager.GetInternalBindAddresses() - .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)); - var udn = CreateUuid(_appHost.SystemId); - if (!bindAddresses.Any()) + var ba = new NetCollection( + _networkManager.GetInternalBindAddresses() + .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0))); + + if (ba.Count == 0) { // No interfaces returned, so use loopback. - bindAddresses = _networkManager.GetLoopbacks(); + ba = _networkManager.GetLoopbacks(); } foreach (var addr in bindAddresses) From d617c0b8b0bd5cac78e9ba9ce968e890a8d5d43a Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 24 Sep 2020 15:47:35 +0100 Subject: [PATCH 016/257] Update SystemController.cs --- Jellyfin.Api/Controllers/SystemController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs index b87e275d0f..7784e8a119 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; From 8c85cfd01d183b62badb0e8853f29c6a178c9e26 Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Thu, 24 Sep 2020 16:02:29 +0100 Subject: [PATCH 017/257] Fixed build --- Emby.Dlna/Main/DlnaEntryPoint.cs | 6 +++--- .../Emby.Server.Implementations.csproj | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 6f626711a2..33a953563b 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -261,14 +261,14 @@ namespace Emby.Dlna.Main { var udn = CreateUuid(_appHost.SystemId); - var ba = new NetCollection( + var bindAddresses = new NetCollection( _networkManager.GetInternalBindAddresses() .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0))); - if (ba.Count == 0) + if (bindAddresses.Count == 0) { // No interfaces returned, so use loopback. - ba = _networkManager.GetLoopbacks(); + bindAddresses = _networkManager.GetLoopbacks(); } foreach (var addr in bindAddresses) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 9a860f4d78..f8fef8bcbf 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,6 @@ - From f97bea53e59b24117e8111467afae37a12d2d80b Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 24 Sep 2020 16:11:24 +0100 Subject: [PATCH 018/257] Update Emby.Server.Implementations.csproj --- .../Emby.Server.Implementations.csproj | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index f8fef8bcbf..444fee33a6 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -13,7 +13,7 @@ - + @@ -31,6 +31,10 @@ + + + + From bbe2400b59563991f1689c68ef36d623afd39f0a Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Wed, 30 Sep 2020 17:51:17 +0100 Subject: [PATCH 019/257] Updating to NetCollection 1.03 --- .../Jellyfin.Networking.csproj | 3 +- Jellyfin.Networking/Manager/NetworkManager.cs | 150 ++++++++++++------ MediaBrowser.Common/Net/INetworkManager.cs | 17 +- .../Configuration/ServerConfiguration.cs | 23 ++- 4 files changed, 128 insertions(+), 65 deletions(-) diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 454dd9b7df..06d387dc87 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -28,8 +28,7 @@ - - + diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 83661d6b31..6f2970ef89 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -12,6 +12,7 @@ using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NetworkCollection; +using NetworkCollection.Udp; namespace Jellyfin.Networking.Manager { @@ -125,29 +126,19 @@ namespace Jellyfin.Networking.Manager public event EventHandler? NetworkChanged; /// - /// Gets the unique network location signature, which is updated on every network change. + /// Gets or sets a value indicating whether testing is taking place. /// - public static string NetworkLocationSignature { get; internal set; } = Guid.NewGuid().ToString(); + public static string MockNetworkSettings { get; set; } = string.Empty; /// - /// Gets a value indicating whether IP6 is enabled. + /// Gets or sets a value indicating whether IP6 is enabled. /// - public static bool IsIP6Enabled { get; internal set; } + public bool IsIP6Enabled { get; set; } /// - /// Gets a value indicating whether IP4 is enabled. + /// Gets or sets 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 bool IsIP4Enabled { get; set; } /// public NetCollection RemoteAddressFilter { get; private set; } @@ -271,7 +262,7 @@ namespace Jellyfin.Networking.Manager } /// - public NetCollection GetAllBindInterfaces() + public NetCollection GetAllBindInterfaces(bool individualInterfaces = false) { lock (_intLock) { @@ -285,6 +276,11 @@ namespace Jellyfin.Networking.Manager return _interfaceAddresses.Exclude(_bindExclusions); } + if (individualInterfaces) + { + return new NetCollection(_interfaceAddresses); + } + // No bind address and no exclusions, so listen on all interfaces. NetCollection result = new NetCollection(); @@ -311,12 +307,6 @@ namespace Jellyfin.Networking.Manager { if (!string.IsNullOrEmpty(source)) { - if (string.Equals(source, "chromecast", StringComparison.OrdinalIgnoreCase)) - { - // Just assign a variable so has source = true; - return GetBindInterface(IPNetAddress.IP4Loopback, out port); - } - if (IPHost.TryParse(source, out IPHost host)) { return GetBindInterface(host, out port); @@ -572,16 +562,18 @@ namespace Jellyfin.Networking.Manager } /// - public bool TryParseInterface(string token, out IPNetAddress result) + public bool TryParseInterface(string token, out NetCollection? result) { + result = null; if (string.IsNullOrEmpty(token)) { - result = IPNetAddress.None; return false; } if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) { + result = new NetCollection(); + _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); // Replace interface tags with the interface IP's. @@ -591,13 +583,14 @@ namespace Jellyfin.Networking.Manager ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { - result = iface; - return true; + result.Add(iface); } } + + return true; } - return IPNetAddress.TryParse(token, out result); + return false; } /// @@ -614,9 +607,27 @@ namespace Jellyfin.Networking.Manager IsIP4Enabled = Socket.OSSupportsIPv6 && config.EnableIPV4; IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; - EnableMultiSocketBinding = config.EnableMultiSocketBinding; + UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; + + if (string.IsNullOrEmpty(MockNetworkSettings)) + { + InitialiseInterfaces(); + } + else // Used in testing only. + { + // Format is ,,: . Set index to -ve to simulate a gateway. + var interfaceList = MockNetworkSettings.Split(':'); + foreach (var details in interfaceList) + { + var parts = details.Split(','); + var address = IPNetAddress.Parse(parts[0]); + var index = int.Parse(parts[1], CultureInfo.InvariantCulture); + address.Tag = index; + _interfaceAddresses.Add(address); + _interfaceNames.Add(parts[2], Math.Abs(index)); + } + } - InitialiseInterfaces(); InitialiseLAN(config); InitialiseBind(config); InitialiseRemote(config); @@ -671,6 +682,40 @@ namespace Jellyfin.Networking.Manager return str; } + /// + /// Checks the string to see if it matches any interface names. + /// + /// String to check. + /// Interface index number. + /// True if an interface name matches the token. + private bool IsInterface(string token, out int index) + { + index = -1; + + // 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 && token.Length > 1) + { + bool partial = token[^1] == '*'; + if (partial) + { + token = token[0..^1]; + } + + foreach ((string interfc, int interfcIndex) in _interfaceNames) + { + if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase)) || + (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture))) + { + index = interfcIndex; + return true; + } + } + } + + return false; + } + /// /// Parses strings into the collection, replacing any interface references. /// @@ -680,7 +725,7 @@ namespace Jellyfin.Networking.Manager { // 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)) + if (IsInterface(token, out int index)) { _logger.LogInformation("Interface {0} used in settings. Using its interface addresses.", token); @@ -774,14 +819,6 @@ namespace Jellyfin.Networking.Manager /// 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."); @@ -800,20 +837,14 @@ namespace Jellyfin.Networking.Manager /// private void InitialiseOverrides(ServerConfiguration config) { - string[] overrides = config.PublishedServerUriBySubnet; - if (overrides == null) - { - lock (_intLock) - { - _publishedServerUrls.Clear(); - } - - return; - } - lock (_intLock) { _publishedServerUrls.Clear(); + string[] overrides = config.PublishedServerUriBySubnet; + if (overrides == null) + { + return; + } foreach (var entry in overrides) { @@ -833,9 +864,16 @@ namespace Jellyfin.Networking.Manager { _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; } - else if (TryParseInterface(parts[0], out IPNetAddress address)) + else if (TryParseInterface(parts[0], out NetCollection? addresses) && addresses != null) { - _publishedServerUrls[address] = replacement; + foreach (IPNetAddress na in addresses) + { + _publishedServerUrls[na] = replacement; + } + } + else if (IPNetAddress.TryParse(parts[0], out IPNetAddress result)) + { + _publishedServerUrls[result] = replacement; } else { @@ -859,6 +897,14 @@ namespace Jellyfin.Networking.Manager // TODO: end fix. + // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. + if (config.IgnoreVirtualInterfaces) + { + var newList = ba.ToList(); + newList.AddRange(config.VirtualInterfaceNames.Split(',').ToList()); + ba = newList.ToArray(); + } + // Read and parse bind addresses and exclusions, removing ones that don't exist. _bindAddresses = CreateIPCollection(ba).Union(_interfaceAddresses); _bindExclusions = CreateIPCollection(ba, true).Union(_interfaceAddresses); diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 32c017aee6..8789cd9d81 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -34,13 +34,24 @@ namespace MediaBrowser.Common.Net /// NetCollection RemoteAddressFilter { get; } + /// + /// Gets or sets a value indicating whether iP6 is enabled. + /// + public bool IsIP6Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether iP4 is enabled. + /// + public bool IsIP4Enabled { get; set; } + /// /// 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(); + /// When false, return or for all interfaces. + NetCollection GetAllBindInterfaces(bool individualInterfaces = false); /// /// Returns a collection containing the loopback interfaces. @@ -166,9 +177,9 @@ namespace MediaBrowser.Common.Net /// eg. "eth1", or "TP-LINK Wireless USB Adapter". /// /// Token to parse. - /// Resultant object if successful. + /// Resultant object's ip addresses, if successful. /// Success of the operation. - bool TryParseInterface(string token, out IPNetAddress result); + bool TryParseInterface(string token, out NetCollection? result); /// /// Parses an array of strings into a NetCollection. diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 073a629829..dbfab1fad4 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable enable #pragma warning disable CS1591 #pragma warning disable CA1819 @@ -111,7 +111,7 @@ namespace MediaBrowser.Model.Configuration /// /// 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. + /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work. /// public bool EnableSSDPTracing { get; set; } = false; @@ -131,13 +131,23 @@ namespace MediaBrowser.Model.Configuration /// public int UDPSendDelay { get; set; } = 100; + /// + /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding. + /// + public bool IgnoreVirtualInterfaces { get; set; } = true; + + /// + /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. . + /// + public string VirtualInterfaceNames { get; set; } = "vEthernet*"; + /// /// 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. + /// Gets a value indicating whether multi-socket binding is available. /// public bool EnableMultiSocketBinding { get; } = true; @@ -158,7 +168,7 @@ namespace MediaBrowser.Model.Configuration public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); /// - /// Gets or sets a value indicating whether gets or sets Autodiscovery tracing. + /// Gets or sets a value indicating whether Autodiscovery tracing is enabled. /// public bool AutoDiscoveryTracing { get; set; } = false; @@ -216,9 +226,7 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets a value indicating whether quick connect is available for use on this server. /// public bool QuickConnectAvailable { get; set; } = false; - - public bool AutoRunWebApp { get; set; } = true; - + /// /// Gets or sets a value indicating whether access outside of the LAN is permitted. /// @@ -419,6 +427,5 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets the known proxies. /// public string[] KnownProxies { get; set; } = Array.Empty(); - } } From 3b64171cde969a7cfca60d28c7782774a173758b Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Wed, 30 Sep 2020 18:02:36 +0100 Subject: [PATCH 020/257] Minor change to get it to compile. --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- Jellyfin.Api/Controllers/StartupController.cs | 6 +++--- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index a95cfea07c..66e48ddc65 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1319,7 +1319,7 @@ namespace Emby.Server.Implementations /// public string GetLoopbackHttpApiUrl() { - if (NetworkManager.IsIP6Enabled) + if (NetManager.IsIP6Enabled) { return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); } diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 9c259cc198..e59c6e1ddf 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -72,9 +72,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration) { - _config.Configuration.UICulture = startupConfiguration.UICulture; - _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode; - _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage; + _config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty; + _config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty; + _config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty; _config.SaveConfiguration(); return NoContent(); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index dbfab1fad4..86b7c4c3d6 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,4 +1,3 @@ -#nullable enable #pragma warning disable CS1591 #pragma warning disable CA1819 From c2276b17cb196de44f1bbb42b91b000cde8fdc7f Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Wed, 30 Sep 2020 19:33:34 -0700 Subject: [PATCH 021/257] Increase library scan and metadata refresh speed --- .../ApplicationHost.cs | 1 + MediaBrowser.Controller/Entities/Folder.cs | 220 +++++++++++------- .../Library/TaskMethods.cs | 133 +++++++++++ .../Configuration/ServerConfiguration.cs | 12 + 4 files changed, 283 insertions(+), 83 deletions(-) create mode 100644 MediaBrowser.Controller/Library/TaskMethods.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 7a46fdf2e7..6d7239f724 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -758,6 +758,7 @@ namespace Emby.Server.Implementations BaseItem.FileSystem = _fileSystemManager; BaseItem.UserDataManager = Resolve(); BaseItem.ChannelManager = Resolve(); + TaskMethods.ConfigurationManager = ServerConfigurationManager; Video.LiveTvManager = Resolve(); Folder.UserViewManager = Resolve(); UserView.TVSeriesManager = Resolve(); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 901ea875bc..666455cff4 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -35,6 +35,46 @@ namespace MediaBrowser.Controller.Entities /// public class Folder : BaseItem { + /// + /// Contains constants used when reporting scan progress. + /// + private static class ProgressHelpers + { + /// + /// Reported after the folders immediate children are retrieved. + /// + public const int RetrievedChildren = 5; + + /// + /// Reported after add, updating, or deleting child items from the LibraryManager. + /// + public const int UpdatedChildItems = 10; + + /// + /// Reported once subfolders are scanned. + /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders]. + /// + public const int ScannedSubfolders = 50; + + /// + /// Reported once metadata is refreshed. + /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed]. + /// + public const int RefreshedMetadata = 100; + + /// + /// Gets the current progress given the previous step, next step, and progress in between. + /// + /// The previous progress step. + /// The next progress step. + /// The current progress step. + /// The progress. + public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress) + { + return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100)); + } + } + public static IUserViewManager UserViewManager { get; set; } /// @@ -327,11 +367,11 @@ namespace MediaBrowser.Controller.Entities return; } - progress.Report(5); + progress.Report(ProgressHelpers.RetrievedChildren); if (recursive) { - ProviderManager.OnRefreshProgress(this, 5); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.RetrievedChildren); } // Build a dictionary of the current children we have now by Id so we can compare quickly and easily @@ -392,11 +432,11 @@ namespace MediaBrowser.Controller.Entities validChildrenNeedGeneration = true; } - progress.Report(10); + progress.Report(ProgressHelpers.UpdatedChildItems); if (recursive) { - ProviderManager.OnRefreshProgress(this, 10); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.UpdatedChildItems); } cancellationToken.ThrowIfCancellationRequested(); @@ -406,11 +446,13 @@ namespace MediaBrowser.Controller.Entities var innerProgress = new ActionableProgress(); var folder = this; - innerProgress.RegisterAction(p => + innerProgress.RegisterAction(innerPercent => { - double newPct = 0.80 * p + 10; - progress.Report(newPct); - ProviderManager.OnRefreshProgress(folder, newPct); + var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.ScannedSubfolders, innerPercent); + + progress.Report(percent); + + ProviderManager.OnRefreshProgress(folder, percent); }); if (validChildrenNeedGeneration) @@ -424,11 +466,11 @@ namespace MediaBrowser.Controller.Entities if (refreshChildMetadata) { - progress.Report(90); + progress.Report(ProgressHelpers.ScannedSubfolders); if (recursive) { - ProviderManager.OnRefreshProgress(this, 90); + ProviderManager.OnRefreshProgress(this, ProgressHelpers.ScannedSubfolders); } var container = this as IMetadataContainer; @@ -436,13 +478,15 @@ namespace MediaBrowser.Controller.Entities var innerProgress = new ActionableProgress(); var folder = this; - innerProgress.RegisterAction(p => + innerProgress.RegisterAction(innerPercent => { - double newPct = 0.10 * p + 90; - progress.Report(newPct); + var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.RefreshedMetadata, innerPercent); + + progress.Report(percent); + if (recursive) { - ProviderManager.OnRefreshProgress(folder, newPct); + ProviderManager.OnRefreshProgress(folder, percent); } }); @@ -457,55 +501,37 @@ namespace MediaBrowser.Controller.Entities validChildren = Children.ToList(); } - await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken); + await RefreshMetadataRecursive(validChildren, refreshOptions, recursive, innerProgress, cancellationToken).ConfigureAwait(false); } } } - private async Task RefreshMetadataRecursive(List children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) + private Task RefreshMetadataRecursive(IList children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { - var numComplete = 0; - var count = children.Count; - double currentPercent = 0; + var progressableTasks = children + .Select, Task>>(child => + innerProgress => RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken)) + .ToList(); - foreach (var child in children) - { - cancellationToken.ThrowIfCancellationRequested(); - - var innerProgress = new ActionableProgress(); - - // Avoid implicitly captured closure - var currentInnerPercent = currentPercent; - - innerProgress.RegisterAction(p => - { - double innerPercent = currentInnerPercent; - innerPercent += p / count; - progress.Report(innerPercent); - }); - - await RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken) - .ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - currentPercent = percent; - - progress.Report(percent); - } + return RunTasks(progressableTasks, progress, cancellationToken); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress progress, CancellationToken cancellationToken) { - var series = container as Series; - if (series != null) - { - await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); - } + // limit the amount of concurrent metadata refreshes + await TaskMethods.RunThrottled( + TaskMethods.SharedThrottleId.RefreshMetadata, + async () => + { + var series = container as Series; + if (series != null) + { + await series.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + } - await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + await container.RefreshAllMetadata(refreshOptions, progress, cancellationToken).ConfigureAwait(false); + }, + cancellationToken).ConfigureAwait(false); } private async Task RefreshChildMetadata(BaseItem child, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) @@ -520,12 +546,16 @@ namespace MediaBrowser.Controller.Entities { if (refreshOptions.RefreshItem(child)) { - await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false); + // limit the amount of concurrent metadata refreshes + await TaskMethods.RunThrottled( + TaskMethods.SharedThrottleId.RefreshMetadata, + async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false), + cancellationToken).ConfigureAwait(false); } if (recursive && child is Folder folder) { - await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken); + await folder.RefreshMetadataRecursive(folder.Children.ToList(), refreshOptions, true, progress, cancellationToken).ConfigureAwait(false); } } } @@ -538,39 +568,63 @@ namespace MediaBrowser.Controller.Entities /// The progress. /// The cancellation token. /// Task. - private async Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) + private Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) { - var numComplete = 0; - var count = children.Count; - double currentPercent = 0; + var progressableTasks = children + .Select, Task>>(child => + innerProgress => child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService)) + .ToList(); - foreach (var child in children) + return RunTasks(progressableTasks, progress, cancellationToken); + } + + /// + /// Runs a set of tasks concurrently with progress. + /// + /// A list of tasks. + /// The progress. + /// The cancellation token. + /// Task. + private async Task RunTasks(IList, Task>> tasks, IProgress progress, CancellationToken cancellationToken) + { + var childrenCount = tasks.Count; + var childrenProgress = new double[childrenCount]; + var actions = new Func[childrenCount]; + + void UpdateProgress() { - cancellationToken.ThrowIfCancellationRequested(); - - var innerProgress = new ActionableProgress(); - - // Avoid implicitly captured closure - var currentInnerPercent = currentPercent; - - innerProgress.RegisterAction(p => - { - double innerPercent = currentInnerPercent; - innerPercent += p / count; - progress.Report(innerPercent); - }); - - await child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService) - .ConfigureAwait(false); - - numComplete++; - double percent = numComplete; - percent /= count; - percent *= 100; - currentPercent = percent; - - progress.Report(percent); + progress.Report(childrenProgress.Average()); } + + for (var i = 0; i < childrenCount; i++) + { + var childIndex = i; + var child = tasks[childIndex]; + + actions[childIndex] = async () => + { + var innerProgress = new ActionableProgress(); + + innerProgress.RegisterAction(innerPercent => + { + // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls + var innerPercentRounded = Math.Round(innerPercent); + if (childrenProgress[childIndex] != innerPercentRounded) + { + childrenProgress[childIndex] = innerPercentRounded; + UpdateProgress(); + } + }); + + await tasks[childIndex](innerProgress).ConfigureAwait(false); + + childrenProgress[childIndex] = 100; + + UpdateProgress(); + }; + } + + await TaskMethods.WhenAllThrottled(TaskMethods.SharedThrottleId.ScanFanout, actions, cancellationToken).ConfigureAwait(false); } /// diff --git a/MediaBrowser.Controller/Library/TaskMethods.cs b/MediaBrowser.Controller/Library/TaskMethods.cs new file mode 100644 index 0000000000..66bfbe0d9e --- /dev/null +++ b/MediaBrowser.Controller/Library/TaskMethods.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; + +namespace MediaBrowser.Controller.Library +{ + /// + /// Helper methods for running tasks concurrently. + /// + public static class TaskMethods + { + private static readonly int _processorCount = Environment.ProcessorCount; + + private static readonly ConcurrentDictionary _sharedThrottlers = new ConcurrentDictionary(); + + /// + /// Throttle id for sharing a concurrency limit. + /// + public enum SharedThrottleId + { + /// + /// Library scan fan out + /// + ScanFanout, + + /// + /// Refresh metadata + /// + RefreshMetadata, + } + + /// + /// Gets or sets the configuration manager. + /// + public static IServerConfigurationManager ConfigurationManager { get; set; } + + /// + /// Similiar to Task.WhenAll but only allows running a certain amount of tasks at the same time. + /// + /// The throttle id. Multiple calls to this method with the same throttle id will share a concurrency limit. + /// List of actions to run. + /// The cancellation token. + /// A representing the result of the asynchronous operation. + public static async Task WhenAllThrottled(SharedThrottleId throttleId, IEnumerable> actions, CancellationToken cancellationToken) + { + var taskThrottler = throttleId == SharedThrottleId.ScanFanout ? + new SemaphoreSlim(GetConcurrencyLimit(throttleId)) : + _sharedThrottlers.GetOrAdd(throttleId, id => new SemaphoreSlim(GetConcurrencyLimit(id))); + + try + { + var tasks = new List(); + + foreach (var action in actions) + { + await taskThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); + + tasks.Add(Task.Run(async () => + { + try + { + await action().ConfigureAwait(false); + } + finally + { + taskThrottler.Release(); + } + })); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + finally + { + if (throttleId == SharedThrottleId.ScanFanout) + { + taskThrottler.Dispose(); + } + } + } + + /// + /// Runs a task within a given throttler. + /// + /// The throttle id. Multiple calls to this method with the same throttle id will share a concurrency limit. + /// The action to run. + /// The cancellation token. + /// A representing the result of the asynchronous operation. + public static async Task RunThrottled(SharedThrottleId throttleId, Func action, CancellationToken cancellationToken) + { + if (throttleId == SharedThrottleId.ScanFanout) + { + // just await the task instead + throw new InvalidOperationException("Invalid throttle id"); + } + + var taskThrottler = _sharedThrottlers.GetOrAdd(throttleId, id => new SemaphoreSlim(GetConcurrencyLimit(id))); + + await taskThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await action().ConfigureAwait(false); + } + finally + { + taskThrottler.Release(); + } + } + + /// + /// Get the concurrency limit for the given throttle id. + /// + /// The throttle id. + /// The concurrency limit. + private static int GetConcurrencyLimit(SharedThrottleId throttleId) + { + var concurrency = throttleId == SharedThrottleId.RefreshMetadata ? + ConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency : + ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; + + if (concurrency <= 0) + { + concurrency = _processorCount; + } + + return concurrency; + } + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 8b78ad842e..14bfcbf9e2 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -271,6 +271,16 @@ namespace MediaBrowser.Model.Configuration /// public string[] KnownProxies { get; set; } + /// + /// Gets or sets the how the library scan fans out. + /// + public int LibraryScanFanoutConcurrency { get; set; } + + /// + /// Gets or sets the how many metadata refreshes can run concurrently. + /// + public int LibraryMetadataRefreshConcurrency { get; set; } + /// /// Initializes a new instance of the class. /// @@ -381,6 +391,8 @@ namespace MediaBrowser.Model.Configuration SlowResponseThresholdMs = 500; CorsHosts = new[] { "*" }; KnownProxies = Array.Empty(); + LibraryMetadataRefreshConcurrency = 0; + LibraryScanFanoutConcurrency = 0; } } From 688d098d61027bc10da7b0dbbb4f89a185e07444 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 1 Oct 2020 20:51:25 +0100 Subject: [PATCH 022/257] Update Emby.Server.Implementations.csproj --- Emby.Server.Implementations/Emby.Server.Implementations.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 444fee33a6..4d7919d8b2 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -13,7 +13,7 @@ - + From 02de44ec0c1e81fac698425ebf54fbe97e7aea41 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Thu, 1 Oct 2020 20:54:39 +0100 Subject: [PATCH 023/257] Update BaseAuthorizationHandler.cs --- Jellyfin.Api/Auth/BaseAuthorizationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs index e245b57681..d732b6bc6a 100644 --- a/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/BaseAuthorizationHandler.cs @@ -1,4 +1,4 @@ -using System.Security.Claims; +using System.Security.Claims; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; From 3ad62329731c1156566bd707f21cad60e74f9a5f Mon Sep 17 00:00:00 2001 From: Stanislav Ionascu Date: Thu, 24 Sep 2020 08:41:42 +0200 Subject: [PATCH 024/257] Add probe / direct-stream / transcoding support for BluRays This commit (and ffmpeg built with libbluray support), adds support for playback of bluray images (bd-iso) and bluray folders. --- .../MediaEncoder/EncodingManager.cs | 20 ++++----- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 4 +- .../MediaEncoding/EncodingHelper.cs | 24 +---------- .../MediaEncoding/IMediaEncoder.cs | 11 ++--- .../MediaEncoding/MediaEncoderHelpers.cs | 10 +---- .../Attachments/AttachmentExtractor.cs | 16 +++---- .../Encoder/EncodingUtils.cs | 14 ++++--- .../Encoder/MediaEncoder.cs | 42 ++++++++++++------- .../Subtitles/SubtitleEncoder.cs | 36 ++++++++-------- .../MediaInfo/FFProbeProvider.cs | 5 --- .../MediaInfo/VideoImageProvider.cs | 21 ++++++++-- 11 files changed, 101 insertions(+), 102 deletions(-) diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 438bbe24a5..791d009637 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -81,12 +82,7 @@ namespace Emby.Server.Implementations.MediaEncoder return false; } - if (video.VideoType == VideoType.Iso) - { - return false; - } - - if (video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd) + if (video.VideoType == VideoType.Dvd) { return false; } @@ -140,15 +136,19 @@ namespace Emby.Server.Implementations.MediaEncoder // Add some time for the first chapter to make sure we don't end up with a black image var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); - var protocol = MediaProtocol.File; - - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, null, Array.Empty()); + var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, Array.Empty()); Directory.CreateDirectory(Path.GetDirectoryName(path)); var container = video.Container; + var mediaSource = new MediaSourceInfo + { + VideoType = video.VideoType, + IsoType = video.IsoType, + Protocol = video.PathProtocol.Value, + }; - var tempFile = await _encoder.ExtractVideoImage(inputPath, container, protocol, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); + var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); File.Copy(tempFile, path, true); try diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 64d1227f7c..465857b01e 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -762,7 +762,7 @@ namespace Jellyfin.Api.Helpers private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) { - if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && _isoManager.CanMount(state.MediaPath)) + if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && state.IsoType == IsoType.Dvd && _isoManager.CanMount(state.MediaPath)) { state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index c5529ad5bd..7481cc7b96 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -379,25 +379,9 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetInputPathArgument(EncodingJobInfo state) { - var protocol = state.InputProtocol; var mediaPath = state.MediaPath ?? string.Empty; - string[] inputPath; - if (state.IsInputVideo - && !(state.VideoType == VideoType.Iso && state.IsoMount == null)) - { - inputPath = MediaEncoderHelpers.GetInputArgument( - _fileSystem, - mediaPath, - state.IsoMount, - state.PlayableStreamFileNames); - } - else - { - inputPath = new[] { mediaPath }; - } - - return _mediaEncoder.GetInputArgument(inputPath, protocol); + return _mediaEncoder.GetInputArgument(mediaPath, state.MediaSource); } /// @@ -2546,14 +2530,10 @@ namespace MediaBrowser.Controller.MediaEncoding { state.VideoType = mediaSource.VideoType.Value; - if (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd) + if (mediaSource.VideoType.Value == VideoType.Dvd) { state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, mediaSource.VideoType.Value).Select(Path.GetFileName).ToArray(); } - else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.BluRay) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.BluRay).Select(Path.GetFileName).ToArray(); - } else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.Dvd) { state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.Dvd).Select(Path.GetFileName).ToArray(); diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 17d6dc5d27..5c070122d0 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -61,9 +62,9 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// Extracts the video image. /// - Task ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); + Task ExtractVideoImage(string[] inputFiles, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); - Task ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); + Task ExtractVideoImage(string[] inputFiles, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); /// /// Extracts the video images on interval. @@ -71,7 +72,7 @@ namespace MediaBrowser.Controller.MediaEncoding Task ExtractVideoImagesOnInterval(string[] inputFiles, string container, MediaStream videoStream, - MediaProtocol protocol, + MediaSourceInfo mediaSource, Video3DFormat? threedFormat, TimeSpan interval, string targetDirectory, @@ -91,9 +92,9 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets the input argument. /// /// The input files. - /// The protocol. + /// The mediaSource. /// System.String. - string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol); + string GetInputArgument(IReadOnlyList inputFiles, MediaSourceInfo mediaSource); /// /// Gets the time parameter. diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index ce53c23ad2..df2d2fa57b 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -18,19 +18,13 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// The file system. /// The video path. - /// The iso mount. /// The playable stream file names. /// string[]. - public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, IIsoMount isoMount, IReadOnlyCollection playableStreamFileNames) + public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, IReadOnlyCollection playableStreamFileNames) { if (playableStreamFileNames.Count > 0) { - if (isoMount == null) - { - return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames); - } - - return GetPlayableStreamFiles(fileSystem, isoMount.MountedPath, playableStreamFileNames); + return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames); } return new[] { videoPath }; diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 21b5d0c5be..466fd75e15 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -87,19 +87,19 @@ namespace MediaBrowser.MediaEncoding.Attachments MediaAttachment mediaAttachment, CancellationToken cancellationToken) { - var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false); + var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource, mediaAttachment, cancellationToken).ConfigureAwait(false); return File.OpenRead(attachmentPath); } private async Task GetReadableFile( string mediaPath, string inputFile, - MediaProtocol protocol, + MediaSourceInfo mediaSource, MediaAttachment mediaAttachment, CancellationToken cancellationToken) { - var outputPath = GetAttachmentCachePath(mediaPath, protocol, mediaAttachment.Index); - await ExtractAttachment(inputFile, protocol, mediaAttachment.Index, outputPath, cancellationToken) + var outputPath = GetAttachmentCachePath(mediaPath, mediaSource, mediaAttachment.Index); + await ExtractAttachment(inputFile, mediaSource, mediaAttachment.Index, outputPath, cancellationToken) .ConfigureAwait(false); return outputPath; @@ -107,7 +107,7 @@ namespace MediaBrowser.MediaEncoding.Attachments private async Task ExtractAttachment( string inputFile, - MediaProtocol protocol, + MediaSourceInfo mediaSource, int attachmentStreamIndex, string outputPath, CancellationToken cancellationToken) @@ -121,7 +121,7 @@ namespace MediaBrowser.MediaEncoding.Attachments if (!File.Exists(outputPath)) { await ExtractAttachmentInternal( - _mediaEncoder.GetInputArgument(new[] { inputFile }, protocol), + _mediaEncoder.GetInputArgument(new[] { inputFile }, mediaSource), attachmentStreamIndex, outputPath, cancellationToken).ConfigureAwait(false); @@ -234,10 +234,10 @@ namespace MediaBrowser.MediaEncoding.Attachments } } - private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex) + private string GetAttachmentCachePath(string mediaPath, MediaSourceInfo mediaSource, int attachmentStreamIndex) { string filename; - if (protocol == MediaProtocol.File) + if (mediaSource.Protocol == MediaProtocol.File) { var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index 63310fdf6b..b1fcba2d1e 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -10,7 +10,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { public static class EncodingUtils { - public static string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol) + public static string GetInputArgument(string inputPrefix, IReadOnlyList inputFiles, MediaProtocol protocol) { if (protocol != MediaProtocol.File) { @@ -19,15 +19,16 @@ namespace MediaBrowser.MediaEncoding.Encoder return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", url); } - return GetConcatInputArgument(inputFiles); + return GetConcatInputArgument(inputFiles, inputPrefix); } /// /// Gets the concat input argument. /// /// The input files. + /// The input prefix. /// System.String. - private static string GetConcatInputArgument(IReadOnlyList inputFiles) + private static string GetConcatInputArgument(IReadOnlyList inputFiles, string inputPrefix) { // Get all streams // If there's more than one we'll need to use the concat command @@ -39,15 +40,16 @@ namespace MediaBrowser.MediaEncoding.Encoder } // Determine the input path for video files - return GetFileInputArgument(inputFiles[0]); + return GetFileInputArgument(inputFiles[0], inputPrefix); } /// /// Gets the file input argument. /// /// The path. + /// The path prefix. /// System.String. - private static string GetFileInputArgument(string path) + private static string GetFileInputArgument(string path, string inputPrefix) { if (path.IndexOf("://", StringComparison.Ordinal) != -1) { @@ -57,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // Quotes are valid path characters in linux and they need to be escaped here with a leading \ path = NormalizePath(path); - return string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", path); + return string.Format(CultureInfo.InvariantCulture, "{1}:\"{0}\"", path, inputPrefix); } /// diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 5a3a9185d7..edf83c8161 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -18,6 +18,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; @@ -319,7 +320,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - var inputFiles = MediaEncoderHelpers.GetInputArgument(_fileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); + var inputFiles = MediaEncoderHelpers.GetInputArgument(_fileSystem, request.MediaSource.Path, request.PlayableStreamFileNames); var probeSize = EncodingHelper.GetProbeSizeArgument(inputFiles.Length); string analyzeDuration; @@ -340,7 +341,7 @@ namespace MediaBrowser.MediaEncoding.Encoder var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File; return GetMediaInfoInternal( - GetInputArgument(inputFiles, request.MediaSource.Protocol), + GetInputArgument(inputFiles, request.MediaSource), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters, @@ -355,11 +356,19 @@ namespace MediaBrowser.MediaEncoding.Encoder /// Gets the input argument. /// /// The input files. - /// The protocol. + /// The mediaSource. /// System.String. /// Unrecognized InputType. - public string GetInputArgument(IReadOnlyList inputFiles, MediaProtocol protocol) - => EncodingUtils.GetInputArgument(inputFiles, protocol); + public string GetInputArgument(IReadOnlyList inputFiles, MediaSourceInfo mediaSource) + { + var prefix = "file"; + if (mediaSource.VideoType == VideoType.BluRay || mediaSource.VideoType == VideoType.Iso) + { + prefix = "bluray"; + } + + return EncodingUtils.GetInputArgument(prefix, inputFiles, mediaSource.Protocol); + } /// /// Gets the media info internal. @@ -457,17 +466,22 @@ namespace MediaBrowser.MediaEncoding.Encoder public Task ExtractAudioImage(string path, int? imageStreamIndex, CancellationToken cancellationToken) { - return ExtractImage(new[] { path }, null, null, imageStreamIndex, MediaProtocol.File, true, null, null, cancellationToken); + var mediaSource = new MediaSourceInfo + { + Protocol = MediaProtocol.File + }; + + return ExtractImage(new[] { path }, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken); } - public Task ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) + public Task ExtractVideoImage(string[] inputFiles, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - return ExtractImage(inputFiles, container, videoStream, null, protocol, false, threedFormat, offset, cancellationToken); + return ExtractImage(inputFiles, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken); } - public Task ExtractVideoImage(string[] inputFiles, string container, MediaProtocol protocol, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken) + public Task ExtractVideoImage(string[] inputFiles, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken) { - return ExtractImage(inputFiles, container, imageStream, imageStreamIndex, protocol, false, null, null, cancellationToken); + return ExtractImage(inputFiles, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken); } private async Task ExtractImage( @@ -475,13 +489,13 @@ namespace MediaBrowser.MediaEncoding.Encoder string container, MediaStream videoStream, int? imageStreamIndex, - MediaProtocol protocol, + MediaSourceInfo mediaSource, bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - var inputArgument = GetInputArgument(inputFiles, protocol); + var inputArgument = GetInputArgument(inputFiles, mediaSource); if (isAudio) { @@ -671,7 +685,7 @@ namespace MediaBrowser.MediaEncoding.Encoder string[] inputFiles, string container, MediaStream videoStream, - MediaProtocol protocol, + MediaSourceInfo mediaSource, Video3DFormat? threedFormat, TimeSpan interval, string targetDirectory, @@ -679,7 +693,7 @@ namespace MediaBrowser.MediaEncoding.Encoder int? maxWidth, CancellationToken cancellationToken) { - var inputArgument = GetInputArgument(inputFiles, protocol); + var inputArgument = GetInputArgument(inputFiles, mediaSource); var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 0a9958b9eb..4fa145d962 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -187,7 +187,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path); } - var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, protocol, subtitleStream, cancellationToken).ConfigureAwait(false); + var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false); var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); @@ -221,7 +221,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private async Task GetReadableFile( string mediaPath, string[] inputFiles, - MediaProtocol protocol, + MediaSourceInfo mediaSource, MediaStream subtitleStream, CancellationToken cancellationToken) { @@ -252,9 +252,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles } // Extract - var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, "." + outputFormat); + var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, "." + outputFormat); - await ExtractTextSubtitle(inputFiles, protocol, subtitleStream.Index, outputCodec, outputPath, cancellationToken) + await ExtractTextSubtitle(inputFiles, mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken) .ConfigureAwait(false); return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false); @@ -266,14 +266,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (GetReader(currentFormat, false) == null) { // Convert - var outputPath = GetSubtitleCachePath(mediaPath, protocol, subtitleStream.Index, ".srt"); + var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, ".srt"); - await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, protocol, outputPath, cancellationToken).ConfigureAwait(false); + await ConvertTextSubtitleToSrt(subtitleStream.Path, subtitleStream.Language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); return new SubtitleInfo(outputPath, MediaProtocol.File, "srt", true); } - return new SubtitleInfo(subtitleStream.Path, protocol, currentFormat, true); + return new SubtitleInfo(subtitleStream.Path, mediaSource.Protocol, currentFormat, true); } private ISubtitleParser GetReader(string format, bool throwIfMissing) @@ -363,11 +363,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// /// The input path. /// The language. - /// The input protocol. + /// The input mediaSource. /// The output path. /// The cancellation token. /// Task. - private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken) + private async Task ConvertTextSubtitleToSrt(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) { var semaphore = GetLock(outputPath); @@ -377,7 +377,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!File.Exists(outputPath)) { - await ConvertTextSubtitleToSrtInternal(inputPath, language, inputProtocol, outputPath, cancellationToken).ConfigureAwait(false); + await ConvertTextSubtitleToSrtInternal(inputPath, language, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); } } finally @@ -391,14 +391,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// /// The input path. /// The language. - /// The input protocol. + /// The input mediaSource. /// The output path. /// The cancellation token. /// Task. /// /// The inputPath or outputPath is null. /// - private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaProtocol inputProtocol, string outputPath, CancellationToken cancellationToken) + private async Task ConvertTextSubtitleToSrtInternal(string inputPath, string language, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(inputPath)) { @@ -412,7 +412,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, inputProtocol, cancellationToken).ConfigureAwait(false); + var encodingParam = await GetSubtitleFileCharacterSet(inputPath, language, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); // FFmpeg automatically convert character encoding when it is UTF-16 // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event" @@ -516,7 +516,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// Extracts the text subtitle. /// /// The input files. - /// The protocol. + /// The mediaSource. /// Index of the subtitle stream. /// The output codec. /// The output path. @@ -525,7 +525,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// Must use inputPath list overload. private async Task ExtractTextSubtitle( string[] inputFiles, - MediaProtocol protocol, + MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputCodec, string outputPath, @@ -540,7 +540,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!File.Exists(outputPath)) { await ExtractTextSubtitleInternal( - _mediaEncoder.GetInputArgument(inputFiles, protocol), + _mediaEncoder.GetInputArgument(inputFiles, mediaSource), subtitleStreamIndex, outputCodec, outputPath, @@ -706,9 +706,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - private string GetSubtitleCachePath(string mediaPath, MediaProtocol protocol, int subtitleStreamIndex, string outputSubtitleExtension) + private string GetSubtitleCachePath(string mediaPath, MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) { - if (protocol == MediaProtocol.File) + if (mediaSource.Protocol == MediaProtocol.File) { var ticksParam = string.Empty; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index c61187fdf6..648329757f 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -150,11 +150,6 @@ namespace MediaBrowser.Providers.MediaInfo public Task FetchVideoInfo(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) where T : Video { - if (item.VideoType == VideoType.Iso) - { - return _cachedTask; - } - if (item.IsPlaceHolder) { return _cachedTask; diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index fc38d3832a..9ef393ec20 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Drawing; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -50,7 +51,7 @@ namespace MediaBrowser.Providers.MediaInfo } // No support for this - if (video.VideoType == VideoType.Iso || video.VideoType == VideoType.Dvd || video.VideoType == VideoType.BluRay) + if (video.VideoType == VideoType.Dvd) { return Task.FromResult(new DynamicImageResponse { HasImage = false }); } @@ -72,7 +73,6 @@ namespace MediaBrowser.Providers.MediaInfo var inputPath = MediaEncoderHelpers.GetInputArgument( _fileSystem, item.Path, - null, item.GetPlayableStreamFileNames()); var mediaStreams = @@ -107,7 +107,14 @@ namespace MediaBrowser.Providers.MediaInfo } } - extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, protocol, imageStream, videoIndex, cancellationToken).ConfigureAwait(false); + MediaSourceInfo mediaSource = new MediaSourceInfo + { + VideoType = item.VideoType, + IsoType = item.IsoType, + Protocol = item.PathProtocol.Value, + }; + + extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, videoIndex, cancellationToken).ConfigureAwait(false); } else { @@ -119,8 +126,14 @@ namespace MediaBrowser.Providers.MediaInfo : TimeSpan.FromSeconds(10); var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + var mediaSource = new MediaSourceInfo + { + VideoType = item.VideoType, + IsoType = item.IsoType, + Protocol = item.PathProtocol.Value, + }; - extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, protocol, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false); + extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false); } return new DynamicImageResponse From cd4641dca02bae552cc7ea1942b0efbd4b791bcb Mon Sep 17 00:00:00 2001 From: Stanislav Ionascu Date: Thu, 1 Oct 2020 22:20:28 +0200 Subject: [PATCH 025/257] Remove PlayableStreamFileNames as unused Since ffmpeg can play and transcode ISO files and BDMV folders directly, there's no use of that property. --- .../MediaEncoder/EncodingManager.cs | 2 +- Jellyfin.Api/Helpers/TranscodingJobHelper.cs | 5 -- MediaBrowser.Controller/Entities/Video.cs | 45 ---------------- .../MediaEncoding/EncodingHelper.cs | 34 +------------ .../MediaEncoding/EncodingJobInfo.cs | 5 -- .../MediaEncoding/IMediaEncoder.cs | 10 ++-- .../MediaEncoding/MediaEncoderHelpers.cs | 27 ---------- .../MediaEncoding/MediaInfoRequest.cs | 11 ---- .../Attachments/AttachmentExtractor.cs | 2 +- .../Encoder/EncodingUtils.cs | 23 +++------ .../Encoder/MediaEncoder.cs | 51 ++++++++----------- .../Subtitles/SubtitleEncoder.cs | 25 +++------ .../MediaInfo/FFProbeVideoInfo.cs | 4 +- .../MediaInfo/VideoImageProvider.cs | 5 +- 14 files changed, 44 insertions(+), 205 deletions(-) diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 791d009637..f27305cbed 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -136,7 +136,7 @@ namespace Emby.Server.Implementations.MediaEncoder // Add some time for the first chapter to make sure we don't end up with a black image var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, Array.Empty()); + var inputPath = video.Path; Directory.CreateDirectory(Path.GetDirectoryName(path)); diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 465857b01e..cc9dcd10e1 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -762,11 +762,6 @@ namespace Jellyfin.Api.Helpers private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) { - if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && state.IsoType == IsoType.Dvd && _isoManager.CanMount(state.MediaPath)) - { - state.IsoMount = await _isoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); - } - if (state.MediaSource.RequiresOpening && string.IsNullOrWhiteSpace(state.Request.LiveStreamId)) { var liveStreamResponse = await _mediaSourceManager.OpenLiveStream( diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 07f3818811..6320b01b87 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -143,26 +143,6 @@ namespace MediaBrowser.Controller.Entities /// The video3 D format. public Video3DFormat? Video3DFormat { get; set; } - public string[] GetPlayableStreamFileNames() - { - var videoType = VideoType; - - if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.BluRay) - { - videoType = VideoType.BluRay; - } - else if (videoType == VideoType.Iso && IsoType == Model.Entities.IsoType.Dvd) - { - videoType = VideoType.Dvd; - } - else - { - return Array.Empty(); - } - - throw new NotImplementedException(); - } - /// /// Gets or sets the aspect ratio. /// @@ -415,31 +395,6 @@ namespace MediaBrowser.Controller.Entities return updateType; } - public static string[] QueryPlayableStreamFiles(string rootPath, VideoType videoType) - { - if (videoType == VideoType.Dvd) - { - return FileSystem.GetFiles(rootPath, new[] { ".vob" }, false, true) - .OrderByDescending(i => i.Length) - .ThenBy(i => i.FullName) - .Take(1) - .Select(i => i.FullName) - .ToArray(); - } - - if (videoType == VideoType.BluRay) - { - return FileSystem.GetFiles(rootPath, new[] { ".m2ts" }, false, true) - .OrderByDescending(i => i.Length) - .ThenBy(i => i.FullName) - .Take(1) - .Select(i => i.FullName) - .ToArray(); - } - - return Array.Empty(); - } - /// /// Gets a value indicating whether [is3 D]. /// diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 7481cc7b96..cf9bf0b5e8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2323,18 +2323,10 @@ namespace MediaBrowser.Controller.MediaEncoding } } - public string GetProbeSizeArgument(int numInputFiles) - => numInputFiles > 1 ? "-probesize " + _configuration.GetFFmpegProbeSize() : string.Empty; - - public string GetAnalyzeDurationArgument(int numInputFiles) - => numInputFiles > 1 ? "-analyzeduration " + _configuration.GetFFmpegAnalyzeDuration() : string.Empty; - public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions) { var inputModifier = string.Empty; - - var numInputFiles = state.PlayableStreamFileNames.Length > 0 ? state.PlayableStreamFileNames.Length : 1; - var probeSizeArgument = GetProbeSizeArgument(numInputFiles); + var probeSizeArgument = string.Empty; string analyzeDurationArgument; if (state.MediaSource.AnalyzeDurationMs.HasValue) @@ -2343,7 +2335,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - analyzeDurationArgument = GetAnalyzeDurationArgument(numInputFiles); + analyzeDurationArgument = string.Empty; } if (!string.IsNullOrEmpty(probeSizeArgument)) @@ -2526,28 +2518,6 @@ namespace MediaBrowser.Controller.MediaEncoding state.IsoType = mediaSource.IsoType; - if (mediaSource.VideoType.HasValue) - { - state.VideoType = mediaSource.VideoType.Value; - - if (mediaSource.VideoType.Value == VideoType.Dvd) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, mediaSource.VideoType.Value).Select(Path.GetFileName).ToArray(); - } - else if (mediaSource.VideoType.Value == VideoType.Iso && state.IsoType == IsoType.Dvd) - { - state.PlayableStreamFileNames = Video.QueryPlayableStreamFiles(state.MediaPath, VideoType.Dvd).Select(Path.GetFileName).ToArray(); - } - else - { - state.PlayableStreamFileNames = Array.Empty(); - } - } - else - { - state.PlayableStreamFileNames = Array.Empty(); - } - if (mediaSource.Timestamp.HasValue) { state.InputTimestamp = mediaSource.Timestamp.Value; diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index c7ec878d2d..c93822f0b5 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -33,10 +33,6 @@ namespace MediaBrowser.Controller.MediaEncoding public bool IsInputVideo { get; set; } - public IIsoMount IsoMount { get; set; } - - public string[] PlayableStreamFileNames { get; set; } - public string OutputAudioCodec { get; set; } public int? OutputVideoBitrate { get; set; } @@ -308,7 +304,6 @@ namespace MediaBrowser.Controller.MediaEncoding { TranscodingType = jobType; RemoteHttpHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - PlayableStreamFileNames = Array.Empty(); SupportedAudioCodecs = Array.Empty(); SupportedVideoCodecs = Array.Empty(); SupportedSubtitleCodecs = Array.Empty(); diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 5c070122d0..2560d0e31d 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -62,14 +62,14 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// Extracts the video image. /// - Task ExtractVideoImage(string[] inputFiles, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); + Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); - Task ExtractVideoImage(string[] inputFiles, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); + Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken); /// /// Extracts the video images on interval. /// - Task ExtractVideoImagesOnInterval(string[] inputFiles, + Task ExtractVideoImagesOnInterval(string inputFile, string container, MediaStream videoStream, MediaSourceInfo mediaSource, @@ -91,10 +91,10 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// Gets the input argument. /// - /// The input files. + /// The input file. /// The mediaSource. /// System.String. - string GetInputArgument(IReadOnlyList inputFiles, MediaSourceInfo mediaSource); + string GetInputArgument(string inputFile, MediaSourceInfo mediaSource); /// /// Gets the time parameter. diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index df2d2fa57b..281d503721 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -13,32 +13,5 @@ namespace MediaBrowser.Controller.MediaEncoding /// public static class MediaEncoderHelpers { - /// - /// Gets the input argument. - /// - /// The file system. - /// The video path. - /// The playable stream file names. - /// string[]. - public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, IReadOnlyCollection playableStreamFileNames) - { - if (playableStreamFileNames.Count > 0) - { - return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames); - } - - return new[] { videoPath }; - } - - private static string[] GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, IEnumerable filenames) - { - var allFiles = fileSystem - .GetFilePaths(rootPath, true) - .ToArray(); - - return filenames.Select(name => allFiles.FirstOrDefault(f => string.Equals(Path.GetFileName(f), name, StringComparison.OrdinalIgnoreCase))) - .Where(f => !string.IsNullOrEmpty(f)) - .ToArray(); - } } } diff --git a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs index 59729de49c..2cb04bdc4f 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs @@ -1,9 +1,7 @@ #pragma warning disable CS1591 -using System; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.IO; namespace MediaBrowser.Controller.MediaEncoding { @@ -14,14 +12,5 @@ namespace MediaBrowser.Controller.MediaEncoding public bool ExtractChapters { get; set; } public DlnaProfileType MediaType { get; set; } - - public IIsoMount MountedIso { get; set; } - - public string[] PlayableStreamFileNames { get; set; } - - public MediaInfoRequest() - { - PlayableStreamFileNames = Array.Empty(); - } } } diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 466fd75e15..c4a2d7779a 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -121,7 +121,7 @@ namespace MediaBrowser.MediaEncoding.Attachments if (!File.Exists(outputPath)) { await ExtractAttachmentInternal( - _mediaEncoder.GetInputArgument(new[] { inputFile }, mediaSource), + _mediaEncoder.GetInputArgument(inputFile, mediaSource), attachmentStreamIndex, outputPath, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs index b1fcba2d1e..d0ea0429b5 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncodingUtils.cs @@ -1,46 +1,35 @@ #pragma warning disable CS1591 using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Encoder { public static class EncodingUtils { - public static string GetInputArgument(string inputPrefix, IReadOnlyList inputFiles, MediaProtocol protocol) + public static string GetInputArgument(string inputPrefix, string inputFile, MediaProtocol protocol) { if (protocol != MediaProtocol.File) { - var url = inputFiles[0]; - - return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", url); + return string.Format(CultureInfo.InvariantCulture, "\"{0}\"", inputFile); } - return GetConcatInputArgument(inputFiles, inputPrefix); + return GetConcatInputArgument(inputFile, inputPrefix); } /// /// Gets the concat input argument. /// - /// The input files. + /// The input file. /// The input prefix. /// System.String. - private static string GetConcatInputArgument(IReadOnlyList inputFiles, string inputPrefix) + private static string GetConcatInputArgument(string inputFile, string inputPrefix) { // Get all streams // If there's more than one we'll need to use the concat command - if (inputFiles.Count > 1) - { - var files = string.Join("|", inputFiles.Select(NormalizePath)); - - return string.Format(CultureInfo.InvariantCulture, "concat:\"{0}\"", files); - } - // Determine the input path for video files - return GetFileInputArgument(inputFiles[0], inputPrefix); + return GetFileInputArgument(inputFile, inputPrefix); } /// diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index edf83c8161..3657f7e311 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -83,8 +83,6 @@ namespace MediaBrowser.MediaEncoding.Encoder _jsonSerializerOptions = JsonDefaults.GetOptions(); } - private EncodingHelper EncodingHelper => _encodingHelperFactory.Value; - /// public string EncoderPath => _ffmpegPath; @@ -319,33 +317,24 @@ namespace MediaBrowser.MediaEncoding.Encoder public Task GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken) { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; + var inputFile = request.MediaSource.Path; - var inputFiles = MediaEncoderHelpers.GetInputArgument(_fileSystem, request.MediaSource.Path, request.PlayableStreamFileNames); - - var probeSize = EncodingHelper.GetProbeSizeArgument(inputFiles.Length); - string analyzeDuration; + string analyzeDuration = string.Empty; if (request.MediaSource.AnalyzeDurationMs > 0) { analyzeDuration = "-analyzeduration " + (request.MediaSource.AnalyzeDurationMs * 1000).ToString(); } - else - { - analyzeDuration = EncodingHelper.GetAnalyzeDurationArgument(inputFiles.Length); - } - - probeSize = probeSize + " " + analyzeDuration; - probeSize = probeSize.Trim(); var forceEnableLogging = request.MediaSource.Protocol != MediaProtocol.File; return GetMediaInfoInternal( - GetInputArgument(inputFiles, request.MediaSource), + GetInputArgument(inputFile, request.MediaSource), request.MediaSource.Path, request.MediaSource.Protocol, extractChapters, - probeSize, + analyzeDuration, request.MediaType == DlnaProfileType.Audio, request.MediaSource.VideoType, forceEnableLogging, @@ -355,11 +344,11 @@ namespace MediaBrowser.MediaEncoding.Encoder /// /// Gets the input argument. /// - /// The input files. + /// The input file. /// The mediaSource. /// System.String. /// Unrecognized InputType. - public string GetInputArgument(IReadOnlyList inputFiles, MediaSourceInfo mediaSource) + public string GetInputArgument(string inputFile, MediaSourceInfo mediaSource) { var prefix = "file"; if (mediaSource.VideoType == VideoType.BluRay || mediaSource.VideoType == VideoType.Iso) @@ -367,7 +356,7 @@ namespace MediaBrowser.MediaEncoding.Encoder prefix = "bluray"; } - return EncodingUtils.GetInputArgument(prefix, inputFiles, mediaSource.Protocol); + return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol); } /// @@ -471,21 +460,21 @@ namespace MediaBrowser.MediaEncoding.Encoder Protocol = MediaProtocol.File }; - return ExtractImage(new[] { path }, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken); + return ExtractImage(path, null, null, imageStreamIndex, mediaSource, true, null, null, cancellationToken); } - public Task ExtractVideoImage(string[] inputFiles, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) + public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream videoStream, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken) { - return ExtractImage(inputFiles, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken); + return ExtractImage(inputFile, container, videoStream, null, mediaSource, false, threedFormat, offset, cancellationToken); } - public Task ExtractVideoImage(string[] inputFiles, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken) + public Task ExtractVideoImage(string inputFile, string container, MediaSourceInfo mediaSource, MediaStream imageStream, int? imageStreamIndex, CancellationToken cancellationToken) { - return ExtractImage(inputFiles, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken); + return ExtractImage(inputFile, container, imageStream, imageStreamIndex, mediaSource, false, null, null, cancellationToken); } private async Task ExtractImage( - string[] inputFiles, + string inputFile, string container, MediaStream videoStream, int? imageStreamIndex, @@ -495,7 +484,7 @@ namespace MediaBrowser.MediaEncoding.Encoder TimeSpan? offset, CancellationToken cancellationToken) { - var inputArgument = GetInputArgument(inputFiles, mediaSource); + var inputArgument = GetInputArgument(inputFile, mediaSource); if (isAudio) { @@ -572,8 +561,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = useIFrame ? string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}{4}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail) : string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg); - var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1); - var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1); + var probeSizeArgument = string.Empty; + var analyzeDurationArgument = string.Empty; if (!string.IsNullOrWhiteSpace(probeSizeArgument)) { @@ -682,7 +671,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } public async Task ExtractVideoImagesOnInterval( - string[] inputFiles, + string inputFile, string container, MediaStream videoStream, MediaSourceInfo mediaSource, @@ -693,7 +682,7 @@ namespace MediaBrowser.MediaEncoding.Encoder int? maxWidth, CancellationToken cancellationToken) { - var inputArgument = GetInputArgument(inputFiles, mediaSource); + var inputArgument = GetInputArgument(inputFile, mediaSource); var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(_usCulture); @@ -709,8 +698,8 @@ namespace MediaBrowser.MediaEncoding.Encoder var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf); - var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1); - var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1); + var probeSizeArgument = string.Empty; + var analyzeDurationArgument = string.Empty; if (!string.IsNullOrWhiteSpace(probeSizeArgument)) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 4fa145d962..ed6c568c57 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -168,18 +168,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles MediaStream subtitleStream, CancellationToken cancellationToken) { - string[] inputFiles; - - if (mediaSource.VideoType.HasValue - && (mediaSource.VideoType.Value == VideoType.BluRay || mediaSource.VideoType.Value == VideoType.Dvd)) - { - var mediaSourceItem = (Video)_libraryManager.GetItemById(new Guid(mediaSource.Id)); - inputFiles = mediaSourceItem.GetPlayableStreamFileNames(); - } - else - { - inputFiles = new[] { mediaSource.Path }; - } + var inputFile = mediaSource.Path; var protocol = mediaSource.Protocol; if (subtitleStream.IsExternal) @@ -187,7 +176,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path); } - var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false); + var fileInfo = await GetReadableFile(mediaSource.Path, inputFile, mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false); var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); @@ -220,7 +209,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private async Task GetReadableFile( string mediaPath, - string[] inputFiles, + string inputFile, MediaSourceInfo mediaSource, MediaStream subtitleStream, CancellationToken cancellationToken) @@ -254,7 +243,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles // Extract var outputPath = GetSubtitleCachePath(mediaPath, mediaSource, subtitleStream.Index, "." + outputFormat); - await ExtractTextSubtitle(inputFiles, mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken) + await ExtractTextSubtitle(inputFile, mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken) .ConfigureAwait(false); return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false); @@ -515,7 +504,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// /// Extracts the text subtitle. /// - /// The input files. + /// The input file. /// The mediaSource. /// Index of the subtitle stream. /// The output codec. @@ -524,7 +513,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// Task. /// Must use inputPath list overload. private async Task ExtractTextSubtitle( - string[] inputFiles, + string inputFile, MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputCodec, @@ -540,7 +529,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (!File.Exists(outputPath)) { await ExtractTextSubtitleInternal( - _mediaEncoder.GetInputArgument(inputFiles, mediaSource), + _mediaEncoder.GetInputArgument(inputFile, mediaSource), subtitleStreamIndex, outputCodec, outputPath, diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 776dee7802..6d39c091ef 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -116,7 +116,7 @@ namespace MediaBrowser.Providers.MediaInfo streamFileNames = Array.Empty(); } - mediaInfoResult = await GetMediaInfo(item, streamFileNames, cancellationToken).ConfigureAwait(false); + mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } @@ -128,7 +128,6 @@ namespace MediaBrowser.Providers.MediaInfo private Task GetMediaInfo( Video item, - string[] streamFileNames, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -145,7 +144,6 @@ namespace MediaBrowser.Providers.MediaInfo return _mediaEncoder.GetMediaInfo( new MediaInfoRequest { - PlayableStreamFileNames = streamFileNames, ExtractChapters = true, MediaType = DlnaProfileType.Video, MediaSource = new MediaSourceInfo diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index 9ef393ec20..c36c3af6a7 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -70,10 +70,7 @@ namespace MediaBrowser.Providers.MediaInfo { var protocol = item.PathProtocol ?? MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument( - _fileSystem, - item.Path, - item.GetPlayableStreamFileNames()); + var inputPath = item.Path; var mediaStreams = item.GetMediaStreams(); From 8f2fbf7a99c5f67daf2a1589fa12b1287a076bed Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Thu, 1 Oct 2020 16:24:35 -0700 Subject: [PATCH 026/257] Switch to TPL dataflow for subfolder scan --- .../ApplicationHost.cs | 1 - MediaBrowser.Controller/Entities/Folder.cs | 173 +++++++++++------- .../Library/TaskMethods.cs | 133 -------------- .../MediaBrowser.Controller.csproj | 3 +- 4 files changed, 106 insertions(+), 204 deletions(-) delete mode 100644 MediaBrowser.Controller/Library/TaskMethods.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 6d7239f724..7a46fdf2e7 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -758,7 +758,6 @@ namespace Emby.Server.Implementations BaseItem.FileSystem = _fileSystemManager; BaseItem.UserDataManager = Resolve(); BaseItem.ChannelManager = Resolve(); - TaskMethods.ConfigurationManager = ServerConfigurationManager; Video.LiveTvManager = Resolve(); Folder.UserViewManager = Resolve(); UserView.TVSeriesManager = Resolve(); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 666455cff4..8cea8755cb 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using System.Threading.Tasks.Dataflow; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Common.Progress; @@ -35,45 +36,16 @@ namespace MediaBrowser.Controller.Entities /// public class Folder : BaseItem { - /// - /// Contains constants used when reporting scan progress. - /// - private static class ProgressHelpers - { - /// - /// Reported after the folders immediate children are retrieved. - /// - public const int RetrievedChildren = 5; + private static Lazy _metadataRefreshThrottler = new Lazy(() => { + var concurrency = ConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency; - /// - /// Reported after add, updating, or deleting child items from the LibraryManager. - /// - public const int UpdatedChildItems = 10; - - /// - /// Reported once subfolders are scanned. - /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders]. - /// - public const int ScannedSubfolders = 50; - - /// - /// Reported once metadata is refreshed. - /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed]. - /// - public const int RefreshedMetadata = 100; - - /// - /// Gets the current progress given the previous step, next step, and progress in between. - /// - /// The previous progress step. - /// The next progress step. - /// The current progress step. - /// The progress. - public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress) + if (concurrency <= 0) { - return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100)); + concurrency = Environment.ProcessorCount; } - } + + return new SemaphoreSlim(concurrency); + }); public static IUserViewManager UserViewManager { get; set; } @@ -508,19 +480,17 @@ namespace MediaBrowser.Controller.Entities private Task RefreshMetadataRecursive(IList children, MetadataRefreshOptions refreshOptions, bool recursive, IProgress progress, CancellationToken cancellationToken) { - var progressableTasks = children - .Select, Task>>(child => - innerProgress => RefreshChildMetadata(child, refreshOptions, recursive && child.IsFolder, innerProgress, cancellationToken)) - .ToList(); - - return RunTasks(progressableTasks, progress, cancellationToken); + return RunTasks( + (baseItem, innerProgress) => RefreshChildMetadata(baseItem, refreshOptions, recursive && baseItem.IsFolder, innerProgress, cancellationToken), + children, + progress, + cancellationToken); } private async Task RefreshAllMetadataForContainer(IMetadataContainer container, MetadataRefreshOptions refreshOptions, IProgress progress, CancellationToken cancellationToken) { // limit the amount of concurrent metadata refreshes - await TaskMethods.RunThrottled( - TaskMethods.SharedThrottleId.RefreshMetadata, + await RunMetadataRefresh( async () => { var series = container as Series; @@ -547,8 +517,7 @@ namespace MediaBrowser.Controller.Entities if (refreshOptions.RefreshItem(child)) { // limit the amount of concurrent metadata refreshes - await TaskMethods.RunThrottled( - TaskMethods.SharedThrottleId.RefreshMetadata, + await RunMetadataRefresh( async () => await child.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); } @@ -570,38 +539,33 @@ namespace MediaBrowser.Controller.Entities /// Task. private Task ValidateSubFolders(IList children, IDirectoryService directoryService, IProgress progress, CancellationToken cancellationToken) { - var progressableTasks = children - .Select, Task>>(child => - innerProgress => child.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService)) - .ToList(); - - return RunTasks(progressableTasks, progress, cancellationToken); + return RunTasks( + (folder, innerProgress) => folder.ValidateChildrenInternal(innerProgress, cancellationToken, true, false, null, directoryService), + children, + progress, + cancellationToken); } /// - /// Runs a set of tasks concurrently with progress. + /// Runs an action block on a list of children. /// - /// A list of tasks. + /// The task to run for each child. + /// The list of children. /// The progress. /// The cancellation token. /// Task. - private async Task RunTasks(IList, Task>> tasks, IProgress progress, CancellationToken cancellationToken) + private async Task RunTasks(Func, Task> task, IList children, IProgress progress, CancellationToken cancellationToken) { - var childrenCount = tasks.Count; + var childrenCount = children.Count; var childrenProgress = new double[childrenCount]; - var actions = new Func[childrenCount]; void UpdateProgress() { progress.Report(childrenProgress.Average()); } - for (var i = 0; i < childrenCount; i++) - { - var childIndex = i; - var child = tasks[childIndex]; - - actions[childIndex] = async () => + var actionBlock = new ActionBlock( + async i => { var innerProgress = new ActionableProgress(); @@ -609,22 +573,33 @@ namespace MediaBrowser.Controller.Entities { // round the percent and only update progress if it changed to prevent excessive UpdateProgress calls var innerPercentRounded = Math.Round(innerPercent); - if (childrenProgress[childIndex] != innerPercentRounded) + if (childrenProgress[i] != innerPercentRounded) { - childrenProgress[childIndex] = innerPercentRounded; + childrenProgress[i] = innerPercentRounded; UpdateProgress(); } }); - await tasks[childIndex](innerProgress).ConfigureAwait(false); + await task(children[i], innerProgress).ConfigureAwait(false); - childrenProgress[childIndex] = 100; + childrenProgress[i] = 100; UpdateProgress(); - }; + }, + new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency, + CancellationToken = cancellationToken, + }); + + for (var i = 0; i < childrenCount; i++) + { + actionBlock.Post(i); } - await TaskMethods.WhenAllThrottled(TaskMethods.SharedThrottleId.ScanFanout, actions, cancellationToken).ConfigureAwait(false); + actionBlock.Complete(); + + await actionBlock.Completion.ConfigureAwait(false); } /// @@ -1272,6 +1247,26 @@ namespace MediaBrowser.Controller.Entities return true; } + /// + /// Runs multiple metadata refreshes concurrently. + /// + /// The action to run. + /// The cancellation token. + /// A representing the result of the asynchronous operation. + private static async Task RunMetadataRefresh(Func action, CancellationToken cancellationToken) + { + await _metadataRefreshThrottler.Value.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + await action().ConfigureAwait(false); + } + finally + { + _metadataRefreshThrottler.Value.Release(); + } + } + public List GetChildren(User user, bool includeLinkedChildren) { if (user == null) @@ -1819,5 +1814,45 @@ namespace MediaBrowser.Controller.Entities } } } + + /// + /// Contains constants used when reporting scan progress. + /// + private static class ProgressHelpers + { + /// + /// Reported after the folders immediate children are retrieved. + /// + public const int RetrievedChildren = 5; + + /// + /// Reported after add, updating, or deleting child items from the LibraryManager. + /// + public const int UpdatedChildItems = 10; + + /// + /// Reported once subfolders are scanned. + /// When scanning subfolders, the progress will be between [UpdatedItems, ScannedSubfolders]. + /// + public const int ScannedSubfolders = 50; + + /// + /// Reported once metadata is refreshed. + /// When refreshing metadata, the progress will be between [ScannedSubfolders, MetadataRefreshed]. + /// + public const int RefreshedMetadata = 100; + + /// + /// Gets the current progress given the previous step, next step, and progress in between. + /// + /// The previous progress step. + /// The next progress step. + /// The current progress step. + /// The progress. + public static double GetProgress(int previousProgressStep, int nextProgressStep, double currentProgress) + { + return previousProgressStep + ((nextProgressStep - previousProgressStep) * (currentProgress / 100)); + } + } } } diff --git a/MediaBrowser.Controller/Library/TaskMethods.cs b/MediaBrowser.Controller/Library/TaskMethods.cs deleted file mode 100644 index 66bfbe0d9e..0000000000 --- a/MediaBrowser.Controller/Library/TaskMethods.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; - -namespace MediaBrowser.Controller.Library -{ - /// - /// Helper methods for running tasks concurrently. - /// - public static class TaskMethods - { - private static readonly int _processorCount = Environment.ProcessorCount; - - private static readonly ConcurrentDictionary _sharedThrottlers = new ConcurrentDictionary(); - - /// - /// Throttle id for sharing a concurrency limit. - /// - public enum SharedThrottleId - { - /// - /// Library scan fan out - /// - ScanFanout, - - /// - /// Refresh metadata - /// - RefreshMetadata, - } - - /// - /// Gets or sets the configuration manager. - /// - public static IServerConfigurationManager ConfigurationManager { get; set; } - - /// - /// Similiar to Task.WhenAll but only allows running a certain amount of tasks at the same time. - /// - /// The throttle id. Multiple calls to this method with the same throttle id will share a concurrency limit. - /// List of actions to run. - /// The cancellation token. - /// A representing the result of the asynchronous operation. - public static async Task WhenAllThrottled(SharedThrottleId throttleId, IEnumerable> actions, CancellationToken cancellationToken) - { - var taskThrottler = throttleId == SharedThrottleId.ScanFanout ? - new SemaphoreSlim(GetConcurrencyLimit(throttleId)) : - _sharedThrottlers.GetOrAdd(throttleId, id => new SemaphoreSlim(GetConcurrencyLimit(id))); - - try - { - var tasks = new List(); - - foreach (var action in actions) - { - await taskThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); - - tasks.Add(Task.Run(async () => - { - try - { - await action().ConfigureAwait(false); - } - finally - { - taskThrottler.Release(); - } - })); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - } - finally - { - if (throttleId == SharedThrottleId.ScanFanout) - { - taskThrottler.Dispose(); - } - } - } - - /// - /// Runs a task within a given throttler. - /// - /// The throttle id. Multiple calls to this method with the same throttle id will share a concurrency limit. - /// The action to run. - /// The cancellation token. - /// A representing the result of the asynchronous operation. - public static async Task RunThrottled(SharedThrottleId throttleId, Func action, CancellationToken cancellationToken) - { - if (throttleId == SharedThrottleId.ScanFanout) - { - // just await the task instead - throw new InvalidOperationException("Invalid throttle id"); - } - - var taskThrottler = _sharedThrottlers.GetOrAdd(throttleId, id => new SemaphoreSlim(GetConcurrencyLimit(id))); - - await taskThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - await action().ConfigureAwait(false); - } - finally - { - taskThrottler.Release(); - } - } - - /// - /// Get the concurrency limit for the given throttle id. - /// - /// The throttle id. - /// The concurrency limit. - private static int GetConcurrencyLimit(SharedThrottleId throttleId) - { - var concurrency = throttleId == SharedThrottleId.RefreshMetadata ? - ConfigurationManager.Configuration.LibraryMetadataRefreshConcurrency : - ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; - - if (concurrency <= 0) - { - concurrency = _processorCount; - } - - return concurrency; - } - } -} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 6544704065..243b8cd02b 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -16,7 +16,8 @@ - + + From ac6d51554c7b358980e6ba181a21b54424d64b69 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Thu, 1 Oct 2020 16:25:01 -0700 Subject: [PATCH 027/257] Fix lastProgressMessageTimes dictionary errors --- .../EntryPoints/LibraryChangedNotifier.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index c9d21d9638..2e9d638ec7 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly List _itemsAdded = new List(); private readonly List _itemsRemoved = new List(); private readonly List _itemsUpdated = new List(); - private readonly Dictionary _lastProgressMessageTimes = new Dictionary(); + private readonly ConcurrentDictionary _lastProgressMessageTimes = new ConcurrentDictionary(); public LibraryChangedNotifier( ILibraryManager libraryManager, @@ -97,7 +98,7 @@ namespace Emby.Server.Implementations.EntryPoints } } - _lastProgressMessageTimes[item.Id] = DateTime.UtcNow; + _lastProgressMessageTimes.AddOrUpdate(item.Id, key => DateTime.UtcNow, (key, existing) => DateTime.UtcNow); var dict = new Dictionary(); dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture); @@ -139,6 +140,8 @@ namespace Emby.Server.Implementations.EntryPoints private void OnProviderRefreshCompleted(object sender, GenericEventArgs e) { OnProviderRefreshProgress(sender, new GenericEventArgs>(new Tuple(e.Argument, 100))); + + _lastProgressMessageTimes.TryRemove(e.Argument.Id, out DateTime removed); } private static bool EnableRefreshMessage(BaseItem item) From a51adec836ae967a827cdddffc5572ef621b69d3 Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Fri, 2 Oct 2020 18:26:10 -0700 Subject: [PATCH 028/257] fix parallelism when set to 0 --- MediaBrowser.Controller/Entities/Folder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 8cea8755cb..db59cdc314 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -564,6 +564,9 @@ namespace MediaBrowser.Controller.Entities progress.Report(childrenProgress.Average()); } + var fanoutConcurrency = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency; + var parallelism = fanoutConcurrency == 0 ? Environment.ProcessorCount : fanoutConcurrency; + var actionBlock = new ActionBlock( async i => { @@ -588,7 +591,7 @@ namespace MediaBrowser.Controller.Entities }, new ExecutionDataflowBlockOptions { - MaxDegreeOfParallelism = ConfigurationManager.Configuration.LibraryScanFanoutConcurrency, + MaxDegreeOfParallelism = parallelism, CancellationToken = cancellationToken, }); From 763862cbd879aceed9277d79c5e38e851403cfe6 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 3 Oct 2020 13:36:53 +0200 Subject: [PATCH 029/257] Defer image pre-fetching until the end of a refresh/scan --- .../Library/ImageFetcherPostScanTask.cs | 118 ++++++++++++++++++ .../Library/LibraryManager.cs | 45 +++++-- MediaBrowser.Controller/Entities/BaseItem.cs | 1 - MediaBrowser.Controller/Entities/Folder.cs | 5 - .../Library/ILibraryManager.cs | 2 + .../Manager/MetadataService.cs | 10 +- 6 files changed, 158 insertions(+), 23 deletions(-) create mode 100644 Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs diff --git a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs new file mode 100644 index 0000000000..94d7f3cd64 --- /dev/null +++ b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library +{ + /// + /// Test. + /// + public class ImageFetcherPostScanTask : ILibraryPostScanTask + { + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly ILogger _logger; + private readonly SemaphoreSlim _imageFetcherLock; + + private ConcurrentDictionary _queuedItems; + + /// + /// Initializes a new instance of the class. + /// + /// Some stuff. + public ImageFetcherPostScanTask( + ILibraryManager libraryManager, + IProviderManager providerManager, + ILogger logger) + { + _libraryManager = libraryManager; + _providerManager = providerManager; + _logger = logger; + _queuedItems = new ConcurrentDictionary(); + _imageFetcherLock = new SemaphoreSlim(1, 1); + _libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated; + _libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated; + _providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted; + } + + /// + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + // Sometimes a library scan will cause this to run twice if there's an item refresh going on. + await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var now = DateTime.UtcNow; + var itemGuids = _queuedItems.Keys.ToList(); + + for (var i = 0; i < itemGuids.Count; i++) + { + if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem)) + { + continue; + } + + _logger.LogDebug( + "Updating remote images for item {ItemId} with media type {ItemMediaType}", + queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture), + queuedItem.item.GetType()); + await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); + + _queuedItems.TryRemove(queuedItem.item.Id, out _); + } + + if (itemGuids.Count > 0) + { + _logger.LogInformation( + "Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.", + itemGuids.Count.ToString(CultureInfo.InvariantCulture), + (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + else + { + _logger.LogDebug("No images were updated."); + } + } + finally + { + _imageFetcherLock.Release(); + } + } + + private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs) + { + if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0) + { + _queuedItems.AddOrUpdate( + itemChangeEventArgs.Item.Id, + (itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason), + (key, existingValue) => existingValue); + } + } + + private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs e) + { + if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0) + { + _queuedItems.AddOrUpdate( + e.Argument.Id, + (e.Argument, ItemUpdateType.None), + (key, existingValue) => existingValue); + } + + // The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on + // the item that was refreshed regardless of children refreshes. So we take it as a signal + // that the refresh is entirely completed. + Run(null, CancellationToken.None).GetAwaiter().GetResult(); + } + } +} diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 00282b71a5..74788a320f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -857,7 +857,21 @@ namespace Emby.Server.Implementations.Library /// Task{Person}. public Person GetPerson(string name) { - return CreateItemByName(Person.GetPath, name, new DtoOptions(true)); + var path = Person.GetPath(name); + var id = GetItemByNameId(path); + if (!(GetItemById(id) is Person item)) + { + item = new Person + { + Name = name, + Id = id, + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + Path = path + }; + } + + return item; } /// @@ -1940,19 +1954,9 @@ namespace Emby.Server.Implementations.Library } /// - public async Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + public Task UpdateItemsAsync(IReadOnlyList items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - foreach (var item in items) - { - if (item.IsFileProtocol) - { - ProviderManager.SaveMetadata(item, updateReason); - } - - item.DateLastSaved = DateTime.UtcNow; - - await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); - } + RunMetadataSavers(items, updateReason); _itemRepository.SaveItems(items, cancellationToken); @@ -1983,12 +1987,27 @@ namespace Emby.Server.Implementations.Library } } } + + return Task.CompletedTask; } /// public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); + public void RunMetadataSavers(IReadOnlyList items, ItemUpdateType updateReason) + { + foreach (var item in items) + { + if (item.IsFileProtocol) + { + ProviderManager.SaveMetadata(item, updateReason); + } + + item.DateLastSaved = DateTime.UtcNow; + } + } + /// /// Reports the item removed. /// diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 68126bd8a8..0e99646084 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1433,7 +1433,6 @@ namespace MediaBrowser.Controller.Entities new List(); var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false); - await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh if (ownedItemsChanged) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 11542c1cad..b8737b48cd 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -352,11 +352,6 @@ namespace MediaBrowser.Controller.Entities { await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } - else - { - // metadata is up-to-date; make sure DB has correct images dimensions and hash - await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false); - } continue; } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 804170d5c9..d329495b9c 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -567,5 +567,7 @@ namespace MediaBrowser.Controller.Library void AddExternalSubtitleStreams(List streams, string videoPath, string[] files); + + void RunMetadataSavers(IReadOnlyList items, ItemUpdateType updateReason); } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index f110eafa5a..7ca51f4b5a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -231,14 +231,14 @@ namespace MediaBrowser.Providers.Manager private async Task SavePeopleMetadataAsync(List people, LibraryOptions libraryOptions, CancellationToken cancellationToken) { + var personsToSave = new List(); + foreach (var person in people) { cancellationToken.ThrowIfCancellationRequested(); if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl)) { - var updateType = ItemUpdateType.MetadataDownload; - var saveEntity = false; var personEntity = LibraryManager.GetPerson(person.Name); foreach (var id in person.ProviderIds) @@ -255,15 +255,17 @@ namespace MediaBrowser.Providers.Manager await AddPersonImageAsync(personEntity, libraryOptions, person.ImageUrl, cancellationToken).ConfigureAwait(false); saveEntity = true; - updateType |= ItemUpdateType.ImageUpdate; } if (saveEntity) { - await personEntity.UpdateToRepositoryAsync(updateType, cancellationToken).ConfigureAwait(false); + personsToSave.Add(personEntity); } } } + + LibraryManager.RunMetadataSavers(personsToSave, ItemUpdateType.MetadataDownload); + LibraryManager.CreateItems(personsToSave, null, CancellationToken.None); } private async Task AddPersonImageAsync(Person personEntity, LibraryOptions libraryOptions, string imageUrl, CancellationToken cancellationToken) From 5d3449e9dc8c43efa16669a390870c52676bcba5 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 3 Oct 2020 13:39:39 +0200 Subject: [PATCH 030/257] Fix xml doc comment --- Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs index 94d7f3cd64..49f920edae 100644 --- a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs +++ b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs @@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library { /// - /// Test. + /// A library post scan/refresh task for pre-fetching remote images. /// public class ImageFetcherPostScanTask : ILibraryPostScanTask { From 1b18f86c8b92eb0c75ee6db27eaffce918afa988 Mon Sep 17 00:00:00 2001 From: cvium Date: Sat, 3 Oct 2020 13:52:51 +0200 Subject: [PATCH 031/257] Add missing parameter comments. --- .../Library/ImageFetcherPostScanTask.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs index 49f920edae..66540b4d40 100644 --- a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs +++ b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs @@ -27,7 +27,9 @@ namespace Emby.Server.Implementations.Library /// /// Initializes a new instance of the class. /// - /// Some stuff. + /// An instance of . + /// An instance of . + /// An instance of . public ImageFetcherPostScanTask( ILibraryManager libraryManager, IProviderManager providerManager, From 53af1e34553917d00e17465f345ea530f04beddf Mon Sep 17 00:00:00 2001 From: Jim Cartlidge Date: Sun, 4 Oct 2020 09:56:33 +0100 Subject: [PATCH 032/257] Updatig netcollection & re-inserting BOM --- Emby.Dlna/Main/DlnaEntryPoint.cs | 14 +++++++------- Emby.Server.Implementations/ApplicationHost.cs | 8 ++++---- .../LiveTv/LiveTvMediaSourceProvider.cs | 3 +-- .../TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 3 ++- Emby.Server.Implementations/Udp/UdpServer.cs | 2 +- Jellyfin.Networking/Jellyfin.Networking.csproj | 2 +- RSSDP/SsdpCommunicationsServer.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 7 ++++--- 8 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 33a953563b..a644d2c8cc 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -271,33 +271,33 @@ namespace Emby.Dlna.Main bindAddresses = _networkManager.GetLoopbacks(); } - foreach (var addr in bindAddresses) + foreach (IPNetAddress address in bindAddresses) { - if (addr.AddressFamily == AddressFamily.InterNetworkV6) + if (address.AddressFamily == AddressFamily.InterNetworkV6) { // Not supporting IPv6 right now continue; } // Limit to LAN addresses only - if (!_networkManager.IsInLocalNetwork(addr)) + if (!_networkManager.IsInLocalNetwork(address)) { continue; } var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - _logger.LogInformation("Registering publisher for {0} on {1}", fullService, addr); + _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); var descriptorUri = "/dlna/" + udn + "/description.xml"; - var uri = new Uri(_appHost.GetSmartApiUrl(addr.Address) + descriptorUri); + var uri = new Uri(_appHost.GetSmartApiUrl(address.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 = addr.Address, - SubnetMask = ((IPNetAddress)addr).Mask, // MIGRATION: This fields is going. + Address = address.Address, + SubnetMask = address.Mask, FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 66e48ddc65..77584e9d0d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -262,8 +262,8 @@ namespace Emby.Server.Implementations IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); - _jsonSerializer = new JsonSerializer(); - + _jsonSerializer = new JsonSerializer(); + ServiceCollection = serviceCollection; ApplicationPaths = applicationPaths; @@ -1079,7 +1079,7 @@ namespace Emby.Server.Implementations { // No metafile, so lets see if the folder is versioned. metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1]; - + int versionIndex = dir.LastIndexOf('_'); if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver)) { @@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, LocalAddress = GetSmartApiUrl(source), - StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted + StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted }; } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 598cf0af7c..3a738fd5d2 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -76,7 +76,6 @@ namespace Emby.Server.Implementations.LiveTv } var list = sources.ToList(); - var serverUrl = _appHost.GetSmartApiUrl(string.Empty); foreach (var source in list) { @@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv // Dummy this up so that direct play checks can still run if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http) { - source.Path = serverUrl; + source.Path = _appHost.GetSmartApiUrl(string.Empty); } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index a8d2a27f78..cfc5278ece 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -16,6 +16,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; +using NetworkCollection.Udp; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -57,7 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var mediaSource = OriginalMediaSource; var uri = new Uri(mediaSource.Path); - var localPort = 50000; // Will return to random after next PR. + var localPort = UdpHelper.GetRandomUnusedUdpPort(); Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 3dc34da5cf..4fd7ac0c18 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] - : _appHost.GetSmartApiUrl(string.Empty); // MIGRATION: Temp value. + : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address); if (!string.IsNullOrEmpty(localUrl)) { diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 06d387dc87..7f01f149ec 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -28,7 +28,7 @@ - + diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index ea9e9a6fb5..8f1f0fa613 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -352,7 +352,7 @@ namespace Rssdp.Infrastructure if (_enableMultiSocketBinding) { - foreach (var address in _networkManager.GetAllBindInterfaces()) + foreach (var address in _networkManager.GetInternalBindAddresses()) { if (address.AddressFamily == AddressFamily.InterNetworkV6) { diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index c70294b38e..ca382c905a 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -301,9 +301,10 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { - 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)) + var root = device.ToRootDevice(); + var source = new IPNetAddress(root.Address, root.SubnetMask); + var destination = new IPNetAddress(remoteEndPoint.Address, root.SubnetMask); + if (!_sendOnlyMatchedHost || source.NetworkAddress.Equals(destination.NetworkAddress)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); } From 04552e86fb9467a121bb079da2cadecd1af2e7fc Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:00:51 +0100 Subject: [PATCH 033/257] Update DefaultAuthorizationHandler.cs --- .../DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index e988ca6cd1..b5913daab9 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; From a3a2ca412b126413cce80525c9df2ee130f100df Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:01:10 +0100 Subject: [PATCH 034/257] Update DownloadHandler.cs --- Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs index cea5ac4734..b61680ab1a 100644 --- a/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs +++ b/Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; From 34ef6492ee3ef4be695af7b16aaf4b319c5ffa63 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:01:32 +0100 Subject: [PATCH 035/257] Update FirstTimeOrIgnoreParentalControlSetupHandler.cs --- .../FirstTimeOrIgnoreParentalControlSetupHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs index 96cf4db4b5..31482a930f 100644 --- a/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeOrIgnoreParentalControlSetupPolicy/FirstTimeOrIgnoreParentalControlSetupHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; From 168a4ec2f2de2aba191660bb471d18a482eb9d0b Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:01:56 +0100 Subject: [PATCH 036/257] Update IgnoreParentalControlHandler.cs --- .../IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs index 3eb3ac6cdc..5213bc4cb7 100644 --- a/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs +++ b/Jellyfin.Api/Auth/IgnoreParentalControlPolicy/IgnoreParentalControlHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; From b5ea16cf7bc7d8fe881ae4b6a5734455fe76005d Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:02:13 +0100 Subject: [PATCH 037/257] Update LocalAccessOrRequiresElevationHandler.cs --- .../LocalAccessOrRequiresElevationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs index 7bede142e0..14722aa57e 100644 --- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Jellyfin.Api.Constants; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; From eaf684a68c8432b6411b5c77db34fe1d865f4aee Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:02:35 +0100 Subject: [PATCH 038/257] Update DynamicHlsHelper.cs --- Jellyfin.Api/Helpers/DynamicHlsHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 1d330a3357..af0519ffa8 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; From bb4c668650bc5e96dd8f290b4309014e8142f1ea Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:02:52 +0100 Subject: [PATCH 039/257] Update LocalAccessHandler.cs --- Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs index 2999c6c8a8..af73352bcc 100644 --- a/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs +++ b/Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; From 5360f5a43dd5f0c862aff32a48d66b37f5376eef Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:03:08 +0100 Subject: [PATCH 040/257] Update UserController.cs --- Jellyfin.Api/Controllers/UserController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 9e61b2b8ad..50bb8bb2aa 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; From 7f1ec1b569053165d05f5bf6a1f6bc4bcc8cdb67 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:03:34 +0100 Subject: [PATCH 041/257] Update MediaInfoHelper.cs --- Jellyfin.Api/Helpers/MediaInfoHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index b4b6e2d35c..1207fb5134 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; From 29133b6452533c6a1ca1e38cd34977e512ac18de Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:04:43 +0100 Subject: [PATCH 042/257] Update PathSubstitution.cs --- MediaBrowser.Model/Configuration/PathSubstitution.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs index 40eb36c2ea..a066886e2b 100644 --- a/MediaBrowser.Model/Configuration/PathSubstitution.cs +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable #pragma warning disable CS1591 #pragma warning disable CA1819 From fae70bbbcfc75c06c6681bc34044944a26d58771 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:05:38 +0100 Subject: [PATCH 043/257] Update LocalAccessHandlerTests.cs --- .../Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs index 553e6a9cab..09ffa84689 100644 --- a/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/LocalAccessPolicy/LocalAccessHandlerTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; From 2e3901252a02c79efa53f21f4695e45f6c580a59 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 4 Oct 2020 10:07:05 +0100 Subject: [PATCH 044/257] Update UserManager.cs --- Jellyfin.Server.Implementations/Users/UserManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index bd853f92f7..8f04baa089 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; From 4a81ee43dc7aed94012c312a8262a1426be9b6d9 Mon Sep 17 00:00:00 2001 From: cvium Date: Tue, 6 Oct 2020 23:36:48 +0200 Subject: [PATCH 045/257] Add try-catch to avoid crashing the whole thing --- .../Library/ImageFetcherPostScanTask.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs index 66540b4d40..b18a0c1a80 100644 --- a/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs +++ b/Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs @@ -8,6 +8,7 @@ using Jellyfin.Data.Events; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library @@ -63,11 +64,20 @@ namespace Emby.Server.Implementations.Library continue; } + var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture); + var itemType = queuedItem.item.GetType(); _logger.LogDebug( "Updating remote images for item {ItemId} with media type {ItemMediaType}", - queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture), - queuedItem.item.GetType()); - await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); + itemId, + itemType); + try + { + await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); + } + catch (HttpException ex) + { + _logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId); + } _queuedItems.TryRemove(queuedItem.item.Id, out _); } From a3f0843ac97c64a8a412d707baa9870d0e53dcff Mon Sep 17 00:00:00 2001 From: Greenback Date: Thu, 8 Oct 2020 19:00:30 +0100 Subject: [PATCH 046/257] Updated NetworkManager to PR1 --- .../Configuration/NetworkConfiguration.cs | 223 ++++++++++++++++++ .../NetworkConfigurationExtensions.cs | 21 ++ .../NetworkConfigurationFactory.cs | 27 +++ Jellyfin.Networking/Manager/NetworkManager.cs | 131 +++++----- MediaBrowser.Common/Net/INetworkManager.cs | 21 +- 5 files changed, 338 insertions(+), 85 deletions(-) create mode 100644 Jellyfin.Networking/Configuration/NetworkConfiguration.cs create mode 100644 Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs create mode 100644 Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs new file mode 100644 index 0000000000..38f11153ab --- /dev/null +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -0,0 +1,223 @@ +#pragma warning disable CA1819 // Properties should not return arrays + +using System; +using MediaBrowser.Model.Configuration; + +namespace Jellyfin.Networking.Configuration +{ + /// + /// Defines the . + /// + public class NetworkConfiguration + { + private string _baseUrl = string.Empty; + + /// + /// Gets the default http port. + /// + public const int DefaultHttpPort = 8096; + + /// + /// Gets the default https port. + /// + public const int DefaultHttpsPort = 8920; + + /// + /// Gets or sets a value indicating whether the server should force connections over HTTPS. + /// + public bool RequireHttps { get; set; } = false; + + /// + /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. + /// + public string BaseUrl + { + get => _baseUrl; + set + { + // Normalize the start of the string + if (string.IsNullOrWhiteSpace(value)) + { + // If baseUrl is empty, set an empty prefix string + _baseUrl = string.Empty; + return; + } + + if (value[0] != '/') + { + // If baseUrl was not configured with a leading slash, append one for consistency + value = "/" + value; + } + + // Normalize the end of the string + if (value[value.Length - 1] == '/') + { + // If baseUrl was configured with a trailing slash, remove it for consistency + value = value.Remove(value.Length - 1); + } + + _baseUrl = value; + } + } + + /// + /// Gets or sets the public HTTPS port. + /// + /// The public HTTPS port. + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; + + /// + /// Gets or sets the HTTP server port number. + /// + /// The HTTP server port number. + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets the HTTPS server port number. + /// + /// The HTTPS server port number. + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; + + /// + /// Gets or sets a value indicating whether to use HTTPS. + /// + /// + /// 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; } = false; + + /// + /// Gets or sets the public mapped port. + /// + /// The public mapped port. + 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 the UDPPortRange + /// 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. + /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work.. + /// + public bool EnableSSDPTracing { get; set; } = false; + + /// + /// Gets or sets the SSDPTracingFilter + /// 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 a value indicating whether address names that match should be Ignore for the purposes of binding.. + /// + public bool IgnoreVirtualInterfaces { get; set; } = true; + + /// + /// Gets or sets the VirtualInterfaceNames + /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. .. + /// + public string VirtualInterfaceNames { get; set; } = "vEthernet*"; + + /// + /// 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 multi-socket binding is 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 the PublishedServerUriBySubnet + /// Gets or sets PublishedServerUri to advertise for specific subnets.. + /// + public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether Autodiscovery tracing is enabled.. + /// + 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 filter for remote IP connectivity. Used in conjuntion with .. + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); + + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist.. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } = false; + + /// + /// Gets or sets a value indicating whether to enable automatic port forwarding.. + /// + public bool EnableUPnP { get; set; } = false; + + /// + /// Gets or sets a value indicating whether access outside of the LAN is permitted.. + /// + public bool EnableRemoteAccess { get; set; } = true; + + /// + /// Gets or sets the subnets that are deemed to make up the LAN.. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); + + /// + /// 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(); + + /// + /// Gets or sets the known proxies. + /// + public string[] KnownProxies { get; set; } = Array.Empty(); + } +} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs new file mode 100644 index 0000000000..e77b17ba92 --- /dev/null +++ b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs @@ -0,0 +1,21 @@ +using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Networking.Configuration +{ + /// + /// Defines the . + /// + public static class NetworkConfigurationExtensions + { + /// + /// Retrieves the network configuration. + /// + /// The . + /// The . + public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config) + { + return config.GetConfiguration("network"); + } + } +} diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs new file mode 100644 index 0000000000..ac0485d871 --- /dev/null +++ b/Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; + +namespace Jellyfin.Networking.Configuration +{ + /// + /// Defines the . + /// + public class NetworkConfigurationFactory : IConfigurationFactory + { + /// + /// The GetConfigurations. + /// + /// The . + public IEnumerable GetConfigurations() + { + return new[] + { + new ConfigurationStore + { + Key = "network", + ConfigurationType = typeof(NetworkConfiguration) + } + }; + } + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 6f2970ef89..491fe824da 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -6,9 +6,9 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; -using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using NetworkCollection; @@ -24,7 +24,7 @@ namespace Jellyfin.Networking.Manager /// /// Contains the description of the interface along with its index. /// - private readonly SortedList _interfaceNames; + private readonly Dictionary _interfaceNames; /// /// Threading lock for network interfaces. @@ -96,7 +96,7 @@ namespace Jellyfin.Networking.Manager /// /// 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. +#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this. public NetworkManager(IConfigurationManager configurationManager, ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -104,15 +104,9 @@ namespace Jellyfin.Networking.Manager _interfaceAddresses = new NetCollection(unique: false); _macAddresses = new List(); - _interfaceNames = new SortedList(); + _interfaceNames = new Dictionary(); _publishedServerUrls = 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; @@ -182,7 +176,7 @@ namespace Jellyfin.Networking.Manager lock (_intLock) { - return _internalInterfaces.Where(i => i.Address.Equals(address) && (i.Tag < 0)).Any(); + return _internalInterfaces.Where(i => i.Address.Equals(address) && i.Tag < 0).Any(); } } @@ -212,49 +206,46 @@ namespace Jellyfin.Networking.Manager /// public bool IsExcluded(EndPoint ip) { - if (ip != null) - { - return _excludedSubnets.Contains(((IPEndPoint)ip).Address); - } - - return false; + return ip != null && IsExcluded(((IPEndPoint)ip).Address); } /// public NetCollection CreateIPCollection(string[] values, bool bracketed = false) { NetCollection col = new NetCollection(); - if (values != null) + if (values == null) { - for (int a = 0; a < values.Length; a++) - { - string v = values[a].Trim(); + return col; + } - try + for (int a = 0; a < values.Length; a++) + { + string v = values[a].Trim(); + + try + { + if (v.StartsWith("[", StringComparison.OrdinalIgnoreCase) && v.EndsWith("]", StringComparison.OrdinalIgnoreCase)) { - if (v.StartsWith("[", StringComparison.OrdinalIgnoreCase) && v.EndsWith("]", StringComparison.OrdinalIgnoreCase)) + if (bracketed) { - 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); + AddToCollection(col, v.Remove(v.Length - 1).Substring(1)); } } - catch (ArgumentException e) + else if (v.StartsWith("!", StringComparison.OrdinalIgnoreCase)) { - _logger.LogInformation("Ignoring LAN value {value}. Reason : {reason}", v, e.Message); + 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); } } @@ -305,12 +296,9 @@ namespace Jellyfin.Networking.Manager /// public string GetBindInterface(string source, out int? port) { - if (!string.IsNullOrEmpty(source)) + if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host)) { - if (IPHost.TryParse(source, out IPHost host)) - { - return GetBindInterface(host, out port); - } + return GetBindInterface(host, out port); } return GetBindInterface(IPHost.None, out port); @@ -345,7 +333,6 @@ namespace Jellyfin.Networking.Manager public string GetBindInterface(IPObject source, out int? port) { port = null; - bool isChromeCast = source == IPNetAddress.IP4Loopback; // Do we have a source? bool haveSource = !source.Address.Equals(IPAddress.None); bool isExternal = false; @@ -354,7 +341,7 @@ namespace Jellyfin.Networking.Manager { 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."); + _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } if (!IsIP4Enabled && source.AddressFamily == AddressFamily.InterNetwork) @@ -364,7 +351,7 @@ namespace Jellyfin.Networking.Manager isExternal = !IsInLocalNetwork(source); - if (MatchesPublishedServerUrl(source, isExternal, isChromeCast, out string result, out port)) + if (MatchesPublishedServerUrl(source, isExternal, out string result, out port)) { _logger.LogInformation("{0}: Using BindAddress {1}:{2}", source, result, port); return result; @@ -529,12 +516,7 @@ namespace Jellyfin.Networking.Manager { lock (_intLock) { - if (_bindExclusions.Count > 0) - { - return _bindExclusions.Contains(address); - } - - return false; + return _bindExclusions.Contains(address); } } @@ -596,16 +578,20 @@ namespace Jellyfin.Networking.Manager /// /// Reloads all settings and re-initialises the instance. /// - /// to use. - public void UpdateSettings(ServerConfiguration config) + /// The configuration to use. + public void UpdateSettings(object configuration) { - if (config == null) - { - throw new ArgumentNullException(nameof(config)); - } + NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); IsIP4Enabled = Socket.OSSupportsIPv6 && config.EnableIPV4; IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; + + if (!IsIP6Enabled && !IsIP4Enabled) + { + _logger.LogError("IPv4 and IPv6 cannot both be disabled."); + IsIP4Enabled = true; + } + TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; @@ -655,7 +641,7 @@ namespace Jellyfin.Networking.Manager private void ConfigurationUpdated(object? sender, EventArgs args) { - UpdateSettings((ServerConfiguration)_configurationManager.CommonConfiguration); + UpdateSettings(_configurationManager.GetNetworkConfiguration()); } /// @@ -804,7 +790,7 @@ namespace Jellyfin.Networking.Manager await Task.Delay(2000).ConfigureAwait(false); InitialiseInterfaces(); // Recalculate LAN caches. - InitialiseLAN((ServerConfiguration)_configurationManager.CommonConfiguration); + InitialiseLAN(_configurationManager.GetNetworkConfiguration()); NetworkChanged?.Invoke(this, EventArgs.Empty); } @@ -835,7 +821,7 @@ namespace Jellyfin.Networking.Manager /// format is subnet=ipaddress|host|uri /// when subnet = 0.0.0.0, any external address matches. /// - private void InitialiseOverrides(ServerConfiguration config) + private void InitialiseOverrides(NetworkConfiguration config) { lock (_intLock) { @@ -884,7 +870,7 @@ namespace Jellyfin.Networking.Manager } } - private void InitialiseBind(ServerConfiguration config) + private void InitialiseBind(NetworkConfiguration config) { string[] ba = config.LocalNetworkAddresses; @@ -912,7 +898,7 @@ namespace Jellyfin.Networking.Manager _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); } - private void InitialiseRemote(ServerConfiguration config) + private void InitialiseRemote(NetworkConfiguration config) { RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); } @@ -920,7 +906,7 @@ namespace Jellyfin.Networking.Manager /// /// Initialises internal LAN cache settings. /// - private void InitialiseLAN(ServerConfiguration config) + private void InitialiseLAN(NetworkConfiguration config) { lock (_intLock) { @@ -1029,8 +1015,7 @@ namespace Jellyfin.Networking.Manager }; 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()) + if ((ipProperties.GatewayAddresses.Count > 0) && !nw.IsLoopback()) { // -ve Tags signify the interface has a gateway. nw.Tag *= -1; @@ -1051,8 +1036,7 @@ namespace Jellyfin.Networking.Manager }; 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()) + if ((ipProperties.GatewayAddresses.Count > 0) && !nw.IsLoopback()) { // -ve Tags signify the interface has a gateway. nw.Tag *= -1; @@ -1112,11 +1096,10 @@ namespace Jellyfin.Networking.Manager /// /// 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) + private bool MatchesPublishedServerUrl(IPObject source, bool isExternal, out string bindPreference, out int? port) { bindPreference = string.Empty; port = null; @@ -1130,7 +1113,7 @@ namespace Jellyfin.Networking.Manager bindPreference = addr.Value; break; } - else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && (isExternal || isChromeCast)) + else if ((addr.Key.Equals(IPAddress.Any) || addr.Key.Equals(IPAddress.IPv6Any)) && isExternal) { // External. bindPreference = addr.Value; @@ -1241,7 +1224,7 @@ namespace Jellyfin.Networking.Manager } /// - /// Attempts to match the source against am external interface. + /// Attempts to match the source against an external interface. /// /// IP source address to use. /// The result, if a match is found. diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 8789cd9d81..f60f369d6a 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation; -using MediaBrowser.Model.Configuration; using Microsoft.AspNetCore.Http; using NetworkCollection; @@ -27,7 +26,7 @@ namespace MediaBrowser.Common.Net /// /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. /// - public bool TrustAllIP6Interfaces { get; } + bool TrustAllIP6Interfaces { get; } /// /// Gets the remote address filter. @@ -37,12 +36,12 @@ namespace MediaBrowser.Common.Net /// /// Gets or sets a value indicating whether iP6 is enabled. /// - public bool IsIP6Enabled { get; set; } + bool IsIP6Enabled { get; set; } /// /// Gets or sets a value indicating whether iP4 is enabled. /// - public bool IsIP4Enabled { get; set; } + bool IsIP4Enabled { get; set; } /// /// Calculates the list of interfaces to use for Kestrel. @@ -57,7 +56,7 @@ namespace MediaBrowser.Common.Net /// Returns a collection containing the loopback interfaces. /// /// Netcollection. - public NetCollection GetLoopbacks(); + NetCollection GetLoopbacks(); /// /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) @@ -93,7 +92,7 @@ namespace MediaBrowser.Common.Net /// /// 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). + /// (See . /// /// Source of the request. /// Optional port returned, if it's part of an override. @@ -103,7 +102,7 @@ namespace MediaBrowser.Common.Net /// /// 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). + /// (See . /// /// IP address of the request. /// Optional port returned, if it's part of an override. @@ -113,7 +112,7 @@ namespace MediaBrowser.Common.Net /// /// 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). + /// (See . /// /// Source of the request. /// Optional port returned, if it's part of an override. @@ -138,7 +137,7 @@ namespace MediaBrowser.Common.Net /// /// IP to check. Can be an IPAddress or an IPObject. /// Result of the check. - public bool IsGatewayInterface(object? addressObj); + bool IsGatewayInterface(object? addressObj); /// /// Returns true if the address is a private address. @@ -226,7 +225,7 @@ namespace MediaBrowser.Common.Net /// /// Reloads all settings and re-initialises the instance. /// - /// to use. - public void UpdateSettings(ServerConfiguration config); + /// The configuration to use. + void UpdateSettings(object configuration); } } From deb4d27857089e8f3a3602399c5b52ad8df170f2 Mon Sep 17 00:00:00 2001 From: Greenback Date: Thu, 8 Oct 2020 19:00:55 +0100 Subject: [PATCH 047/257] Moved all settings across to network.xml --- .../AppBase/BaseConfigurationManager.cs | 7 +- .../ApplicationHost.cs | 38 +++++++--- .../EntryPoints/ExternalPortForwarding.cs | 11 +-- .../Controllers/ConfigurationController.cs | 12 ++++ Jellyfin.Api/Controllers/StartupController.cs | 8 ++- Jellyfin.Api/Helpers/ClassMigrationHelper.cs | 70 +++++++++++++++++++ .../ApiApplicationBuilderExtensions.cs | 5 +- .../BaseUrlRedirectionMiddleware.cs | 3 +- .../IpBasedAccessValidationMiddleware.cs | 5 +- .../Middleware/LanFilteringMiddleware.cs | 3 +- Jellyfin.Server/Startup.cs | 5 +- .../Configuration/IConfigurationManager.cs | 3 +- 12 files changed, 143 insertions(+), 27 deletions(-) create mode 100644 Jellyfin.Api/Helpers/ClassMigrationHelper.cs diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 4ab0a2a3f2..fa4b3080c9 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; +using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.AppBase @@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.AppBase } /// - public object GetConfiguration(string key) + public object GetConfiguration(string key, Type objectType = null) { return _configurations.GetOrAdd(key, k => { @@ -277,12 +278,12 @@ namespace Emby.Server.Implementations.AppBase var configurationInfo = _configurationStores .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase)); - if (configurationInfo == null) + if (configurationInfo == null && objectType == null) { throw new ResourceNotFoundException("Configuration with key " + key + " not found."); } - var configurationType = configurationInfo.ConfigurationType; + var configurationType = configurationInfo?.ConfigurationType ?? objectType; lock (_configurationSyncLock) { diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 77584e9d0d..9d70c15269 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -16,6 +16,7 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Xml.Serialization; using Emby.Dlna; using Emby.Dlna.Main; using Emby.Dlna.Ssdp; @@ -48,6 +49,8 @@ using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.Api.Migrations; +using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -100,6 +103,7 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; +using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -273,6 +277,7 @@ namespace Emby.Server.Implementations ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); + NetManager.UpdateSettings(GetNetworkConfiguration()); Logger = LoggerFactory.CreateLogger(); @@ -298,6 +303,21 @@ namespace Emby.Server.Implementations ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; } + private NetworkConfiguration GetNetworkConfiguration() + { + string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml"); + if (!File.Exists(path)) + { + var networkSettings = new NetworkConfiguration(); + ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings); + _xmlSerializer.SerializeToFile(networkSettings, path); + + return networkSettings; + } + + return (NetworkConfiguration)ConfigurationManager.GetConfiguration("network", typeof(NetworkConfiguration)); + } + public string ExpandVirtualPath(string path) { var appPaths = ApplicationPaths; @@ -480,14 +500,15 @@ namespace Emby.Server.Implementations /// public void Init() { - HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; - HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; + var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); + HttpPort = networkConfiguration.HttpServerPortNumber; + HttpsPort = networkConfiguration.HttpsPortNumber; // Safeguard against invalid configuration if (HttpPort == HttpsPort) { - HttpPort = ServerConfiguration.DefaultHttpPort; - HttpsPort = ServerConfiguration.DefaultHttpsPort; + HttpPort = NetworkConfiguration.DefaultHttpPort; + HttpsPort = NetworkConfiguration.DefaultHttpsPort; } if (Plugins != null) @@ -929,9 +950,10 @@ namespace Emby.Server.Implementations // Don't do anything if these haven't been set yet if (HttpPort != 0 && HttpsPort != 0) { + var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration(); // Need to restart if ports have changed - if (ServerConfigurationManager.Configuration.HttpServerPortNumber != HttpPort || - ServerConfigurationManager.Configuration.HttpsPortNumber != HttpsPort) + if (networkConfiguration.HttpServerPortNumber != HttpPort || + networkConfiguration.HttpsPortNumber != HttpsPort) { if (ServerConfigurationManager.Configuration.IsPortAuthorized) { @@ -1253,7 +1275,7 @@ namespace Emby.Server.Implementations } /// - public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps; + public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps; /// public string GetSmartApiUrl(IPAddress ipAddress, int? port = null) @@ -1337,7 +1359,7 @@ namespace Emby.Server.Implementations Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp), Host = host, Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort), - Path = ServerConfigurationManager.Configuration.BaseUrl + Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl }.ToString().TrimEnd('/'); } diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 2e8cc76d23..14201ead29 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; +using Jellyfin.Networking.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; @@ -56,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints private string GetConfigIdentifier() { const char Separator = '|'; - var config = _config.Configuration; + var config = _config.GetNetworkConfiguration(); return new StringBuilder(32) .Append(config.EnableUPnP).Append(Separator) @@ -93,7 +94,8 @@ namespace Emby.Server.Implementations.EntryPoints private void Start() { - if (!_config.Configuration.EnableUPnP || !_config.Configuration.EnableRemoteAccess) + var config = _config.GetNetworkConfiguration(); + if (!config.EnableUPnP || !config.EnableRemoteAccess) { return; } @@ -156,11 +158,12 @@ namespace Emby.Server.Implementations.EntryPoints private IEnumerable CreatePortMaps(INatDevice device) { - yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort); + var config = _config.GetNetworkConfiguration(); + yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort); if (_appHost.ListenWithHttps) { - yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort); + yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort); } } diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index e1c9f69f61..09d72e8b10 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -4,7 +4,9 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; +using Jellyfin.Api.Migrations; using Jellyfin.Api.Models.ConfigurationDtos; +using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; @@ -49,6 +51,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetConfiguration() { + // TODO: Temp workaround until the web can be changed. + var net = _configurationManager.GetNetworkConfiguration(); + ClassMigrationHelper.CopyProperties(net, _configurationManager.Configuration); + return _configurationManager.Configuration; } @@ -64,6 +70,12 @@ namespace Jellyfin.Api.Controllers public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration) { _configurationManager.ReplaceConfiguration(configuration); + + // TODO: Temp workaround until the web can be changed. + var network = _configurationManager.GetNetworkConfiguration(); + ClassMigrationHelper.CopyProperties(configuration, network); + _configurationManager.SaveConfiguration("Network", network); + return NoContent(); } diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index e59c6e1ddf..d9cb34557b 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.StartupDtos; +using Jellyfin.Networking.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; @@ -89,9 +90,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto) { - _config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; - _config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; - _config.SaveConfiguration(); + NetworkConfiguration settings = _config.GetNetworkConfiguration(); + settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess; + settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping; + _config.SaveConfiguration("network", settings); return NoContent(); } diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs new file mode 100644 index 0000000000..123fd012de --- /dev/null +++ b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs @@ -0,0 +1,70 @@ +using System; +using System.Reflection; + +namespace Jellyfin.Api.Migrations +{ + /// + /// A static class for reflection type functions. Temporary until web changed. + /// + public static class ClassMigrationHelper + { + /// + /// Extension for 'Object' that copies the properties to a destination object. + /// + /// The source. + /// The destination. + public static void CopyProperties(this object source, object destination) + { + // If any this null throw an exception + if (source == null || destination == null) + { + throw new Exception("Source or/and Destination Objects are null"); + } + + // Getting the Types of the objects + Type typeDest = destination.GetType(); + Type typeSrc = source.GetType(); + + // Iterate the Properties of the source instance and populate them from their desination counterparts. + PropertyInfo[] srcProps = typeSrc.GetProperties(); + foreach (PropertyInfo srcProp in srcProps) + { + if (!srcProp.CanRead) + { + continue; + } + + var targetProperty = typeDest.GetProperty(srcProp.Name); + if (targetProperty == null) + { + continue; + } + + if (!targetProperty.CanWrite) + { + continue; + } + + var obj = targetProperty.GetSetMethod(true); + if (obj != null && obj.IsPrivate) + { + continue; + } + + var target = targetProperty.GetSetMethod(); + if (target != null && (target.Attributes & MethodAttributes.Static) != 0) + { + continue; + } + + if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType)) + { + continue; + } + + // Passed all tests, lets set the value + targetProperty.SetValue(destination, srcProp.GetValue(source, null), null); + } + } + } +} diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index c7fbfa4d02..6bf6f383fc 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Jellyfin.Networking.Configuration; using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -24,8 +25,8 @@ namespace Jellyfin.Server.Extensions // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. - var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/'); - var apiDocBaseUrl = serverConfigurationManager.Configuration.BaseUrl; + var baseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl.Trim('/'); + var apiDocBaseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; if (!string.IsNullOrEmpty(baseUrl)) { baseUrl += '/'; diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs index 9316737bdf..c23da2fd63 100644 --- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -42,7 +43,7 @@ namespace Jellyfin.Server.Middleware public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) { var localPath = httpContext.Request.Path.ToString(); - var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl; + var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl; if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 0713d97d65..6f636819fc 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -1,5 +1,6 @@ using System.Net; using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -42,7 +43,7 @@ namespace Jellyfin.Server.Middleware var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; - if (serverConfigurationManager.Configuration.EnableRemoteAccess) + if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) { // 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. @@ -52,7 +53,7 @@ namespace Jellyfin.Server.Middleware { // remoteAddressFilter is a whitelist or blacklist. bool isListed = remoteAddressFilter.Contains(remoteIp); - if (!serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) + if (!serverConfigurationManager.GetNetworkConfiguration().IsRemoteIPFilterBlacklist) { // Black list, so flip over. isListed = !isListed; diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 1ef460bd76..1f4e80053d 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Net; using System.Threading.Tasks; +using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -37,7 +38,7 @@ namespace Jellyfin.Server.Middleware { var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback; - if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.Configuration.EnableRemoteAccess) + if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess) { return; } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 2f4620aa63..3484598b8c 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Net.Http.Headers; using Jellyfin.Api.TypeConverters; +using Jellyfin.Networking.Configuration; using Jellyfin.Server.Extensions; using Jellyfin.Server.Implementations; using Jellyfin.Server.Middleware; @@ -52,7 +53,7 @@ namespace Jellyfin.Server { options.HttpsPort = _serverApplicationHost.HttpsPort; }); - services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.Configuration.KnownProxies); + services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration().KnownProxies); services.AddJellyfinApiSwagger(); @@ -96,7 +97,7 @@ namespace Jellyfin.Server app.UseBaseUrlRedirection(); // Wrap rest of configuration so everything only listens on BaseUrl. - app.Map(_serverConfigurationManager.Configuration.BaseUrl, mainApp => + app.Map(_serverConfigurationManager.GetNetworkConfiguration().BaseUrl, mainApp => { if (env.IsDevelopment()) { diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index fe726090d6..7bcd4d8ed3 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -50,8 +50,9 @@ namespace MediaBrowser.Common.Configuration /// Gets the configuration. /// /// The key. + /// Optional parameter containing the key object to create, if it hasn't been registered. /// System.Object. - object GetConfiguration(string key); + object GetConfiguration(string key, Type objectType = null); /// /// Gets the type of the configuration. From 39754b840dc818f6c809c37813208e1ff06a50a9 Mon Sep 17 00:00:00 2001 From: Greenback Date: Thu, 8 Oct 2020 19:16:47 +0100 Subject: [PATCH 048/257] minor fixes --- .../Configuration/NetworkConfiguration.cs | 4 ++-- Jellyfin.Networking/Manager/NetworkManager.cs | 19 ++++++++++--------- .../Configuration/ServerConfiguration.cs | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 38f11153ab..aa75ac305d 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -105,12 +105,12 @@ namespace Jellyfin.Networking.Configuration public string UDPPortRange { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether gets or sets IPV6 capability.. + /// Gets or sets a value indicating whether IPV6 capability is enabled. /// public bool EnableIPV6 { get; set; } = false; /// - /// Gets or sets a value indicating whether gets or sets IPV4 capability.. + /// Gets or sets a value indicating whether IPV6 capability is enabled. /// public bool EnableIPV4 { get; set; } = true; diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 491fe824da..a094212d37 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -346,7 +346,7 @@ namespace Jellyfin.Networking.Manager 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."); + _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } isExternal = !IsInLocalNetwork(source); @@ -872,28 +872,29 @@ namespace Jellyfin.Networking.Manager private void InitialiseBind(NetworkConfiguration config) { - string[] ba = config.LocalNetworkAddresses; + string[] lanAddresses = 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) + if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1) { - ba = ba[0].Split(','); + lanAddresses = lanAddresses[0].Split(','); } + // TODO: end fix. // TODO: end fix. // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. if (config.IgnoreVirtualInterfaces) { - var newList = ba.ToList(); + var newList = lanAddresses.ToList(); newList.AddRange(config.VirtualInterfaceNames.Split(',').ToList()); - ba = newList.ToArray(); + lanAddresses = newList.ToArray(); } // Read and parse bind addresses and exclusions, removing ones that don't exist. - _bindAddresses = CreateIPCollection(ba).Union(_interfaceAddresses); - _bindExclusions = CreateIPCollection(ba, true).Union(_interfaceAddresses); + _bindAddresses = CreateIPCollection(lanAddresses).Union(_interfaceAddresses); + _bindExclusions = CreateIPCollection(lanAddresses, true).Union(_interfaceAddresses); _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); } @@ -923,7 +924,7 @@ namespace Jellyfin.Networking.Manager // 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. + // NOTE: The order of the commands in this statement matters, otherwise the lists won't initialise correctly. if (_usingPrivateAddresses) { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 86b7c4c3d6..a40105212b 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -99,12 +99,12 @@ namespace MediaBrowser.Model.Configuration public string UDPPortRange { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether gets or sets IPV6 capability. + /// Gets or sets a value indicating whether IPV6 capability is enabled. /// public bool EnableIPV6 { get; set; } = false; /// - /// Gets or sets a value indicating whether gets or sets IPV4 capability. + /// Gets or sets a value indicating whether IPV4 capability is enabled. /// public bool EnableIPV4 { get; set; } = true; From 8a4f81c9a7a03c6a45a8a9331f71ba06ae0ce521 Mon Sep 17 00:00:00 2001 From: Greenback Date: Fri, 9 Oct 2020 14:04:55 +0100 Subject: [PATCH 049/257] Updated networkmanager to the lastest. --- Jellyfin.Networking/Manager/NetworkManager.cs | 254 ++++++++---------- 1 file changed, 113 insertions(+), 141 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index a094212d37..26614c85e0 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -45,6 +45,8 @@ namespace Jellyfin.Networking.Manager private readonly IConfigurationManager _configurationManager; + private readonly object _eventFireLock; + /// /// Holds the bind address overrides. /// @@ -106,6 +108,7 @@ namespace Jellyfin.Networking.Manager _macAddresses = new List(); _interfaceNames = new Dictionary(); _publishedServerUrls = new Dictionary(); + _eventFireLock = new object(); NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; @@ -158,10 +161,7 @@ namespace Jellyfin.Networking.Manager public List GetMacAddresses() { // Populated in construction - so always has values. - lock (_intLock) - { - return _macAddresses.ToList(); - } + return _macAddresses.ToList(); } /// @@ -174,10 +174,7 @@ namespace Jellyfin.Networking.Manager _ => IPAddress.None }; - lock (_intLock) - { - return _internalInterfaces.Where(i => i.Address.Equals(address) && i.Tag < 0).Any(); - } + return _internalInterfaces.Any(i => i.Address.Equals(address) && i.Tag < 0); } /// @@ -224,14 +221,14 @@ namespace Jellyfin.Networking.Manager try { - if (v.StartsWith("[", StringComparison.OrdinalIgnoreCase) && v.EndsWith("]", StringComparison.OrdinalIgnoreCase)) + if (v.StartsWith('[') && v.EndsWith(']')) { if (bracketed) { - AddToCollection(col, v.Remove(v.Length - 1).Substring(1)); + AddToCollection(col, v.Substring(1, v.Length - 2)); } } - else if (v.StartsWith("!", StringComparison.OrdinalIgnoreCase)) + else if (v.StartsWith('!')) { if (bracketed) { @@ -255,42 +252,39 @@ namespace Jellyfin.Networking.Manager /// public NetCollection GetAllBindInterfaces(bool individualInterfaces = false) { - lock (_intLock) + int count = _bindAddresses.Count; + + if (count == 0) { - int count = _bindAddresses.Count; - - if (count == 0) + if (_bindExclusions.Count > 0) { - if (_bindExclusions.Count > 0) - { - // Return all the interfaces except the ones specifically excluded. - return _interfaceAddresses.Exclude(_bindExclusions); - } - - if (individualInterfaces) - { - return new NetCollection(_interfaceAddresses); - } - - // 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; + // Return all the interfaces except the ones specifically excluded. + return _interfaceAddresses.Exclude(_bindExclusions); } - // Remove any excluded bind interfaces. - return _bindAddresses.Exclude(_bindExclusions); + if (individualInterfaces) + { + return new NetCollection(_interfaceAddresses); + } + + // 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); } /// @@ -351,84 +345,78 @@ namespace Jellyfin.Networking.Manager isExternal = !IsInLocalNetwork(source); - if (MatchesPublishedServerUrl(source, isExternal, out string result, out port)) + if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) { - _logger.LogInformation("{0}: Using BindAddress {1}:{2}", source, result, port); - return result; + _logger.LogInformation("{0}: Using BindAddress {1}:{2}", source, res, port); + return res; } } - _logger.LogDebug("GetBindInterface: Souce: {0}, External: {1}:", haveSource, isExternal); + _logger.LogDebug("GetBindInterface: Source: {0}, External: {1}:", haveSource, isExternal); // No preference given, so move on to bind addresses. - lock (_intLock) + if (MatchesBindInterface(source, isExternal, out string result)) { - if (MatchesBindInterface(source, isExternal, out string result)) - { - return result; - } - - if (isExternal && MatchesExternalInterface(source, out result)) - { - return result; - } - - // Get the first LAN interface address that isn't a loopback. - var interfaces = new NetCollection(_interfaceAddresses - .Exclude(_bindExclusions) - .Where(p => IsInLocalNetwork(p)) - .OrderBy(p => p.Tag)); - - 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 interfaces) - { - if (intf.Contains(source)) - { - result = FormatIP6String(intf.Address); - _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", source, result); - return result; - } - } - } - - 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. - result = IsIP6Enabled ? "::" : "127.0.0.1"; - _logger.LogWarning("{0}: GetBindInterface: Loopback return.", source, result); return result; } + + if (isExternal && MatchesExternalInterface(source, out result)) + { + return result; + } + + // Get the first LAN interface address that isn't a loopback. + var interfaces = new NetCollection(_interfaceAddresses + .Exclude(_bindExclusions) + .Where(p => IsInLocalNetwork(p)) + .OrderBy(p => p.Tag)); + + 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 interfaces) + { + if (intf.Contains(source)) + { + result = FormatIP6String(intf.Address); + _logger.LogDebug("{0}: GetBindInterface: Has source, matched best internal interface on range. {1}", source, result); + return result; + } + } + } + + 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. + result = IsIP6Enabled ? "::" : "127.0.0.1"; + _logger.LogWarning("{0}: GetBindInterface: Loopback return.", source, result); + return result; } /// public NetCollection GetInternalBindAddresses() { - lock (_intLock) + int count = _bindAddresses.Count; + + if (count == 0) { - int count = _bindAddresses.Count; - - if (count == 0) + if (_bindExclusions.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 all the internal interfaces except the ones excluded. + return new NetCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); } - return new NetCollection(_bindAddresses); + // No bind address, so return all internal interfaces. + return new NetCollection(_internalInterfaces.Where(p => !p.IsLoopback())); } + + return new NetCollection(_bindAddresses); } /// @@ -450,11 +438,8 @@ namespace Jellyfin.Networking.Manager return true; } - lock (_intLock) - { - // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); - } + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); } /// @@ -462,10 +447,7 @@ namespace Jellyfin.Networking.Manager { if (IPHost.TryParse(address, out IPHost ep)) { - lock (_intLock) - { - return _lanSubnets.Contains(ep) && !_excludedSubnets.Contains(ep); - } + return _lanSubnets.Contains(ep) && !_excludedSubnets.Contains(ep); } return false; @@ -485,11 +467,8 @@ namespace Jellyfin.Networking.Manager return true; } - lock (_intLock) - { - // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); - } + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); } /// @@ -514,33 +493,24 @@ namespace Jellyfin.Networking.Manager /// public bool IsExcludedInterface(IPAddress address) { - lock (_intLock) - { - return _bindExclusions.Contains(address); - } + return _bindExclusions.Contains(address); } /// public NetCollection GetFilteredLANSubnets(NetCollection? filter = null) { - lock (_intLock) + if (filter == null) { - if (filter == null) - { - return NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets)); - } - - return _lanSubnets.Exclude(filter); + return NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets)); } + + return _lanSubnets.Exclude(filter); } /// public bool IsValidInterfaceAddress(IPAddress address) { - lock (_intLock) - { - return _interfaceAddresses.Contains(address); - } + return _interfaceAddresses.Contains(address); } /// @@ -583,7 +553,7 @@ namespace Jellyfin.Networking.Manager { NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); - IsIP4Enabled = Socket.OSSupportsIPv6 && config.EnableIPV4; + IsIP4Enabled = Socket.OSSupportsIPv4 && config.EnableIPV4; IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6; if (!IsIP6Enabled && !IsIP4Enabled) @@ -761,7 +731,7 @@ namespace Jellyfin.Networking.Manager /// Handler for network change events. /// /// Sender. - /// Network availablity information. + /// Network availability information. private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) { _logger.LogDebug("Network availability changed."); @@ -805,12 +775,15 @@ namespace Jellyfin.Networking.Manager /// private void OnNetworkChanged() { - if (!_eventfire) + lock (_eventFireLock) { - _logger.LogDebug("Network Address Change Event."); - // As network events tend to fire one after the other only fire once every second. - _eventfire = true; - _ = OnNetworkChangeAsync(); + 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().GetAwaiter().GetResult(); + } } } @@ -881,8 +854,7 @@ namespace Jellyfin.Networking.Manager lanAddresses = lanAddresses[0].Split(','); } - // TODO: end fix. - // TODO: end fix. + // TODO: end fix: https://github.com/jellyfin/jellyfin-web/issues/1334 // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded. if (config.IgnoreVirtualInterfaces) @@ -924,7 +896,7 @@ namespace Jellyfin.Networking.Manager // 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, otherwise the lists won't initialise correctly. + // NOTE: The order of the commands in this statement matters. if (_usingPrivateAddresses) { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); @@ -975,7 +947,7 @@ namespace Jellyfin.Networking.Manager /// /// 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. + /// Generate a list of all active mac addresses that aren't loopback addresses. /// private void InitialiseInterfaces() { From 6dc81ec8e8e25ed73d6d8ad932de57774f3a4418 Mon Sep 17 00:00:00 2001 From: Greenback Date: Sat, 10 Oct 2020 14:05:19 +0100 Subject: [PATCH 050/257] Changes to support network config --- .../AppBase/BaseConfigurationManager.cs | 35 +++++++++++++++++-- .../ApplicationHost.cs | 11 +++--- .../Controllers/ConfigurationController.cs | 10 ------ Jellyfin.Networking/Manager/NetworkManager.cs | 13 ++++--- .../Configuration/IConfigurationManager.cs | 10 +++--- 5 files changed, 51 insertions(+), 28 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index fa4b3080c9..8503a358e4 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -134,6 +134,35 @@ namespace Emby.Server.Implementations.AppBase } } + /// + /// Manually pre-loads a factory so that it is available pre system initialisation. + /// + /// Class to register. + public virtual void RegisterConfiguration() + { + if (!typeof(IConfigurationFactory).IsAssignableFrom(typeof(T))) + { + throw new ArgumentException("Parameter does not implement IConfigurationFactory"); + } + + IConfigurationFactory factory = (IConfigurationFactory)Activator.CreateInstance(typeof(T)); + + if (_configurationFactories == null) + { + _configurationFactories = new IConfigurationFactory[] { factory }; + } + else + { + var list = _configurationFactories.ToList(); + list.Add(factory); + _configurationFactories = list.ToArray(); + } + + _configurationStores = _configurationFactories + .SelectMany(i => i.GetConfigurations()) + .ToArray(); + } + /// /// Adds parts. /// @@ -269,7 +298,7 @@ namespace Emby.Server.Implementations.AppBase } /// - public object GetConfiguration(string key, Type objectType = null) + public object GetConfiguration(string key) { return _configurations.GetOrAdd(key, k => { @@ -278,12 +307,12 @@ namespace Emby.Server.Implementations.AppBase var configurationInfo = _configurationStores .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase)); - if (configurationInfo == null && objectType == null) + if (configurationInfo == null) { throw new ResourceNotFoundException("Configuration with key " + key + " not found."); } - var configurationType = configurationInfo?.ConfigurationType ?? objectType; + var configurationType = configurationInfo.ConfigurationType; lock (_configurationSyncLock) { diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ecd26c0d84..768f7a9c8d 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -276,9 +276,11 @@ namespace Emby.Server.Implementations _fileSystemManager = fileSystem; ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); + MigrateNetworkConfiguration(); + // Have to pre-register the NetworkConfigurationFactory. + ConfigurationManager.RegisterConfiguration(); NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger()); - NetManager.UpdateSettings(GetNetworkConfiguration()); Logger = LoggerFactory.CreateLogger(); @@ -304,7 +306,7 @@ namespace Emby.Server.Implementations ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; } - private NetworkConfiguration GetNetworkConfiguration() + private void MigrateNetworkConfiguration() { string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml"); if (!File.Exists(path)) @@ -312,11 +314,8 @@ namespace Emby.Server.Implementations var networkSettings = new NetworkConfiguration(); ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings); _xmlSerializer.SerializeToFile(networkSettings, path); - - return networkSettings; + Logger.LogDebug("Successfully migrated network settings."); } - - return (NetworkConfiguration)ConfigurationManager.GetConfiguration("network", typeof(NetworkConfiguration)); } public string ExpandVirtualPath(string path) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 09d72e8b10..53f94cf375 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -51,10 +51,6 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetConfiguration() { - // TODO: Temp workaround until the web can be changed. - var net = _configurationManager.GetNetworkConfiguration(); - ClassMigrationHelper.CopyProperties(net, _configurationManager.Configuration); - return _configurationManager.Configuration; } @@ -70,12 +66,6 @@ namespace Jellyfin.Api.Controllers public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration) { _configurationManager.ReplaceConfiguration(configuration); - - // TODO: Temp workaround until the web can be changed. - var network = _configurationManager.GetNetworkConfiguration(); - ClassMigrationHelper.CopyProperties(configuration, network); - _configurationManager.SaveConfiguration("Network", network); - return NoContent(); } diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 26614c85e0..6d6b7ebc4e 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -110,10 +110,12 @@ namespace Jellyfin.Networking.Manager _publishedServerUrls = new Dictionary(); _eventFireLock = new object(); + UpdateSettings(_configurationManager.GetNetworkConfiguration()); + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; - _configurationManager.ConfigurationUpdated += ConfigurationUpdated; + _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated; } #pragma warning restore CS8618 // Non-nullable field is uninitialized. @@ -600,7 +602,7 @@ namespace Jellyfin.Networking.Manager { if (disposing) { - _configurationManager.ConfigurationUpdated -= ConfigurationUpdated; + _configurationManager.NamedConfigurationUpdated -= ConfigurationUpdated; NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; } @@ -609,9 +611,12 @@ namespace Jellyfin.Networking.Manager } } - private void ConfigurationUpdated(object? sender, EventArgs args) + private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) { - UpdateSettings(_configurationManager.GetNetworkConfiguration()); + if (evt.Key.Equals("network", StringComparison.Ordinal)) + { + UpdateSettings(evt.NewConfiguration); + } } /// diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index 7bcd4d8ed3..17520f8a37 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -47,12 +47,12 @@ namespace MediaBrowser.Common.Configuration void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration); /// - /// Gets the configuration. + /// Manually pre-loads a factory so that it is available pre system initialisation. /// - /// The key. - /// Optional parameter containing the key object to create, if it hasn't been registered. - /// System.Object. - object GetConfiguration(string key, Type objectType = null); + /// Class to register. + void RegisterConfiguration(); + + object GetConfiguration(string key); /// /// Gets the type of the configuration. From b34d6fec3db8b00aee11a8ff0c165048abed0ec1 Mon Sep 17 00:00:00 2001 From: Greenback Date: Sat, 10 Oct 2020 14:08:26 +0100 Subject: [PATCH 051/257] fixed tests --- Emby.Server.Implementations/ApplicationHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 768f7a9c8d..e9cac8fa47 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -314,7 +314,7 @@ namespace Emby.Server.Implementations var networkSettings = new NetworkConfiguration(); ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings); _xmlSerializer.SerializeToFile(networkSettings, path); - Logger.LogDebug("Successfully migrated network settings."); + Logger?.LogDebug("Successfully migrated network settings."); } } From 1ee1f9c8a7d6f7a32d77d14351df040d5f3349fc Mon Sep 17 00:00:00 2001 From: Greenback Date: Sat, 10 Oct 2020 15:27:02 +0100 Subject: [PATCH 052/257] Fixed web interface. --- Emby.Server.Implementations/ApplicationHost.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e9cac8fa47..559a040a81 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1111,9 +1111,9 @@ namespace Emby.Server.Implementations } else { - // Un-versioned folder - Add it under the path name and version 0.0.0.1. + // Un-versioned folder - Add it under the path name and version 0.0.0.1. versions.Add((new Version(0, 0, 0, 1), metafile, dir)); - } + } } } catch @@ -1212,6 +1212,9 @@ namespace Emby.Server.Implementations // Xbmc yield return typeof(ArtistNfoProvider).Assembly; + // Network + yield return typeof(NetworkManager).Assembly; + foreach (var i in GetAssembliesWithPartsInternal()) { yield return i; From be4e5eff9ce6a5d93088625575dcc888d955ba69 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Fri, 16 Oct 2020 14:58:37 +0100 Subject: [PATCH 053/257] Update BasePlugin.cs --- MediaBrowser.Common/Plugins/BasePlugin.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 8545fd5dcf..a6130a8e7a 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -252,20 +252,23 @@ namespace MediaBrowser.Common.Plugins } catch { - return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); + var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType)); + SaveConfiguration(config); + return config; } } /// /// Saves the current configuration to the file system. /// - public virtual void SaveConfiguration() + /// Configuration to save. + public virtual void SaveConfiguration(TConfigurationType config) { lock (_configurationSaveLock) { _directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath)); - XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath); + XmlSerializer.SerializeToFile(config, ConfigurationFilePath); } } @@ -279,7 +282,7 @@ namespace MediaBrowser.Common.Plugins Configuration = (TConfigurationType)configuration; - SaveConfiguration(); + SaveConfiguration(Configuration); } /// From face3f8c6538624ab96e03801e958df26b708a7a Mon Sep 17 00:00:00 2001 From: Greenback Date: Fri, 30 Oct 2020 13:54:37 +0000 Subject: [PATCH 054/257] Updating to latest network code. --- .../Configuration/NetworkConfiguration.cs | 28 ++++---- .../Jellyfin.Networking.csproj | 10 +-- Jellyfin.Networking/Manager/NetworkManager.cs | 67 ++++++++++++++----- 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index aa75ac305d..301e432511 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -10,8 +10,6 @@ namespace Jellyfin.Networking.Configuration /// public class NetworkConfiguration { - private string _baseUrl = string.Empty; - /// /// Gets the default http port. /// @@ -22,10 +20,12 @@ namespace Jellyfin.Networking.Configuration /// public const int DefaultHttpsPort = 8920; + private string _baseUrl = string.Empty; + /// /// Gets or sets a value indicating whether the server should force connections over HTTPS. /// - public bool RequireHttps { get; set; } = false; + public bool RequireHttps { get; set; } /// /// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at. @@ -50,7 +50,7 @@ namespace Jellyfin.Networking.Configuration } // Normalize the end of the string - if (value[value.Length - 1] == '/') + if (value[^1] == '/') { // If baseUrl was configured with a trailing slash, remove it for consistency value = value.Remove(value.Length - 1); @@ -85,7 +85,7 @@ namespace Jellyfin.Networking.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; } = false; + public bool EnableHttps { get; set; } /// /// Gets or sets the public mapped port. @@ -96,7 +96,7 @@ namespace Jellyfin.Networking.Configuration /// /// 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; + public bool UPnPCreateHttpPortMap { get; set; } /// /// Gets or sets the UDPPortRange @@ -105,12 +105,12 @@ namespace Jellyfin.Networking.Configuration public string UDPPortRange { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether IPV6 capability is enabled. + /// Gets or sets a value indicating whether gets or sets IPV6 capability.. /// - public bool EnableIPV6 { get; set; } = false; + public bool EnableIPV6 { get; set; } /// - /// Gets or sets a value indicating whether IPV6 capability is enabled. + /// Gets or sets a value indicating whether gets or sets IPV4 capability.. /// public bool EnableIPV4 { get; set; } = true; @@ -118,7 +118,7 @@ namespace Jellyfin.Networking.Configuration /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log. /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work.. /// - public bool EnableSSDPTracing { get; set; } = false; + public bool EnableSSDPTracing { get; set; } /// /// Gets or sets the SSDPTracingFilter @@ -162,7 +162,7 @@ namespace Jellyfin.Networking.Configuration /// 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; + public bool TrustAllIP6Interfaces { get; set; } /// /// Gets or sets the ports that HDHomerun uses.. @@ -178,7 +178,7 @@ namespace Jellyfin.Networking.Configuration /// /// Gets or sets a value indicating whether Autodiscovery tracing is enabled.. /// - public bool AutoDiscoveryTracing { get; set; } = false; + public bool AutoDiscoveryTracing { get; set; } /// /// Gets or sets a value indicating whether Autodiscovery is enabled.. @@ -193,12 +193,12 @@ namespace Jellyfin.Networking.Configuration /// /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist.. /// - public bool IsRemoteIPFilterBlacklist { get; set; } = false; + public bool IsRemoteIPFilterBlacklist { get; set; } /// /// Gets or sets a value indicating whether to enable automatic port forwarding.. /// - public bool EnableUPnP { get; set; } = false; + public bool EnableUPnP { get; set; } /// /// Gets or sets a value indicating whether access outside of the LAN is permitted.. diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 7f01f149ec..f9cdfc2907 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -1,7 +1,5 @@ - Jellyfin.Networking - Library netstandard2.1 false true @@ -15,7 +13,8 @@ - + + @@ -25,10 +24,7 @@ - - - - + diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 6d6b7ebc4e..616774d043 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1021 // Avoid out parameters + using System; using System.Collections.Generic; using System.Globalization; @@ -27,7 +29,7 @@ namespace Jellyfin.Networking.Manager private readonly Dictionary _interfaceNames; /// - /// Threading lock for network interfaces. + /// Threading lock for network properties. /// private readonly object _intLock = new object(); @@ -104,7 +106,7 @@ namespace Jellyfin.Networking.Manager _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager)); - _interfaceAddresses = new NetCollection(unique: false); + _interfaceAddresses = new NetCollection(); _macAddresses = new List(); _interfaceNames = new Dictionary(); _publishedServerUrls = new Dictionary(); @@ -199,7 +201,7 @@ namespace Jellyfin.Networking.Manager /// public bool IsExcluded(IPAddress ip) { - return _excludedSubnets.Contains(ip); + return _excludedSubnets.ContainsAddress(ip); } /// @@ -227,7 +229,7 @@ namespace Jellyfin.Networking.Manager { if (bracketed) { - AddToCollection(col, v.Substring(1, v.Length - 2)); + AddToCollection(col, v[1..^1]); } } else if (v.StartsWith('!')) @@ -329,6 +331,11 @@ namespace Jellyfin.Networking.Manager public string GetBindInterface(IPObject source, out int? port) { port = null; + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + // Do we have a source? bool haveSource = !source.Address.Equals(IPAddress.None); bool isExternal = false; @@ -470,7 +477,7 @@ namespace Jellyfin.Networking.Manager } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address); } /// @@ -495,7 +502,7 @@ namespace Jellyfin.Networking.Manager /// public bool IsExcludedInterface(IPAddress address) { - return _bindExclusions.Contains(address); + return _bindExclusions.ContainsAddress(address); } /// @@ -503,7 +510,7 @@ namespace Jellyfin.Networking.Manager { if (filter == null) { - return NetCollection.AsNetworks(_lanSubnets.Exclude(_excludedSubnets)); + return _lanSubnets.Exclude(_excludedSubnets).AsNetworks(); } return _lanSubnets.Exclude(filter); @@ -512,7 +519,7 @@ namespace Jellyfin.Networking.Manager /// public bool IsValidInterfaceAddress(IPAddress address) { - return _interfaceAddresses.Contains(address); + return _interfaceAddresses.ContainsAddress(address); } /// @@ -611,12 +618,32 @@ namespace Jellyfin.Networking.Manager } } - private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) + /// + /// Trys to identify the string and return an object of that class. + /// + /// String to parse. + /// IPObject to return. + /// True if the value parsed successfully. + private static bool TryParse(string addr, out IPObject result) { - if (evt.Key.Equals("network", StringComparison.Ordinal)) + if (!string.IsNullOrEmpty(addr)) { - UpdateSettings(evt.NewConfiguration); + // Is it an IP address + if (IPNetAddress.TryParse(addr, out IPNetAddress nw)) + { + result = nw; + return true; + } + + if (IPHost.TryParse(addr, out IPHost h)) + { + result = h; + return true; + } } + + result = IPNetAddress.None; + return false; } /// @@ -625,7 +652,7 @@ namespace Jellyfin.Networking.Manager /// /// Address to convert. /// URI save conversion of the address. - private string FormatIP6String(IPAddress address) + private static string FormatIP6String(IPAddress address) { var str = address.ToString(); if (address.AddressFamily == AddressFamily.InterNetworkV6) @@ -643,6 +670,14 @@ namespace Jellyfin.Networking.Manager return str; } + private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) + { + if (evt.Key.Equals("network", StringComparison.Ordinal)) + { + UpdateSettings(evt.NewConfiguration); + } + } + /// /// Checks the string to see if it matches any interface names. /// @@ -701,7 +736,7 @@ namespace Jellyfin.Networking.Manager } } } - else if (NetCollection.TryParse(token, out IPObject obj)) + else if (TryParse(token, out IPObject obj)) { if (!IsIP6Enabled) { @@ -896,7 +931,7 @@ namespace Jellyfin.Networking.Manager // Create lists from user settings. _lanSubnets = CreateIPCollection(subnets); - _excludedSubnets = NetCollection.AsNetworks(CreateIPCollection(subnets, true)); + _excludedSubnets = CreateIPCollection(subnets, true).AsNetworks(); // If no LAN addresses are specified - all private subnets are deemed to be the LAN _usingPrivateAddresses = _lanSubnets.Count == 0; @@ -946,7 +981,7 @@ namespace Jellyfin.Networking.Manager _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))); + _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks()); } } @@ -986,7 +1021,7 @@ namespace Jellyfin.Networking.Manager { if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { - IPNetAddress nw = new IPNetAddress(info.Address, info.IPv4Mask) + IPNetAddress nw = new IPNetAddress(info.Address, IPObject.MaskToCidr(info.IPv4Mask)) { // Keep the number of gateways on this interface, along with its index. Tag = ipProperties.GetIPv4Properties().Index From ec57eeff2b53081e28118fa29c435d2352cdb636 Mon Sep 17 00:00:00 2001 From: Greenback Date: Fri, 30 Oct 2020 14:06:11 +0000 Subject: [PATCH 055/257] Updated to latest version of code. --- Emby.Dlna/Main/DlnaEntryPoint.cs | 2 +- .../Middleware/IpBasedAccessValidationMiddleware.cs | 2 +- RSSDP/SsdpDevicePublisher.cs | 4 ++-- RSSDP/SsdpRootDevice.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index a644d2c8cc..504bde996b 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -297,7 +297,7 @@ namespace Emby.Dlna.Main 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.Address, - SubnetMask = address.Mask, + PrefixLength = address.PrefixLength, FriendlyName = "Jellyfin", Manufacturer = "Jellyfin", ModelName = "Jellyfin Server", diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index 6f636819fc..c3533d7956 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Server.Middleware if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp)) { // remoteAddressFilter is a whitelist or blacklist. - bool isListed = remoteAddressFilter.Contains(remoteIp); + bool isListed = remoteAddressFilter.ContainsAddress(remoteIp); if (!serverConfigurationManager.GetNetworkConfiguration().IsRemoteIPFilterBlacklist) { // Black list, so flip over. diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index ca382c905a..ae175d8c9d 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -302,8 +302,8 @@ namespace Rssdp.Infrastructure foreach (var device in deviceList) { var root = device.ToRootDevice(); - var source = new IPNetAddress(root.Address, root.SubnetMask); - var destination = new IPNetAddress(remoteEndPoint.Address, root.SubnetMask); + var source = new IPNetAddress(root.Address, root.PrefixLength); + var destination = new IPNetAddress(remoteEndPoint.Address, root.PrefixLength); if (!_sendOnlyMatchedHost || source.NetworkAddress.Equals(destination.NetworkAddress)) { SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs index 8937ec331f..d5abe4a4ca 100644 --- a/RSSDP/SsdpRootDevice.cs +++ b/RSSDP/SsdpRootDevice.cs @@ -45,9 +45,9 @@ namespace Rssdp public IPAddress Address { get; set; } /// - /// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required. + /// Gets or sets the prefix length used to check if the received message from same interface with this device/tree. Required. /// - public IPAddress SubnetMask { get; set; } + public byte PrefixLength { get; set; } /// /// The base URL to use for all relative url's provided in other propertise (and those of child devices). Optional. From c25ec72864768633417e06e6e34b1a2af7cc0df0 Mon Sep 17 00:00:00 2001 From: Greenback Date: Fri, 30 Oct 2020 14:48:33 +0000 Subject: [PATCH 056/257] Updated libraries. --- Jellyfin.Networking/Jellyfin.Networking.csproj | 2 +- MediaBrowser.Common/MediaBrowser.Common.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index f9cdfc2907..330d36a80d 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -13,7 +13,7 @@ - + diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index d992f6b092..0b8f431c04 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -20,9 +20,9 @@ - + - + From 83af636c610744903b709117f7f2a7b7e34da1f0 Mon Sep 17 00:00:00 2001 From: Greenback Date: Sat, 31 Oct 2020 18:21:46 +0000 Subject: [PATCH 057/257] Updated with new NetManager --- Emby.Dlna/Main/DlnaEntryPoint.cs | 4 +- .../HdHomerun/HdHomerunUdpStream.cs | 25 +- .../Jellyfin.Networking.csproj | 4 - Jellyfin.Networking/Manager/NetworkManager.cs | 49 +- .../IpBasedAccessValidationMiddleware.cs | 3 +- .../Middleware/LanFilteringMiddleware.cs | 1 - Jellyfin.Server/Program.cs | 4 +- .../MediaBrowser.Common.csproj | 1 - MediaBrowser.Common/Net/INetworkManager.cs | 4 +- MediaBrowser.Common/Net/IPHost.cs | 447 ++++++++++++++++++ MediaBrowser.Common/Net/IPNetAddress.cs | 277 +++++++++++ MediaBrowser.Common/Net/IPObject.cs | 395 ++++++++++++++++ MediaBrowser.Common/Net/NetworkExtensions.cs | 254 ++++++++++ MediaBrowser.sln | 7 + RSSDP/SsdpDevicePublisher.cs | 1 - .../Jellyfin.Networking.Tests.csproj | 28 ++ .../NetworkTesting/UnitTesting.cs | 425 +++++++++++++++++ 17 files changed, 1895 insertions(+), 34 deletions(-) create mode 100644 MediaBrowser.Common/Net/IPHost.cs create mode 100644 MediaBrowser.Common/Net/IPNetAddress.cs create mode 100644 MediaBrowser.Common/Net/IPObject.cs create mode 100644 MediaBrowser.Common/Net/NetworkExtensions.cs create mode 100644 tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj create mode 100644 tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 504bde996b..be618be2b3 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -9,6 +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; @@ -26,7 +27,6 @@ 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; @@ -261,7 +261,7 @@ namespace Emby.Dlna.Main { var udn = CreateUuid(_appHost.SystemId); - var bindAddresses = new NetCollection( + var bindAddresses = NetworkManager.CreateCollection( _networkManager.GetInternalBindAddresses() .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0))); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index cfc5278ece..c4176eb7ba 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; @@ -16,7 +18,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; -using NetworkCollection.Udp; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -51,6 +52,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun EnableStreamSharing = true; } + /// + /// Returns an unused UDP port number in the range specified. + /// + /// Upper and Lower boundary of ports to select. + /// System.Int32. + private static int GetUdpPortFromRange((int Min, int Max) range) + { + var properties = IPGlobalProperties.GetIPGlobalProperties(); + + // Get active udp listeners. + var udpListenerPorts = properties.GetActiveUdpListeners() + .Where(n => n.Port >= range.Min && n.Port <= range.Max) + .Select(n => n.Port); + + return Enumerable.Range(range.Min, range.Max) + .Where(i => !udpListenerPorts.Contains(i)) + .FirstOrDefault(); + } + public override async Task Open(CancellationToken openCancellationToken) { LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); @@ -58,7 +78,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var mediaSource = OriginalMediaSource; var uri = new Uri(mediaSource.Path); - var localPort = UdpHelper.GetRandomUnusedUdpPort(); + // Temporary Code to reduce PR size. + var localPort = GetUdpPortFromRange((49152, 65535)); Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 330d36a80d..1747a1dc7a 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -23,10 +23,6 @@ ../jellyfin.ruleset - - - - diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 616774d043..76ac02d791 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -13,8 +13,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using NetworkCollection; -using NetworkCollection.Udp; +using NetCollection = System.Collections.ObjectModel.Collection; namespace Jellyfin.Networking.Manager { @@ -154,6 +153,22 @@ namespace Jellyfin.Networking.Manager /// public Dictionary PublishedServerUrls => _publishedServerUrls; + /// + /// Creates a new network collection. + /// + /// Items to assign the collection, or null. + /// The collection created. + public static NetCollection CreateCollection(IEnumerable? source) + { + var result = new NetCollection(); + if (source != null) + { + return result.AddRange(source); + } + + return result; + } + /// public void Dispose() { @@ -162,10 +177,10 @@ namespace Jellyfin.Networking.Manager } /// - public List GetMacAddresses() + public IReadOnlyCollection GetMacAddresses() { // Populated in construction - so always has values. - return _macAddresses.ToList(); + return _macAddresses.AsReadOnly(); } /// @@ -187,12 +202,12 @@ namespace Jellyfin.Networking.Manager NetCollection nc = new NetCollection(); if (IsIP4Enabled) { - nc.Add(IPAddress.Loopback); + nc.AddItem(IPAddress.Loopback); } if (IsIP6Enabled) { - nc.Add(IPAddress.IPv6Loopback); + nc.AddItem(IPAddress.IPv6Loopback); } return nc; @@ -276,12 +291,12 @@ namespace Jellyfin.Networking.Manager if (IsIP4Enabled) { - result.Add(IPAddress.Any); + result.AddItem(IPAddress.Any); } if (IsIP6Enabled) { - result.Add(IPAddress.IPv6Any); + result.AddItem(IPAddress.IPv6Any); } return result; @@ -375,7 +390,7 @@ namespace Jellyfin.Networking.Manager } // Get the first LAN interface address that isn't a loopback. - var interfaces = new NetCollection(_interfaceAddresses + var interfaces = CreateCollection(_interfaceAddresses .Exclude(_bindExclusions) .Where(p => IsInLocalNetwork(p)) .OrderBy(p => p.Tag)); @@ -418,11 +433,11 @@ namespace Jellyfin.Networking.Manager if (_bindExclusions.Count > 0) { // Return all the internal interfaces except the ones excluded. - return new NetCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); + return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); } // No bind address, so return all internal interfaces. - return new NetCollection(_internalInterfaces.Where(p => !p.IsLoopback())); + return CreateCollection(_internalInterfaces.Where(p => !p.IsLoopback())); } return new NetCollection(_bindAddresses); @@ -572,7 +587,7 @@ namespace Jellyfin.Networking.Manager } TrustAllIP6Interfaces = config.TrustAllIP6Interfaces; - UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; + // UdpHelper.EnableMultiSocketBinding = config.EnableMultiSocketBinding; if (string.IsNullOrEmpty(MockNetworkSettings)) { @@ -941,7 +956,7 @@ namespace Jellyfin.Networking.Manager { _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))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i))); // Subnets are the same as the calculated internal interface. _lanSubnets = new NetCollection(); @@ -976,7 +991,7 @@ namespace Jellyfin.Networking.Manager } // 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))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i))); } _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); @@ -1082,7 +1097,7 @@ namespace Jellyfin.Networking.Manager IPHost host = new IPHost(Dns.GetHostName()); foreach (var a in host.GetAddresses()) { - _interfaceAddresses.Add(a); + _interfaceAddresses.AddItem(a); } if (_interfaceAddresses.Count == 0) @@ -1189,7 +1204,7 @@ namespace Jellyfin.Networking.Manager 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 + bindResult = CreateCollection(nc .Where(p => !IsInLocalNetwork(p)) .OrderBy(p => p.Tag)); defaultGateway = bindResult.FirstOrDefault()?.Address; @@ -1246,7 +1261,7 @@ namespace Jellyfin.Networking.Manager { result = string.Empty; // Get the first WAN interface address that isn't a loopback. - var extResult = new NetCollection(_interfaceAddresses + var extResult = CreateCollection(_interfaceAddresses .Exclude(_bindExclusions) .Where(p => !IsInLocalNetwork(p)) .OrderBy(p => p.Tag)); diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs index c3533d7956..525cd9ffe2 100644 --- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -5,7 +5,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; -using NetworkCollection; namespace Jellyfin.Server.Middleware { @@ -47,7 +46,7 @@ namespace Jellyfin.Server.Middleware { // 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; + var remoteAddressFilter = networkManager.RemoteAddressFilter; if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp)) { diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs index 1f4e80053d..8065054a1e 100644 --- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -7,7 +7,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Http; -using NetworkCollection; namespace Jellyfin.Server.Middleware { diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index fd300da7f1..61f7da16a6 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -14,6 +14,7 @@ using Emby.Server.Implementations; using Emby.Server.Implementations.IO; using Jellyfin.Api.Controllers; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -23,7 +24,6 @@ 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; @@ -271,7 +271,7 @@ namespace Jellyfin.Server return builder .UseKestrel((builderContext, options) => { - NetCollection addresses = appHost.NetManager.GetAllBindInterfaces(); + var 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 0b8f431c04..777136f8bf 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -22,7 +22,6 @@ - diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index f60f369d6a..a7beabbdcb 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation; using Microsoft.AspNetCore.Http; -using NetworkCollection; +using NetCollection = System.Collections.ObjectModel.Collection; namespace MediaBrowser.Common.Net { @@ -130,7 +130,7 @@ namespace MediaBrowser.Common.Net /// Get a list of all the MAC addresses associated with active interfaces. /// /// List of MAC addresses. - List GetMacAddresses(); + IReadOnlyCollection GetMacAddresses(); /// /// Checks to see if the IP Address provided matches an interface that has a gateway. diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs new file mode 100644 index 0000000000..80052727af --- /dev/null +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -0,0 +1,447 @@ +#nullable enable +using System; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Net +{ + /// + /// Object that holds a host name. + /// + public class IPHost : IPObject + { + /// + /// Represents an IPHost that has no value. + /// + public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None); + + /// + /// Time when last resolved. + /// + private long _lastResolved; + + /// + /// Gets the IP Addresses, attempting to resolve the name, if there are none. + /// + private IPAddress[] _addresses; + + /// + /// Initializes a new instance of the class. + /// + /// Host name to assign. + public IPHost(string name) + { + HostName = name ?? throw new ArgumentNullException(nameof(name)); + _addresses = Array.Empty(); + Resolved = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// Host name to assign. + /// Address to assign. + private IPHost(string name, IPAddress address) + { + HostName = name ?? throw new ArgumentNullException(nameof(name)); + _addresses = new IPAddress[] { address ?? throw new ArgumentNullException(nameof(address)) }; + Resolved = !address.Equals(IPAddress.None); + } + + /// + /// Gets or sets the object's first IP address. + /// + public override IPAddress Address + { + get + { + return ResolveHost() ? this[0] : IPAddress.None; + } + + set + { + // Not implemented. + } + } + + /// + /// Gets or sets the object's first IP's subnet prefix. + /// The setter does nothing, but shouldn't raise an exception. + /// + public override byte PrefixLength + { + get + { + return (byte)(ResolveHost() ? 128 : 0); + } + + set + { + // Not implemented. + } + } + + /// + /// Gets or sets timeout value before resolve required, in minutes. + /// + public byte Timeout { get; set; } = 30; + + /// + /// Gets a value indicating whether the address has a value. + /// + public bool HasAddress + { + get + { + return _addresses.Length > 0; + } + } + + /// + /// Gets the host name of this object. + /// + public string HostName { get; } + + /// + /// Gets a value indicating whether this host has attempted to be resolved. + /// + public bool Resolved { get; private set; } + + /// + /// Gets or sets the IP Addresses associated with this object. + /// + /// Index of address. + public IPAddress this[int index] + { + get + { + ResolveHost(); + return index >= 0 && index < _addresses.Length ? _addresses[index] : IPAddress.None; + } + } + + /// + /// Attempts to parse the host string. + /// + /// Host name to parse. + /// Object representing the string, if it has successfully been parsed. + /// Success result of the parsing. + public static bool TryParse(string host, out IPHost hostObj) + { + if (!string.IsNullOrEmpty(host)) + { + // See if it's an IPv6 with port address e.g. [::1]:120. + int i = host.IndexOf("]:", StringComparison.OrdinalIgnoreCase); + if (i != -1) + { + return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); + } + else + { + // See if it's an IPv6 in [] with no port. + i = host.IndexOf("]", StringComparison.OrdinalIgnoreCase); + if (i != -1) + { + return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); + } + + // Is it a host or IPv4 with port? + string[] hosts = host.Split(':'); + + if (hosts.Length > 2) + { + hostObj = new IPHost(string.Empty, IPAddress.None); + return false; + } + + // Remove port from IPv4 if it exists. + host = hosts[0]; + + if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase)) + { + hostObj = new IPHost(host, new IPAddress(Ipv4Loopback)); + return true; + } + + if (IPNetAddress.TryParse(host, out IPNetAddress netIP)) + { + // Host name is an ip address, so fake resolve. + hostObj = new IPHost(host, netIP.Address); + return true; + } + } + + // Only thing left is to see if it's a host string. + if (!string.IsNullOrEmpty(host)) + { + // Use regular expression as CheckHostName isn't RFC5892 compliant. + // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation + Regex re = new Regex(@"^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline); + if (re.Match(host).Success) + { + hostObj = new IPHost(host); + return true; + } + } + } + + hostObj = IPHost.None; + return false; + } + + /// + /// Attempts to parse the host string. + /// + /// Host name to parse. + /// Object representing the string, if it has successfully been parsed. + public static IPHost Parse(string host) + { + if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + { + return res; + } + + throw new InvalidCastException("Host does not contain a valid value. {host}"); + } + + /// + /// Attempts to parse the host string, ensuring that it resolves only to a specific IP type. + /// + /// Host name to parse. + /// Addressfamily filter. + /// Object representing the string, if it has successfully been parsed. + public static IPHost Parse(string host, AddressFamily family) + { + if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res)) + { + if (family == AddressFamily.InterNetwork) + { + res.Remove(AddressFamily.InterNetworkV6); + } + else + { + res.Remove(AddressFamily.InterNetwork); + } + + return res; + } + + throw new InvalidCastException("Host does not contain a valid value. {host}"); + } + + /// + /// Returns the Addresses that this item resolved to. + /// + /// IPAddress Array. + public IPAddress[] GetAddresses() + { + ResolveHost(); + return _addresses; + } + + /// + public override bool Contains(IPAddress address) + { + if (address != null && !Address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + foreach (var addr in GetAddresses()) + { + if (address.Equals(addr)) + { + return true; + } + } + } + + return false; + } + + /// + public override bool Equals(IPObject? other) + { + if (other is IPHost otherObj) + { + // Do we have the name Hostname? + if (string.Equals(otherObj.HostName, HostName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (!ResolveHost() || !otherObj.ResolveHost()) + { + return false; + } + + // Do any of our IP addresses match? + foreach (IPAddress addr in _addresses) + { + foreach (IPAddress otherAddress in otherObj._addresses) + { + if (addr.Equals(otherAddress)) + { + return true; + } + } + } + } + + return false; + } + + /// + public override bool IsIP6() + { + // Returns true if interfaces are only IP6. + if (ResolveHost()) + { + foreach (IPAddress i in _addresses) + { + if (i.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + } + + return true; + } + + return false; + } + + /// + public override string ToString() + { + // StringBuilder not optimum here. + string output = string.Empty; + if (_addresses.Length > 0) + { + bool moreThanOne = _addresses.Length > 1; + if (moreThanOne) + { + output = "["; + } + + foreach (var i in _addresses) + { + if (Address.Equals(IPAddress.None) && Address.AddressFamily == AddressFamily.Unspecified) + { + output += HostName + ","; + } + else if (i.Equals(IPAddress.Any)) + { + output += "Any IP4 Address,"; + } + else if (Address.Equals(IPAddress.IPv6Any)) + { + output += "Any IP6 Address,"; + } + else if (i.Equals(IPAddress.Broadcast)) + { + output += "Any Address,"; + } + else + { + output += $"{i}/32,"; + } + } + + output = output[0..^1]; + + if (moreThanOne) + { + output += "]"; + } + } + else + { + output = HostName; + } + + return output; + } + + /// + public override void Remove(AddressFamily family) + { + if (ResolveHost()) + { + _addresses = _addresses.Where(p => p.AddressFamily != family).ToArray(); + } + } + + /// + public override bool Contains(IPObject address) + { + // An IPHost cannot contain another IPObject, it can only be equal. + return Equals(address); + } + + /// + protected override IPObject CalculateNetworkAddress() + { + var netAddr = NetworkAddressOf(this[0], PrefixLength); + return new IPNetAddress(netAddr.Address, netAddr.PrefixLength); + } + + /// + /// Attempt to resolve the ip address of a host. + /// + /// The result of the comparison function. + private bool ResolveHost() + { + // When was the last time we resolved? + if (_lastResolved == 0) + { + _lastResolved = DateTime.Now.Ticks; + } + + // If we haven't resolved before, or out timer has run out... + if ((_addresses.Length == 0 && !Resolved) || (TimeSpan.FromTicks(DateTime.Now.Ticks - _lastResolved).TotalMinutes > Timeout)) + { + _lastResolved = DateTime.Now.Ticks; + ResolveHostInternal().GetAwaiter().GetResult(); + Resolved = true; + } + + return _addresses.Length > 0; + } + + /// + /// Task that looks up a Host name and returns its IP addresses. + /// + /// Array of IPAddress objects. + private async Task ResolveHostInternal() + { + if (!string.IsNullOrEmpty(HostName)) + { + // Resolves the host name - so save a DNS lookup. + if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase)) + { + _addresses = new IPAddress[] { new IPAddress(Ipv4Loopback), new IPAddress(Ipv6Loopback) }; + return; + } + + if (Uri.CheckHostName(HostName).Equals(UriHostNameType.Dns)) + { + try + { + IPHostEntry ip = await Dns.GetHostEntryAsync(HostName).ConfigureAwait(false); + _addresses = ip.AddressList; + } + catch (SocketException) + { + // Ignore socket errors, as the result value will just be an empty array. + } + } + } + } + } +} diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs new file mode 100644 index 0000000000..bcd049f3d5 --- /dev/null +++ b/MediaBrowser.Common/Net/IPNetAddress.cs @@ -0,0 +1,277 @@ +#nullable enable +using System; +using System.Net; +using System.Net.Sockets; + +namespace MediaBrowser.Common.Net +{ + /// + /// An object that holds and IP address and subnet mask. + /// + public class IPNetAddress : IPObject + { + /// + /// Represents an IPNetAddress that has no value. + /// + public static readonly IPNetAddress None = new IPNetAddress(IPAddress.None); + + /// + /// IPv4 multicast address. + /// + public static readonly IPAddress MulticastIPv4 = IPAddress.Parse("239.255.255.250"); + + /// + /// IPv6 local link multicast address. + /// + public static readonly IPAddress MulticastIPv6LinkLocal = IPAddress.Parse("ff02::C"); + + /// + /// IPv6 site local multicast address. + /// + public static readonly IPAddress MulticastIPv6SiteLocal = IPAddress.Parse("ff05::C"); + + /// + /// IP4Loopback address host. + /// + public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/32"); + + /// + /// IP6Loopback address host. + /// + public static readonly IPNetAddress IP6Loopback = IPNetAddress.Parse("::1"); + + /// + /// Object's IP address. + /// + private IPAddress _address; + + /// + /// Initializes a new instance of the class. + /// + /// Address to assign. + public IPNetAddress(IPAddress address) + { + _address = address ?? throw new ArgumentNullException(nameof(address)); + PrefixLength = (byte)(address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128); + } + + /// + /// Initializes a new instance of the class. + /// + /// IP Address. + /// Mask as a CIDR. + public IPNetAddress(IPAddress address, byte prefixLength) + { + if (address?.IsIPv4MappedToIPv6 ?? throw new ArgumentNullException(nameof(address))) + { + _address = address.MapToIPv4(); + } + else + { + _address = address; + } + + PrefixLength = prefixLength; + } + + /// + /// Gets or sets the object's IP address. + /// + public override IPAddress Address + { + get + { + return _address; + } + + set + { + _address = value ?? IPAddress.None; + } + } + + /// + public override byte PrefixLength { get; set; } + + /// + /// Try to parse the address and subnet strings into an IPNetAddress object. + /// + /// IP address to parse. Can be CIDR or X.X.X.X notation. + /// Resultant object. + /// True if the values parsed successfully. False if not, resulting in the IP being null. + public static bool TryParse(string addr, out IPNetAddress ip) + { + if (!string.IsNullOrEmpty(addr)) + { + addr = addr.Trim(); + + // Try to parse it as is. + if (IPAddress.TryParse(addr, out IPAddress res)) + { + ip = new IPNetAddress(res); + return true; + } + + // Is it a network? + string[] tokens = addr.Split("/"); + + if (tokens.Length == 2) + { + tokens[0] = tokens[0].TrimEnd(); + tokens[1] = tokens[1].TrimStart(); + + if (IPAddress.TryParse(tokens[0], out res)) + { + // Is the subnet part a cidr? + if (byte.TryParse(tokens[1], out byte cidr)) + { + ip = new IPNetAddress(res, cidr); + return true; + } + + // Is the subnet in x.y.a.b form? + if (IPAddress.TryParse(tokens[1], out IPAddress mask)) + { + ip = new IPNetAddress(res, MaskToCidr(mask)); + return true; + } + } + } + } + + ip = None; + return false; + } + + /// + /// Parses the string provided, throwing an exception if it is badly formed. + /// + /// String to parse. + /// IPNetAddress object. + public static IPNetAddress Parse(string addr) + { + if (TryParse(addr, out IPNetAddress o)) + { + return o; + } + + throw new ArgumentException("Unable to recognise object :" + addr); + } + + /// + public override bool Contains(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + var altAddress = NetworkAddressOf(address, PrefixLength); + return NetworkAddress.Address.Equals(altAddress.Address) && NetworkAddress.PrefixLength >= altAddress.PrefixLength; + } + + /// + public override bool Contains(IPObject address) + { + if (address is IPHost addressObj && addressObj.HasAddress) + { + foreach (IPAddress addr in addressObj.GetAddresses()) + { + if (Contains(addr)) + { + return true; + } + } + } + else if (address is IPNetAddress netaddrObj) + { + // Have the same network address, but different subnets? + if (NetworkAddress.Address.Equals(netaddrObj.NetworkAddress.Address)) + { + return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength; + } + + var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength); + return NetworkAddress.Address.Equals(altAddress.Address); + } + + return false; + } + + /// + public override bool Equals(IPObject? other) + { + if (other is IPNetAddress otherObj && !Address.Equals(IPAddress.None) && !otherObj.Address.Equals(IPAddress.None)) + { + return Address.Equals(otherObj.Address) && + PrefixLength == otherObj.PrefixLength; + } + + return false; + } + + /// + public override bool Equals(IPAddress address) + { + if (address != null && !address.Equals(IPAddress.None) && !Address.Equals(IPAddress.None)) + { + return address.Equals(Address); + } + + return false; + } + + /// + public override string ToString() + { + return ToString(false); + } + + /// + /// Returns a textual representation of this object. + /// + /// Set to true, if the subnet is to be included as part of the address. + /// String representation of this object. + public string ToString(bool shortVersion) + { + if (!Address.Equals(IPAddress.None)) + { + if (Address.Equals(IPAddress.Any)) + { + return "Any IP4 Address"; + } + + if (Address.Equals(IPAddress.IPv6Any)) + { + return "Any IP6 Address"; + } + + if (Address.Equals(IPAddress.Broadcast)) + { + return "Any Address"; + } + + if (shortVersion) + { + return Address.ToString(); + } + + return $"{Address}/{PrefixLength}"; + } + + return string.Empty; + } + + /// + protected override IPObject CalculateNetworkAddress() + { + var value = NetworkAddressOf(_address, PrefixLength); + return new IPNetAddress(value.Address, value.PrefixLength); + } + } +} diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs new file mode 100644 index 0000000000..a08694c266 --- /dev/null +++ b/MediaBrowser.Common/Net/IPObject.cs @@ -0,0 +1,395 @@ +#nullable enable +using System; +using System.Net; +using System.Net.Sockets; + +namespace MediaBrowser.Common.Net +{ + /// + /// Base network object class. + /// + public abstract class IPObject : IEquatable + { + /// + /// IPv6 Loopback address. + /// + protected static readonly byte[] Ipv6Loopback = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }; + + /// + /// IPv4 Loopback address. + /// + protected static readonly byte[] Ipv4Loopback = { 127, 0, 0, 1 }; + + /// + /// The network address of this object. + /// + private IPObject? _networkAddress; + + /// + /// Gets or sets the user defined functions that need storage in this object. + /// + public int Tag { get; set; } + + /// + /// Gets or sets the object's IP address. + /// + public abstract IPAddress Address { get; set; } + + /// + /// Gets the object's network address. + /// + public IPObject NetworkAddress + { + get + { + if (_networkAddress == null) + { + _networkAddress = CalculateNetworkAddress(); + } + + return _networkAddress; + } + } + + /// + /// Gets or sets the object's IP address. + /// + public abstract byte PrefixLength { get; set; } + + /// + /// Gets the AddressFamily of this object. + /// + public AddressFamily AddressFamily + { + get + { + // Keep terms separate as Address performs other functions in inherited objects. + IPAddress address = Address; + return address.Equals(IPAddress.None) ? AddressFamily.Unspecified : address.AddressFamily; + } + } + + /// + /// Returns the network address of an object. + /// + /// IP Address to convert. + /// Subnet prefix. + /// IPAddress. + public static (IPAddress Address, byte PrefixLength) NetworkAddressOf(IPAddress address, byte prefixLength) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (IsLoopback(address)) + { + return (Address: address, PrefixLength: prefixLength); + } + + byte[] addressBytes = address.GetAddressBytes(); + + int div = prefixLength / 8; + int mod = prefixLength % 8; + if (mod != 0) + { + mod = 8 - mod; + addressBytes[div] = (byte)((int)addressBytes[div] >> mod << mod); + div++; + } + + for (int octet = div; octet < addressBytes.Length; octet++) + { + addressBytes[octet] = 0; + } + + return (Address: new IPAddress(addressBytes), PrefixLength: prefixLength); + } + + /// + /// Tests to see if the ip address is a Loopback address. + /// + /// Value to test. + /// True if it is. + public static bool IsLoopback(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.Equals(IPAddress.None)) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback); + } + + return false; + } + + /// + /// Tests to see if the ip address is an IP6 address. + /// + /// Value to test. + /// True if it is. + public static bool IsIP6(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return !address.Equals(IPAddress.None) && (address.AddressFamily == AddressFamily.InterNetworkV6); + } + + /// + /// Tests to see if the address in the private address range. + /// + /// Object to test. + /// True if it contains a private address. + public static bool IsPrivateAddressRange(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (!address.Equals(IPAddress.None)) + { + if (address.AddressFamily == AddressFamily.InterNetwork) + { + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + byte[] octet = address.GetAddressBytes(); + + return (octet[0] == 10) || + (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) || // RFC1918 + (octet[0] == 192 && octet[1] == 168) || // RFC1918 + (octet[0] == 127); // RFC1122 + } + else + { + byte[] octet = address.GetAddressBytes(); + uint word = (uint)(octet[0] << 8) + octet[1]; + + return (word >= 0xfe80 && word <= 0xfebf) || // fe80::/10 :Local link. + (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address. + } + } + + return false; + } + + /// + /// Returns true if the IPAddress contains an IP6 Local link address. + /// + /// IPAddress object to check. + /// True if it is a local link address. + /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress + /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. + /// + public static bool IsIPv6LinkLocal(IPAddress address) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily != AddressFamily.InterNetworkV6) + { + return false; + } + + byte[] octet = address.GetAddressBytes(); + uint word = (uint)(octet[0] << 8) + octet[1]; + + return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link. + } + + /// + /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only. + /// + /// Subnet mask in CIDR notation. + /// IPv4 or IPv6 family. + /// String value of the subnet mask in dotted decimal notation. + public static IPAddress CidrToMask(byte cidr, AddressFamily family) + { + uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr); + addr = + ((addr & 0xff000000) >> 24) | + ((addr & 0x00ff0000) >> 8) | + ((addr & 0x0000ff00) << 8) | + ((addr & 0x000000ff) << 24); + return new IPAddress(addr); + } + + /// + /// Convert a mask to a CIDR. IPv4 only. + /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask. + /// + /// Subnet mask. + /// Byte CIDR representing the mask. + public static byte MaskToCidr(IPAddress mask) + { + if (mask == null) + { + throw new ArgumentNullException(nameof(mask)); + } + + byte cidrnet = 0; + if (!mask.Equals(IPAddress.Any)) + { + byte[] bytes = mask.GetAddressBytes(); + + var zeroed = false; + for (var i = 0; i < bytes.Length; i++) + { + for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1) + { + if (zeroed) + { + // Invalid netmask. + return (byte)~cidrnet; + } + + if ((v & 0x80) == 0) + { + zeroed = true; + } + else + { + cidrnet++; + } + } + } + } + + return cidrnet; + } + + /// + /// Tests to see if this object is a Loopback address. + /// + /// True if it is. + public virtual bool IsLoopback() + { + return IsLoopback(Address); + } + + /// + /// Removes all addresses of a specific type from this object. + /// + /// Type of address to remove. + public virtual void Remove(AddressFamily family) + { + // This method only peforms a function in the IPHost implementation of IPObject. + } + + /// + /// Tests to see if this object is an IPv6 address. + /// + /// True if it is. + public virtual bool IsIP6() + { + return IsIP6(Address); + } + + /// + /// Returns true if this IP address is in the RFC private address range. + /// + /// True this object has a private address. + public virtual bool IsPrivateAddressRange() + { + return IsPrivateAddressRange(Address); + } + + /// + /// Compares this to the object passed as a parameter. + /// + /// Object to compare to. + /// Equality result. + public virtual bool Equals(IPAddress ip) + { + if (ip != null) + { + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + return !Address.Equals(IPAddress.None) && Address.Equals(ip); + } + + return false; + } + + /// + /// Compares this to the object passed as a parameter. + /// + /// Object to compare to. + /// Equality result. + public virtual bool Equals(IPObject? other) + { + if (other != null && other is IPObject otherObj) + { + return !Address.Equals(IPAddress.None) && Address.Equals(otherObj.Address); + } + + return false; + } + + /// + /// Compares the address in this object and the address in the object passed as a parameter. + /// + /// Object's IP address to compare to. + /// Comparison result. + public abstract bool Contains(IPObject address); + + /// + /// Compares the address in this object and the address in the object passed as a parameter. + /// + /// Object's IP address to compare to. + /// Comparison result. + public abstract bool Contains(IPAddress address); + + /// + public override int GetHashCode() + { + return Address.GetHashCode(); + } + + /// + public override bool Equals(object obj) + { + return Equals(obj as IPObject); + } + + /// + /// Calculates the network address of this object. + /// + /// Returns the network address of this object. + protected abstract IPObject CalculateNetworkAddress(); + } +} diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs new file mode 100644 index 0000000000..6e9cb46dc1 --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -0,0 +1,254 @@ +#pragma warning disable CA1062 // Validate arguments of public methods +using System; +using System.Collections; +using System.Collections.Generic; +using System.Net; +using System.Runtime.CompilerServices; +using NetCollection = System.Collections.ObjectModel.Collection; + +namespace MediaBrowser.Common.Net +{ + /// + /// Defines the . + /// + public static class NetworkExtensions + { + /// + /// Add an address to the collection. + /// + /// The . + /// Item to add. + public static void AddItem(this NetCollection source, IPAddress ip) + { + if (!source.ContainsAddress(ip)) + { + source.Add(new IPNetAddress(ip, 32)); + } + } + + /// + /// Add multiple items to the collection. + /// + /// The . + /// Item to add. + /// Return the collection. + public static NetCollection AddRange(this NetCollection destination, IEnumerable source) + { + foreach (var item in source) + { + destination.Add(item); + } + + return destination; + } + + /// + /// Adds a network to the collection. + /// + /// The . + /// Item to add. + public static void AddItem(this NetCollection source, IPObject item) + { + if (!source.ContainsAddress(item)) + { + source.Add(item); + } + } + + /// + /// Converts this object to a string. + /// + /// The . + /// Returns a string representation of this object. + public static string Readable(this NetCollection source) + { + string output = "["; + if (source.Count > 0) + { + foreach (var i in source) + { + output += $"{i},"; + } + + output = output[0..^1]; + } + + return $"{output}]"; + } + + /// + /// Returns true if the collection contains an item with the ip address, + /// or the ip address falls within any of the collection's network ranges. + /// + /// The . + /// The item to look for. + /// True if the collection contains the item. + public static bool ContainsAddress(this NetCollection source, IPAddress item) + { + if (source.Count == 0) + { + return false; + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + if (item.IsIPv4MappedToIPv6) + { + item = item.MapToIPv4(); + } + + foreach (var i in source) + { + if (i.Contains(item)) + { + return true; + } + } + + return false; + } + + /// + /// Returns true if the collection contains an item with the ip address, + /// or the ip address falls within any of the collection's network ranges. + /// + /// The . + /// The item to look for. + /// True if the collection contains the item. + public static bool ContainsAddress(this NetCollection source, IPObject item) + { + if (source.Count == 0) + { + return false; + } + + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + foreach (var i in source) + { + if (i.Contains(item)) + { + return true; + } + } + + return false; + } + + /// + /// Returns a collection containing the subnets of this collection given. + /// + /// The . + /// NetCollection object containing the subnets. + public static NetCollection AsNetworks(this NetCollection source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + NetCollection res = new NetCollection(); + + foreach (IPObject i in source) + { + if (i is IPNetAddress nw) + { + // Add the subnet calculated from the interface address/mask. + var na = nw.NetworkAddress; + na.Tag = i.Tag; + res.Add(na); + } + else + { + // Flatten out IPHost and add all its ip addresses. + foreach (var addr in ((IPHost)i).GetAddresses()) + { + IPNetAddress host = new IPNetAddress(addr) + { + Tag = i.Tag + }; + + res.Add(host); + } + } + } + + return res; + } + + /// + /// Excludes all the items from this list that are found in excludeList. + /// + /// The . + /// Items to exclude. + /// A new collection, with the items excluded. + public static NetCollection Exclude(this NetCollection source, NetCollection excludeList) + { + if (source.Count == 0 || excludeList == null) + { + return new NetCollection(source); + } + + NetCollection results = new NetCollection(); + + bool found; + foreach (var outer in source) + { + found = false; + + foreach (var inner in excludeList) + { + if (outer.Equals(inner)) + { + found = true; + break; + } + } + + if (!found) + { + results.Add(outer); + } + } + + return results; + } + + /// + /// Returns all items that co-exist in this object and target. + /// + /// The . + /// Collection to compare with. + /// A collection containing all the matches. + public static NetCollection Union(this NetCollection source, NetCollection target) + { + if (source.Count == 0) + { + return new NetCollection(); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + NetCollection nc = new NetCollection(); + + foreach (IPObject i in source) + { + if (target.ContainsAddress(i)) + { + nc.Add(i); + } + } + + return nc; + } + } +} diff --git a/MediaBrowser.sln b/MediaBrowser.sln index d460c0ab0c..cb204137bd 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -68,6 +68,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -182,6 +184,10 @@ Global {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 + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,6 +199,7 @@ Global {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} + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index ae175d8c9d..3e89d7f602 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -6,7 +6,6 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; -using NetworkCollection; namespace Rssdp.Infrastructure { diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj new file mode 100644 index 0000000000..fa18316dff --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj @@ -0,0 +1,28 @@ + + + + + {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} + + + + netcoreapp3.1 + false + true + enable + + + + + + + + + + + + + + + + diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs new file mode 100644 index 0000000000..9e7e8d3ac3 --- /dev/null +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -0,0 +1,425 @@ +using System; +using System.Net; +using Emby.Dlna.PlayTo; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Moq; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; +using NetCollection = System.Collections.ObjectModel.Collection; +using XMLProperties = System.Collections.Generic.Dictionary; + +namespace NetworkTesting +{ + public class NetTesting + { + /// + /// Trys to identify the string and return an object of that class. + /// + /// String to parse. + /// IPObject to return. + /// True if the value parsed successfully. + private static bool TryParse(string addr, out IPObject result) + { + if (!string.IsNullOrEmpty(addr)) + { + // Is it an IP address + if (IPNetAddress.TryParse(addr, out IPNetAddress nw)) + { + result = nw; + return true; + } + + if (IPHost.TryParse(addr, out IPHost h)) + { + result = h; + return true; + } + } + + result = IPNetAddress.None; + return false; + } + + + private IConfigurationManager GetMockConfig(NetworkConfiguration conf) + { + var configManager = new Mock + { + CallBase = true + }; + configManager.Setup(x => x.GetConfiguration(It.IsAny())).Returns(conf); + return (IConfigurationManager)configManager.Object; + } + + [Theory] + [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")] + [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] + [InlineData("192.168.1.208/24,-16,vEthernet1:192.168.1.208/24,-16,vEthernet212;200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")] + public void IgnoreVirtualInterfaces(string interfaces, string lan, string value) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = lan.Split(';') + }; + + NetworkManager.MockNetworkSettings = interfaces; + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + NetworkManager.MockNetworkSettings = string.Empty; + + Assert.True(string.Equals(nm.GetInternalBindAddresses().ToString(), value, StringComparison.Ordinal)); + } + + [Theory] + [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] + public void TextIsInNetwork(string network, string value) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + LocalNetworkSubnets = network.Split(',') + }; + + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + Assert.True(!nm.IsInLocalNetwork(value)); + } + + [Theory] + [InlineData("127.0.0.1")] + [InlineData("127.0.0.1:123")] + [InlineData("localhost")] + [InlineData("localhost:1345")] + [InlineData("www.google.co.uk")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] + [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] + [InlineData("fe80::7add:12ff:febb:c67b%16")] + [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")] + [InlineData("192.168.1.2/255.255.255.0")] + [InlineData("192.168.1.2/24")] + + public void TestCollectionCreation(string address) + { + Assert.True(TryParse(address, out _)); + } + + [Theory] + [InlineData("256.128.0.0.0.1")] + [InlineData("127.0.0.1#")] + [InlineData("localhost!")] + [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")] + public void TestInvalidCollectionCreation(string address) + { + Assert.False(TryParse(address, out _)); + } + + [Theory] + // Src, IncIP6, incIP4, exIP6, ecIP4, net + [InlineData("127.0.0.1#", + "[]", + "[]", + "[]", + "[]", + "[]")] + [InlineData("[127.0.0.1]", + "[]", + "[]", + "[127.0.0.1/32]", + "[127.0.0.1/32]", + "[]")] + [InlineData("", + "[]", + "[]", + "[]", + "[]", + "[]")] + [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8", + "[192.158.1.2/16,192.169.1.2/8]", + "[192.158.1.2/16,192.169.1.2/8]", + "[]", + "[]", + "[192.158.0.0/16,192.0.0.0/8]")] + [InlineData("192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, [10.10.10.10]", + "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]", + "[192.158.1.2/16,127.0.0.1/32]", + "[10.10.10.10/32]", + "[10.10.10.10/32]", + "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] + public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + // Test included, IP6. + NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); + Assert.True(string.Equals(nc.ToString(), result1, System.StringComparison.OrdinalIgnoreCase)); + + // Text excluded, non IP6. + nc = nm.CreateIPCollection(settings.Split(","), true); + Assert.True(string.Equals(nc?.ToString(), result3, System.StringComparison.OrdinalIgnoreCase)); + + conf.EnableIPV6 = false; + nm.UpdateSettings(conf); + + // Test included, non IP6. + nc = nm.CreateIPCollection(settings.Split(","), false); + Assert.True(string.Equals(nc.ToString(), result2, System.StringComparison.OrdinalIgnoreCase)); + + // Test excluded, including IPv6. + nc = nm.CreateIPCollection(settings.Split(","), true); + Assert.True(string.Equals(nc.ToString(), result4, System.StringComparison.OrdinalIgnoreCase)); + + conf.EnableIPV6 = true; + nm.UpdateSettings(conf); + + // Test network addresses of collection. + nc = nm.CreateIPCollection(settings.Split(","), false); + nc = nc.AsNetworks(); + Assert.True(string.Equals(nc.ToString(), result5, System.StringComparison.OrdinalIgnoreCase)); + } + + [Theory] + [InlineData("127.0.0.1", "fd23:184f:2029:0:3139:7386:67d7:d517/64,fd23:184f:2029:0:c0f0:8a8a:7605:fffa/128,fe80::3139:7386:67d7:d517%16/64,192.168.1.208/24,::1/128,127.0.0.1/8", "[127.0.0.1/32]")] + [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")] + public void UnionCheck(string settings, string compare, string result) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true, + }; + + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); + NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); + + Assert.True(nc1.Union(nc2).ToString() == result); + } + + [Theory] + [InlineData("192.168.5.85/24", "192.168.5.1")] + [InlineData("192.168.5.85/24", "192.168.5.254")] + [InlineData("10.128.240.50/30", "10.128.240.48")] + [InlineData("10.128.240.50/30", "10.128.240.49")] + [InlineData("10.128.240.50/30", "10.128.240.50")] + [InlineData("10.128.240.50/30", "10.128.240.51")] + [InlineData("127.0.0.1/8", "127.0.0.1")] + public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("192.168.5.85/24", "192.168.4.254")] + [InlineData("192.168.5.85/24", "191.168.5.254")] + [InlineData("10.128.240.50/30", "10.128.240.47")] + [InlineData("10.128.240.50/30", "10.128.240.52")] + [InlineData("10.128.240.50/30", "10.128.239.50")] + [InlineData("10.128.240.50/30", "10.127.240.51")] + public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0001:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFF0")] + [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")] + public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFFF")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0000:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0001:0000:0000:0000")] + [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFF0")] + [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")] + public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress) + { + var ipAddressObj = IPNetAddress.Parse(netMask); + Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress))); + } + + [Theory] + [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1/32")] + [InlineData("10.0.0.0/8", "10.10.10.1/32")] + [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1")] + + [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1/32")] + [InlineData("10.10.0.0/16", "10.10.10.1/32")] + [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1")] + + [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1/32")] + [InlineData("10.10.10.0/24", "10.10.10.1/32")] + [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1")] + + public void TestSubnets(string network, string ip) + { + Assert.True(TryParse(network, out IPObject? networkObj)); + Assert.True(TryParse(ip, out IPObject? ipObj)); + +#pragma warning disable CS8602 // Dereference of a possibly null reference. +#pragma warning disable CS8604 // Possible null reference argument. + Assert.True(networkObj.Contains(ipObj)); +#pragma warning restore CS8604 // Possible null reference argument. +#pragma warning restore CS8602 // Dereference of a possibly null reference. + } + + [Theory] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24", "172.168.1.2/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24, 10.10.10.1", "172.168.1.2/24,10.10.10.1/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/255.255.255.0, 10.10.10.1", "192.168.1.2/24,10.10.10.1/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/24, 100.10.10.1", "192.168.1.2/24")] + [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "194.168.1.2/24, 100.10.10.1", "")] + + public void TestMatches(string source, string dest, string result) + { + var conf = new NetworkConfiguration() + { + EnableIPV6 = true, + EnableIPV4 = true + }; + + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + // Test included, IP6. + NetCollection ncSource = nm.CreateIPCollection(source.Split(",")); + NetCollection ncDest = nm.CreateIPCollection(dest.Split(",")); + NetCollection ncResult = ncSource.Union(ncDest); + NetCollection resultCollection = nm.CreateIPCollection(result.Split(",")); + Assert.True(ncResult.Equals(resultCollection)); + } + + + [Theory] + [InlineData("10.1.1.1/32", "10.1.1.1")] + [InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")] + + public void TestEquals(string source, string dest) + { + Assert.True(IPNetAddress.Parse(source).Equals(IPNetAddress.Parse(dest))); + Assert.True(IPNetAddress.Parse(dest).Equals(IPNetAddress.Parse(source))); + } + + [Theory] + + // Testing bind interfaces. These are set for my system so won't work elsewhere. + // On my system eth16 is internal, eth11 external (Windows defines the indexes). + // + // This test is to replicate how DNLA requests work throughout the system. + + // User on internal network, we're bound internal and external - so result is internal. + [InlineData("192.168.1.1", "eth16,eth11", false, "eth16")] + // User on external network, we're bound internal and external - so result is external. + [InlineData("8.8.8.8", "eth16,eth11", false, "eth11")] + // User on internal network, we're bound internal only - so result is internal. + [InlineData("10.10.10.10", "eth16", false, "eth16")] + // User on internal network, no binding specified - so result is the 1st internal. + [InlineData("192.168.1.1", "", false, "eth16")] + // User on external network, internal binding only - so result is the 1st internal. + [InlineData("jellyfin.org", "eth16", false, "eth16")] + // User on external network, no binding - so result is the 1st external. + [InlineData("jellyfin.org", "", false, "eth11")] + // User assumed to be internal, no binding - so result is the 1st internal. + [InlineData("", "", false, "eth16")] + public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result) + { + var conf = new NetworkConfiguration() + { + LocalNetworkAddresses = bindAddresses.Split(','), + EnableIPV6 = ipv6enabled, + EnableIPV4 = true + }; + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + NetworkManager.MockNetworkSettings = string.Empty; + + _ = nm.TryParseInterface(result, out NetCollection? resultObj); + + if (resultObj != null) + { + result = ((IPNetAddress)resultObj[0]).ToString(true); + var intf = nm.GetBindInterface(source, out int? _); + + Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); + } + } + + [Theory] + + // Testing bind interfaces. These are set for my system so won't work elsewhere. + // On my system eth16 is internal, eth11 external (Windows defines the indexes). + // + // This test is to replicate how subnet bound ServerPublisherUri work throughout the system. + + // User on internal network, we're bound internal and external - so result is internal override. + [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] + + // User on external network, we're bound internal and external - so result is override. + [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + + // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override. + [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] + + // User on internal network, no binding specified - so result is the 1st internal. + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + + // User on external network, internal binding only - so asumption is a proxy forward, return external override. + [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + + // User on external network, no binding - so result is the 1st external which is overriden. + [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0 = http://helloworld.com", "http://helloworld.com")] + + // User assumed to be internal, no binding - so result is the 1st internal. + [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + + // User is internal, no binding - so result is the 1st internal, which is then overridden. + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] + + public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) + { + var conf = new NetworkConfiguration() + { + LocalNetworkSubnets = lan.Split(','), + LocalNetworkAddresses = bindAddresses.Split(','), + EnableIPV6 = ipv6enabled, + EnableIPV4 = true, + PublishedServerUriBySubnet = new string[] { publishedServers } + }; + + NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; + var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + NetworkManager.MockNetworkSettings = string.Empty; + + if (nm.TryParseInterface(result, out NetCollection? resultObj) && resultObj != null) + { + // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks). + result = ((IPNetAddress)resultObj[0]).ToString(true); + } + + var intf = nm.GetBindInterface(source, out int? _); + + Assert.True(string.Equals(intf, result, System.StringComparison.OrdinalIgnoreCase)); + } + } +} From f06e4826c764c1214478a741b0f93315a8bba76b Mon Sep 17 00:00:00 2001 From: Greenback Date: Sat, 31 Oct 2020 19:16:28 +0000 Subject: [PATCH 058/257] Fixed testing units --- Jellyfin.Networking/Manager/NetworkManager.cs | 55 +++++++++-------- MediaBrowser.Common/Net/NetworkExtensions.cs | 60 ++++++++++++------- .../NetworkTesting/UnitTesting.cs | 16 ++--- 3 files changed, 77 insertions(+), 54 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 76ac02d791..2ec2a0ba52 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -158,12 +158,15 @@ namespace Jellyfin.Networking.Manager /// /// Items to assign the collection, or null. /// The collection created. - public static NetCollection CreateCollection(IEnumerable? source) + public static NetCollection CreateCollection(IEnumerable? source = null) { var result = new NetCollection(); if (source != null) { - return result.AddRange(source); + foreach (var item in source) + { + result.AddItem(item); + } } return result; @@ -433,7 +436,7 @@ namespace Jellyfin.Networking.Manager if (_bindExclusions.Count > 0) { // Return all the internal interfaces except the ones excluded. - return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.Contains(p))); + return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.ContainsAddress(p))); } // No bind address, so return all internal interfaces. @@ -463,7 +466,7 @@ namespace Jellyfin.Networking.Manager } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return _lanSubnets.Contains(address) && !_excludedSubnets.Contains(address); + return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address); } /// @@ -471,7 +474,7 @@ namespace Jellyfin.Networking.Manager { if (IPHost.TryParse(address, out IPHost ep)) { - return _lanSubnets.Contains(ep) && !_excludedSubnets.Contains(ep); + return _lanSubnets.ContainsAddress(ep) && !_excludedSubnets.ContainsAddress(ep); } return false; @@ -559,7 +562,7 @@ namespace Jellyfin.Networking.Manager ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { - result.Add(iface); + result.AddItem(iface); } } @@ -603,7 +606,7 @@ namespace Jellyfin.Networking.Manager var address = IPNetAddress.Parse(parts[0]); var index = int.Parse(parts[1], CultureInfo.InvariantCulture); address.Tag = index; - _interfaceAddresses.Add(address); + _interfaceAddresses.AddItem(address); _interfaceNames.Add(parts[2], Math.Abs(index)); } } @@ -747,7 +750,7 @@ namespace Jellyfin.Networking.Manager ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { - col.Add(iface); + col.AddItem(iface); } } } @@ -759,7 +762,7 @@ namespace Jellyfin.Networking.Manager obj.Remove(AddressFamily.InterNetworkV6); if (!obj.IsIP6()) { - col.Add(obj); + col.AddItem(obj); } } else if (!IsIP4Enabled) @@ -768,12 +771,12 @@ namespace Jellyfin.Networking.Manager obj.Remove(AddressFamily.InterNetwork); if (obj.IsIP6()) { - col.Add(obj); + col.AddItem(obj); } } else { - col.Add(obj); + col.AddItem(obj); } } else @@ -956,7 +959,7 @@ namespace Jellyfin.Networking.Manager { _logger.LogDebug("Using LAN interface addresses as user provided no LAN details."); // Internal interfaces must be private and not excluded. - _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.Contains(i))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.ContainsAddress(i))); // Subnets are the same as the calculated internal interface. _lanSubnets = new NetCollection(); @@ -964,17 +967,17 @@ namespace Jellyfin.Networking.Manager // 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 + _lanSubnets.AddItem(IPNetAddress.IP6Loopback); + _lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA + _lanSubnets.AddItem(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")); + _lanSubnets.AddItem(IPNetAddress.IP4Loopback); + _lanSubnets.AddItem(IPNetAddress.Parse("10.0.0.0/8")); + _lanSubnets.AddItem(IPNetAddress.Parse("172.16.0.0/12")); + _lanSubnets.AddItem(IPNetAddress.Parse("192.168.0.0/16")); } } else @@ -982,16 +985,16 @@ namespace Jellyfin.Networking.Manager // We must listen on loopback for LiveTV to function regardless of the settings. if (IsIP6Enabled) { - _lanSubnets.Add(IPNetAddress.IP6Loopback); + _lanSubnets.AddItem(IPNetAddress.IP6Loopback); } if (IsIP4Enabled) { - _lanSubnets.Add(IPNetAddress.IP4Loopback); + _lanSubnets.AddItem(IPNetAddress.IP4Loopback); } // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. - _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i) && !_excludedSubnets.Contains(i) && _lanSubnets.Contains(i))); + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i))); } _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); @@ -1049,7 +1052,7 @@ namespace Jellyfin.Networking.Manager nw.Tag *= -1; } - _interfaceAddresses.Add(nw); + _interfaceAddresses.AddItem(nw); // Store interface name so we can use the name in Collections. _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; @@ -1070,7 +1073,7 @@ namespace Jellyfin.Networking.Manager nw.Tag *= -1; } - _interfaceAddresses.Add(nw); + _interfaceAddresses.AddItem(nw); // Store interface name so we can use the name in Collections. _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag; @@ -1104,10 +1107,10 @@ namespace Jellyfin.Networking.Manager { _logger.LogError("No interfaces information available. Resolving DNS name."); // Last ditch attempt - use loopback address. - _interfaceAddresses.Add(IPNetAddress.IP4Loopback); + _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); if (IsIP6Enabled) { - _interfaceAddresses.Add(IPNetAddress.IP6Loopback); + _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); } } } diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 6e9cb46dc1..39499460ea 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -4,6 +4,7 @@ using System.Collections; using System.Collections.Generic; using System.Net; using System.Runtime.CompilerServices; +using System.Text; using NetCollection = System.Collections.ObjectModel.Collection; namespace MediaBrowser.Common.Net @@ -26,22 +27,6 @@ namespace MediaBrowser.Common.Net } } - /// - /// Add multiple items to the collection. - /// - /// The . - /// Item to add. - /// Return the collection. - public static NetCollection AddRange(this NetCollection destination, IEnumerable source) - { - foreach (var item in source) - { - destination.Add(item); - } - - return destination; - } - /// /// Adds a network to the collection. /// @@ -62,6 +47,7 @@ namespace MediaBrowser.Common.Net /// Returns a string representation of this object. public static string Readable(this NetCollection source) { + var sb = new StringBuilder(); string output = "["; if (source.Count > 0) { @@ -141,6 +127,40 @@ namespace MediaBrowser.Common.Net return false; } + /// + /// Compares two NetCollection objects. The order is ignored. + /// + /// The . + /// Item to compare to. + /// True if both are equal. + public static bool Compare(this NetCollection source, NetCollection dest) + { + if (dest == null || source.Count != dest.Count) + { + return false; + } + + foreach (var sourceItem in source) + { + bool found = false; + foreach (var destItem in dest) + { + if (sourceItem.Equals(destItem)) + { + found = true; + break; + } + } + + if (!found) + { + return false; + } + } + + return true; + } + /// /// Returns a collection containing the subnets of this collection given. /// @@ -162,7 +182,7 @@ namespace MediaBrowser.Common.Net // Add the subnet calculated from the interface address/mask. var na = nw.NetworkAddress; na.Tag = i.Tag; - res.Add(na); + res.AddItem(na); } else { @@ -174,7 +194,7 @@ namespace MediaBrowser.Common.Net Tag = i.Tag }; - res.Add(host); + res.AddItem(host); } } } @@ -213,7 +233,7 @@ namespace MediaBrowser.Common.Net if (!found) { - results.Add(outer); + results.AddItem(outer); } } @@ -244,7 +264,7 @@ namespace MediaBrowser.Common.Net { if (target.ContainsAddress(i)) { - nc.Add(i); + nc.AddItem(i); } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index 9e7e8d3ac3..fe64d1cfce 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -71,7 +71,7 @@ namespace NetworkTesting var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - Assert.True(string.Equals(nm.GetInternalBindAddresses().ToString(), value, StringComparison.Ordinal)); + Assert.True(string.Equals(nm.GetInternalBindAddresses().Readable(), value, StringComparison.Ordinal)); } [Theory] @@ -163,22 +163,22 @@ namespace NetworkTesting // Test included, IP6. NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc.ToString(), result1, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc.Readable(), result1, System.StringComparison.OrdinalIgnoreCase)); // Text excluded, non IP6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc?.ToString(), result3, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.Readable(), result3, System.StringComparison.OrdinalIgnoreCase)); conf.EnableIPV6 = false; nm.UpdateSettings(conf); // Test included, non IP6. nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc.ToString(), result2, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc.Readable(), result2, System.StringComparison.OrdinalIgnoreCase)); // Test excluded, including IPv6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc.ToString(), result4, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc.Readable(), result4, System.StringComparison.OrdinalIgnoreCase)); conf.EnableIPV6 = true; nm.UpdateSettings(conf); @@ -186,7 +186,7 @@ namespace NetworkTesting // Test network addresses of collection. nc = nm.CreateIPCollection(settings.Split(","), false); nc = nc.AsNetworks(); - Assert.True(string.Equals(nc.ToString(), result5, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc.Readable(), result5, System.StringComparison.OrdinalIgnoreCase)); } [Theory] @@ -205,7 +205,7 @@ namespace NetworkTesting NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); - Assert.True(nc1.Union(nc2).ToString() == result); + Assert.True(nc1.Union(nc2).Readable() == result); } [Theory] @@ -306,7 +306,7 @@ namespace NetworkTesting NetCollection ncDest = nm.CreateIPCollection(dest.Split(",")); NetCollection ncResult = ncSource.Union(ncDest); NetCollection resultCollection = nm.CreateIPCollection(result.Split(",")); - Assert.True(ncResult.Equals(resultCollection)); + Assert.True(ncResult.Compare(resultCollection)); } From 3e62557959b1233311ab5d4e8f578f84438580d9 Mon Sep 17 00:00:00 2001 From: Greenback Date: Wed, 4 Nov 2020 20:29:55 +0000 Subject: [PATCH 059/257] amended testing unit. --- Jellyfin.Networking/Manager/NetworkManager.cs | 14 ++++++++------ MediaBrowser.Common/Net/NetworkExtensions.cs | 2 +- .../NetworkTesting/UnitTesting.cs | 14 +++++++------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 2ec2a0ba52..289b1dc720 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -19,6 +19,8 @@ namespace Jellyfin.Networking.Manager { /// /// Class to take care of network interface management. + /// + /// Note: The normal collection methods and properties will not work with NetCollection. . /// public class NetworkManager : INetworkManager, IDisposable { @@ -925,8 +927,8 @@ namespace Jellyfin.Networking.Manager // Read and parse bind addresses and exclusions, removing ones that don't exist. _bindAddresses = CreateIPCollection(lanAddresses).Union(_interfaceAddresses); _bindExclusions = CreateIPCollection(lanAddresses, true).Union(_interfaceAddresses); - _logger.LogInformation("Using bind addresses: {0}", _bindAddresses); - _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions); + _logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString()); + _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString()); } private void InitialiseRemote(NetworkConfiguration config) @@ -997,9 +999,9 @@ namespace Jellyfin.Networking.Manager _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i))); } - _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets); - _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets); - _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks()); + _logger.LogInformation("Defined LAN addresses : {0}", _lanSubnets.AsString()); + _logger.LogInformation("Defined LAN exclusions : {0}", _excludedSubnets.AsString()); + _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets).AsNetworks().AsString()); } } @@ -1090,7 +1092,7 @@ namespace Jellyfin.Networking.Manager } _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); - _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses); + _logger.LogDebug("Interfaces addresses : {0}", _interfaceAddresses.AsString()); // If for some reason we don't have an interface info, resolve our DNS name. if (_interfaceAddresses.Count == 0) diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index 39499460ea..e801de5eb8 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -45,7 +45,7 @@ namespace MediaBrowser.Common.Net /// /// The . /// Returns a string representation of this object. - public static string Readable(this NetCollection source) + public static string AsString(this NetCollection source) { var sb = new StringBuilder(); string output = "["; diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index fe64d1cfce..c96c7defd7 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -71,7 +71,7 @@ namespace NetworkTesting var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; - Assert.True(string.Equals(nm.GetInternalBindAddresses().Readable(), value, StringComparison.Ordinal)); + Assert.True(string.Equals(nm.GetInternalBindAddresses().AsString(), value, StringComparison.Ordinal)); } [Theory] @@ -163,22 +163,22 @@ namespace NetworkTesting // Test included, IP6. NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc.Readable(), result1, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result1, System.StringComparison.OrdinalIgnoreCase)); // Text excluded, non IP6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc?.Readable(), result3, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result3, System.StringComparison.OrdinalIgnoreCase)); conf.EnableIPV6 = false; nm.UpdateSettings(conf); // Test included, non IP6. nc = nm.CreateIPCollection(settings.Split(","), false); - Assert.True(string.Equals(nc.Readable(), result2, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result2, System.StringComparison.OrdinalIgnoreCase)); // Test excluded, including IPv6. nc = nm.CreateIPCollection(settings.Split(","), true); - Assert.True(string.Equals(nc.Readable(), result4, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result4, System.StringComparison.OrdinalIgnoreCase)); conf.EnableIPV6 = true; nm.UpdateSettings(conf); @@ -186,7 +186,7 @@ namespace NetworkTesting // Test network addresses of collection. nc = nm.CreateIPCollection(settings.Split(","), false); nc = nc.AsNetworks(); - Assert.True(string.Equals(nc.Readable(), result5, System.StringComparison.OrdinalIgnoreCase)); + Assert.True(string.Equals(nc?.AsString(), result5, System.StringComparison.OrdinalIgnoreCase)); } [Theory] @@ -205,7 +205,7 @@ namespace NetworkTesting NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); - Assert.True(nc1.Union(nc2).Readable() == result); + Assert.True(nc1.Union(nc2).AsString() == result); } [Theory] From e2769671a79dfd73ec2796bcc0cbe97584ee4023 Mon Sep 17 00:00:00 2001 From: Greenback Date: Wed, 4 Nov 2020 20:38:47 +0000 Subject: [PATCH 060/257] removed github merge introduced spaces. --- Jellyfin.Server/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 2e9def9af7..db67f64708 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -281,8 +281,8 @@ namespace Jellyfin.Server if (appHost.ListenWithHttps) { options.Listen( - netAdd.Address, - appHost.HttpsPort, + netAdd.Address, + appHost.HttpsPort, listenOptions => listenOptions.UseHttps(appHost.Certificate)); } else if (builderContext.HostingEnvironment.IsDevelopment()) @@ -292,7 +292,7 @@ namespace Jellyfin.Server options.Listen( netAdd.Address, appHost.HttpsPort, - listenOptions => listenOptions.UseHttps()); + listenOptions => listenOptions.UseHttps()); } catch (InvalidOperationException) { From 802d37aac5a806dac5ef7e4c64b63b64dcf30302 Mon Sep 17 00:00:00 2001 From: BaronGreenback Date: Sun, 8 Nov 2020 12:09:32 +0000 Subject: [PATCH 061/257] Update Emby.Naming/Common/NamingOptions.cs --- Emby.Naming/Common/NamingOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 0a7224774c..20667abd27 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -137,7 +137,7 @@ namespace Emby.Naming.Common CleanDateTimes = new[] { @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", - @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*" + @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*", @"(.+\w)\W+\p{Ps}(19[0-9]{2}|20[0-9]{2})\p{Pe}", // Prefer year enclosed in parenthesis (){}[] @"(.+\w)\W+(19[0-9]{2}|20[0-9]{2})(\W|$)", // Secondary year surrounded by non-word chars }; From f680a0fbbed346a5a5e98b2066604c463791190c Mon Sep 17 00:00:00 2001 From: Gary Wilber Date: Sun, 8 Nov 2020 11:55:38 -0800 Subject: [PATCH 062/257] fix merge --- MediaBrowser.Model/Configuration/ServerConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 4d24e45b0c..be313bde1b 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -271,6 +271,7 @@ namespace MediaBrowser.Model.Configuration /// public string[] KnownProxies { get; set; } + /// /// Gets or sets the number of days we should retain activity logs. /// public int? ActivityLogRetentionDays { get; set; } From 547ee885613591021ad5ff5595249b1f8448d742 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 15 Nov 2020 10:58:39 -0700 Subject: [PATCH 063/257] Remove api client generator errors --- Jellyfin.Api/Controllers/AudioController.cs | 169 ++++- .../Controllers/DynamicHlsController.cs | 4 +- Jellyfin.Api/Controllers/ImageController.cs | 696 +++++++++++++++++- .../Controllers/InstantMixController.cs | 4 +- Jellyfin.Api/Controllers/ItemsController.cs | 257 ++++++- .../Controllers/SubtitleController.cs | 42 +- .../Controllers/TrailersController.cs | 1 - .../Controllers/VideoAttachmentsController.cs | 3 +- Jellyfin.Api/Controllers/VideosController.cs | 165 ++++- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 10 files changed, 1289 insertions(+), 54 deletions(-) diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index d4c6e4af94..330e56614d 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -85,15 +85,178 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Audio stream returned. /// A containing the audio file. - [HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")] [HttpGet("{itemId}/stream", Name = "GetAudioStream")] - [HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")] [HttpHead("{itemId}/stream", Name = "HeadAudioStream")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesAudioFile] public async Task GetAudioStream( [FromRoute, Required] Guid itemId, - [FromRoute] string? container, + [FromQuery] string? container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodingReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext? context, + [FromQuery] Dictionary? streamOptions) + { + StreamingRequestDto streamingRequest = new StreamingRequestDto + { + Id = itemId, + Container = container, + Static = @static ?? true, + Params = @params, + Tag = tag, + DeviceProfileId = deviceProfileId, + PlaySessionId = playSessionId, + SegmentContainer = segmentContainer, + SegmentLength = segmentLength, + MinSegments = minSegments, + MediaSourceId = mediaSourceId, + DeviceId = deviceId, + AudioCodec = audioCodec, + EnableAutoStreamCopy = enableAutoStreamCopy ?? true, + AllowAudioStreamCopy = allowAudioStreamCopy ?? true, + AllowVideoStreamCopy = allowVideoStreamCopy ?? true, + BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, + AudioSampleRate = audioSampleRate, + MaxAudioChannels = maxAudioChannels, + AudioBitRate = audioBitRate, + MaxAudioBitDepth = maxAudioBitDepth, + AudioChannels = audioChannels, + Profile = profile, + Level = level, + Framerate = framerate, + MaxFramerate = maxFramerate, + CopyTimestamps = copyTimestamps ?? true, + StartTimeTicks = startTimeTicks, + Width = width, + Height = height, + VideoBitRate = videoBitRate, + SubtitleStreamIndex = subtitleStreamIndex, + SubtitleMethod = subtitleMethod, + MaxRefFrames = maxRefFrames, + MaxVideoBitDepth = maxVideoBitDepth, + RequireAvc = requireAvc ?? true, + DeInterlace = deInterlace ?? true, + RequireNonAnamorphic = requireNonAnamorphic ?? true, + TranscodingMaxAudioChannels = transcodingMaxAudioChannels, + CpuCoreLimit = cpuCoreLimit, + LiveStreamId = liveStreamId, + EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true, + VideoCodec = videoCodec, + SubtitleCodec = subtitleCodec, + TranscodeReasons = transcodingReasons, + AudioStreamIndex = audioStreamIndex, + VideoStreamIndex = videoStreamIndex, + Context = context ?? EncodingContext.Static, + StreamOptions = streamOptions + }; + + return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false); + } + + /// + /// Gets an audio stream. + /// + /// The item id. + /// The audio container. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment lenght. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamporphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Audio stream returned. + /// A containing the audio file. + [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")] + [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesAudioFile] + public async Task GetAudioStreamByContainer( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index e07690e110..a6b249ccfa 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -833,7 +833,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute] string container, + [FromRoute, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -1004,7 +1004,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute] string container, + [FromRoute, Required] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index 4a67c1aede..bb0f273122 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -85,7 +85,6 @@ 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?}", Name = "PostUserImage_2")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -94,7 +93,53 @@ namespace Jellyfin.Api.Controllers public async Task PostUserImage( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, - [FromRoute] int? index = null) + [FromQuery] int? index = null) + { + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + { + return Forbid("User is not allowed to update the image."); + } + + var user = _userManager.GetUserById(userId); + await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + if (user.ProfileImage != null) + { + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + } + + user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); + + await _providerManager + .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) + .ConfigureAwait(false); + await _userManager.UpdateUserAsync(user).ConfigureAwait(false); + + return NoContent(); + } + + /// + /// Sets the user image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image updated. + /// User does not have permission to delete the image. + /// A . + [HttpPost("Users/{userId}/Images/{imageType}/{index}")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task PostUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int index) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) { @@ -131,8 +176,7 @@ namespace Jellyfin.Api.Controllers /// Image deleted. /// User does not have permission to delete the image. /// A . - [HttpDelete("Users/{userId}/Images/{itemType}")] - [HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")] + [HttpDelete("Users/{userId}/Images/{imageType}")] [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")] @@ -141,7 +185,46 @@ namespace Jellyfin.Api.Controllers public async Task DeleteUserImage( [FromRoute, Required] Guid userId, [FromRoute, Required] ImageType imageType, - [FromRoute] int? index = null) + [FromQuery] int? index = null) + { + if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) + { + return Forbid("User is not allowed to delete the image."); + } + + var user = _userManager.GetUserById(userId); + try + { + System.IO.File.Delete(user.ProfileImage.Path); + } + catch (IOException e) + { + _logger.LogError(e, "Error deleting user profile image:"); + } + + await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Delete the user's image. + /// + /// User Id. + /// (Unused) Image type. + /// (Unused) Image index. + /// Image deleted. + /// User does not have permission to delete the image. + /// A . + [HttpDelete("Users/{userId}/Images/{imageType}/{index}")] + [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)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task DeleteUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int index) { if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true)) { @@ -172,14 +255,13 @@ 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?}", Name = "DeleteItemImage_2")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteItemImage( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -191,6 +273,65 @@ namespace Jellyfin.Api.Controllers return NoContent(); } + /// + /// Delete an item's image. + /// + /// Item id. + /// Image type. + /// The image index. + /// Image deleted. + /// Item not found. + /// A on success, or a if item not found. + [HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteItemImageByIndex( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false); + return NoContent(); + } + + /// + /// Set item image. + /// + /// Item id. + /// Image type. + /// Image saved. + /// Item not found. + /// A on success, or a if item not found. + [HttpPost("Items/{itemId}/Images/{imageType}")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] + public async Task SetItemImage( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + // Handle image/png; charset=utf-8 + var mimeType = Request.ContentType.Split(';').FirstOrDefault(); + await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); + + return NoContent(); + } + /// /// Set item image. /// @@ -200,16 +341,15 @@ namespace Jellyfin.Api.Controllers /// Image saved. /// Item not found. /// A on success, or a if item not found. - [HttpPost("Items/{itemId}/Images/{imageType}")] - [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")] + [HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] - public async Task SetItemImage( + public async Task SetItemImageByIndex( [FromRoute, Required] Guid itemId, [FromRoute, Required] ImageType imageType, - [FromRoute] int? imageIndex = null) + [FromRoute] int imageIndex) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -349,8 +489,6 @@ namespace Jellyfin.Api.Controllers /// [HttpGet("Items/{itemId}/Images/{imageType}")] [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)] [ProducesImageFile] @@ -371,7 +509,86 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + itemId, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Gets the item's image. + /// + /// Item id. + /// Image type. + /// Image index. + /// The maximum image width to return. + /// The maximum image height to return. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. The of the returned image. + /// Optional. Add a played indicator. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")] + [HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetItemImageByIndex( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] ImageType imageType, + [FromRoute] int imageIndex, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] string? tag, + [FromQuery] bool? cropWhitespace, + [FromQuery] ImageFormat? format, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetItemById(itemId); if (item == null) @@ -507,8 +724,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")] + [HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] @@ -586,8 +803,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")] + [HttpGet("Genres/{name}/Images/{imageType}")] + [HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] @@ -608,7 +825,86 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetGenre(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get genre image by name. + /// + /// Genre name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetGenreImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetGenre(name); if (item == null) @@ -665,8 +961,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")] + [HttpGet("MusicGenres/{name}/Images/{imageType}")] + [HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] @@ -687,7 +983,86 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetMusicGenre(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get music genre image by name. + /// + /// Music genre name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetMusicGenreImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetMusicGenre(name); if (item == null) @@ -744,8 +1119,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")] + [HttpGet("Persons/{name}/Images/{imageType}")] + [HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] @@ -766,7 +1141,86 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetPerson(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get person image by name. + /// + /// Person name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetPersonImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetPerson(name); if (item == null) @@ -823,16 +1277,16 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")] + [HttpGet("Studios/{name}/Images/{imageType}")] + [HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] public async Task GetStudioImage( [FromRoute, Required] string name, [FromRoute, Required] ImageType imageType, - [FromRoute, Required] string tag, - [FromRoute, Required] ImageFormat format, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, [FromQuery] int? maxWidth, [FromQuery] int? maxHeight, [FromQuery] double? percentPlayed, @@ -845,7 +1299,86 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) + { + var item = _libraryManager.GetStudio(name); + if (item == null) + { + return NotFound(); + } + + return await GetImageInternal( + item.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + item, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase)) + .ConfigureAwait(false); + } + + /// + /// Get studio image by name. + /// + /// Studio name. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")] + [HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetStudioImageByIndex( + [FromRoute, Required] string name, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) { var item = _libraryManager.GetStudio(name); if (item == null) @@ -902,8 +1435,8 @@ namespace Jellyfin.Api.Controllers /// A containing the file stream on success, /// or a if item not found. /// - [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")] - [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")] + [HttpGet("Users/{userId}/Images/{imageType}")] + [HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesImageFile] @@ -924,7 +1457,104 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? blur, [FromQuery] string? backgroundColor, [FromQuery] string? foregroundLayer, - [FromRoute] int? imageIndex = null) + [FromQuery] int? imageIndex) + { + var user = _userManager.GetUserById(userId); + if (user == null) + { + return NotFound(); + } + + var info = new ItemImageInfo + { + Path = user.ProfileImage.Path, + Type = ImageType.Profile, + DateModified = user.ProfileImage.LastModified + }; + + if (width.HasValue) + { + info.Width = width.Value; + } + + if (height.HasValue) + { + info.Height = height.Value; + } + + return await GetImageInternal( + user.Id, + imageType, + imageIndex, + tag, + format, + maxWidth, + maxHeight, + percentPlayed, + unplayedCount, + width, + height, + quality, + cropWhitespace, + addPlayedIndicator, + blur, + backgroundColor, + foregroundLayer, + null, + Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase), + info) + .ConfigureAwait(false); + } + + /// + /// Get user profile image. + /// + /// User id. + /// Image type. + /// Image index. + /// Optional. Supply the cache tag from the item object to receive strong caching headers. + /// Determines the output format of the image - original,gif,jpg,png. + /// The maximum image width to return. + /// The maximum image height to return. + /// Optional. Percent to render for the percent played overlay. + /// Optional. Unplayed count overlay to render. + /// The fixed image width to return. + /// The fixed image height to return. + /// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases. + /// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art. + /// Optional. Add a played indicator. + /// Optional. Blur image. + /// Optional. Apply a background color for transparent images. + /// Optional. Apply a foreground layer on top of the image. + /// Image stream returned. + /// Item not found. + /// + /// A containing the file stream on success, + /// or a if item not found. + /// + [HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")] + [HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesImageFile] + public async Task GetUserImageByIndex( + [FromRoute, Required] Guid userId, + [FromRoute, Required] ImageType imageType, + [FromRoute, Required] int imageIndex, + [FromQuery] string? tag, + [FromQuery] ImageFormat? format, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] double? percentPlayed, + [FromQuery] int? unplayedCount, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? quality, + [FromQuery] bool? cropWhitespace, + [FromQuery] bool? addPlayedIndicator, + [FromQuery] int? blur, + [FromQuery] string? backgroundColor, + [FromQuery] string? foregroundLayer) { var user = _userManager.GetUserById(userId); if (user == null) diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index d17a26db43..efb0ae19bf 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("Artists/InstantMix")] + [HttpGet("Artists/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromArtists( [FromRoute, Required] Guid id, @@ -242,7 +242,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The image types to include in the output. /// Instant playlist returned. /// A with the playlist items. - [HttpGet("MusicGenres/InstantMix")] + [HttpGet("MusicGenres/{id}/InstantMix")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetInstantMixFromMusicGenres( [FromRoute, Required] Guid id, diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index d8d371ebc3..63985c72af 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -60,7 +60,6 @@ namespace Jellyfin.Api.Controllers /// /// Gets items based on a query. /// - /// The user id supplied in the /Users/{uid}/Items. /// The user id supplied as query parameter. /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). /// Optional filter by items with theme songs. @@ -143,10 +142,8 @@ namespace Jellyfin.Api.Controllers /// Optional, include image information in output. /// A with the items. [HttpGet("Items")] - [HttpGet("Users/{uId}/Items", Name = "GetItems_2")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetItems( - [FromRoute] Guid? uId, [FromQuery] Guid? userId, [FromQuery] string? maxOfficialRating, [FromQuery] bool? hasThemeSong, @@ -228,9 +225,6 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - // use user id route parameter over query parameter - userId = uId ?? userId; - var user = userId.HasValue && !userId.Equals(Guid.Empty) ? _userManager.GetUserById(userId.Value) : null; @@ -505,6 +499,257 @@ namespace Jellyfin.Api.Controllers return new QueryResult { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) }; } + /// + /// Gets items based on a query. + /// + /// The user id supplied as query parameter. + /// Optional filter by maximum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items with theme songs. + /// Optional filter by items with theme videos. + /// Optional filter by items with subtitles. + /// Optional filter by items with special features. + /// Optional filter by items with trailers. + /// Optional. Return items that are siblings of a supplied item. + /// Optional filter by parent index number. + /// Optional filter by items that have or do not have a parental rating. + /// Optional filter by items that are HD or not. + /// Optional filter by items that are 4K or not. + /// Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted. + /// Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted. + /// Optional filter by items that are missing episodes or not. + /// Optional filter by items that are unaired episodes or not. + /// Optional filter by minimum community rating. + /// Optional filter by minimum critic rating. + /// Optional. The minimum premiere date. Format = ISO. + /// Optional. The minimum last saved date. Format = ISO. + /// Optional. The minimum last saved date for the current user. Format = ISO. + /// Optional. The maximum premiere date. Format = ISO. + /// Optional filter by items that have an overview or not. + /// Optional filter by items that have an imdb id or not. + /// Optional filter by items that have a tmdb id or not. + /// Optional filter by items that have a tvdb id or not. + /// Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. + /// Optional. The maximum number of records to return. + /// When searching within folders, this determines whether or not the search will be recursive. true/false. + /// Optional. Filter based on a search term. + /// Sort Order - Ascending,Descending. + /// Specify this to localize the search to a specific item or folder. Omit to use the root. + /// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines. + /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted. + /// Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted. + /// Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional filter by items that are marked as favorite, or not. + /// Optional filter by MediaType. Allows multiple, comma delimited. + /// Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited. + /// Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime. + /// Optional filter by items that are played, or not. + /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted. + /// Optional, include user data. + /// Optional, the max number of images to return, per image type. + /// Optional. The image types to include in the output. + /// Optional. If specified, results will be filtered to include only those containing the specified person. + /// Optional. If specified, results will be filtered to include only those containing the specified person id. + /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited. + /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered to include only those containing the specified artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified album artist id. + /// Optional. If specified, results will be filtered to include only those containing the specified contributing artist id. + /// Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted. + /// Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited. + /// Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted. + /// Optional filter by minimum official rating (PG, PG-13, TV-MA, etc). + /// Optional filter by items that are locked. + /// Optional filter by items that are placeholders. + /// Optional filter by items that have official ratings. + /// Whether or not to hide items behind their boxsets. + /// Optional. Filter by the minimum width of the item. + /// Optional. Filter by the minimum height of the item. + /// Optional. Filter by the maximum width of the item. + /// Optional. Filter by the maximum height of the item. + /// Optional filter by items that are 3D, or not. + /// Optional filter by Series Status. Allows multiple, comma delimeted. + /// Optional filter by items whose name is sorted equally or greater than a given input string. + /// Optional filter by items whose name is sorted equally than a given input string. + /// Optional filter by items whose name is equally or lesser than a given input string. + /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted. + /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted. + /// Optional. Enable the total record count. + /// Optional, include image information in output. + /// A with the items. + [HttpGet("Users/{userId}/Items")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetItemsByUserId( + [FromRoute] Guid userId, + [FromQuery] string? maxOfficialRating, + [FromQuery] bool? hasThemeSong, + [FromQuery] bool? hasThemeVideo, + [FromQuery] bool? hasSubtitles, + [FromQuery] bool? hasSpecialFeature, + [FromQuery] bool? hasTrailer, + [FromQuery] string? adjacentTo, + [FromQuery] int? parentIndexNumber, + [FromQuery] bool? hasParentalRating, + [FromQuery] bool? isHd, + [FromQuery] bool? is4K, + [FromQuery] string? locationTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, + [FromQuery] bool? isMissing, + [FromQuery] bool? isUnaired, + [FromQuery] double? minCommunityRating, + [FromQuery] double? minCriticRating, + [FromQuery] DateTime? minPremiereDate, + [FromQuery] DateTime? minDateLastSaved, + [FromQuery] DateTime? minDateLastSavedForUser, + [FromQuery] DateTime? maxPremiereDate, + [FromQuery] bool? hasOverview, + [FromQuery] bool? hasImdbId, + [FromQuery] bool? hasTmdbId, + [FromQuery] bool? hasTvdbId, + [FromQuery] string? excludeItemIds, + [FromQuery] int? startIndex, + [FromQuery] int? limit, + [FromQuery] bool? recursive, + [FromQuery] string? searchTerm, + [FromQuery] string? sortOrder, + [FromQuery] string? parentId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, + [FromQuery] string? excludeItemTypes, + [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, + [FromQuery] bool? isFavorite, + [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, + [FromQuery] string? sortBy, + [FromQuery] bool? isPlayed, + [FromQuery] string? genres, + [FromQuery] string? officialRatings, + [FromQuery] string? tags, + [FromQuery] string? years, + [FromQuery] bool? enableUserData, + [FromQuery] int? imageTypeLimit, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, + [FromQuery] string? person, + [FromQuery] string? personIds, + [FromQuery] string? personTypes, + [FromQuery] string? studios, + [FromQuery] string? artists, + [FromQuery] string? excludeArtistIds, + [FromQuery] string? artistIds, + [FromQuery] string? albumArtistIds, + [FromQuery] string? contributingArtistIds, + [FromQuery] string? albums, + [FromQuery] string? albumIds, + [FromQuery] string? ids, + [FromQuery] string? videoTypes, + [FromQuery] string? minOfficialRating, + [FromQuery] bool? isLocked, + [FromQuery] bool? isPlaceHolder, + [FromQuery] bool? hasOfficialRating, + [FromQuery] bool? collapseBoxSetItems, + [FromQuery] int? minWidth, + [FromQuery] int? minHeight, + [FromQuery] int? maxWidth, + [FromQuery] int? maxHeight, + [FromQuery] bool? is3D, + [FromQuery] string? seriesStatus, + [FromQuery] string? nameStartsWithOrGreater, + [FromQuery] string? nameStartsWith, + [FromQuery] string? nameLessThan, + [FromQuery] string? studioIds, + [FromQuery] string? genreIds, + [FromQuery] bool enableTotalRecordCount = true, + [FromQuery] bool? enableImages = true) + { + return GetItems( + userId, + maxOfficialRating, + hasThemeSong, + hasThemeVideo, + hasSubtitles, + hasSpecialFeature, + hasTrailer, + adjacentTo, + parentIndexNumber, + hasParentalRating, + isHd, + is4K, + locationTypes, + excludeLocationTypes, + isMissing, + isUnaired, + minCommunityRating, + minCriticRating, + minPremiereDate, + minDateLastSaved, + minDateLastSavedForUser, + maxPremiereDate, + hasOverview, + hasImdbId, + hasTmdbId, + hasTvdbId, + excludeItemIds, + startIndex, + limit, + recursive, + searchTerm, + sortOrder, + parentId, + fields, + excludeItemTypes, + includeItemTypes, + filters, + isFavorite, + mediaTypes, + imageTypes, + sortBy, + isPlayed, + genres, + officialRatings, + tags, + years, + enableUserData, + imageTypeLimit, + enableImageTypes, + person, + personIds, + personTypes, + studios, + artists, + excludeArtistIds, + artistIds, + albumArtistIds, + contributingArtistIds, + albums, + albumIds, + ids, + videoTypes, + minOfficialRating, + isLocked, + isPlaceHolder, + hasOfficialRating, + collapseBoxSetItems, + minWidth, + minHeight, + maxWidth, + maxHeight, + is3D, + seriesStatus, + nameStartsWithOrGreater, + nameStartsWith, + nameLessThan, + studioIds, + genreIds, + enableTotalRecordCount, + enableImages); + } + /// /// Gets items based on a query. /// diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index a01ae31a0e..dcb8e803b8 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -193,7 +193,6 @@ 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}", Name = "GetSubtitle_2")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesFile("text/*")] public async Task GetSubtitle( @@ -204,7 +203,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] long? endPositionTicks, [FromQuery] bool copyTimestamps = false, [FromQuery] bool addVttTimeMap = false, - [FromRoute] long startPositionTicks = 0) + [FromQuery] long startPositionTicks = 0) { if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase)) { @@ -249,6 +248,43 @@ namespace Jellyfin.Api.Controllers MimeTypes.GetMimeType("file." + format)); } + /// + /// Gets subtitles in a specified format. + /// + /// The item id. + /// The media source id. + /// The subtitle stream index. + /// Optional. The start position of the subtitle in ticks. + /// The format of the returned subtitle. + /// Optional. The end position of the subtitle in ticks. + /// Optional. Whether to copy the timestamps. + /// Optional. Whether to add a VTT time map. + /// File returned. + /// A with the subtitle file. + [HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile("text/*")] + public Task GetSubtitleWithTicks( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string mediaSourceId, + [FromRoute, Required] int index, + [FromRoute, Required] long startPositionTicks, + [FromRoute, Required] string format, + [FromQuery] long? endPositionTicks, + [FromQuery] bool copyTimestamps = false, + [FromQuery] bool addVttTimeMap = false) + { + return GetSubtitle( + itemId, + mediaSourceId, + index, + format, + endPositionTicks, + copyTimestamps, + addVttTimeMap, + startPositionTicks); + } + /// /// Gets an HLS subtitle playlist. /// @@ -335,6 +371,7 @@ namespace Jellyfin.Api.Controllers /// Subtitle uploaded. /// A . [HttpPost("Videos/{itemId}/Subtitles")] + [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task UploadSubtitle( [FromRoute, Required] Guid itemId, [FromBody, Required] UploadSubtitleDto body) @@ -446,6 +483,7 @@ namespace Jellyfin.Api.Controllers [HttpGet("FallbackFont/Fonts/{name}")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesFile("font/*")] public ActionResult GetFallbackFont([FromRoute, Required] string name) { var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index d78adcbcdc..ffdbb15df4 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -197,7 +197,6 @@ namespace Jellyfin.Api.Controllers return _itemsController .GetItems( - userId, userId, maxOfficialRating, hasThemeSong, diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 418c0c1230..c2bb0dfffe 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; @@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers /// Video or attachment not found. /// An containing the attachment stream on success, or a if the attachment could not be found. [HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")] - [Produces(MediaTypeNames.Application.Octet)] + [ProducesFile(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetAttachment( diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 4de7aac71d..1a23076f14 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -326,15 +326,13 @@ namespace Jellyfin.Api.Controllers /// Optional. The streaming options. /// Video stream returned. /// A containing the audio file. - [HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")] [HttpGet("{itemId}/stream")] - [HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")] [HttpHead("{itemId}/stream", Name = "HeadVideoStream")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesVideoFile] public async Task GetVideoStream( [FromRoute, Required] Guid itemId, - [FromRoute] string? container, + [FromQuery] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, @@ -529,5 +527,166 @@ namespace Jellyfin.Api.Controllers _transcodingJobType, cancellationTokenSource).ConfigureAwait(false); } + + /// + /// Gets a video stream. + /// + /// The item id. + /// The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. + /// Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false. + /// The streaming parameters. + /// The tag. + /// Optional. The dlna device profile id to utilize. + /// The play session id. + /// The segment container. + /// The segment lenght. + /// The minimum number of segments. + /// The media version id, if playing an alternate version. + /// The device id of the client requesting. Used to stop encoding processes when needed. + /// Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma. + /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true. + /// Whether or not to allow copying of the video stream url. + /// Whether or not to allow copying of the audio stream url. + /// Optional. Whether to break on non key frames. + /// Optional. Specify a specific audio sample rate, e.g. 44100. + /// Optional. The maximum audio bit depth. + /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults. + /// Optional. Specify a specific number of audio channels to encode to, e.g. 2. + /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2. + /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high. + /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1. + /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements. + /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false. + /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms. + /// Optional. The fixed horizontal resolution of the encoded video. + /// Optional. The fixed vertical resolution of the encoded video. + /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults. + /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used. + /// Optional. Specify the subtitle delivery method. + /// Optional. + /// Optional. The maximum video bit depth. + /// Optional. Whether to require avc. + /// Optional. Whether to deinterlace the video. + /// Optional. Whether to require a non anamporphic stream. + /// Optional. The maximum number of audio channels to transcode. + /// Optional. The limit of how many cpu cores to use. + /// The live stream id. + /// Optional. Whether to enable the MpegtsM2Ts mode. + /// Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv. + /// Optional. Specify a subtitle codec to encode to. + /// Optional. The transcoding reason. + /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used. + /// Optional. The index of the video stream to use. If omitted the first video stream will be used. + /// Optional. The . + /// Optional. The streaming options. + /// Video stream returned. + /// A containing the audio file. + [HttpGet("{itemId}/{stream=stream}.{container}")] + [HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesVideoFile] + public Task GetVideoStreamByContainer( + [FromRoute, Required] Guid itemId, + [FromRoute, Required] string container, + [FromQuery] bool? @static, + [FromQuery] string? @params, + [FromQuery] string? tag, + [FromQuery] string? deviceProfileId, + [FromQuery] string? playSessionId, + [FromQuery] string? segmentContainer, + [FromQuery] int? segmentLength, + [FromQuery] int? minSegments, + [FromQuery] string? mediaSourceId, + [FromQuery] string? deviceId, + [FromQuery] string? audioCodec, + [FromQuery] bool? enableAutoStreamCopy, + [FromQuery] bool? allowVideoStreamCopy, + [FromQuery] bool? allowAudioStreamCopy, + [FromQuery] bool? breakOnNonKeyFrames, + [FromQuery] int? audioSampleRate, + [FromQuery] int? maxAudioBitDepth, + [FromQuery] int? audioBitRate, + [FromQuery] int? audioChannels, + [FromQuery] int? maxAudioChannels, + [FromQuery] string? profile, + [FromQuery] string? level, + [FromQuery] float? framerate, + [FromQuery] float? maxFramerate, + [FromQuery] bool? copyTimestamps, + [FromQuery] long? startTimeTicks, + [FromQuery] int? width, + [FromQuery] int? height, + [FromQuery] int? videoBitRate, + [FromQuery] int? subtitleStreamIndex, + [FromQuery] SubtitleDeliveryMethod subtitleMethod, + [FromQuery] int? maxRefFrames, + [FromQuery] int? maxVideoBitDepth, + [FromQuery] bool? requireAvc, + [FromQuery] bool? deInterlace, + [FromQuery] bool? requireNonAnamorphic, + [FromQuery] int? transcodingMaxAudioChannels, + [FromQuery] int? cpuCoreLimit, + [FromQuery] string? liveStreamId, + [FromQuery] bool? enableMpegtsM2TsMode, + [FromQuery] string? videoCodec, + [FromQuery] string? subtitleCodec, + [FromQuery] string? transcodingReasons, + [FromQuery] int? audioStreamIndex, + [FromQuery] int? videoStreamIndex, + [FromQuery] EncodingContext context, + [FromQuery] Dictionary streamOptions) + { + return GetVideoStream( + itemId, + container, + @static, + @params, + tag, + deviceProfileId, + playSessionId, + segmentContainer, + segmentLength, + minSegments, + mediaSourceId, + deviceId, + audioCodec, + enableAutoStreamCopy, + allowVideoStreamCopy, + allowAudioStreamCopy, + breakOnNonKeyFrames, + audioSampleRate, + maxAudioBitDepth, + audioBitRate, + audioChannels, + maxAudioChannels, + profile, + level, + framerate, + maxFramerate, + copyTimestamps, + startTimeTicks, + width, + height, + videoBitRate, + subtitleStreamIndex, + subtitleMethod, + maxRefFrames, + maxVideoBitDepth, + requireAvc, + deInterlace, + requireNonAnamorphic, + transcodingMaxAudioChannels, + cpuCoreLimit, + liveStreamId, + enableMpegtsM2TsMode, + videoCodec, + subtitleCodec, + transcodingReasons, + audioStreamIndex, + videoStreamIndex, + context, + streamOptions); + } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 13234c3813..4959a9b922 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1647,7 +1647,7 @@ namespace MediaBrowser.Model.Dlna // strip spaces to avoid having to encode var values = value - .Split('|', StringSplitOptions.RemoveEmptyEntries); + .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) { From 53697ac39a624d3a32b79883f4e0321b3a7ecb49 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 15 Nov 2020 10:59:56 -0700 Subject: [PATCH 064/257] revert --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 4959a9b922..13234c3813 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1647,7 +1647,7 @@ namespace MediaBrowser.Model.Dlna // strip spaces to avoid having to encode var values = value - .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + .Split('|', StringSplitOptions.RemoveEmptyEntries); if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) { From 81d5eb4db5918debe3194236288492f26ad135bb Mon Sep 17 00:00:00 2001 From: Ryan Petris Date: Sun, 15 Nov 2020 19:44:11 -0700 Subject: [PATCH 065/257] Return a Task object and discard it instead of using async void. --- .../LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 5a95b28b5b..4a16cda4ef 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -114,7 +114,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var taskCompletionSource = new TaskCompletionSource(); - StartStreaming( + _ = StartStreaming( udpClient, hdHomerunManager, remoteAddress, @@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await taskCompletionSource.Task.ConfigureAwait(false); } - private async void StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) + private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource openTaskCompletionSource, CancellationToken cancellationToken) { using (udpClient) using (hdHomerunManager) From 9481fd07476a99c30a0db99f4c6171e2fbc84a8f Mon Sep 17 00:00:00 2001 From: Greenback Date: Mon, 16 Nov 2020 17:17:49 +0000 Subject: [PATCH 066/257] Upgraded to .Net5 --- MediaBrowser.Common/Configuration/IConfigurationManager.cs | 2 ++ MediaBrowser.Common/Net/IPNetAddress.cs | 4 ++-- MediaBrowser.Common/Net/IPObject.cs | 2 +- MediaBrowser.Controller/IServerApplicationHost.cs | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index 4c2b3a742a..790121274e 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -56,6 +56,8 @@ namespace MediaBrowser.Common.Configuration /// /// Gets the configuration. /// + /// /// The key. + /// System.Object. object GetConfiguration(string key); /// diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs index bcd049f3d5..0d28c35cb8 100644 --- a/MediaBrowser.Common/Net/IPNetAddress.cs +++ b/MediaBrowser.Common/Net/IPNetAddress.cs @@ -106,7 +106,7 @@ namespace MediaBrowser.Common.Net addr = addr.Trim(); // Try to parse it as is. - if (IPAddress.TryParse(addr, out IPAddress res)) + if (IPAddress.TryParse(addr, out IPAddress? res)) { ip = new IPNetAddress(res); return true; @@ -130,7 +130,7 @@ namespace MediaBrowser.Common.Net } // Is the subnet in x.y.a.b form? - if (IPAddress.TryParse(tokens[1], out IPAddress mask)) + if (IPAddress.TryParse(tokens[1], out IPAddress? mask)) { ip = new IPNetAddress(res, MaskToCidr(mask)); return true; diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs index a08694c266..36f3357cce 100644 --- a/MediaBrowser.Common/Net/IPObject.cs +++ b/MediaBrowser.Common/Net/IPObject.cs @@ -381,7 +381,7 @@ namespace MediaBrowser.Common.Net } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return Equals(obj as IPObject); } diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index eced9bf697..2456da826f 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller { From 979de240cb758acc7866095e6a50ed5ccc6f1d38 Mon Sep 17 00:00:00 2001 From: Greenback Date: Mon, 16 Nov 2020 17:30:27 +0000 Subject: [PATCH 067/257] Updated tests to .Net5 --- Emby.Dlna/Main/DlnaEntryPoint.cs | 3 - .../Jellyfin.Networking.csproj | 2 +- .../Jellyfin.Networking.Tests.csproj | 21 ++++- .../NetworkTesting/UnitTesting.cs | 92 +++++++++++++++---- 4 files changed, 93 insertions(+), 25 deletions(-) diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 463ed8d67a..fb4454a343 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -259,8 +259,6 @@ namespace Emby.Dlna.Main private void RegisterServerEndpoints() { - var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false); - var udn = CreateUuid(_appHost.SystemId); var descriptorUri = "/dlna/" + udn + "/description.xml"; @@ -292,7 +290,6 @@ namespace Emby.Dlna.Main _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); - var descriptorUri = "/dlna/" + udn + "/description.xml"; var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); var device = new SsdpRootDevice diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj index 1747a1dc7a..cbda74361f 100644 --- a/Jellyfin.Networking/Jellyfin.Networking.csproj +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -1,6 +1,6 @@ - netstandard2.1 + net5.0 false true true diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj index fa18316dff..703d43210b 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj @@ -6,23 +6,34 @@ - netcoreapp3.1 + net5.0 false - true enable + true - + - + + + + + + + - + + ../../jellyfin-tests.ruleset + + + DEBUG + diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs index c96c7defd7..03b733a9ab 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/UnitTesting.cs @@ -43,8 +43,7 @@ namespace NetworkTesting return false; } - - private IConfigurationManager GetMockConfig(NetworkConfiguration conf) + private static IConfigurationManager GetMockConfig(NetworkConfiguration conf) { var configManager = new Mock { @@ -64,11 +63,11 @@ namespace NetworkTesting { EnableIPV6 = true, EnableIPV4 = true, - LocalNetworkSubnets = lan.Split(';') + LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan)) }; NetworkManager.MockNetworkSettings = interfaces; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; Assert.True(string.Equals(nm.GetInternalBindAddresses().AsString(), value, StringComparison.Ordinal)); @@ -78,6 +77,11 @@ namespace NetworkTesting [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")] public void TextIsInNetwork(string network, string value) { + if (network == null) + { + throw new ArgumentNullException(nameof(network)); + } + var conf = new NetworkConfiguration() { EnableIPV6 = true, @@ -85,7 +89,7 @@ namespace NetworkTesting LocalNetworkSubnets = network.Split(',') }; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); Assert.True(!nm.IsInLocalNetwork(value)); } @@ -153,16 +157,21 @@ namespace NetworkTesting "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")] public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5) { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + var conf = new NetworkConfiguration() { EnableIPV6 = true, EnableIPV4 = true, }; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); // Test included, IP6. - NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); + NetCollection nc = nm.CreateIPCollection(settings.Split(","), false); Assert.True(string.Equals(nc?.AsString(), result1, System.StringComparison.OrdinalIgnoreCase)); // Text excluded, non IP6. @@ -194,13 +203,29 @@ namespace NetworkTesting [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")] public void UnionCheck(string settings, string compare, string result) { + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (compare == null) + { + throw new ArgumentNullException(nameof(compare)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + var conf = new NetworkConfiguration() { EnableIPV6 = true, EnableIPV4 = true, }; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetCollection nc1 = nm.CreateIPCollection(settings.Split(","), false); NetCollection nc2 = nm.CreateIPCollection(compare.Split(","), false); @@ -276,12 +301,7 @@ namespace NetworkTesting { Assert.True(TryParse(network, out IPObject? networkObj)); Assert.True(TryParse(ip, out IPObject? ipObj)); - -#pragma warning disable CS8602 // Dereference of a possibly null reference. -#pragma warning disable CS8604 // Possible null reference argument. Assert.True(networkObj.Contains(ipObj)); -#pragma warning restore CS8604 // Possible null reference argument. -#pragma warning restore CS8602 // Dereference of a possibly null reference. } [Theory] @@ -293,13 +313,28 @@ namespace NetworkTesting public void TestMatches(string source, string dest, string result) { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (dest == null) + { + throw new ArgumentNullException(nameof(dest)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + var conf = new NetworkConfiguration() { EnableIPV6 = true, EnableIPV4 = true }; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); // Test included, IP6. NetCollection ncSource = nm.CreateIPCollection(source.Split(",")); @@ -343,6 +378,21 @@ namespace NetworkTesting [InlineData("", "", false, "eth16")] public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result) { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (bindAddresses == null) + { + throw new ArgumentNullException(nameof(bindAddresses)); + } + + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + var conf = new NetworkConfiguration() { LocalNetworkAddresses = bindAddresses.Split(','), @@ -351,7 +401,7 @@ namespace NetworkTesting }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; _ = nm.TryParseInterface(result, out NetCollection? resultObj); @@ -398,6 +448,16 @@ namespace NetworkTesting public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) { + if (lan == null) + { + throw new ArgumentNullException(nameof(lan)); + } + + if (bindAddresses == null) + { + throw new ArgumentNullException(nameof(bindAddresses)); + } + var conf = new NetworkConfiguration() { LocalNetworkSubnets = lan.Split(','), @@ -408,7 +468,7 @@ namespace NetworkTesting }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11"; - var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; if (nm.TryParseInterface(result, out NetCollection? resultObj) && resultObj != null) From 978aa38f3bd19a55ed4f3587bf5fd7d583c317f5 Mon Sep 17 00:00:00 2001 From: Greenback Date: Mon, 16 Nov 2020 19:37:38 +0000 Subject: [PATCH 068/257] Updated PR1 code. --- .../Configuration/NetworkConfiguration.cs | 50 +++---- Jellyfin.Networking/Manager/NetworkManager.cs | 141 +++++++++--------- MediaBrowser.Common/Net/INetworkManager.cs | 38 ++--- MediaBrowser.Common/Net/IPHost.cs | 33 ++-- MediaBrowser.Common/Net/IPObject.cs | 56 +++---- MediaBrowser.Common/Net/NetworkExtensions.cs | 70 ++++----- 6 files changed, 188 insertions(+), 200 deletions(-) diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs index 301e432511..e710eb3c76 100644 --- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -11,12 +11,12 @@ namespace Jellyfin.Networking.Configuration public class NetworkConfiguration { /// - /// Gets the default http port. + /// The default value for . /// public const int DefaultHttpPort = 8096; /// - /// Gets the default https port. + /// The default value for and . /// public const int DefaultHttpsPort = 8920; @@ -94,124 +94,124 @@ namespace Jellyfin.Networking.Configuration 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.. + /// 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; } /// /// Gets or sets the UDPPortRange - /// Gets or sets client udp port range.. + /// 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.. + /// Gets or sets a value indicating whether gets or sets IPV6 capability. /// public bool EnableIPV6 { get; set; } /// - /// Gets or sets a value indicating whether gets or sets IPV4 capability.. + /// 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. - /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work.. + /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work. /// public bool EnableSSDPTracing { get; set; } /// /// Gets or sets the SSDPTracingFilter /// 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.. + /// 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.. + /// 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).. + /// Gets or sets the delay between each groups of SSDP messages (in ms). /// public int UDPSendDelay { get; set; } = 100; /// - /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding.. + /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding. /// public bool IgnoreVirtualInterfaces { get; set; } = true; /// /// Gets or sets the VirtualInterfaceNames - /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. .. + /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. . /// public string VirtualInterfaceNames { get; set; } = "vEthernet*"; /// - /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor.. + /// 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 multi-socket binding is available.. + /// Gets a value indicating whether multi-socket binding is 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.. + /// Depending on the address range implemented ULA ranges might not be used. /// public bool TrustAllIP6Interfaces { get; set; } /// - /// Gets or sets the ports that HDHomerun uses.. + /// Gets or sets the ports that HDHomerun uses. /// public string HDHomerunPortRange { get; set; } = string.Empty; /// /// Gets or sets the PublishedServerUriBySubnet - /// Gets or sets PublishedServerUri to advertise for specific subnets.. + /// Gets or sets PublishedServerUri to advertise for specific subnets. /// public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty(); /// - /// Gets or sets a value indicating whether Autodiscovery tracing is enabled.. + /// Gets or sets a value indicating whether Autodiscovery tracing is enabled. /// public bool AutoDiscoveryTracing { get; set; } /// - /// Gets or sets a value indicating whether Autodiscovery is enabled.. + /// Gets or sets a value indicating whether Autodiscovery is enabled. /// public bool AutoDiscovery { get; set; } = true; /// - /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with .. + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . /// public string[] RemoteIPFilter { get; set; } = Array.Empty(); /// - /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist.. + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. /// public bool IsRemoteIPFilterBlacklist { get; set; } /// - /// Gets or sets a value indicating whether to enable automatic port forwarding.. + /// Gets or sets a value indicating whether to enable automatic port forwarding. /// public bool EnableUPnP { get; set; } /// - /// Gets or sets a value indicating whether access outside of the LAN is permitted.. + /// Gets or sets a value indicating whether access outside of the LAN is permitted. /// public bool EnableRemoteAccess { get; set; } = true; /// - /// Gets or sets the subnets that are deemed to make up the LAN.. + /// Gets or sets the subnets that are deemed to make up the LAN. /// public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); /// - /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.. + /// 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(); diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index 289b1dc720..00711f1620 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -1,7 +1,6 @@ -#pragma warning disable CA1021 // Avoid out parameters - using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; @@ -13,14 +12,12 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using NetCollection = System.Collections.ObjectModel.Collection; namespace Jellyfin.Networking.Manager { /// /// Class to take care of network interface management. - /// - /// Note: The normal collection methods and properties will not work with NetCollection. . + /// Note: The normal collection methods and properties will not work with Collection{IPObject}. . /// public class NetworkManager : INetworkManager, IDisposable { @@ -37,7 +34,7 @@ namespace Jellyfin.Networking.Manager /// /// List of all interface addresses and masks. /// - private readonly NetCollection _interfaceAddresses; + private readonly Collection _interfaceAddresses; /// /// List of all interface MAC addresses. @@ -61,30 +58,30 @@ namespace Jellyfin.Networking.Manager private bool _eventfire; /// - /// Unfiltered user defined LAN subnets. (Configuration.LocalNetworkSubnets). + /// Unfiltered user defined LAN subnets. () /// or internal interface network subnets if undefined by user. /// - private NetCollection _lanSubnets; + private Collection _lanSubnets; /// /// User defined list of subnets to excluded from the LAN. /// - private NetCollection _excludedSubnets; + private Collection _excludedSubnets; /// /// List of interface addresses to bind the WS. /// - private NetCollection _bindAddresses; + private Collection _bindAddresses; /// /// List of interface addresses to exclude from bind. /// - private NetCollection _bindExclusions; + private Collection _bindExclusions; /// /// Caches list of all internal filtered interface addresses and masks. /// - private NetCollection _internalInterfaces; + private Collection _internalInterfaces; /// /// Flag set when no custom LAN has been defined in the config. @@ -107,7 +104,7 @@ namespace Jellyfin.Networking.Manager _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager)); - _interfaceAddresses = new NetCollection(); + _interfaceAddresses = new Collection(); _macAddresses = new List(); _interfaceNames = new Dictionary(); _publishedServerUrls = new Dictionary(); @@ -143,7 +140,7 @@ namespace Jellyfin.Networking.Manager public bool IsIP4Enabled { get; set; } /// - public NetCollection RemoteAddressFilter { get; private set; } + public Collection RemoteAddressFilter { get; private set; } /// /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. @@ -160,9 +157,9 @@ namespace Jellyfin.Networking.Manager /// /// Items to assign the collection, or null. /// The collection created. - public static NetCollection CreateCollection(IEnumerable? source = null) + public static Collection CreateCollection(IEnumerable? source = null) { - var result = new NetCollection(); + var result = new Collection(); if (source != null) { foreach (var item in source) @@ -189,22 +186,22 @@ namespace Jellyfin.Networking.Manager } /// - public bool IsGatewayInterface(object? addressObj) + public bool IsGatewayInterface(IPObject? addressObj) { - var address = addressObj switch - { - IPAddress addressIp => addressIp, - IPObject addressIpObj => addressIpObj.Address, - _ => IPAddress.None - }; - + var address = addressObj?.Address ?? IPAddress.None; return _internalInterfaces.Any(i => i.Address.Equals(address) && i.Tag < 0); } /// - public NetCollection GetLoopbacks() + public bool IsGatewayInterface(IPAddress? addressObj) { - NetCollection nc = new NetCollection(); + return _internalInterfaces.Any(i => i.Address.Equals(addressObj ?? IPAddress.None) && i.Tag < 0); + } + + /// + public Collection GetLoopbacks() + { + Collection nc = new Collection(); if (IsIP4Enabled) { nc.AddItem(IPAddress.Loopback); @@ -231,9 +228,9 @@ namespace Jellyfin.Networking.Manager } /// - public NetCollection CreateIPCollection(string[] values, bool bracketed = false) + public Collection CreateIPCollection(string[] values, bool bracketed = false) { - NetCollection col = new NetCollection(); + Collection col = new Collection(); if (values == null) { return col; @@ -256,7 +253,7 @@ namespace Jellyfin.Networking.Manager { if (bracketed) { - AddToCollection(col, v.Substring(1)); + AddToCollection(col, v[1..]); } } else if (!bracketed) @@ -266,7 +263,7 @@ namespace Jellyfin.Networking.Manager } catch (ArgumentException e) { - _logger.LogInformation("Ignoring LAN value {value}. Reason : {reason}", v, e.Message); + _logger.LogWarning(e, "Ignoring LAN value {value}.", v); } } @@ -274,7 +271,7 @@ namespace Jellyfin.Networking.Manager } /// - public NetCollection GetAllBindInterfaces(bool individualInterfaces = false) + public Collection GetAllBindInterfaces(bool individualInterfaces = false) { int count = _bindAddresses.Count; @@ -288,11 +285,11 @@ namespace Jellyfin.Networking.Manager if (individualInterfaces) { - return new NetCollection(_interfaceAddresses); + return new Collection(_interfaceAddresses); } // No bind address and no exclusions, so listen on all interfaces. - NetCollection result = new NetCollection(); + Collection result = new Collection(); if (IsIP4Enabled) { @@ -376,7 +373,7 @@ namespace Jellyfin.Networking.Manager if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) { - _logger.LogInformation("{0}: Using BindAddress {1}:{2}", source, res, port); + _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); return res; } } @@ -429,7 +426,7 @@ namespace Jellyfin.Networking.Manager } /// - public NetCollection GetInternalBindAddresses() + public Collection GetInternalBindAddresses() { int count = _bindAddresses.Count; @@ -445,7 +442,7 @@ namespace Jellyfin.Networking.Manager return CreateCollection(_internalInterfaces.Where(p => !p.IsLoopback())); } - return new NetCollection(_bindAddresses); + return new Collection(_bindAddresses); } /// @@ -526,7 +523,7 @@ namespace Jellyfin.Networking.Manager } /// - public NetCollection GetFilteredLANSubnets(NetCollection? filter = null) + public Collection GetFilteredLANSubnets(Collection? filter = null) { if (filter == null) { @@ -543,7 +540,7 @@ namespace Jellyfin.Networking.Manager } /// - public bool TryParseInterface(string token, out NetCollection? result) + public bool TryParseInterface(string token, out Collection? result) { result = null; if (string.IsNullOrEmpty(token)) @@ -553,16 +550,16 @@ namespace Jellyfin.Networking.Manager if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) { - result = new NetCollection(); + result = new Collection(); _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))) + if (Math.Abs(iface.Tag) == index + && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) + || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { result.AddItem(iface); } @@ -577,8 +574,8 @@ namespace Jellyfin.Networking.Manager /// /// Reloads all settings and re-initialises the instance. /// - /// The configuration to use. - public void UpdateSettings(object configuration) + /// The to use. + public void UpdateSettings(NetworkConfiguration configuration) { NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); @@ -622,7 +619,7 @@ namespace Jellyfin.Networking.Manager /// /// Protected implementation of Dispose pattern. /// - /// True to dispose the managed state. + /// True to dispose the managed state. protected virtual void Dispose(bool disposing) { if (!_disposed) @@ -639,11 +636,11 @@ namespace Jellyfin.Networking.Manager } /// - /// Trys to identify the string and return an object of that class. + /// Tries to identify the string and return an object of that class. /// /// String to parse. /// IPObject to return. - /// True if the value parsed successfully. + /// true if the value parsed successfully, false otherwise. private static bool TryParse(string addr, out IPObject result) { if (!string.IsNullOrEmpty(addr)) @@ -671,7 +668,7 @@ namespace Jellyfin.Networking.Manager /// Ipv6 addresses are returned in [ ], with their scope removed. /// /// Address to convert. - /// URI save conversion of the address. + /// URI safe conversion of the address. private static string FormatIP6String(IPAddress address) { var str = address.ToString(); @@ -694,7 +691,7 @@ namespace Jellyfin.Networking.Manager { if (evt.Key.Equals("network", StringComparison.Ordinal)) { - UpdateSettings(evt.NewConfiguration); + UpdateSettings((NetworkConfiguration)evt.NewConfiguration); } } @@ -703,7 +700,7 @@ namespace Jellyfin.Networking.Manager /// /// String to check. /// Interface index number. - /// True if an interface name matches the token. + /// true if an interface name matches the token, False otherwise. private bool IsInterface(string token, out int index) { index = -1; @@ -720,8 +717,8 @@ namespace Jellyfin.Networking.Manager foreach ((string interfc, int interfcIndex) in _interfaceNames) { - if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase)) || - (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture))) + if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase)) + || (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture))) { index = interfcIndex; return true; @@ -733,11 +730,11 @@ namespace Jellyfin.Networking.Manager } /// - /// Parses strings into the collection, replacing any interface references. + /// Parses a string and adds it into the the collection, replacing any interface references. /// - /// Collection. - /// String to parse. - private void AddToCollection(NetCollection col, string token) + /// Collection. + /// String value to parse. + private void AddToCollection(Collection 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. @@ -748,9 +745,9 @@ namespace Jellyfin.Networking.Manager // 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))) + if (Math.Abs(iface.Tag) == index + && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork) + || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6))) { col.AddItem(iface); } @@ -791,7 +788,7 @@ namespace Jellyfin.Networking.Manager /// Handler for network change events. /// /// Sender. - /// Network availability information. + /// A containing network availability information. private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) { _logger.LogDebug("Network availability changed."); @@ -802,7 +799,7 @@ namespace Jellyfin.Networking.Manager /// Handler for network change events. /// /// Sender. - /// Event arguments. + /// An . private void OnNetworkAddressChanged(object? sender, EventArgs e) { _logger.LogDebug("Network address change detected."); @@ -812,7 +809,7 @@ namespace Jellyfin.Networking.Manager /// /// 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. + /// A representing the asynchronous operation. private async Task OnNetworkChangeAsync() { try @@ -883,7 +880,7 @@ namespace Jellyfin.Networking.Manager { _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; } - else if (TryParseInterface(parts[0], out NetCollection? addresses) && addresses != null) + else if (TryParseInterface(parts[0], out Collection? addresses) && addresses != null) { foreach (IPNetAddress na in addresses) { @@ -903,6 +900,9 @@ namespace Jellyfin.Networking.Manager } } + /// + /// Initialises the network bind addresses. + /// private void InitialiseBind(NetworkConfiguration config) { string[] lanAddresses = config.LocalNetworkAddresses; @@ -931,6 +931,9 @@ namespace Jellyfin.Networking.Manager _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString()); } + /// + /// Initialises the remote address values. + /// private void InitialiseRemote(NetworkConfiguration config) { RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); @@ -964,7 +967,7 @@ namespace Jellyfin.Networking.Manager _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.ContainsAddress(i))); // Subnets are the same as the calculated internal interface. - _lanSubnets = new NetCollection(); + _lanSubnets = new Collection(); // We must listen on loopback for LiveTV to function regardless of the settings. if (IsIP6Enabled) @@ -1131,7 +1134,7 @@ namespace Jellyfin.Networking.Manager /// True if the source is in the external subnet. /// The published server url that matches the source address. /// The resultant port, if one exists. - /// True if a match is found. + /// true if a match is found, false otherwise. private bool MatchesPublishedServerUrl(IPObject source, bool isExternal, out string bindPreference, out int? port) { bindPreference = string.Empty; @@ -1185,7 +1188,7 @@ namespace Jellyfin.Networking.Manager /// 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. + /// true if a match is found, false otherwise. private bool MatchesBindInterface(IPObject source, bool isExternal, out string result) { result = string.Empty; @@ -1202,7 +1205,7 @@ namespace Jellyfin.Networking.Manager { // Check to see if any of the bind interfaces are in the same subnet. - NetCollection bindResult; + Collection bindResult; IPAddress? defaultGateway = null; IPAddress? bindAddress; @@ -1246,7 +1249,6 @@ namespace Jellyfin.Networking.Manager if (isExternal) { - // TODO: remove this after testing. _logger.LogWarning("{0}: External request received, however, only an internal interface bind found.", source); } @@ -1261,7 +1263,7 @@ namespace Jellyfin.Networking.Manager /// /// IP source address to use. /// The result, if a match is found. - /// True if a match is found. + /// true if a match is found, false otherwise. private bool MatchesExternalInterface(IPObject source, out string result) { result = string.Empty; @@ -1292,7 +1294,6 @@ namespace Jellyfin.Networking.Manager // 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/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index a7beabbdcb..43562afe38 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -1,10 +1,11 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Net; using System.Net.NetworkInformation; +using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; -using NetCollection = System.Collections.ObjectModel.Collection; namespace MediaBrowser.Common.Net { @@ -31,7 +32,7 @@ namespace MediaBrowser.Common.Net /// /// Gets the remote address filter. /// - NetCollection RemoteAddressFilter { get; } + Collection RemoteAddressFilter { get; } /// /// Gets or sets a value indicating whether iP6 is enabled. @@ -46,17 +47,17 @@ namespace MediaBrowser.Common.Net /// /// Calculates the list of interfaces to use for Kestrel. /// - /// A NetCollection object containing all the interfaces to bind. + /// A Collection{IPObject} 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. /// When false, return or for all interfaces. - NetCollection GetAllBindInterfaces(bool individualInterfaces = false); + Collection GetAllBindInterfaces(bool individualInterfaces = false); /// /// Returns a collection containing the loopback interfaces. /// - /// Netcollection. - NetCollection GetLoopbacks(); + /// Collection{IPObject}. + Collection GetLoopbacks(); /// /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) @@ -137,7 +138,14 @@ namespace MediaBrowser.Common.Net /// /// IP to check. Can be an IPAddress or an IPObject. /// Result of the check. - bool IsGatewayInterface(object? addressObj); + bool IsGatewayInterface(IPObject? addressObj); + + /// + /// 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. + bool IsGatewayInterface(IPAddress? addressObj); /// /// Returns true if the address is a private address. @@ -178,21 +186,21 @@ namespace MediaBrowser.Common.Net /// Token to parse. /// Resultant object's ip addresses, if successful. /// Success of the operation. - bool TryParseInterface(string token, out NetCollection? result); + bool TryParseInterface(string token, out Collection? result); /// - /// Parses an array of strings into a NetCollection. + /// Parses an array of strings into a Collection{IPObject}. /// /// 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); + Collection CreateIPCollection(string[] values, bool bracketed = false); /// /// Returns all the internal Bind interface addresses. /// /// An internal list of interfaces addresses. - NetCollection GetInternalBindAddresses(); + Collection GetInternalBindAddresses(); /// /// Checks to see if an IP address is still a valid interface address. @@ -220,12 +228,6 @@ namespace MediaBrowser.Common.Net /// /// 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. - /// - /// The configuration to use. - void UpdateSettings(object configuration); + Collection GetFilteredLANSubnets(Collection? filter = null); } } diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs index 80052727af..f9e1568efc 100644 --- a/MediaBrowser.Common/Net/IPHost.cs +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Common.Net public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None); /// - /// Time when last resolved. + /// Time when last resolved in ticks. /// private long _lastResolved; @@ -63,7 +63,8 @@ namespace MediaBrowser.Common.Net set { - // Not implemented. + // Not implemented, as a host's address is determined by DNS. + throw new NotImplementedException("The address of a host is determined by DNS."); } } @@ -75,12 +76,14 @@ namespace MediaBrowser.Common.Net { get { - return (byte)(ResolveHost() ? 128 : 0); + return (byte)(ResolveHost() ? 128 : 32); } set { - // Not implemented. + // Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length, + // which is automatically determined by it's IP type. Anything else is meaningless. + throw new NotImplementedException("The prefix length on a host cannot be set."); } } @@ -92,13 +95,7 @@ namespace MediaBrowser.Common.Net /// /// Gets a value indicating whether the address has a value. /// - public bool HasAddress - { - get - { - return _addresses.Length > 0; - } - } + public bool HasAddress => _addresses.Length != 0; /// /// Gets the host name of this object. @@ -128,7 +125,7 @@ namespace MediaBrowser.Common.Net /// /// Host name to parse. /// Object representing the string, if it has successfully been parsed. - /// Success result of the parsing. + /// true if the parsing is successful, false if not. public static bool TryParse(string host, out IPHost hostObj) { if (!string.IsNullOrEmpty(host)) @@ -142,7 +139,7 @@ namespace MediaBrowser.Common.Net else { // See if it's an IPv6 in [] with no port. - i = host.IndexOf("]", StringComparison.OrdinalIgnoreCase); + i = host.IndexOf(']', StringComparison.OrdinalIgnoreCase); if (i != -1) { return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj); @@ -394,19 +391,19 @@ namespace MediaBrowser.Common.Net /// /// Attempt to resolve the ip address of a host. /// - /// The result of the comparison function. + /// true if any addresses have been resolved, otherwise false. private bool ResolveHost() { // When was the last time we resolved? if (_lastResolved == 0) { - _lastResolved = DateTime.Now.Ticks; + _lastResolved = DateTime.UtcNow.Ticks; } // If we haven't resolved before, or out timer has run out... - if ((_addresses.Length == 0 && !Resolved) || (TimeSpan.FromTicks(DateTime.Now.Ticks - _lastResolved).TotalMinutes > Timeout)) + if ((_addresses.Length == 0 && !Resolved) || (TimeSpan.FromTicks(DateTime.UtcNow.Ticks - _lastResolved).TotalMinutes > Timeout)) { - _lastResolved = DateTime.Now.Ticks; + _lastResolved = DateTime.UtcNow.Ticks; ResolveHostInternal().GetAwaiter().GetResult(); Resolved = true; } @@ -417,7 +414,7 @@ namespace MediaBrowser.Common.Net /// /// Task that looks up a Host name and returns its IP addresses. /// - /// Array of IPAddress objects. + /// A representing the asynchronous operation. private async Task ResolveHostInternal() { if (!string.IsNullOrEmpty(HostName)) diff --git a/MediaBrowser.Common/Net/IPObject.cs b/MediaBrowser.Common/Net/IPObject.cs index 36f3357cce..d18ac98933 100644 --- a/MediaBrowser.Common/Net/IPObject.cs +++ b/MediaBrowser.Common/Net/IPObject.cs @@ -26,7 +26,7 @@ namespace MediaBrowser.Common.Net private IPObject? _networkAddress; /// - /// Gets or sets the user defined functions that need storage in this object. + /// Gets or sets a user defined value that is associated with this object. /// public int Tag { get; set; } @@ -38,18 +38,7 @@ namespace MediaBrowser.Common.Net /// /// Gets the object's network address. /// - public IPObject NetworkAddress - { - get - { - if (_networkAddress == null) - { - _networkAddress = CalculateNetworkAddress(); - } - - return _networkAddress; - } - } + public IPObject NetworkAddress => _networkAddress ??= CalculateNetworkAddress(); /// /// Gets or sets the object's IP address. @@ -92,22 +81,33 @@ namespace MediaBrowser.Common.Net return (Address: address, PrefixLength: prefixLength); } + // An ip address is just a list of bytes, each one representing a segment on the network. + // This separates the IP address into octets and calculates how many octets will need to be altered or set to zero dependant upon the + // prefix length value. eg. /16 on a 4 octet ip4 address (192.168.2.240) will result in the 2 and the 240 being zeroed out. + // Where there is not an exact boundary (eg /23), mod is used to calculate how many bits of this value are to be kept. + byte[] addressBytes = address.GetAddressBytes(); int div = prefixLength / 8; int mod = prefixLength % 8; if (mod != 0) { + // Prefix length is counted right to left, so subtract 8 so we know how many bits to clear. mod = 8 - mod; + + // Shift out the bits from the octet that we don't want, by moving right then back left. addressBytes[div] = (byte)((int)addressBytes[div] >> mod << mod); + // Move on the next byte. div++; } + // Blank out the remaining octets from mod + 1 to the end of the byte array. (192.168.2.240/16 becomes 192.168.0.0) for (int octet = div; octet < addressBytes.Length; octet++) { addressBytes[octet] = 0; } + // Return the network address for the prefix. return (Address: new IPAddress(addressBytes), PrefixLength: prefixLength); } @@ -179,18 +179,18 @@ namespace MediaBrowser.Common.Net byte[] octet = address.GetAddressBytes(); - return (octet[0] == 10) || - (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) || // RFC1918 - (octet[0] == 192 && octet[1] == 168) || // RFC1918 - (octet[0] == 127); // RFC1122 + return (octet[0] == 10) + || (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) // RFC1918 + || (octet[0] == 192 && octet[1] == 168) // RFC1918 + || (octet[0] == 127); // RFC1122 } else { byte[] octet = address.GetAddressBytes(); uint word = (uint)(octet[0] << 8) + octet[1]; - return (word >= 0xfe80 && word <= 0xfebf) || // fe80::/10 :Local link. - (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address. + return (word >= 0xfe80 && word <= 0xfebf) // fe80::/10 :Local link. + || (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address. } } @@ -202,7 +202,8 @@ namespace MediaBrowser.Common.Net /// /// IPAddress object to check. /// True if it is a local link address. - /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress + /// + /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress /// it appears that the IPAddress.IsIPv6LinkLocal is out of date. /// public static bool IsIPv6LinkLocal(IPAddress address) @@ -237,11 +238,10 @@ namespace MediaBrowser.Common.Net public static IPAddress CidrToMask(byte cidr, AddressFamily family) { uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr); - addr = - ((addr & 0xff000000) >> 24) | - ((addr & 0x00ff0000) >> 8) | - ((addr & 0x0000ff00) << 8) | - ((addr & 0x000000ff) << 24); + addr = ((addr & 0xff000000) >> 24) + | ((addr & 0x00ff0000) >> 8) + | ((addr & 0x0000ff00) << 8) + | ((addr & 0x000000ff) << 24); return new IPAddress(addr); } @@ -304,7 +304,7 @@ namespace MediaBrowser.Common.Net /// Type of address to remove. public virtual void Remove(AddressFamily family) { - // This method only peforms a function in the IPHost implementation of IPObject. + // This method only performs a function in the IPHost implementation of IPObject. } /// @@ -352,9 +352,9 @@ namespace MediaBrowser.Common.Net /// Equality result. public virtual bool Equals(IPObject? other) { - if (other != null && other is IPObject otherObj) + if (other != null) { - return !Address.Equals(IPAddress.None) && Address.Equals(otherObj.Address); + return !Address.Equals(IPAddress.None) && Address.Equals(other.Address); } return false; diff --git a/MediaBrowser.Common/Net/NetworkExtensions.cs b/MediaBrowser.Common/Net/NetworkExtensions.cs index e801de5eb8..d07bba249b 100644 --- a/MediaBrowser.Common/Net/NetworkExtensions.cs +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -2,10 +2,10 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Net; using System.Runtime.CompilerServices; using System.Text; -using NetCollection = System.Collections.ObjectModel.Collection; namespace MediaBrowser.Common.Net { @@ -17,9 +17,9 @@ namespace MediaBrowser.Common.Net /// /// Add an address to the collection. /// - /// The . + /// The . /// Item to add. - public static void AddItem(this NetCollection source, IPAddress ip) + public static void AddItem(this Collection source, IPAddress ip) { if (!source.ContainsAddress(ip)) { @@ -30,9 +30,9 @@ namespace MediaBrowser.Common.Net /// /// Adds a network to the collection. /// - /// The . + /// The . /// Item to add. - public static void AddItem(this NetCollection source, IPObject item) + public static void AddItem(this Collection source, IPObject item) { if (!source.ContainsAddress(item)) { @@ -43,33 +43,21 @@ namespace MediaBrowser.Common.Net /// /// Converts this object to a string. /// - /// The . + /// The . /// Returns a string representation of this object. - public static string AsString(this NetCollection source) + public static string AsString(this Collection source) { - var sb = new StringBuilder(); - string output = "["; - if (source.Count > 0) - { - foreach (var i in source) - { - output += $"{i},"; - } - - output = output[0..^1]; - } - - return $"{output}]"; + return $"[{string.Join(',', source)}]"; } /// /// Returns true if the collection contains an item with the ip address, /// or the ip address falls within any of the collection's network ranges. /// - /// The . + /// The . /// The item to look for. /// True if the collection contains the item. - public static bool ContainsAddress(this NetCollection source, IPAddress item) + public static bool ContainsAddress(this Collection source, IPAddress item) { if (source.Count == 0) { @@ -101,10 +89,10 @@ namespace MediaBrowser.Common.Net /// Returns true if the collection contains an item with the ip address, /// or the ip address falls within any of the collection's network ranges. /// - /// The . + /// The . /// The item to look for. /// True if the collection contains the item. - public static bool ContainsAddress(this NetCollection source, IPObject item) + public static bool ContainsAddress(this Collection source, IPObject item) { if (source.Count == 0) { @@ -128,12 +116,12 @@ namespace MediaBrowser.Common.Net } /// - /// Compares two NetCollection objects. The order is ignored. + /// Compares two Collection{IPObject} objects. The order is ignored. /// - /// The . + /// The . /// Item to compare to. /// True if both are equal. - public static bool Compare(this NetCollection source, NetCollection dest) + public static bool Compare(this Collection source, Collection dest) { if (dest == null || source.Count != dest.Count) { @@ -164,16 +152,16 @@ namespace MediaBrowser.Common.Net /// /// Returns a collection containing the subnets of this collection given. /// - /// The . - /// NetCollection object containing the subnets. - public static NetCollection AsNetworks(this NetCollection source) + /// The . + /// Collection{IPObject} object containing the subnets. + public static Collection AsNetworks(this Collection source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } - NetCollection res = new NetCollection(); + Collection res = new Collection(); foreach (IPObject i in source) { @@ -184,10 +172,10 @@ namespace MediaBrowser.Common.Net na.Tag = i.Tag; res.AddItem(na); } - else + else if (i is IPHost ipHost) { // Flatten out IPHost and add all its ip addresses. - foreach (var addr in ((IPHost)i).GetAddresses()) + foreach (var addr in ipHost.GetAddresses()) { IPNetAddress host = new IPNetAddress(addr) { @@ -205,17 +193,17 @@ namespace MediaBrowser.Common.Net /// /// Excludes all the items from this list that are found in excludeList. /// - /// The . + /// The . /// Items to exclude. /// A new collection, with the items excluded. - public static NetCollection Exclude(this NetCollection source, NetCollection excludeList) + public static Collection Exclude(this Collection source, Collection excludeList) { if (source.Count == 0 || excludeList == null) { - return new NetCollection(source); + return new Collection(source); } - NetCollection results = new NetCollection(); + Collection results = new Collection(); bool found; foreach (var outer in source) @@ -243,14 +231,14 @@ namespace MediaBrowser.Common.Net /// /// Returns all items that co-exist in this object and target. /// - /// The . + /// The . /// Collection to compare with. /// A collection containing all the matches. - public static NetCollection Union(this NetCollection source, NetCollection target) + public static Collection Union(this Collection source, Collection target) { if (source.Count == 0) { - return new NetCollection(); + return new Collection(); } if (target == null) @@ -258,7 +246,7 @@ namespace MediaBrowser.Common.Net throw new ArgumentNullException(nameof(target)); } - NetCollection nc = new NetCollection(); + Collection nc = new Collection(); foreach (IPObject i in source) { From 60a6627140a83408b8157b9543e62ff48918ef7a Mon Sep 17 00:00:00 2001 From: Greenback Date: Mon, 16 Nov 2020 19:45:21 +0000 Subject: [PATCH 069/257] Removing left over edits left from the acceptance of previous PR's. --- Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs | 1 - Jellyfin.Api/Controllers/ConfigurationController.cs | 2 -- .../Jellyfin.Server.Implementations.csproj | 1 - MediaBrowser.Common/Configuration/IConfigurationManager.cs | 2 +- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 76b039672c..4f72c8ce15 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -10,7 +10,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; -using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.AppBase diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 53f94cf375..e1c9f69f61 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -4,9 +4,7 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; -using Jellyfin.Api.Migrations; using Jellyfin.Api.Models.ConfigurationDtos; -using Jellyfin.Networking.Configuration; using MediaBrowser.Common.Json; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index b626bf1702..e663798da0 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -37,7 +37,6 @@ - diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index 790121274e..fc63d93503 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.Common.Configuration /// /// Gets the configuration. /// - /// /// The key. + /// The key. /// System.Object. object GetConfiguration(string key); From 3cc0dd7e12380a7e4a0ef86591890ece8421b14c Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 16 Nov 2020 20:29:46 -0700 Subject: [PATCH 070/257] Reduce RequestHelpers.Split usage and remove RequestHelpers.GetGuids usage. --- .../Channels/ChannelManager.cs | 2 +- .../Data/SqliteItemRepository.cs | 8 +- .../Library/LibraryManager.cs | 2 +- .../Playlists/PlaylistManager.cs | 6 +- Jellyfin.Api/Controllers/ArtistsController.cs | 100 ++++---- .../Controllers/CollectionController.cs | 17 +- Jellyfin.Api/Controllers/GenresController.cs | 10 +- Jellyfin.Api/Controllers/ItemsController.cs | 103 ++++---- Jellyfin.Api/Controllers/LibraryController.cs | 13 +- Jellyfin.Api/Controllers/LiveTvController.cs | 28 +-- .../Controllers/MusicGenresController.cs | 10 +- Jellyfin.Api/Controllers/PersonsController.cs | 8 +- .../Controllers/PlaylistsController.cs | 13 +- Jellyfin.Api/Controllers/SearchController.cs | 13 +- Jellyfin.Api/Controllers/SessionController.cs | 8 +- Jellyfin.Api/Controllers/StudiosController.cs | 13 +- .../Controllers/SuggestionsController.cs | 9 +- .../Controllers/TrailersController.cs | 38 +-- .../Controllers/UniversalAudioController.cs | 8 +- .../Controllers/UserLibraryController.cs | 4 +- .../Controllers/UserViewsController.cs | 7 +- Jellyfin.Api/Controllers/VideosController.cs | 5 +- Jellyfin.Api/Controllers/YearsController.cs | 18 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 43 ---- .../PipeDelimitedArrayModelBinder.cs | 90 +++++++ .../Models/LiveTvDtos/GetProgramsDto.cs | 9 +- .../Models/PlaylistDtos/CreatePlaylistDto.cs | 6 +- .../JsonPipeDelimitedArrayConverter.cs | 74 ++++++ .../JsonPipeDelimitedArrayConverterFactory.cs | 28 +++ .../MediaBrowser.Common.csproj | 2 +- MediaBrowser.Controller/Entities/Folder.cs | 6 +- .../Entities/InternalItemsQuery.cs | 6 +- .../Entities/UserViewBuilder.cs | 4 +- .../Playlists/IPlaylistManager.cs | 2 +- .../Playlists/PlaylistCreationRequest.cs | 8 +- .../PipeDelimitedArrayModelBinderTests.cs | 226 ++++++++++++++++++ 36 files changed, 659 insertions(+), 288 deletions(-) create mode 100644 Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs create mode 100644 MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverter.cs create mode 100644 MediaBrowser.Common/Json/Converters/JsonPipeDelimitedArrayConverterFactory.cs create mode 100644 tests/Jellyfin.Api.Tests/ModelBinders/PipeDelimitedArrayModelBinderTests.cs diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 19045b72b4..3d97a6ca8d 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -634,7 +634,7 @@ namespace Emby.Server.Implementations.Channels { var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray(); - if (query.ChannelIds.Length > 0) + if (query.ChannelIds.Count > 0) { // Avoid implicitly captured closure var ids = query.ChannelIds; diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 638c7a9b49..7e01bd4b64 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3611,12 +3611,12 @@ namespace Emby.Server.Implementations.Data whereClauses.Add($"type in ({inClause})"); } - if (query.ChannelIds.Length == 1) + if (query.ChannelIds.Count == 1) { whereClauses.Add("ChannelId=@ChannelId"); statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); } - else if (query.ChannelIds.Length > 1) + else if (query.ChannelIds.Count > 1) { var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); whereClauses.Add($"ChannelId in ({inClause})"); @@ -4076,7 +4076,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - if (query.GenreIds.Length > 0) + if (query.GenreIds.Count > 0) { var clauses = new List(); var index = 0; @@ -4097,7 +4097,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add(clause); } - if (query.Genres.Length > 0) + if (query.Genres.Count > 0) { var clauses = new List(); var index = 0; diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8ab5e4aef1..03bf1c8747 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1503,7 +1503,7 @@ namespace Emby.Server.Implementations.Library { if (query.AncestorIds.Length == 0 && query.ParentId.Equals(Guid.Empty) && - query.ChannelIds.Length == 0 && + query.ChannelIds.Count == 0 && query.TopParentIds.Length == 0 && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index d3b64fb318..932f721ab4 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.Playlists await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) .ConfigureAwait(false); - if (options.ItemIdList.Length > 0) + if (options.ItemIdList.Count > 0) { await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false) { @@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.Playlists return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } - public Task AddToPlaylistAsync(Guid playlistId, ICollection itemIds, Guid userId) + public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection itemIds, Guid userId) { var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); @@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Playlists }); } - private async Task AddToPlaylistInternal(Guid playlistId, ICollection newItemIds, User user, DtoOptions options) + private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection newItemIds, User user, DtoOptions options) { // Retrieve the existing playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 9bad206e02..0123bb8fa3 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -89,24 +89,24 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery] string? mediaTypes, - [FromQuery] string? genres, - [FromQuery] string? genreIds, - [FromQuery] string? officialRatings, - [FromQuery] string? tags, - [FromQuery] string? years, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery] string? personIds, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? studios, - [FromQuery] string? studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, @@ -131,30 +131,26 @@ namespace Jellyfin.Api.Controllers parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); } - var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); - var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); - var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); - var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypesArr, - IncludeItemTypes = includeItemTypesArr, - MediaTypes = mediaTypesArr, + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, NameLessThan = nameLessThan, NameStartsWith = nameStartsWith, NameStartsWithOrGreater = nameStartsWithOrGreater, - Tags = RequestHelpers.Split(tags, '|', true), - OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), - Genres = RequestHelpers.Split(genres, '|', true), - GenreIds = RequestHelpers.GetGuids(genreIds), - StudioIds = RequestHelpers.GetGuids(studioIds), + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + GenreIds = genreIds, + StudioIds = studioIds, Person = person, - PersonIds = RequestHelpers.GetGuids(personIds), - PersonTypes = RequestHelpers.Split(personTypes, ',', true), - Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, @@ -230,7 +226,7 @@ namespace Jellyfin.Api.Controllers var (baseItem, itemCounts) = i; var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); - if (!string.IsNullOrWhiteSpace(includeItemTypes)) + if (includeItemTypes.Length != 0) { dto.ChildCount = itemCounts.ItemCount; dto.ProgramCount = itemCounts.ProgramCount; @@ -297,24 +293,24 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery] string? mediaTypes, - [FromQuery] string? genres, - [FromQuery] string? genreIds, - [FromQuery] string? officialRatings, - [FromQuery] string? tags, - [FromQuery] string? years, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery] string? personIds, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? studios, - [FromQuery] string? studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, [FromQuery] Guid? userId, [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, @@ -339,30 +335,26 @@ namespace Jellyfin.Api.Controllers parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); } - var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); - var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); - var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true); - var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypesArr, - IncludeItemTypes = includeItemTypesArr, - MediaTypes = mediaTypesArr, + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, + MediaTypes = mediaTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, NameLessThan = nameLessThan, NameStartsWith = nameStartsWith, NameStartsWithOrGreater = nameStartsWithOrGreater, - Tags = RequestHelpers.Split(tags, '|', true), - OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), - Genres = RequestHelpers.Split(genres, '|', true), - GenreIds = RequestHelpers.GetGuids(genreIds), - StudioIds = RequestHelpers.GetGuids(studioIds), + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + GenreIds = genreIds, + StudioIds = studioIds, Person = person, - PersonIds = RequestHelpers.GetGuids(personIds), - PersonTypes = RequestHelpers.Split(personTypes, ',', true), - Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, MinCommunityRating = minCommunityRating, DtoOptions = dtoOptions, SearchTerm = searchTerm, @@ -438,7 +430,7 @@ namespace Jellyfin.Api.Controllers var (baseItem, itemCounts) = i; var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); - if (!string.IsNullOrWhiteSpace(includeItemTypes)) + if (includeItemTypes.Length != 0) { dto.ChildCount = itemCounts.ItemCount; dto.ProgramCount = itemCounts.ProgramCount; diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs index eae06b767c..2a342c2cbe 100644 --- a/Jellyfin.Api/Controllers/CollectionController.cs +++ b/Jellyfin.Api/Controllers/CollectionController.cs @@ -4,6 +4,7 @@ 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; using MediaBrowser.Controller.Net; @@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreateCollection( [FromQuery] string? name, - [FromQuery] string? ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids, [FromQuery] Guid? parentId, [FromQuery] bool isLocked = false) { @@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers IsLocked = isLocked, Name = name, ParentId = parentId, - ItemIdList = RequestHelpers.Split(ids, ',', true), + ItemIdList = ids, UserIds = new[] { userId } }).ConfigureAwait(false); @@ -88,9 +89,11 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpPost("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids) + public async Task AddToCollection( + [FromRoute, Required] Guid collectionId, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { - await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true); + await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true); return NoContent(); } @@ -103,9 +106,11 @@ namespace Jellyfin.Api.Controllers /// A indicating success. [HttpDelete("{collectionId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids) + public async Task RemoveFromCollection( + [FromRoute, Required] Guid collectionId, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids) { - await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false); + await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 9c222135d0..2dd504770d 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers result = _libraryManager.GetGenres(query); } - var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); + var shouldIncludeItemTypes = includeItemTypes.Length != 0; return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index d8d371ebc3..cff06aafe7 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, - [FromQuery] string? excludeItemIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? recursive, @@ -181,34 +181,34 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? sortOrder, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery] string? sortBy, [FromQuery] bool? isPlayed, - [FromQuery] string? genres, - [FromQuery] string? officialRatings, - [FromQuery] string? tags, - [FromQuery] string? years, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery] string? personIds, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? studios, [FromQuery] string? artists, - [FromQuery] string? excludeArtistIds, - [FromQuery] string? artistIds, - [FromQuery] string? albumArtistIds, - [FromQuery] string? contributingArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, [FromQuery] string? albums, - [FromQuery] string? albumIds, - [FromQuery] string? ids, - [FromQuery] string? videoTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, [FromQuery] string? minOfficialRating, [FromQuery] bool? isLocked, [FromQuery] bool? isPlaceHolder, @@ -223,8 +223,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery] string? studioIds, - [FromQuery] string? genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { @@ -238,8 +238,9 @@ namespace Jellyfin.Api.Controllers .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) - || string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) + if (includeItemTypes.Length == 1 + && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase) + || includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase))) { parentId = null; } @@ -262,7 +263,7 @@ namespace Jellyfin.Api.Controllers && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { recursive = true; - includeItemTypes = "Playlist"; + includeItemTypes = new[] { "Playlist" }; } bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) @@ -291,14 +292,14 @@ namespace Jellyfin.Api.Controllers return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); } - if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder)) + if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder)) { var query = new InternalItemsQuery(user!) { IsPlayed = isPlayed, - MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), + MediaTypes = mediaTypes, + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, Recursive = recursive ?? false, OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), IsFavorite = isFavorite, @@ -330,28 +331,28 @@ namespace Jellyfin.Api.Controllers HasTrailer = hasTrailer, IsHD = isHd, Is4K = is4K, - Tags = RequestHelpers.Split(tags, '|', true), - OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), - Genres = RequestHelpers.Split(genres, '|', true), - ArtistIds = RequestHelpers.GetGuids(artistIds), - AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds), - ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds), - GenreIds = RequestHelpers.GetGuids(genreIds), - StudioIds = RequestHelpers.GetGuids(studioIds), + Tags = tags, + OfficialRatings = officialRatings, + Genres = genres, + ArtistIds = artistIds, + AlbumArtistIds = albumArtistIds, + ContributingArtistIds = contributingArtistIds, + GenreIds = genreIds, + StudioIds = studioIds, Person = person, - PersonIds = RequestHelpers.GetGuids(personIds), - PersonTypes = RequestHelpers.Split(personTypes, ',', true), - Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), + PersonIds = personIds, + PersonTypes = personTypes, + Years = years, ImageTypes = imageTypes, - VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse(v, true)).ToArray(), + VideoTypes = videoTypes, AdjacentTo = adjacentTo, - ItemIds = RequestHelpers.GetGuids(ids), + ItemIds = ids, MinCommunityRating = minCommunityRating, MinCriticRating = minCriticRating, ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId), ParentIndexNumber = parentIndexNumber, EnableTotalRecordCount = enableTotalRecordCount, - ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds), + ExcludeItemIds = excludeItemIds, DtoOptions = dtoOptions, SearchTerm = searchTerm, MinDateLastSaved = minDateLastSaved?.ToUniversalTime(), @@ -360,7 +361,7 @@ namespace Jellyfin.Api.Controllers MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), }; - if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm)) + if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm)) { query.CollapseBoxSetItems = false; } @@ -449,14 +450,14 @@ namespace Jellyfin.Api.Controllers } // ExcludeArtistIds - if (!string.IsNullOrWhiteSpace(excludeArtistIds)) + if (excludeArtistIds.Length != 0) { - query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); + query.ExcludeArtistIds = excludeArtistIds; } - if (!string.IsNullOrWhiteSpace(albumIds)) + if (albumIds.Length != 0) { - query.AlbumIds = RequestHelpers.GetGuids(albumIds); + query.AlbumIds = albumIds; } // Albums @@ -533,12 +534,12 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { @@ -569,13 +570,13 @@ namespace Jellyfin.Api.Controllers ParentId = parentIdGuid, Recursive = true, DtoOptions = dtoOptions, - MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), + MediaTypes = mediaTypes, IsVirtualItem = false, CollapseBoxSetItems = false, EnableTotalRecordCount = enableTotalRecordCount, AncestorIds = ancestorIds, - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, SearchTerm = searchTerm }); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 1b115d800b..3ff77e8e01 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -362,15 +362,14 @@ namespace Jellyfin.Api.Controllers [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public ActionResult DeleteItems([FromQuery] string? ids) + public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids) { - if (string.IsNullOrEmpty(ids)) + if (ids.Length == 0) { return NoContent(); } - var itemIds = RequestHelpers.Split(ids, ',', true); - foreach (var i in itemIds) + foreach (var i in ids) { var item = _libraryManager.GetItemById(i); var auth = _authContext.GetAuthorizationInfo(Request); @@ -691,7 +690,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSimilarItems( [FromRoute, Required] Guid itemId, - [FromQuery] string? excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, [FromQuery] Guid? userId, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) @@ -753,9 +752,9 @@ namespace Jellyfin.Api.Controllers }; // ExcludeArtistIds - if (!string.IsNullOrEmpty(excludeArtistIds)) + if (excludeArtistIds.Length != 0) { - query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); + query.ExcludeArtistIds = excludeArtistIds; } List itemsResult = _libraryManager.GetItemList(query); diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 29c0f1df4d..eb8b42b341 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, - [FromQuery] string? sortBy, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery] SortOrder? sortOrder, [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) @@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers IsNews = isNews, IsKids = isKids, IsSports = isSports, - SortBy = RequestHelpers.Split(sortBy, ',', true), + SortBy = sortBy, SortOrder = sortOrder ?? SortOrder.Ascending, AddCurrentProgram = addCurrentProgram }, @@ -539,7 +539,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [Authorize(Policy = Policies.DefaultAuthorization)] public async Task>> GetLiveTvPrograms( - [FromQuery] string? channelIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds, [FromQuery] Guid? userId, [FromQuery] DateTime? minStartDate, [FromQuery] bool? hasAired, @@ -556,8 +556,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] string? sortBy, [FromQuery] string? sortOrder, - [FromQuery] string? genres, - [FromQuery] string? genreIds, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -573,8 +573,7 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ChannelIds = RequestHelpers.Split(channelIds, ',', true) - .Select(i => new Guid(i)).ToArray(), + ChannelIds = channelIds, HasAired = hasAired, IsAiring = isAiring, EnableTotalRecordCount = enableTotalRecordCount, @@ -591,8 +590,8 @@ namespace Jellyfin.Api.Controllers IsKids = isKids, IsSports = isSports, SeriesTimerId = seriesTimerId, - Genres = RequestHelpers.Split(genres, '|', true), - GenreIds = RequestHelpers.GetGuids(genreIds) + Genres = genres, + GenreIds = genreIds }; if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) @@ -628,8 +627,7 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true) - .Select(i => new Guid(i)).ToArray(), + ChannelIds = body.ChannelIds, HasAired = body.HasAired, IsAiring = body.IsAiring, EnableTotalRecordCount = body.EnableTotalRecordCount, @@ -646,8 +644,8 @@ namespace Jellyfin.Api.Controllers IsKids = body.IsKids, IsSports = body.IsSports, SeriesTimerId = body.SeriesTimerId, - Genres = RequestHelpers.Split(body.Genres, '|', true), - GenreIds = RequestHelpers.GetGuids(body.GenreIds) + Genres = body.Genres, + GenreIds = body.GenreIds }; if (!body.LibrarySeriesId.Equals(Guid.Empty)) @@ -703,7 +701,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) @@ -723,7 +721,7 @@ namespace Jellyfin.Api.Controllers IsNews = isNews, IsSports = isSports, EnableTotalRecordCount = enableTotalRecordCount, - GenreIds = RequestHelpers.GetGuids(genreIds) + GenreIds = genreIds }; var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 229d9ff022..8c61043028 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, @@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery(user) { - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers var result = _libraryManager.GetMusicGenres(query); - var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes); + var shouldIncludeItemTypes = includeItemTypes.Length != 0; return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); } diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 6ac3e6417a..9dc79b3884 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -77,8 +77,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? excludePersonTypes, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? appearsInItemId, [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) @@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery { - PersonTypes = RequestHelpers.Split(personTypes, ',', true), - ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true), + PersonTypes = personTypes, + ExcludePersonTypes = excludePersonTypes, NameContains = searchTerm, User = user, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 4b3d8d3d39..bc47ecbd1c 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -63,11 +63,10 @@ namespace Jellyfin.Api.Controllers public async Task> CreatePlaylist( [FromBody, Required] CreatePlaylistDto createPlaylistRequest) { - Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids); var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest { Name = createPlaylistRequest.Name, - ItemIdList = idGuidArray, + ItemIdList = createPlaylistRequest.Ids, UserId = createPlaylistRequest.UserId, MediaType = createPlaylistRequest.MediaType }).ConfigureAwait(false); @@ -87,10 +86,10 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task AddToPlaylist( [FromRoute, Required] Guid playlistId, - [FromQuery] string? ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { - await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false); + await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false); return NoContent(); } @@ -122,9 +121,11 @@ namespace Jellyfin.Api.Controllers /// An on success. [HttpDelete("{playlistId}/Items")] [ProducesResponseType(StatusCodes.Status204NoContent)] - public async Task RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds) + public async Task RemoveFromPlaylist( + [FromRoute, Required] string playlistId, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds) { - await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false); + await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index e75f0d06bb..076fe58f11 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -82,9 +83,9 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] Guid? userId, [FromQuery, Required] string searchTerm, - [FromQuery] string? includeItemTypes, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery] string? parentId, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, @@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers IncludeStudios = includeStudios, StartIndex = startIndex, UserId = userId ?? Guid.Empty, - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), - ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), - MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), + IncludeItemTypes = includeItemTypes, + ExcludeItemTypes = excludeItemTypes, + MediaTypes = mediaTypes, ParentId = parentId, IsKids = isKids, diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index e506ac7bf1..6c9b9050e6 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -160,12 +160,12 @@ namespace Jellyfin.Api.Controllers public ActionResult Play( [FromRoute, Required] string sessionId, [FromQuery, Required] PlayCommand playCommand, - [FromQuery, Required] string itemIds, + [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds, [FromQuery] long? startPositionTicks) { var playRequest = new PlayRequest { - ItemIds = RequestHelpers.GetGuids(itemIds), + ItemIds = itemIds, StartPositionTicks = startPositionTicks, PlayCommand = playCommand }; @@ -378,7 +378,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PostCapabilities( [FromQuery] string? id, - [FromQuery] string? playableMediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsSync = false, @@ -391,7 +391,7 @@ namespace Jellyfin.Api.Controllers _sessionManager.ReportCapabilities(id, new ClientCapabilities { - PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), + PlayableMediaTypes = playableMediaTypes, SupportedCommands = supportedCommands, SupportsMediaControl = supportsMediaControl, SupportsSync = supportsSync, diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 27dcd51bc2..af28b4f597 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -73,8 +73,8 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? searchTerm, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isFavorite, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, @@ -94,13 +94,10 @@ namespace Jellyfin.Api.Controllers var parentItem = _libraryManager.GetParentItem(parentId, userId); - var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); - var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); - var query = new InternalItemsQuery(user) { - ExcludeItemTypes = excludeItemTypesArr, - IncludeItemTypes = includeItemTypesArr, + ExcludeItemTypes = excludeItemTypes, + IncludeItemTypes = includeItemTypes, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -125,7 +122,7 @@ namespace Jellyfin.Api.Controllers } var result = _libraryManager.GetStudios(query); - var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); + var shouldIncludeItemTypes = includeItemTypes.Length != 0; return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); } diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index ad64adfbad..69292186e5 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -4,6 +4,7 @@ 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; using MediaBrowser.Controller.Entities; @@ -58,8 +59,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetSuggestions( [FromRoute, Required] Guid userId, - [FromQuery] string? mediaType, - [FromQuery] string? type, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) @@ -70,8 +71,8 @@ namespace Jellyfin.Api.Controllers var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple(i, SortOrder.Descending)).ToArray(), - MediaTypes = RequestHelpers.Split(mediaType!, ',', true), - IncludeItemTypes = RequestHelpers.Split(type!, ',', true), + MediaTypes = mediaType, + IncludeItemTypes = type, IsVirtualItem = false, StartIndex = startIndex, Limit = limit, diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index d78adcbcdc..f09a6a91aa 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? hasImdbId, [FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTvdbId, - [FromQuery] string? excludeItemIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds, [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? recursive, @@ -147,33 +147,33 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? sortOrder, [FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? excludeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, - [FromQuery] string? mediaTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery] string? sortBy, [FromQuery] bool? isPlayed, - [FromQuery] string? genres, - [FromQuery] string? officialRatings, - [FromQuery] string? tags, - [FromQuery] string? years, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings, + [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years, [FromQuery] bool? enableUserData, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] string? person, - [FromQuery] string? personIds, - [FromQuery] string? personTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes, [FromQuery] string? studios, [FromQuery] string? artists, - [FromQuery] string? excludeArtistIds, - [FromQuery] string? artistIds, - [FromQuery] string? albumArtistIds, - [FromQuery] string? contributingArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds, [FromQuery] string? albums, - [FromQuery] string? albumIds, - [FromQuery] string? ids, - [FromQuery] string? videoTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes, [FromQuery] string? minOfficialRating, [FromQuery] bool? isLocked, [FromQuery] bool? isPlaceHolder, @@ -188,12 +188,12 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWith, [FromQuery] string? nameLessThan, - [FromQuery] string? studioIds, - [FromQuery] string? genreIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds, [FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool? enableImages = true) { - var includeItemTypes = "Trailer"; + var includeItemTypes = new[] { "Trailer" }; return _itemsController .GetItems( diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index e10f1fe912..5141aebca6 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; @@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers [ProducesAudioFile] public async Task GetUniversalAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] string? container, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, @@ -258,7 +259,7 @@ namespace Jellyfin.Api.Controllers } private DeviceProfile GetDeviceProfile( - string? container, + string[] containers, string? transcodingContainer, string? audioCodec, string? transcodingProtocol, @@ -270,7 +271,6 @@ namespace Jellyfin.Api.Controllers { var deviceProfile = new DeviceProfile(); - var containers = RequestHelpers.Split(container, ',', true); int len = containers.Length; var directPlayProfiles = new DirectPlayProfile[len]; for (int i = 0; i < len; i++) @@ -327,7 +327,7 @@ namespace Jellyfin.Api.Controllers if (conditions.Count > 0) { // codec profile - codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() }); + codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() }); } deviceProfile.CodecProfiles = codecProfiles.ToArray(); diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index cfd8511297..a7fb4f116c 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers [FromRoute, Required] Guid userId, [FromQuery] Guid? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, - [FromQuery] string? includeItemTypes, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes, [FromQuery] bool? isPlayed, [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, @@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers new LatestItemsQuery { GroupItems = groupItems, - IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), + IncludeItemTypes = includeItemTypes, IsPlayed = isPlayed, Limit = limit, ParentId = parentId ?? Guid.Empty, diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index d575bfc3b8..60fd1df013 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -5,6 +5,7 @@ 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; using MediaBrowser.Controller.Entities; @@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers public ActionResult> GetUserViews( [FromRoute, Required] Guid userId, [FromQuery] bool? includeExternalContent, - [FromQuery] string? presetViews, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews, [FromQuery] bool includeHidden = false) { var query = new UserViewQuery @@ -81,9 +82,9 @@ namespace Jellyfin.Api.Controllers query.IncludeExternalContent = includeExternalContent.Value; } - if (!string.IsNullOrWhiteSpace(presetViews)) + if (presetViews.Length != 0) { - query.PresetViews = RequestHelpers.Split(presetViews, ',', true); + query.PresetViews = presetViews; } var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty; diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 4de7aac71d..dd5e70a50a 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -10,6 +10,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -203,9 +204,9 @@ 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds) { - var items = RequestHelpers.Split(itemIds, ',', true) + var items = itemIds .Select(i => _libraryManager.GetItemById(i)) .OfType