From dc27d8f9cd3b6cf280ba8e5e2520db387f651fb4 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Wed, 11 Oct 2023 00:02:37 +0200 Subject: [PATCH] Refactor URI overrides (#10051) --- Emby.Dlna/Main/DlnaEntryPoint.cs | 9 +- .../ApplicationHost.cs | 6 +- .../EntryPoints/UdpServerEntryPoint.cs | 29 ++- .../Net/SocketFactory.cs | 33 ++- Emby.Server.Implementations/Udp/UdpServer.cs | 11 +- .../Extensions/NetworkExtensions.cs | 13 +- Jellyfin.Networking/Manager/NetworkManager.cs | 244 +++++++++++------- .../ApiServiceCollectionExtensions.cs | 2 +- .../Extensions/WebHostBuilderExtensions.cs | 3 +- MediaBrowser.Model/Net/IPData.cs | 5 + .../Net/PublishedServerUriOverride.cs | 42 +++ RSSDP/SsdpCommunicationsServer.cs | 124 +++++---- RSSDP/SsdpDeviceLocator.cs | 42 ++- RSSDP/SsdpDevicePublisher.cs | 89 ++++--- .../NetworkExtensionsTests.cs | 1 - .../NetworkManagerTests.cs | 10 +- .../NetworkParseTests.cs | 39 +-- .../ParseNetworkTests.cs | 5 +- 18 files changed, 431 insertions(+), 276 deletions(-) create mode 100644 MediaBrowser.Model/Net/PublishedServerUriOverride.cs diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 1a4bec398c..83b0ef3164 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -201,9 +201,10 @@ namespace Emby.Dlna.Main { if (_communicationsServer is null) { - var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); - - _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) + _communicationsServer = new SsdpCommunicationsServer( + _socketFactory, + _networkManager, + _logger) { IsShared = true }; @@ -213,7 +214,7 @@ namespace Emby.Dlna.Main } catch (Exception ex) { - _logger.LogError(ex, "Error starting ssdp handlers"); + _logger.LogError(ex, "Error starting SSDP handlers"); } } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 3253ea0267..c247178ee9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -450,7 +450,7 @@ namespace Emby.Server.Implementations ConfigurationManager.AddParts(GetExports()); - NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger()); + NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger()); // Initialize runtime stat collection if (ConfigurationManager.Configuration.EnableMetrics) @@ -913,7 +913,7 @@ namespace Emby.Server.Implementations /// public string GetSmartApiUrl(HttpRequest request) { - // Return the host in the HTTP request as the API url + // Return the host in the HTTP request as the API URL if not configured otherwise if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; @@ -948,7 +948,7 @@ namespace Emby.Server.Implementations public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) { // With an empty source, the port will be null - var smart = NetManager.GetBindAddress(ipAddress, out _, true); + var smart = NetManager.GetBindAddress(ipAddress, out _, false); var scheme = !allowHttps ? Uri.UriSchemeHttp : null; int? port = !allowHttps ? HttpPort : null; return GetLocalApiUrl(smart, scheme, port); diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 54191649da..2c03f9ffda 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { /// - /// Class UdpServerEntryPoint. + /// Class responsible for registering all UDP broadcast endpoints and their handlers. /// public sealed class UdpServerEntryPoint : IServerEntryPoint { @@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; private readonly INetworkManager _networkManager; - private readonly bool _enableMultiSocketBinding; /// /// The UDP server. @@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints _configurationManager = configurationManager; _networkManager = networkManager; _udpServers = new List(); - _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } /// @@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints try { - if (_enableMultiSocketBinding) + // Linux needs to bind to the broadcast addresses to get broadcast traffic + // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses + if (OperatingSystem.IsLinux()) { - // Add global broadcast socket + // Add global broadcast listener var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); server.Start(_cancellationTokenSource.Token); _udpServers.Add(server); - // Add bind address specific broadcast sockets + // Add bind address specific broadcast listeners // IPv6 is currently unsupported var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); foreach (var intf in validInterfaces) @@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints } else { - var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); + // Add bind address specific broadcast listeners + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); + foreach (var intf in validInterfaces) + { + var intfAddress = intf.Address; + _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber); + + var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); + } } } catch (SocketException ex) @@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints { if (_disposed) { - throw new ObjectDisposedException(this.GetType().Name); + throw new ObjectDisposedException(GetType().Name); } } diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 51e92953df..505984cfb2 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,12 +1,15 @@ -#pragma warning disable CS1591 - using System; +using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.Net { + /// + /// Factory class to create different kinds of sockets. + /// public class SocketFactory : ISocketFactory { /// @@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net /// public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) { - ArgumentNullException.ThrowIfNull(bindInterface.Address); + var interfaceAddress = bindInterface.Address; + ArgumentNullException.ThrowIfNull(interfaceAddress); if (localPort < 0) { @@ -49,7 +53,7 @@ namespace Emby.Server.Implementations.Net try { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(new IPEndPoint(bindInterface.Address, localPort)); + socket.Bind(new IPEndPoint(interfaceAddress, localPort)); return socket; } @@ -82,16 +86,25 @@ namespace Emby.Server.Implementations.Net try { - var interfaceIndex = bindInterface.Index; - var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex); - socket.MulticastLoopback = false; socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); - socket.Bind(new IPEndPoint(multicastAddress, localPort)); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress)); + socket.Bind(new IPEndPoint(multicastAddress, localPort)); + } + else + { + // Only create socket if interface supports multicast + var interfaceIndex = bindInterface.Index; + var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex); + + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); + socket.Bind(new IPEndPoint(bindIPAddress, localPort)); + } return socket; } diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index a3bbd6df0c..10376ed76c 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp _endpoint = new IPEndPoint(bindAddress, port); - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) + { + MulticastLoopback = false, + }; _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } @@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp try { + _logger.LogDebug("Sending AutoDiscovery response"); await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); } catch (SocketException ex) @@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp { try { - var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false); + var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false); var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) { @@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp } catch (OperationCanceledException) { - // Don't throw + _logger.LogDebug("Broadcast socket operation cancelled"); } } } diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index e45fa3bcb7..2d921a4826 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -231,12 +231,12 @@ public static partial class NetworkExtensions } else if (address.AddressFamily == AddressFamily.InterNetwork) { - result = new IPNetwork(address, Network.MinimumIPv4PrefixSize); + result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize); return true; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { - result = new IPNetwork(address, Network.MinimumIPv6PrefixSize); + result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize); return true; } } @@ -284,12 +284,15 @@ public static partial class NetworkExtensions if (hosts.Count <= 2) { + var firstPart = hosts[0]; + // Is hostname or hostname:port - if (FqdnGeneratedRegex().IsMatch(hosts[0])) + if (FqdnGeneratedRegex().IsMatch(firstPart)) { try { - addresses = Dns.GetHostAddresses(hosts[0]); + // .NET automatically filters only supported returned addresses based on OS support. + addresses = Dns.GetHostAddresses(firstPart); return true; } catch (SocketException) @@ -299,7 +302,7 @@ public static partial class NetworkExtensions } // Is an IPv4 or IPv4:port - if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) + if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address)) { if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index f20e285263..9c59500d77 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -15,7 +15,9 @@ 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; namespace Jellyfin.Networking.Manager { @@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager private readonly IConfigurationManager _configurationManager; + private readonly IConfiguration _startupConfig; + private readonly object _networkEventLock; /// /// Holds the published server URLs and the IPs to use them on. /// - private IReadOnlyDictionary _publishedServerUrls; + private IReadOnlyList _publishedServerUrls; private IReadOnlyList _remoteAddressFilter; @@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager /// /// Initializes a new instance of the class. /// - /// IServerConfigurationManager instance. + /// The instance. + /// The instance holding startup parameters. /// Logger to use for messages. #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) + public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger logger) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(configurationManager); _logger = logger; _configurationManager = configurationManager; + _startupConfig = startupConfig; _initLock = new(); _interfaces = new List(); _macAddresses = new List(); - _publishedServerUrls = new Dictionary(); + _publishedServerUrls = new List(); _networkEventLock = new object(); _remoteAddressFilter = new List(); @@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager /// /// Gets the Published server override list. /// - public IReadOnlyDictionary PublishedServerUrls => _publishedServerUrls; + public IReadOnlyList PublishedServerUrls => _publishedServerUrls; /// public void Dispose() @@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager { 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; OnNetworkChange(); @@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager } else { - InitialiseInterfaces(); - InitialiseLan(networkConfig); + InitializeInterfaces(); + InitializeLan(networkConfig); EnforceBindSettings(networkConfig); } + PrintNetworkInformation(networkConfig); NetworkChanged?.Invoke(this, EventArgs.Empty); } finally @@ -210,7 +216,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 addresses. /// - private void InitialiseInterfaces() + private void InitializeInterfaces() { lock (_initLock) { @@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager try { var nics = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); + .Where(i => i.OperationalStatus == OperationalStatus.Up); foreach (NetworkInterface adapter in nics) { @@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager { if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); - interfaceObject.Index = ipProperties.GetIPv4Properties().Index; - interfaceObject.Name = adapter.Name; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) + { + Index = ipProperties.GetIPv4Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; interfaces.Add(interfaceObject); } else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); - interfaceObject.Index = ipProperties.GetIPv6Properties().Index; - interfaceObject.Name = adapter.Name; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) + { + Index = ipProperties.GetIPv6Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; interfaces.Add(interfaceObject); } } } -#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types { // Ignore error, and attempt to continue. _logger.LogError(ex, "Error encountered parsing interfaces."); } } } -#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types { _logger.LogError(ex, "Error obtaining interfaces."); } @@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager { _logger.LogWarning("No interface information available. Using loopback interface(s)."); - if (IsIPv4Enabled && !IsIPv6Enabled) + if (IsIPv4Enabled) { - interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } - if (!IsIPv4Enabled && IsIPv6Enabled) + if (IsIPv6Enabled) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } } @@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager } /// - /// Initialises internal LAN cache. + /// Initializes internal LAN cache. /// - private void InitialiseLan(NetworkConfiguration config) + private void InitializeLan(NetworkConfiguration config) { lock (_initLock) { @@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) ? excludedSubnets : new List(); - - _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); } } @@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager .ToHashSet(); interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); - if (bindAddresses.Contains(IPAddress.Loopback)) + if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) { interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } - if (bindAddresses.Contains(IPAddress.IPv6Loopback)) + if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) { interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } @@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } - _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _interfaces = interfaces; } } /// - /// Initialises the remote address values. + /// Initializes the remote address values. /// - private void InitialiseRemote(NetworkConfiguration config) + private void InitializeRemote(NetworkConfiguration config) { lock (_initLock) { @@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager /// format is subnet=ipaddress|host|uri /// when subnet = 0.0.0.0, any external address matches. /// - private void InitialiseOverrides(NetworkConfiguration config) + private void InitializeOverrides(NetworkConfiguration config) { lock (_initLock) { - var publishedServerUrls = new Dictionary(); - var overrides = config.PublishedServerUriBySubnet; + var publishedServerUrls = new List(); + + // Prefer startup configuration. + var startupOverrideKey = _startupConfig[AddressOverrideKey]; + if (!string.IsNullOrEmpty(startupOverrideKey)) + { + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + startupOverrideKey, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + startupOverrideKey, + true, + true)); + _publishedServerUrls = publishedServerUrls; + return; + } + var overrides = config.PublishedServerUriBySubnet; foreach (var entry in overrides) { var parts = entry.Split('='); @@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager var identifier = parts[0]; if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; + // Drop any other overrides in case an "all" override exists + publishedServerUrls.Clear(); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + replacement, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + replacement, + true, + true)); + break; } else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement; - publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + replacement, + false, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + replacement, + false, + true)); } else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) { foreach (var lan in _lanSubnets) { var lanPrefix = lan.Prefix; - publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), + replacement, + true, + false)); } } else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) { var data = new IPData(result.Prefix, result); - publishedServerUrls[data] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + data, + replacement, + true, + true)); } else if (TryParseInterface(identifier, out var ifaces)) { foreach (var iface in ifaces) { - publishedServerUrls[iface] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + iface, + replacement, + true, + true)); } } else @@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager } /// - /// Reloads all settings and re-initialises the instance. + /// Reloads all settings and re-Initializes the instance. /// /// The to use. public void UpdateSettings(object configuration) @@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager var config = (NetworkConfiguration)configuration; HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6; - InitialiseLan(config); - InitialiseRemote(config); + InitializeLan(config); + InitializeRemote(config); if (string.IsNullOrEmpty(MockNetworkSettings)) { - InitialiseInterfaces(); + InitializeInterfaces(); } else // Used in testing only. { @@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { - var data = new IPData(address, subnet, parts[2]); - data.Index = index; + var data = new IPData(address, subnet, parts[2]) + { + Index = index + }; interfaces.Add(data); } } @@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager } EnforceBindSettings(config); - InitialiseOverrides(config); + InitializeOverrides(config); + + PrintNetworkInformation(config, false); } /// @@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager /// public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false) { - if (_interfaces.Count != 0) + if (_interfaces.Count > 0 || individualInterfaces) { return _interfaces; } // No bind address and no exclusions, so listen on all interfaces. var result = new List(); - - if (individualInterfaces) - { - result.AddRange(_interfaces); - return result; - } - if (IsIPv4Enabled && IsIPv6Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default @@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager bindPreference = string.Empty; int? port = null; - var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any) - || x.Key.Subnet.Contains(source)) - .DistinctBy(x => x.Key) - .OrderBy(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any)) + // Only consider subnets including the source IP, prefering specific overrides + List validPublishedServerUrls; + if (!isInExternalSubnet) + { + // Only use matching internal subnets + // Prefer more specific (bigger subnet prefix) overrides + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source)) + .OrderByDescending(x => x.Data.Subnet.PrefixLength) + .ToList(); + } + else + { + // Only use matching external subnets + // Prefer more specific (bigger subnet prefix) overrides + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source)) + .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); + } - // Check for user override. foreach (var data in validPublishedServerUrls) { - if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any))) - { - // External. - bindPreference = data.Value; - break; - } - - // Get address interface. - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); + // Get interface matching override subnet + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address)); if (intf?.Address is not null) { - // Match IP address. - bindPreference = data.Value; + // If matching interface is found, use override + bindPreference = data.OverrideUri; break; } } @@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager return false; } - // Has it got a port defined? + // Handle override specifying port var parts = bindPreference.Split(':'); if (parts.Length > 1) { @@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager { bindPreference = parts[0]; port = p; + _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); + return true; } } - if (port is not null) - { - _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); - } - else - { - _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); - } - + _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); return true; } @@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } + + private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true) + { + 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, "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)); + } + } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 89dbbdd2fe..cb1680558f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } - else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) + else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) { foreach (var address in addresses) { diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 3cb791b571..c9d5b54def 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -3,7 +3,6 @@ using System.IO; using System.Net; using Jellyfin.Server.Helpers; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.NetManager.GetAllBindInterfaces(); + var addresses = appHost.NetManager.GetAllBindInterfaces(true); bool flagged = false; foreach (var netAdd in addresses) diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index 985b16c6e4..e9fcd67975 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -47,6 +47,11 @@ public class IPData /// public int Index { get; set; } + /// + /// Gets or sets a value indicating whether the network supports multicast. + /// + public bool SupportsMulticast { get; set; } = false; + /// /// Gets or sets the interface name. /// diff --git a/MediaBrowser.Model/Net/PublishedServerUriOverride.cs b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs new file mode 100644 index 0000000000..476d1ba382 --- /dev/null +++ b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs @@ -0,0 +1,42 @@ +namespace MediaBrowser.Model.Net; + +/// +/// Class holding information for a published server URI override. +/// +public class PublishedServerUriOverride +{ + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The override. + /// A value indicating whether the override is for internal requests. + /// A value indicating whether the override is for external requests. + public PublishedServerUriOverride(IPData data, string overrideUri, bool internalOverride, bool externalOverride) + { + Data = data; + OverrideUri = overrideUri; + IsInternalOverride = internalOverride; + IsExternalOverride = externalOverride; + } + + /// + /// Gets or sets the object's IP address. + /// + public IPData Data { get; set; } + + /// + /// Gets or sets the override URI. + /// + public string OverrideUri { get; set; } + + /// + /// Gets or sets a value indicating whether the override should be applied to internal requests. + /// + public bool IsInternalOverride { get; set; } + + /// + /// Gets or sets a value indicating whether the override should be applied to external requests. + /// + public bool IsExternalOverride { get; set; } +} diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 0dce6c3bfa..a3f30c174f 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -32,10 +32,10 @@ namespace Rssdp.Infrastructure * port to use, we will default to 0 which allows the underlying system to auto-assign a free port. */ - private object _BroadcastListenSocketSynchroniser = new object(); + private object _BroadcastListenSocketSynchroniser = new(); private List _MulticastListenSockets; - private object _SendSocketSynchroniser = new object(); + private object _SendSocketSynchroniser = new(); private List _sendSockets; private HttpRequestParser _RequestParser; @@ -48,7 +48,6 @@ namespace Rssdp.Infrastructure private int _MulticastTtl; private bool _IsShared; - private readonly bool _enableMultiSocketBinding; /// /// Raised when a HTTPU request message is received by a socket (unicast or multicast). @@ -64,9 +63,11 @@ namespace Rssdp.Infrastructure /// Minimum constructor. /// /// The argument is null. - public SsdpCommunicationsServer(ISocketFactory socketFactory, - INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) - : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) + public SsdpCommunicationsServer( + ISocketFactory socketFactory, + INetworkManager networkManager, + ILogger logger) + : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger) { } @@ -76,7 +77,12 @@ namespace Rssdp.Infrastructure /// /// The argument is null. /// The argument is less than or equal to zero. - public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) + public SsdpCommunicationsServer( + ISocketFactory socketFactory, + int localPort, + int multicastTimeToLive, + INetworkManager networkManager, + ILogger logger) { if (socketFactory is null) { @@ -88,19 +94,18 @@ namespace Rssdp.Infrastructure throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero."); } - _BroadcastListenSocketSynchroniser = new object(); - _SendSocketSynchroniser = new object(); + _BroadcastListenSocketSynchroniser = new(); + _SendSocketSynchroniser = new(); _LocalPort = localPort; _SocketFactory = socketFactory; - _RequestParser = new HttpRequestParser(); - _ResponseParser = new HttpResponseParser(); + _RequestParser = new(); + _ResponseParser = new(); _MulticastTtl = multicastTimeToLive; _networkManager = networkManager; _logger = logger; - _enableMultiSocketBinding = enableMultiSocketBinding; } /// @@ -335,7 +340,7 @@ namespace Rssdp.Infrastructure { sockets = sockets.ToList(); - var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))) + var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address)) .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); return Task.WhenAll(tasks); } @@ -347,33 +352,26 @@ namespace Rssdp.Infrastructure { var sockets = new List(); var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress); - if (_enableMultiSocketBinding) - { - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork) - .DistinctBy(x => x.Index); - foreach (var intf in validInterfaces) + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.SupportsMulticast) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork) + .DistinctBy(x => x.Index); + + foreach (var intf in validInterfaces) + { + try { - try - { - var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address); - } + var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index); } - } - else - { - var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); } return sockets; @@ -382,34 +380,32 @@ namespace Rssdp.Infrastructure private List CreateSendSockets() { var sockets = new List(); - if (_enableMultiSocketBinding) + + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.SupportsMulticast) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork); + + if (OperatingSystem.IsMacOS()) { - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork); + // Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340 + validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback)); + } - foreach (var intf in validInterfaces) + foreach (var intf in validInterfaces) + { + try { - try - { - var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address); - } + var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index); } } - else - { - var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - return sockets; } @@ -423,7 +419,7 @@ namespace Rssdp.Infrastructure { try { - var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);; + var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);; if (result.ReceivedBytes > 0) { @@ -431,7 +427,7 @@ namespace Rssdp.Infrastructure var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface); ProcessMessage( - UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), + Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), remoteEndpoint, localEndpointAdapter.Address); } @@ -511,7 +507,7 @@ namespace Rssdp.Infrastructure return; } - var handlers = this.RequestReceived; + var handlers = RequestReceived; if (handlers is not null) { handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); @@ -520,7 +516,7 @@ namespace Rssdp.Infrastructure private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress) { - var handlers = this.ResponseReceived; + var handlers = ResponseReceived; if (handlers is not null) { handlers(this, new ResponseReceivedEventArgs(data, endPoint) diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 59f4c5070b..82b09c4b45 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -17,7 +17,7 @@ namespace Rssdp.Infrastructure private ISsdpCommunicationsServer _CommunicationsServer; private Timer _BroadcastTimer; - private object _timerLock = new object(); + private object _timerLock = new(); private string _OSName; @@ -221,12 +221,12 @@ namespace Rssdp.Infrastructure /// protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) { - if (this.IsDisposed) + if (IsDisposed) { return; } - var handlers = this.DeviceAvailable; + var handlers = DeviceAvailable; if (handlers is not null) { handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) @@ -244,12 +244,12 @@ namespace Rssdp.Infrastructure /// protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) { - if (this.IsDisposed) + if (IsDisposed) { return; } - var handlers = this.DeviceUnavailable; + var handlers = DeviceUnavailable; if (handlers is not null) { handlers(this, new DeviceUnavailableEventArgs(device, expired)); @@ -291,8 +291,8 @@ namespace Rssdp.Infrastructure _CommunicationsServer = null; if (commsServer is not null) { - commsServer.ResponseReceived -= this.CommsServer_ResponseReceived; - commsServer.RequestReceived -= this.CommsServer_RequestReceived; + commsServer.ResponseReceived -= CommsServer_ResponseReceived; + commsServer.RequestReceived -= CommsServer_RequestReceived; } } } @@ -341,7 +341,7 @@ namespace Rssdp.Infrastructure var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - values["HOST"] = "239.255.255.250:1900"; + values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort); values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["MAN"] = "\"ssdp:discover\""; @@ -382,17 +382,17 @@ namespace Rssdp.Infrastructure private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress) { - if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) + if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) { return; } var notificationType = GetFirstHeaderStringValue("NTS", message); - if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) { ProcessAliveNotification(message, IPAddress); } - else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) + else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) { ProcessByeByeNotification(message); } @@ -420,7 +420,7 @@ namespace Rssdp.Infrastructure private void ProcessByeByeNotification(HttpRequestMessage message) { var notficationType = GetFirstHeaderStringValue("NT", message); - if (!String.IsNullOrEmpty(notficationType)) + if (!string.IsNullOrEmpty(notficationType)) { var usn = GetFirstHeaderStringValue("USN", message); @@ -447,10 +447,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) { string retVal = null; - IEnumerable values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -463,10 +462,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) { string retVal = null; - IEnumerable values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -479,10 +477,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) { string value = null; - IEnumerable values; if (request.Headers.Contains(headerName)) { - request.Headers.TryGetValues(headerName, out values); + request.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -496,10 +493,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) { string value = null; - IEnumerable values; if (response.Headers.Contains(headerName)) { - response.Headers.TryGetValues(headerName, out values); + response.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -529,7 +525,7 @@ namespace Rssdp.Infrastructure foreach (var device in expiredDevices) { - if (this.IsDisposed) + if (IsDisposed) { return; } @@ -543,7 +539,7 @@ namespace Rssdp.Infrastructure // problems. foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct()) { - if (this.IsDisposed) + if (IsDisposed) { return; } @@ -560,7 +556,7 @@ namespace Rssdp.Infrastructure existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn); foreach (var existingDevice in existingDevices) { - if (this.IsDisposed) + if (IsDisposed) { return true; } diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 950e6fec86..65ae658a45 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -206,9 +206,9 @@ namespace Rssdp.Infrastructure IPAddress receivedOnlocalIPAddress, CancellationToken cancellationToken) { - if (String.IsNullOrEmpty(searchTarget)) + if (string.IsNullOrEmpty(searchTarget)) { - WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); + WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); return; } @@ -232,7 +232,7 @@ namespace Rssdp.Infrastructure // return; } - if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) + if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) { return; } @@ -243,27 +243,27 @@ namespace Rssdp.Infrastructure } // Do not block synchronously as that may tie up a threadpool thread for several seconds. - Task.Delay(_Random.Next(16, (maxWaitInterval * 1000))).ContinueWith((parentTask) => + Task.Delay(_Random.Next(16, maxWaitInterval * 1000)).ContinueWith((parentTask) => { // Copying devices to local array here to avoid threading issues/enumerator exceptions. IEnumerable devices = null; lock (_Devices) { - if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) { devices = GetAllDevicesAsFlatEnumerable().ToArray(); } - else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) + else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) { devices = _Devices.ToArray(); } else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) { - devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) { - devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } } @@ -299,7 +299,7 @@ namespace Rssdp.Infrastructure if (isRootDevice) { SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); } @@ -312,7 +312,7 @@ namespace Rssdp.Infrastructure private string GetUsn(string udn, string fullDeviceType) { - return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); + return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); } private async void SendSearchResponse( @@ -326,16 +326,17 @@ namespace Rssdp.Infrastructure const string header = "HTTP/1.1 200 OK"; var rootDevice = device.ToRootDevice(); - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - values["EXT"] = ""; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["HOST"] = "239.255.255.250:1900"; - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["ST"] = searchTarget; - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["USN"] = uniqueServiceName; - values["LOCATION"] = rootDevice.Location.ToString(); + var values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["EXT"] = "", + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds, + ["ST"] = searchTarget, + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["USN"] = uniqueServiceName, + ["LOCATION"] = rootDevice.Location.ToString() + }; var message = BuildMessage(header, values); @@ -439,7 +440,7 @@ namespace Rssdp.Infrastructure if (isRoot) { SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); } @@ -460,17 +461,18 @@ namespace Rssdp.Infrastructure const string header = "NOTIFY * HTTP/1.1"; - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["LOCATION"] = rootDevice.Location.ToString(); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["NTS"] = "ssdp:alive"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; + var values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // If needed later for non-server devices, these headers will need to be dynamic + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds, + ["LOCATION"] = rootDevice.Location.ToString(), + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["NTS"] = "ssdp:alive", + ["NT"] = notificationType, + ["USN"] = uniqueServiceName + }; var message = BuildMessage(header, values); @@ -485,7 +487,7 @@ namespace Rssdp.Infrastructure if (isRoot) { tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken)); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken)); } @@ -506,20 +508,21 @@ namespace Rssdp.Infrastructure { const string header = "NOTIFY * HTTP/1.1"; - var values = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["NTS"] = "ssdp:byebye"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; + var values = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + // If needed later for non-server devices, these headers will need to be dynamic + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["NTS"] = "ssdp:byebye", + ["NT"] = notificationType, + ["USN"] = uniqueServiceName + }; var message = BuildMessage(header, values); var sendCount = IsDisposed ? 1 : 3; - WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); + WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); } diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs index 2ff7c7de4f..072e0a8c53 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs @@ -16,7 +16,6 @@ namespace Jellyfin.Networking.Tests [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")] diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 0b07a3c53f..2302f90b8d 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -1,7 +1,9 @@ using System.Net; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Xunit; namespace Jellyfin.Networking.Tests @@ -28,7 +30,8 @@ namespace Jellyfin.Networking.Tests LocalNetworkSubnets = network.Split(',') }; - using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); Assert.True(networkManager.IsInLocalNetwork(ip)); } @@ -56,9 +59,10 @@ namespace Jellyfin.Networking.Tests LocalNetworkSubnets = network.Split(',') }; - using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); - Assert.False(nm.IsInLocalNetwork(ip)); + Assert.False(networkManager.IsInLocalNetwork(ip)); } } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 731cbbafbc..022b8a3d04 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -7,6 +7,7 @@ using Jellyfin.Networking.Extensions; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Net; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -54,7 +55,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); @@ -200,7 +202,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; // Check to see if DNS resolution is working. If not, skip test. @@ -229,24 +232,24 @@ namespace Jellyfin.Networking.Tests [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")] + [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "all=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")] + [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "external=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")] + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "external=http://helloworld.com", "eth16")] // User on external network, internal binding only - so assumption 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")] + [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=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")] + [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=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 assumed to be internal, no binding - so result is the 1st matching interface. + [InlineData("", "192.168.1.0/24", "", false, "all=http://helloworld.com", "eth16")] - // User is internal, no binding - so result is the 1st internal, which is then overridden. + // User is internal, no binding - so result is the 1st internal interface, 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) { @@ -264,7 +267,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); NetworkManager.MockNetworkSettings = string.Empty; if (nm.TryParseInterface(result, out IReadOnlyList? resultObj) && resultObj is not null) @@ -293,7 +297,9 @@ namespace Jellyfin.Networking.Tests RemoteIPFilter = addresses.Split(','), IsRemoteIPFilterBlacklist = false }; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } @@ -314,7 +320,8 @@ namespace Jellyfin.Networking.Tests IsRemoteIPFilterBlacklist = true }; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } @@ -334,7 +341,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); var interfaceToUse = nm.GetBindAddress(string.Empty, out _); @@ -358,7 +366,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger()); var interfaceToUse = nm.GetBindAddress(source, out _); diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 49516ccccd..2881020375 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -7,6 +7,7 @@ using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -119,8 +120,8 @@ namespace Jellyfin.Server.Tests EnableIPv6 = true, EnableIPv4 = true, }; - - return new NetworkManager(GetMockConfig(conf), new NullLogger()); + var startupConf = new Mock(); + return new NetworkManager(GetMockConfig(conf), startupConf.Object, new NullLogger()); } } }