From 99e0d46ad93c1f2e62aed67c26b92f256610f1a6 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 23 Oct 2023 23:45:01 +0200 Subject: [PATCH] Use System.Net.IPNetwork --- Emby.Dlna/Main/DlnaEntryPoint.cs | 363 ++++++++++++++++++ Jellyfin.Networking/Manager/NetworkManager.cs | 17 +- .../ApiServiceCollectionExtensions.cs | 3 +- MediaBrowser.Common/Net/NetworkConstants.cs | 1 - MediaBrowser.Common/Net/NetworkUtils.cs | 3 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 1 - MediaBrowser.Model/Net/IPData.cs | 5 +- .../Jellyfin.Drawing.Skia.csproj | 2 + .../ParseNetworkTests.cs | 3 +- 9 files changed, 379 insertions(+), 19 deletions(-) create mode 100644 Emby.Dlna/Main/DlnaEntryPoint.cs diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs new file mode 100644 index 0000000000..dbc47d9816 --- /dev/null +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -0,0 +1,363 @@ +#nullable disable + +#pragma warning disable CS1591 + +using System; +using System.Globalization; +using System.Linq; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading.Tasks; +using Emby.Dlna.PlayTo; +using Emby.Dlna.Ssdp; +using Jellyfin.Networking.Configuration; +using Jellyfin.Networking.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using Microsoft.Extensions.Logging; +using Rssdp; +using Rssdp.Infrastructure; + +namespace Emby.Dlna.Main +{ + public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup + { + private readonly IServerConfigurationManager _config; + private readonly ILogger _logger; + private readonly IServerApplicationHost _appHost; + private readonly ISessionManager _sessionManager; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILibraryManager _libraryManager; + private readonly IUserManager _userManager; + private readonly IDlnaManager _dlnaManager; + private readonly IImageProcessor _imageProcessor; + private readonly IUserDataManager _userDataManager; + private readonly ILocalizationManager _localization; + private readonly IMediaSourceManager _mediaSourceManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IDeviceDiscovery _deviceDiscovery; + private readonly ISsdpCommunicationsServer _communicationsServer; + private readonly INetworkManager _networkManager; + private readonly object _syncLock = new(); + private readonly bool _disabled; + + private PlayToManager _manager; + private SsdpDevicePublisher _publisher; + + private bool _disposed; + + public DlnaEntryPoint( + IServerConfigurationManager config, + ILoggerFactory loggerFactory, + IServerApplicationHost appHost, + ISessionManager sessionManager, + IHttpClientFactory httpClientFactory, + ILibraryManager libraryManager, + IUserManager userManager, + IDlnaManager dlnaManager, + IImageProcessor imageProcessor, + IUserDataManager userDataManager, + ILocalizationManager localizationManager, + IMediaSourceManager mediaSourceManager, + IDeviceDiscovery deviceDiscovery, + IMediaEncoder mediaEncoder, + ISsdpCommunicationsServer communicationsServer, + INetworkManager networkManager) + { + _config = config; + _appHost = appHost; + _sessionManager = sessionManager; + _httpClientFactory = httpClientFactory; + _libraryManager = libraryManager; + _userManager = userManager; + _dlnaManager = dlnaManager; + _imageProcessor = imageProcessor; + _userDataManager = userDataManager; + _localization = localizationManager; + _mediaSourceManager = mediaSourceManager; + _deviceDiscovery = deviceDiscovery; + _mediaEncoder = mediaEncoder; + _communicationsServer = communicationsServer; + _networkManager = networkManager; + _logger = loggerFactory.CreateLogger(); + + var netConfig = config.GetConfiguration(NetworkConfigurationStore.StoreKey); + _disabled = appHost.ListenWithHttps && netConfig.RequireHttps; + + if (_disabled && _config.GetDlnaConfiguration().EnableServer) + { + _logger.LogError("The DLNA specification does not support HTTPS."); + } + } + + public async Task RunAsync() + { + await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); + + if (_disabled) + { + // No use starting as dlna won't work, as we're running purely on HTTPS. + return; + } + + ReloadComponents(); + + _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated; + } + + private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + { + if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) + { + ReloadComponents(); + } + } + + private void ReloadComponents() + { + var options = _config.GetDlnaConfiguration(); + StartDeviceDiscovery(); + + if (options.EnableServer) + { + StartDevicePublisher(options); + } + else + { + DisposeDevicePublisher(); + } + + if (options.EnablePlayTo) + { + StartPlayToManager(); + } + else + { + DisposePlayToManager(); + } + } + + private void StartDeviceDiscovery() + { + try + { + ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting device discovery"); + } + } + + public void StartDevicePublisher(Configuration.DlnaOptions options) + { + if (_publisher is not null) + { + return; + } + + try + { + _publisher = new SsdpDevicePublisher( + _communicationsServer, + Environment.OSVersion.Platform.ToString(), + // Can not use VersionString here since that includes OS and version + Environment.OSVersion.Version.ToString(), + _config.GetDlnaConfiguration().SendOnlyMatchedHost) + { + LogFunction = (msg) => _logger.LogDebug("{Msg}", msg), + SupportPnpRootDevice = false + }; + + RegisterServerEndpoints(); + + if (options.BlastAliveMessages) + { + _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error registering endpoint"); + } + } + + private void RegisterServerEndpoints() + { + var udn = CreateUuid(_appHost.SystemId); + var descriptorUri = "/dlna/" + udn + "/description.xml"; + + // Only get bind addresses in LAN + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6) + .ToList(); + + if (validInterfaces.Count == 0) + { + // No interfaces returned, fall back to loopback + validInterfaces = _networkManager.GetLoopbacks().ToList(); + } + + foreach (var intf in validInterfaces) + { + var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; + + _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address); + + var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri); + + var device = new SsdpRootDevice + { + CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. + Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. + Address = intf.Address, + PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.BaseAddress), + FriendlyName = "Jellyfin", + Manufacturer = "Jellyfin", + ModelName = "Jellyfin Server", + Uuid = udn + // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }; + + SetProperties(device, fullService); + _publisher.AddDevice(device); + + var embeddedDevices = new[] + { + "urn:schemas-upnp-org:service:ContentDirectory:1", + "urn:schemas-upnp-org:service:ConnectionManager:1", + // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" + }; + + foreach (var subDevice in embeddedDevices) + { + var embeddedDevice = new SsdpEmbeddedDevice + { + FriendlyName = device.FriendlyName, + Manufacturer = device.Manufacturer, + ModelName = device.ModelName, + Uuid = udn + // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. + }; + + SetProperties(embeddedDevice, subDevice); + device.AddDevice(embeddedDevice); + } + } + } + + private static string CreateUuid(string text) + { + if (!Guid.TryParse(text, out var guid)) + { + guid = text.GetMD5(); + } + + return guid.ToString("D", CultureInfo.InvariantCulture); + } + + private static void SetProperties(SsdpDevice device, string fullDeviceType) + { + var serviceParts = fullDeviceType + .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) + .Split(':'); + + device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); + device.DeviceClass = serviceParts[1]; + device.DeviceType = serviceParts[2]; + } + + private void StartPlayToManager() + { + lock (_syncLock) + { + if (_manager is not null) + { + return; + } + + try + { + _manager = new PlayToManager( + _logger, + _sessionManager, + _libraryManager, + _userManager, + _dlnaManager, + _appHost, + _imageProcessor, + _deviceDiscovery, + _httpClientFactory, + _userDataManager, + _localization, + _mediaSourceManager, + _mediaEncoder); + + _manager.Start(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting PlayTo manager"); + } + } + } + + private void DisposePlayToManager() + { + lock (_syncLock) + { + if (_manager is not null) + { + try + { + _logger.LogInformation("Disposing PlayToManager"); + _manager.Dispose(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error disposing PlayTo manager"); + } + + _manager = null; + } + } + } + + public void DisposeDevicePublisher() + { + if (_publisher is not null) + { + _logger.LogInformation("Disposing SsdpDevicePublisher"); + _publisher.Dispose(); + _publisher = null; + } + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + DisposeDevicePublisher(); + DisposePlayToManager(); + _disposed = true; + } + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index b0fe4aba65..3c03d137be 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -11,7 +11,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -530,7 +529,7 @@ namespace Jellyfin.Networking.Manager { foreach (var lan in _lanSubnets) { - var lanPrefix = lan.Prefix; + var lanPrefix = lan.BaseAddress; publishedServerUrls.Add( new PublishedServerUriOverride( new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), @@ -541,7 +540,7 @@ namespace Jellyfin.Networking.Manager } else if (NetworkUtils.TryParseToSubnet(identifier, out var result) && result is not null) { - var data = new IPData(result.Prefix, result); + var data = new IPData(result.BaseAddress, result); publishedServerUrls.Add( new PublishedServerUriOverride( data, @@ -607,7 +606,7 @@ namespace Jellyfin.Networking.Manager var parts = details.Split(','); if (NetworkUtils.TryParseToSubnet(parts[0], out var subnet)) { - var address = subnet.Prefix; + var address = subnet.BaseAddress; var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { @@ -881,7 +880,7 @@ namespace Jellyfin.Networking.Manager { if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { - return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); + return IPAddress.IsLoopback(subnet.BaseAddress) || (_lanSubnets.Any(x => x.Contains(subnet.BaseAddress)) && !_excludedSubnets.Any(x => x.Contains(subnet.BaseAddress))); } if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) @@ -1112,12 +1111,12 @@ namespace Jellyfin.Networking.Manager var logLevel = debug ? LogLevel.Debug : LogLevel.Information; if (_logger.IsEnabled(logLevel)) { - _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.BaseAddress + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.BaseAddress + "/" + s.PrefixLength)); _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); - _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.BaseAddress + "/" + s.PrefixLength)); } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 89f9c08e7e..753029f2cd 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -30,7 +30,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; @@ -280,7 +279,7 @@ namespace Jellyfin.Server.Extensions { if (subnet is not null) { - AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); + AddIPAddress(config, options, subnet.BaseAddress, subnet.PrefixLength); } } else if (NetworkUtils.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) diff --git a/MediaBrowser.Common/Net/NetworkConstants.cs b/MediaBrowser.Common/Net/NetworkConstants.cs index 396bc8fb50..fe7ab89632 100644 --- a/MediaBrowser.Common/Net/NetworkConstants.cs +++ b/MediaBrowser.Common/Net/NetworkConstants.cs @@ -1,5 +1,4 @@ using System.Net; -using Microsoft.AspNetCore.HttpOverrides; namespace MediaBrowser.Common.Net; diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index f3bff7fa95..452cb694ed 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Sockets; using System.Text.RegularExpressions; using Jellyfin.Extensions; +using Jellyfin.Networking.Constants; using Microsoft.AspNetCore.HttpOverrides; namespace MediaBrowser.Common.Net; @@ -335,7 +336,7 @@ public static partial class NetworkUtils /// The broadcast address. public static IPAddress GetBroadcastAddress(IPNetwork network) { - var addressBytes = network.Prefix.GetAddressBytes(); + var addressBytes = network.BaseAddress.GetAddressBytes(); uint ipAddress = BitConverter.ToUInt32(addressBytes, 0); uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0); uint broadCastIPAddress = ipAddress | ~ipMaskV4; diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 89ec156a9f..1ca5e4327c 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -33,7 +33,6 @@ - diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index e9fcd67975..e016ffea10 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -1,6 +1,5 @@ using System.Net; using System.Net.Sockets; -using Microsoft.AspNetCore.HttpOverrides; namespace MediaBrowser.Model.Net; @@ -66,9 +65,9 @@ public class IPData { if (Address.Equals(IPAddress.None)) { - return Subnet.Prefix.AddressFamily.Equals(IPAddress.None) + return Subnet.BaseAddress.AddressFamily.Equals(IPAddress.None) ? AddressFamily.Unspecified - : Subnet.Prefix.AddressFamily; + : Subnet.BaseAddress.AddressFamily; } else { diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index f0f8e7afcd..0590ded32a 100644 --- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -9,6 +9,8 @@ net8.0 false true + + NU1903 diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index e0b65cbc44..8d464d4e75 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -6,7 +6,6 @@ using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; @@ -99,7 +98,7 @@ namespace Jellyfin.Server.Tests Assert.Equal(knownNetworks.Length, options.KnownNetworks.Count); foreach (var item in knownNetworks) { - Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.Prefix.Equals(item.Prefix) && x.PrefixLength == item.PrefixLength)); + Assert.NotNull(options.KnownNetworks.FirstOrDefault(x => x.BaseAddress.Equals(item.BaseAddress) && x.PrefixLength == item.PrefixLength)); } }