diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 089ec30e6b..ff0a2a3617 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -18,13 +18,12 @@ namespace Emby.Server.Implementations.Networking 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 IPAddress[] _localIpAddresses; + private List _macAddresses; /// @@ -157,7 +156,9 @@ namespace Emby.Server.Implementations.Networking return false; } - byte[] octet = ipAddress.GetAddressBytes(); + // GetAddressBytes + Span octet = stackalloc byte[ipAddress.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + ipAddress.TryWriteBytes(octet, out _); if ((octet[0] == 10) || (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 @@ -260,7 +261,9 @@ namespace Emby.Server.Implementations.Networking /// public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC) { - byte[] octet = address.GetAddressBytes(); + // GetAddressBytes + Span octet = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + address.TryWriteBytes(octet, out _); if ((octet[0] == 127) || // RFC1122 (octet[0] == 169 && octet[1] == 254)) // RFC3927 @@ -503,18 +506,25 @@ namespace Emby.Server.Implementations.Networking private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask) { - byte[] ipAdressBytes = address.GetAddressBytes(); - byte[] subnetMaskBytes = subnetMask.GetAddressBytes(); + int size = address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16; + + // GetAddressBytes + Span ipAddressBytes = stackalloc byte[size]; + address.TryWriteBytes(ipAddressBytes, out _); + + // GetAddressBytes + Span subnetMaskBytes = stackalloc byte[size]; + subnetMask.TryWriteBytes(subnetMaskBytes, out _); - if (ipAdressBytes.Length != subnetMaskBytes.Length) + if (ipAddressBytes.Length != subnetMaskBytes.Length) { throw new ArgumentException("Lengths of IP address and subnet mask do not match."); } - byte[] broadcastAddress = new byte[ipAdressBytes.Length]; + byte[] broadcastAddress = new byte[ipAddressBytes.Length]; for (int i = 0; i < broadcastAddress.Length; i++) { - broadcastAddress[i] = (byte)(ipAdressBytes[i] & subnetMaskBytes[i]); + broadcastAddress[i] = (byte)(ipAddressBytes[i] & subnetMaskBytes[i]); } return new IPAddress(broadcastAddress); 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/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs new file mode 100644 index 0000000000..df420f48a2 --- /dev/null +++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs @@ -0,0 +1,221 @@ +#pragma warning disable CA1819 // Properties should not return arrays + +using System; +using MediaBrowser.Model.Configuration; + +namespace Jellyfin.Networking.Configuration +{ + /// + /// Defines the . + /// + public class NetworkConfiguration + { + /// + /// The default value for . + /// + public const int DefaultHttpPort = 8096; + + /// + /// The default value for and . + /// + 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; } + + /// + /// 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[^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; } + + /// + /// 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; } + + /// + /// Gets or sets the UDPPortRange. + /// + public string UDPPortRange { get; set; } = string.Empty; + + /// + /// 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. + /// + 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 have any effect. + /// + 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. + /// + 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 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; } + + /// + /// 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; } + + /// + /// 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; } + + /// + /// 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. + /// + 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/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj new file mode 100644 index 0000000000..cbda74361f --- /dev/null +++ b/Jellyfin.Networking/Jellyfin.Networking.csproj @@ -0,0 +1,30 @@ + + + net5.0 + 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..eababa6a90 --- /dev/null +++ b/Jellyfin.Networking/Manager/INetworkManager.cs @@ -0,0 +1,234 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; +using System.Net.NetworkInformation; +using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Interface for the NetworkManager class. + /// + public interface INetworkManager + { + /// + /// Event triggered on network changes. + /// + event EventHandler NetworkChanged; + + /// + /// Gets the published server urls list. + /// + Dictionary PublishedServerUrls { get; } + + /// + /// Gets a value indicating whether is all IPv6 interfaces are trusted as internal. + /// + bool TrustAllIP6Interfaces { get; } + + /// + /// Gets the remote address filter. + /// + Collection RemoteAddressFilter { get; } + + /// + /// Gets or sets a value indicating whether iP6 is enabled. + /// + bool IsIP6Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether iP4 is enabled. + /// + bool IsIP4Enabled { get; set; } + + /// + /// Calculates the list of interfaces to use for Kestrel. + /// + /// 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. + Collection GetAllBindInterfaces(bool individualInterfaces = false); + + /// + /// Returns a collection containing the loopback interfaces. + /// + /// Collection{IPObject}. + Collection GetLoopbacks(); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// The priority of selection is as follows:- + /// + /// The value contained in the startup parameter --published-server-url. + /// + /// If the user specified custom subnet overrides, the correct subnet for the source address. + /// + /// If the user specified bind interfaces to use:- + /// The bind interface that contains the source subnet. + /// The first bind interface specified that suits best first the source's endpoint. eg. external or internal. + /// + /// If the source is from a public subnet address range and the user hasn't specified any bind addresses:- + /// The first public interface that isn't a loopback and contains the source subnet. + /// The first public interface that isn't a loopback. Priority is given to interfaces with gateways. + /// An internal interface if there are no public ip addresses. + /// + /// If the source is from a private subnet address range and the user hasn't specified any bind addresses:- + /// The first private interface that contains the source subnet. + /// The first private interface that isn't a loopback. Priority is given to interfaces with gateways. + /// + /// If no interfaces meet any of these criteria, then a loopback address is returned. + /// + /// Interface that have been specifically excluded from binding are not used in any of the calculations. + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(IPObject source, out int? port); + + /// + /// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo) + /// If no bind addresses are specified, an internal interface address is selected. + /// (See . + /// + /// 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 . + /// + /// 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 . + /// + /// Source of the request. + /// Optional port returned, if it's part of an override. + /// IP Address to use, or loopback address if all else fails. + string GetBindInterface(string source, out int? port); + + /// + /// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses. + /// + /// IP address to check. + /// True if it is. + bool IsExcludedInterface(IPAddress address); + + /// + /// Get a list of all the MAC addresses associated with active interfaces. + /// + /// List of MAC addresses. + IReadOnlyCollection 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. + 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. + /// 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's ip addresses, if successful. + /// Success of the operation. + bool TryParseInterface(string token, out Collection? result); + + /// + /// 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. + Collection CreateIPCollection(string[] values, bool bracketed = false); + + /// + /// Returns all the internal Bind interface addresses. + /// + /// An internal list of interfaces addresses. + Collection 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. + Collection GetFilteredLANSubnets(Collection? filter = null); + } +} diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs new file mode 100644 index 0000000000..515ae669a1 --- /dev/null +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -0,0 +1,1319 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +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 Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Networking.Manager +{ + /// + /// Class to take care of network interface management. + /// Note: The normal collection methods and properties will not work with Collection{IPObject}. . + /// + public class NetworkManager : INetworkManager, IDisposable + { + /// + /// Contains the description of the interface along with its index. + /// + private readonly Dictionary _interfaceNames; + + /// + /// Threading lock for network properties. + /// + private readonly object _intLock = new object(); + + /// + /// List of all interface addresses and masks. + /// + private readonly Collection _interfaceAddresses; + + /// + /// List of all interface MAC addresses. + /// + private readonly List _macAddresses; + + private readonly ILogger _logger; + + private readonly IConfigurationManager _configurationManager; + + private readonly object _eventFireLock; + + /// + /// Holds the bind address overrides. + /// + private readonly Dictionary _publishedServerUrls; + + /// + /// Used to stop "event-racing conditions". + /// + private bool _eventfire; + + /// + /// Unfiltered user defined LAN subnets. () + /// or internal interface network subnets if undefined by user. + /// + private Collection _lanSubnets; + + /// + /// User defined list of subnets to excluded from the LAN. + /// + private Collection _excludedSubnets; + + /// + /// List of interface addresses to bind the WS. + /// + private Collection _bindAddresses; + + /// + /// List of interface addresses to exclude from bind. + /// + private Collection _bindExclusions; + + /// + /// Caches list of all internal filtered interface addresses and masks. + /// + private Collection _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 UpdateSettings 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 Collection(); + _macAddresses = new List(); + _interfaceNames = new Dictionary(); + _publishedServerUrls = new Dictionary(); + _eventFireLock = new object(); + + UpdateSettings(_configurationManager.GetNetworkConfiguration()); + + NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; + + _configurationManager.NamedConfigurationUpdated += ConfigurationUpdated; + } +#pragma warning restore CS8618 // Non-nullable field is uninitialized. + + /// + /// Event triggered on network changes. + /// + public event EventHandler? NetworkChanged; + + /// + /// Gets or sets a value indicating whether testing is taking place. + /// + public static string MockNetworkSettings { get; set; } = string.Empty; + + /// + /// 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; } + + /// + public Collection 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 PublishedServerUrls => _publishedServerUrls; + + /// + /// Creates a new network collection. + /// + /// Items to assign the collection, or null. + /// The collection created. + public static Collection CreateCollection(IEnumerable? source = null) + { + var result = new Collection(); + if (source != null) + { + foreach (var item in source) + { + result.AddItem(item); + } + } + + return result; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public IReadOnlyCollection GetMacAddresses() + { + // Populated in construction - so always has values. + return _macAddresses; + } + + /// + public bool IsGatewayInterface(IPObject? addressObj) + { + var address = addressObj?.Address ?? IPAddress.None; + return _internalInterfaces.Any(i => i.Address.Equals(address) && i.Tag < 0); + } + + /// + public bool IsGatewayInterface(IPAddress? addressObj) + { + 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); + } + + if (IsIP6Enabled) + { + nc.AddItem(IPAddress.IPv6Loopback); + } + + return nc; + } + + /// + public bool IsExcluded(IPAddress ip) + { + return _excludedSubnets.ContainsAddress(ip); + } + + /// + public bool IsExcluded(EndPoint ip) + { + return ip != null && IsExcluded(((IPEndPoint)ip).Address); + } + + /// + public Collection CreateIPCollection(string[] values, bool bracketed = false) + { + Collection col = new Collection(); + if (values == null) + { + return col; + } + + for (int a = 0; a < values.Length; a++) + { + string v = values[a].Trim(); + + try + { + if (v.StartsWith('[') && v.EndsWith(']')) + { + if (bracketed) + { + AddToCollection(col, v[1..^1]); + } + } + else if (v.StartsWith('!')) + { + if (bracketed) + { + AddToCollection(col, v[1..]); + } + } + else if (!bracketed) + { + AddToCollection(col, v); + } + } + catch (ArgumentException e) + { + _logger.LogWarning(e, "Ignoring LAN value {value}.", v); + } + } + + return col; + } + + /// + public Collection GetAllBindInterfaces(bool individualInterfaces = false) + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the interfaces except the ones specifically excluded. + return _interfaceAddresses.Exclude(_bindExclusions); + } + + if (individualInterfaces) + { + return new Collection(_interfaceAddresses); + } + + // No bind address and no exclusions, so listen on all interfaces. + Collection result = new Collection(); + + if (IsIP4Enabled) + { + result.AddItem(IPAddress.Any); + } + + if (IsIP6Enabled) + { + result.AddItem(IPAddress.IPv6Any); + } + + return result; + } + + // Remove any excluded bind interfaces. + return _bindAddresses.Exclude(_bindExclusions); + } + + /// + public string GetBindInterface(string source, out int? port) + { + if (!string.IsNullOrEmpty(source) && IPHost.TryParse(source, out IPHost host)) + { + return GetBindInterface(host, out port); + } + + 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)) + { + result = GetBindInterface(host, out port); + port ??= source.Host.Port; + } + else + { + result = GetBindInterface(IPNetAddress.None, out port); + port ??= source?.Host.Port; + } + + return result; + } + + /// + 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; + + if (haveSource) + { + 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 && source.AddressFamily == AddressFamily.InterNetwork) + { + _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); + } + + isExternal = !IsInLocalNetwork(source); + + if (MatchesPublishedServerUrl(source, isExternal, out string res, out port)) + { + _logger.LogInformation("{Source}: Using BindAddress {Address}:{Port}", source, res, port); + return res; + } + } + + _logger.LogDebug("GetBindInterface: Source: {HaveSource}, External: {IsExternal}:", haveSource, isExternal); + + // No preference given, so move on to bind addresses. + 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 = CreateCollection(_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("{Source}: GetBindInterface: Has source, matched best internal interface on range. {Result}", source, result); + return result; + } + } + } + + result = FormatIP6String(interfaces.First().Address); + _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface. {Result}", source, result); + return result; + } + + // There isn't any others, so we'll use the loopback. + result = IsIP6Enabled ? "::" : "127.0.0.1"; + _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result); + return result; + } + + /// + public Collection GetInternalBindAddresses() + { + int count = _bindAddresses.Count; + + if (count == 0) + { + if (_bindExclusions.Count > 0) + { + // Return all the internal interfaces except the ones excluded. + return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.ContainsAddress(p))); + } + + // No bind address, so return all internal interfaces. + return CreateCollection(_internalInterfaces.Where(p => !p.IsLoopback())); + } + + return new Collection(_bindAddresses); + } + + /// + 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; + } + + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address); + } + + /// + public bool IsInLocalNetwork(string address) + { + if (IPHost.TryParse(address, out IPHost ep)) + { + return _lanSubnets.ContainsAddress(ep) && !_excludedSubnets.ContainsAddress(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; + } + + // As private addresses can be redefined by Configuration.LocalNetworkAddresses + return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(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) + { + return _bindExclusions.ContainsAddress(address); + } + + /// + public Collection GetFilteredLANSubnets(Collection? filter = null) + { + if (filter == null) + { + return _lanSubnets.Exclude(_excludedSubnets).AsNetworks(); + } + + return _lanSubnets.Exclude(filter); + } + + /// + public bool IsValidInterfaceAddress(IPAddress address) + { + return _interfaceAddresses.ContainsAddress(address); + } + + /// + public bool TryParseInterface(string token, out Collection? result) + { + result = null; + if (string.IsNullOrEmpty(token)) + { + return false; + } + + if (_interfaceNames != null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index)) + { + result = new Collection(); + + _logger.LogInformation("Interface {Token} 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.AddItem(iface); + } + } + + return true; + } + + return false; + } + + /// + /// Reloads all settings and re-initialises the instance. + /// + /// The to use. + public void UpdateSettings(object configuration) + { + NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); + + IsIP4Enabled = Socket.OSSupportsIPv4 && 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; + + 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.AddItem(address); + _interfaceNames.Add(parts[2], Math.Abs(index)); + } + } + + 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.NamedConfigurationUpdated -= ConfigurationUpdated; + NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged; + NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged; + } + + _disposed = true; + } + } + + /// + /// Tries to identify the string and return an object of that class. + /// + /// String to parse. + /// IPObject to return. + /// true if the value parsed successfully, false otherwise. + 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; + } + + /// + /// Converts an IPAddress into a string. + /// Ipv6 addresses are returned in [ ], with their scope removed. + /// + /// Address to convert. + /// URI safe conversion of the address. + private static 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; + } + + private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt) + { + if (evt.Key.Equals("network", StringComparison.Ordinal)) + { + UpdateSettings((NetworkConfiguration)evt.NewConfiguration); + } + } + + /// + /// 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, False otherwise. + 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 a string and adds it into the the collection, replacing any interface references. + /// + /// 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. + if (IsInterface(token, out int index)) + { + _logger.LogInformation("Interface {Token} 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.AddItem(iface); + } + } + } + else if (TryParse(token, out IPObject obj)) + { + if (!IsIP6Enabled) + { + // Remove IP6 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetworkV6); + if (!obj.IsIP6()) + { + col.AddItem(obj); + } + } + else if (!IsIP4Enabled) + { + // Remove IP4 addresses from multi-homed IPHosts. + obj.Remove(AddressFamily.InterNetwork); + if (obj.IsIP6()) + { + col.AddItem(obj); + } + } + else + { + col.AddItem(obj); + } + } + else + { + _logger.LogDebug("Invalid or unknown network {Token}.", token); + } + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// A containing network availability information. + private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e) + { + _logger.LogDebug("Network availability changed."); + OnNetworkChanged(); + } + + /// + /// Handler for network change events. + /// + /// Sender. + /// An . + 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. + /// + /// A representing the asynchronous operation. + private async Task OnNetworkChangeAsync() + { + try + { + await Task.Delay(2000).ConfigureAwait(false); + InitialiseInterfaces(); + // Recalculate LAN caches. + InitialiseLAN(_configurationManager.GetNetworkConfiguration()); + + NetworkChanged?.Invoke(this, EventArgs.Empty); + } + finally + { + _eventfire = false; + } + } + + /// + /// Triggers our event, and re-loads interface information. + /// + private void OnNetworkChanged() + { + lock (_eventFireLock) + { + 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(); + } + } + } + + /// + /// 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(NetworkConfiguration config) + { + lock (_intLock) + { + _publishedServerUrls.Clear(); + string[] overrides = config.PublishedServerUriBySubnet; + if (overrides == null) + { + return; + } + + foreach (var entry in overrides) + { + var parts = entry.Split('='); + if (parts.Length != 2) + { + _logger.LogError("Unable to parse bind override: {Entry}", entry); + } + else + { + var replacement = parts[1].Trim(); + if (string.Equals(parts[0], "remaining", StringComparison.OrdinalIgnoreCase)) + { + _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement; + } + else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase)) + { + _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement; + } + else if (TryParseInterface(parts[0], out Collection? addresses) && addresses != null) + { + foreach (IPNetAddress na in addresses) + { + _publishedServerUrls[na] = replacement; + } + } + else if (IPNetAddress.TryParse(parts[0], out IPNetAddress result)) + { + _publishedServerUrls[result] = replacement; + } + else + { + _logger.LogError("Unable to parse bind ip address. {Parts}", parts[1]); + } + } + } + } + } + + /// + /// Initialises the network bind addresses. + /// + private void InitialiseBind(NetworkConfiguration config) + { + lock (_intLock) + { + string[] lanAddresses = config.LocalNetworkAddresses; + + // TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334 + + if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1) + { + lanAddresses = lanAddresses[0].Split(','); + } + + // 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) + { + var virtualInterfaceNames = config.VirtualInterfaceNames.Split(','); + var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length]; + Array.Copy(lanAddresses, newList, lanAddresses.Length); + Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length); + lanAddresses = newList; + } + + // 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.AsString()); + _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString()); + } + } + + /// + /// Initialises the remote address values. + /// + private void InitialiseRemote(NetworkConfiguration config) + { + lock (_intLock) + { + RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter); + } + } + + /// + /// Initialises internal LAN cache settings. + /// + private void InitialiseLAN(NetworkConfiguration config) + { + lock (_intLock) + { + _logger.LogDebug("Refreshing LAN information."); + + // Get config options. + string[] subnets = config.LocalNetworkSubnets; + + // Create lists from user settings. + + _lanSubnets = CreateIPCollection(subnets); + _excludedSubnets = CreateIPCollection(subnets, true).AsNetworks(); + + // 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 generating the collection in this statement matters. + // Altering the order will cause the collections to be created incorrectly. + if (_usingPrivateAddresses) + { + _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.ContainsAddress(i))); + + // Subnets are the same as the calculated internal interface. + _lanSubnets = new Collection(); + + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.AddItem(IPNetAddress.IP6Loopback); + _lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA + _lanSubnets.AddItem(IPNetAddress.Parse("fe80::/10")); // Site local + } + + if (IsIP4Enabled) + { + _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 + { + // We must listen on loopback for LiveTV to function regardless of the settings. + if (IsIP6Enabled) + { + _lanSubnets.AddItem(IPNetAddress.IP6Loopback); + } + + if (IsIP4Enabled) + { + _lanSubnets.AddItem(IPNetAddress.IP4Loopback); + } + + // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet. + _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsInLocalNetwork(i))); + } + + _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()); + } + } + + /// + /// 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() + { + lock (_intLock) + { + _logger.LogDebug("Refreshing interfaces."); + + _interfaceNames.Clear(); + _interfaceAddresses.Clear(); + _macAddresses.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, IPObject.MaskToCidr(info.IPv4Mask)) + { + // Keep the number of gateways on this interface, along with its index. + Tag = ipProperties.GetIPv4Properties().Index + }; + + int tag = nw.Tag; + if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.AddItem(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; + if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback()) + { + // -ve Tags signify the interface has a gateway. + nw.Tag *= -1; + } + + _interfaceAddresses.AddItem(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 (Exception ex) + { + // Ignore error, and attempt to continue. + _logger.LogError(ex, "Error encountered parsing interfaces."); + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count); + _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) + { + _logger.LogError("No interfaces information available. Resolving DNS name."); + IPHost host = new IPHost(Dns.GetHostName()); + foreach (var a in host.GetAddresses()) + { + _interfaceAddresses.AddItem(a); + } + + if (_interfaceAddresses.Count == 0) + { + _logger.LogWarning("No interfaces information available. Using loopback."); + // Last ditch attempt - use loopback address. + _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback); + if (IsIP6Enabled) + { + _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback); + } + } + } + } + catch (NetworkInformationException ex) + { + _logger.LogError(ex, "Error in InitialiseInterfaces."); + } + } + } + + /// + /// 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 published server url that matches the source address. + /// The resultant port, if one exists. + /// true if a match is found, false otherwise. + private bool MatchesPublishedServerUrl(IPObject source, bool isInExternalSubnet, 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.Address.Equals(IPAddress.Broadcast)) + { + bindPreference = addr.Value; + break; + } + else if ((addr.Key.Address.Equals(IPAddress.Any) || addr.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet) + { + // External. + bindPreference = addr.Value; + break; + } + else if (addr.Key.Contains(source)) + { + // Match ip address. + bindPreference = addr.Value; + break; + } + } + + if (string.IsNullOrEmpty(bindPreference)) + { + return false; + } + + // 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; + } + + /// + /// 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, false otherwise. + private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result) + { + result = string.Empty; + var addresses = _bindAddresses.Exclude(_bindExclusions); + + int count = addresses.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. + + IPAddress? defaultGateway = null; + IPAddress? bindAddress = null; + + if (isInExternalSubnet) + { + // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first. + foreach (var addr in addresses.OrderBy(p => p.Tag)) + { + if (defaultGateway == null && !IsInLocalNetwork(addr)) + { + defaultGateway = addr.Address; + } + + if (bindAddress == null && addr.Contains(source)) + { + bindAddress = addr.Address; + } + + if (defaultGateway != null && bindAddress != null) + { + break; + } + } + } + else + { + // Look for the best internal address. + bindAddress = addresses + .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("{Source}: GetBindInterface: Has source, found a match bind interface subnets. {Result}", source, result); + return true; + } + + if (isInExternalSubnet && defaultGateway != null) + { + result = FormatIP6String(defaultGateway); + _logger.LogDebug("{Source}: GetBindInterface: Using first user defined external interface. {Result}", source, result); + return true; + } + + result = FormatIP6String(addresses[0].Address); + _logger.LogDebug("{Source}: GetBindInterface: Selected first user defined interface. {Result}", source, result); + + if (isInExternalSubnet) + { + _logger.LogWarning("{Source}: External request received, however, only an internal interface bind found.", source); + } + + return true; + } + + return false; + } + + /// + /// Attempts to match the source against an external interface. + /// + /// IP source address to use. + /// The result, if a match is found. + /// true if a match is found, false otherwise. + private bool MatchesExternalInterface(IPObject source, out string result) + { + result = string.Empty; + // 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(source)) + { + result = FormatIP6String(intf.Address); + _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range. {Result}", source, result); + return true; + } + } + + result = FormatIP6String(extResult.First().Address); + _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface. {Result}", source, result); + return true; + } + + // Have to return something, so return an internal address + + _logger.LogWarning("{Source}: External request received, however, no WAN interface found.", source); + return false; + } + } +} diff --git a/MediaBrowser.Common/Net/IPHost.cs b/MediaBrowser.Common/Net/IPHost.cs new file mode 100644 index 0000000000..4cede9ab16 --- /dev/null +++ b/MediaBrowser.Common/Net/IPHost.cs @@ -0,0 +1,445 @@ +#nullable enable +using System; +using System.Diagnostics; +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 + { + /// + /// Gets or sets timeout value before resolve required, in minutes. + /// + public const int Timeout = 30; + + /// + /// Represents an IPHost that has no value. + /// + public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None); + + /// + /// Time when last resolved in ticks. + /// + private DateTime? _lastResolved = null; + + /// + /// 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, as a host's address is determined by DNS. + throw new NotImplementedException("The address of a host is determined by DNS."); + } + } + + /// + /// 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 : 32); + } + + set + { + // 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. + } + } + + /// + /// Gets a value indicating whether the address has a value. + /// + public bool HasAddress => _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. + /// true if the parsing is successful, false if not. + 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. + /// + /// true if any addresses have been resolved, otherwise false. + private bool ResolveHost() + { + // When was the last time we resolved? + if (_lastResolved == null) + { + _lastResolved = DateTime.UtcNow; + } + + // If we haven't resolved before, or our timer has run out... + if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved?.AddMinutes(Timeout))) + { + _lastResolved = DateTime.UtcNow; + ResolveHostInternal().GetAwaiter().GetResult(); + Resolved = true; + } + + return _addresses.Length > 0; + } + + /// + /// Task that looks up a Host name and returns its IP addresses. + /// + /// A representing the asynchronous operation. + 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 ex) + { + // Log and then ignore socket errors, as the result value will just be an empty array. + Debug.WriteLine("GetHostEntryAsync failed with {Message}.", ex.Message); + } + } + } + } + } +} diff --git a/MediaBrowser.Common/Net/IPNetAddress.cs b/MediaBrowser.Common/Net/IPNetAddress.cs new file mode 100644 index 0000000000..a6f5fe4b37 --- /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 SSDPMulticastIPv4 = IPAddress.Parse("239.255.255.250"); + + /// + /// IPv6 local link multicast address. + /// + public static readonly IPAddress SSDPMulticastIPv6LinkLocal = IPAddress.Parse("ff02::C"); + + /// + /// IPv6 site local multicast address. + /// + public static readonly IPAddress SSDPMulticastIPv6SiteLocal = 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 excluded 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..69cd57f8ae --- /dev/null +++ b/MediaBrowser.Common/Net/IPObject.cs @@ -0,0 +1,406 @@ +#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 a user defined value that is associated with 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 => _networkAddress ??= CalculateNetworkAddress(); + + /// + /// 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); + } + + // 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. + + // GetAddressBytes + Span addressBytes = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + address.TryWriteBytes(addressBytes, out _); + + 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); + } + + /// + /// 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.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + if (address.AddressFamily == AddressFamily.InterNetwork) + { + // GetAddressBytes + Span octet = stackalloc byte[4]; + address.TryWriteBytes(octet, out _); + + 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 + { + // GetAddressBytes + Span octet = stackalloc byte[16]; + address.TryWriteBytes(octet, out _); + + 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; + } + + // GetAddressBytes + Span octet = stackalloc byte[16]; + address.TryWriteBytes(octet, out _); + 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)) + { + // GetAddressBytes + Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16]; + mask.TryWriteBytes(bytes, out _); + + 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 performs 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) + { + return !Address.Equals(IPAddress.None) && Address.Equals(other.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..d07bba249b --- /dev/null +++ b/MediaBrowser.Common/Net/NetworkExtensions.cs @@ -0,0 +1,262 @@ +#pragma warning disable CA1062 // Validate arguments of public methods +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text; + +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 Collection source, IPAddress ip) + { + if (!source.ContainsAddress(ip)) + { + source.Add(new IPNetAddress(ip, 32)); + } + } + + /// + /// Adds a network to the collection. + /// + /// The . + /// Item to add. + public static void AddItem(this Collection 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 AsString(this Collection source) + { + 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 item to look for. + /// True if the collection contains the item. + public static bool ContainsAddress(this Collection 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 Collection 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; + } + + /// + /// Compares two Collection{IPObject} objects. The order is ignored. + /// + /// The . + /// Item to compare to. + /// True if both are equal. + public static bool Compare(this Collection source, Collection 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. + /// + /// The . + /// Collection{IPObject} object containing the subnets. + public static Collection AsNetworks(this Collection source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + Collection res = new Collection(); + + 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.AddItem(na); + } + else if (i is IPHost ipHost) + { + // Flatten out IPHost and add all its ip addresses. + foreach (var addr in ipHost.GetAddresses()) + { + IPNetAddress host = new IPNetAddress(addr) + { + Tag = i.Tag + }; + + res.AddItem(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 Collection Exclude(this Collection source, Collection excludeList) + { + if (source.Count == 0 || excludeList == null) + { + return new Collection(source); + } + + Collection results = new Collection(); + + bool found; + foreach (var outer in source) + { + found = false; + + foreach (var inner in excludeList) + { + if (outer.Equals(inner)) + { + found = true; + break; + } + } + + if (!found) + { + results.AddItem(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 Collection Union(this Collection source, Collection target) + { + if (source.Count == 0) + { + return new Collection(); + } + + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + Collection nc = new Collection(); + + foreach (IPObject i in source) + { + if (target.ContainsAddress(i)) + { + nc.AddItem(i); + } + } + + return nc; + } + } +} diff --git a/MediaBrowser.Model/Configuration/PathSubstitution.cs b/MediaBrowser.Model/Configuration/PathSubstitution.cs new file mode 100644 index 0000000000..bffaa85945 --- /dev/null +++ b/MediaBrowser.Model/Configuration/PathSubstitution.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace MediaBrowser.Model.Configuration +{ + /// + /// Defines the . + /// + public class PathSubstitution + { + /// + /// Gets or sets the value to substitute. + /// + public string From { get; set; } = string.Empty; + + /// + /// Gets or sets the value to substitution with. + /// + public string To { get; set; } = string.Empty; + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 23a5201f7a..06985ebf40 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -1,5 +1,5 @@ -#nullable disable #pragma warning disable CS1591 +#pragma warning disable CA1819 using System; using System.Collections.Generic; @@ -13,43 +13,194 @@ namespace MediaBrowser.Model.Configuration /// public class ServerConfiguration : BaseApplicationConfiguration { + /// + /// The default value for . + /// public const int DefaultHttpPort = 8096; + + /// + /// The default value for and . + /// public const int DefaultHttpsPort = 8920; - private string _baseUrl; + + private string _baseUrl = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + public ServerConfiguration() + { + MetadataOptions = new[] + { + new MetadataOptions() + { + ItemType = "Book" + }, + new MetadataOptions() + { + ItemType = "Movie" + }, + new MetadataOptions + { + ItemType = "MusicVideo", + DisabledMetadataFetchers = new[] { "The Open Movie Database" }, + DisabledImageFetchers = new[] { "The Open Movie Database" } + }, + new MetadataOptions + { + ItemType = "Series", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + DisabledImageFetchers = new[] { "TheMovieDb" } + }, + new MetadataOptions + { + ItemType = "MusicAlbum", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "MusicArtist", + DisabledMetadataFetchers = new[] { "TheAudioDB" } + }, + new MetadataOptions + { + ItemType = "BoxSet" + }, + new MetadataOptions + { + ItemType = "Season", + DisabledMetadataFetchers = new[] { "TheMovieDb" }, + }, + new MetadataOptions + { + ItemType = "Episode", + DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, + DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } + } + }; + } /// /// Gets or sets a value indicating whether to enable automatic port forwarding. /// - public bool EnableUPnP { get; set; } + public bool EnableUPnP { get; set; } = false; /// /// Gets or sets a value indicating whether to enable prometheus metrics exporting. /// - public bool EnableMetrics { get; set; } + public bool EnableMetrics { get; set; } = false; /// /// Gets or sets the public mapped port. /// /// The public mapped port. - public int PublicPort { get; set; } + public int PublicPort { get; set; } = DefaultHttpPort; + + /// + /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding. + /// + public bool UPnPCreateHttpPortMap { get; set; } = false; + + /// + /// Gets or sets client udp port range. + /// + public string UDPPortRange { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether IPV6 capability is enabled. + /// + public bool EnableIPV6 { get; set; } = false; + + /// + /// Gets or sets a value indicating whether IPV4 capability is enabled. + /// + 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 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 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 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 public HTTPS port. /// /// The public HTTPS port. - public int PublicHttpsPort { get; set; } + public int PublicHttpsPort { get; set; } = DefaultHttpsPort; /// /// Gets or sets the HTTP server port number. /// /// The HTTP server port number. - public int HttpServerPortNumber { get; set; } + public int HttpServerPortNumber { get; set; } = DefaultHttpPort; /// /// Gets or sets the HTTPS server port number. /// /// The HTTPS server port number. - public int HttpsPortNumber { get; set; } + public int HttpsPortNumber { get; set; } = DefaultHttpsPort; /// /// Gets or sets a value indicating whether to use HTTPS. @@ -58,19 +209,19 @@ namespace MediaBrowser.Model.Configuration /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be /// provided for and . /// - public bool EnableHttps { get; set; } + public bool EnableHttps { get; set; } = false; - public bool EnableNormalizedItemByNameIds { get; set; } + public bool EnableNormalizedItemByNameIds { get; set; } = true; /// /// Gets or sets the filesystem path of an X.509 certificate to use for SSL. /// - public string CertificatePath { get; set; } + public string CertificatePath { get; set; } = string.Empty; /// /// Gets or sets the password required to access the X.509 certificate data in the file specified by . /// - public string CertificatePassword { get; set; } + public string CertificatePassword { get; set; } = string.Empty; /// /// Gets or sets a value indicating whether this instance is port authorized. @@ -79,90 +230,93 @@ namespace MediaBrowser.Model.Configuration public bool IsPortAuthorized { get; set; } /// - /// Gets or sets if quick connect is available for use on this server. + /// Gets or sets a value indicating whether quick connect is available for use on this server. /// - public bool QuickConnectAvailable { get; set; } - - public bool EnableRemoteAccess { get; set; } + public bool QuickConnectAvailable { 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 a value indicating whether [enable case sensitive item ids]. /// /// true if [enable case sensitive item ids]; otherwise, false. - public bool EnableCaseSensitiveItemIds { get; set; } + public bool EnableCaseSensitiveItemIds { get; set; } = true; - public bool DisableLiveTvChannelUserDataName { get; set; } + public bool DisableLiveTvChannelUserDataName { get; set; } = true; /// /// Gets or sets the metadata path. /// /// The metadata path. - public string MetadataPath { get; set; } + public string MetadataPath { get; set; } = string.Empty; - public string MetadataNetworkPath { get; set; } + public string MetadataNetworkPath { get; set; } = string.Empty; /// /// Gets or sets the preferred metadata language. /// /// The preferred metadata language. - public string PreferredMetadataLanguage { get; set; } + public string PreferredMetadataLanguage { get; set; } = string.Empty; /// /// Gets or sets the metadata country code. /// /// The metadata country code. - public string MetadataCountryCode { get; set; } + public string MetadataCountryCode { get; set; } = "US"; /// - /// Characters to be replaced with a ' ' in strings to create a sort name. + /// Gets or sets characters to be replaced with a ' ' in strings to create a sort name. /// /// The sort replace characters. - public string[] SortReplaceCharacters { get; set; } + public string[] SortReplaceCharacters { get; set; } = new[] { ".", "+", "%" }; /// - /// Characters to be removed from strings to create a sort name. + /// Gets or sets characters to be removed from strings to create a sort name. /// /// The sort remove characters. - public string[] SortRemoveCharacters { get; set; } + public string[] SortRemoveCharacters { get; set; } = new[] { ",", "&", "-", "{", "}", "'" }; /// - /// Words to be removed from strings to create a sort name. + /// Gets or sets words to be removed from strings to create a sort name. /// /// The sort remove words. - public string[] SortRemoveWords { get; set; } + public string[] SortRemoveWords { get; set; } = new[] { "the", "a", "an" }; /// /// Gets or sets the minimum percentage of an item that must be played in order for playstate to be updated. /// /// The min resume PCT. - public int MinResumePct { get; set; } + public int MinResumePct { get; set; } = 5; /// /// Gets or sets the maximum percentage of an item that can be played while still saving playstate. If this percentage is crossed playstate will be reset to the beginning and the item will be marked watched. /// /// The max resume PCT. - public int MaxResumePct { get; set; } + public int MaxResumePct { get; set; } = 90; /// /// Gets or sets the minimum duration that an item must have in order to be eligible for playstate updates.. /// /// The min resume duration seconds. - public int MinResumeDurationSeconds { get; set; } + public int MinResumeDurationSeconds { get; set; } = 300; /// - /// The delay in seconds that we will wait after a file system change to try and discover what has been added/removed + /// Gets or sets the delay in seconds that we will wait after a file system change to try and discover what has been added/removed /// Some delay is necessary with some items because their creation is not atomic. It involves the creation of several /// different directories and files. /// /// The file watcher delay. - public int LibraryMonitorDelay { get; set; } + public int LibraryMonitorDelay { get; set; } = 60; /// /// Gets or sets a value indicating whether [enable dashboard response caching]. /// Allows potential contributors without visual studio to modify production dashboard code and test changes. /// /// true if [enable dashboard response caching]; otherwise, false. - public bool EnableDashboardResponseCaching { get; set; } + public bool EnableDashboardResponseCaching { get; set; } = true; /// /// Gets or sets the image saving convention. @@ -172,9 +326,9 @@ namespace MediaBrowser.Model.Configuration public MetadataOptions[] MetadataOptions { get; set; } - public bool SkipDeserializationForBasicTypes { get; set; } + public bool SkipDeserializationForBasicTypes { get; set; } = true; - public string ServerName { get; set; } + public string ServerName { get; set; } = string.Empty; public string BaseUrl { @@ -206,194 +360,84 @@ namespace MediaBrowser.Model.Configuration } } - public string UICulture { get; set; } - - public bool SaveMetadataHidden { get; set; } + public string UICulture { get; set; } = "en-US"; - public NameValuePair[] ContentTypes { get; set; } + public bool SaveMetadataHidden { get; set; } = false; - public int RemoteClientBitrateLimit { get; set; } + public NameValuePair[] ContentTypes { get; set; } = Array.Empty(); - public bool EnableFolderView { get; set; } + public int RemoteClientBitrateLimit { get; set; } = 0; - public bool EnableGroupingIntoCollections { get; set; } + public bool EnableFolderView { get; set; } = false; - public bool DisplaySpecialsWithinSeasons { get; set; } + public bool EnableGroupingIntoCollections { get; set; } = false; - public string[] LocalNetworkSubnets { get; set; } + public bool DisplaySpecialsWithinSeasons { get; set; } = true; - public string[] LocalNetworkAddresses { get; set; } + /// + /// Gets or sets the subnets that are deemed to make up the LAN. + /// + public string[] LocalNetworkSubnets { get; set; } = Array.Empty(); - public string[] CodecsUsed { get; set; } + /// + /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used. + /// + public string[] LocalNetworkAddresses { get; set; } = Array.Empty(); - public List PluginRepositories { get; set; } + public string[] CodecsUsed { get; set; } = Array.Empty(); - public bool IgnoreVirtualInterfaces { get; set; } + public List PluginRepositories { get; set; } = new List(); - public bool EnableExternalContentInSuggestions { get; set; } + public bool EnableExternalContentInSuggestions { get; set; } = true; /// /// Gets or sets a value indicating whether the server should force connections over HTTPS. /// - public bool RequireHttps { get; set; } + public bool RequireHttps { get; set; } = false; - public bool EnableNewOmdbSupport { get; set; } + public bool EnableNewOmdbSupport { get; set; } = true; - public string[] RemoteIPFilter { get; set; } + /// + /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with . + /// + public string[] RemoteIPFilter { get; set; } = Array.Empty(); - public bool IsRemoteIPFilterBlacklist { get; set; } + /// + /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist. + /// + public bool IsRemoteIPFilterBlacklist { get; set; } = false; - public int ImageExtractionTimeoutMs { get; set; } + public int ImageExtractionTimeoutMs { get; set; } = 0; - public PathSubstitution[] PathSubstitutions { get; set; } + public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty(); - public bool EnableSimpleArtistDetection { get; set; } + public bool EnableSimpleArtistDetection { get; set; } = false; - public string[] UninstalledPlugins { get; set; } + public string[] UninstalledPlugins { get; set; } = Array.Empty(); /// /// Gets or sets a value indicating whether slow server responses should be logged as a warning. /// - public bool EnableSlowResponseWarning { get; set; } + public bool EnableSlowResponseWarning { get; set; } = true; /// /// Gets or sets the threshold for the slow response time warning in ms. /// - public long SlowResponseThresholdMs { get; set; } + public long SlowResponseThresholdMs { get; set; } = 500; /// /// Gets or sets the cors hosts. /// - public string[] CorsHosts { get; set; } + public string[] CorsHosts { get; set; } = new[] { "*" }; /// /// Gets or sets the known proxies. /// - public string[] KnownProxies { get; set; } + public string[] KnownProxies { get; set; } = Array.Empty(); /// /// Gets or sets the number of days we should retain activity logs. /// - public int? ActivityLogRetentionDays { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public ServerConfiguration() - { - UninstalledPlugins = Array.Empty(); - RemoteIPFilter = Array.Empty(); - LocalNetworkSubnets = Array.Empty(); - LocalNetworkAddresses = Array.Empty(); - CodecsUsed = Array.Empty(); - PathSubstitutions = Array.Empty(); - IgnoreVirtualInterfaces = false; - EnableSimpleArtistDetection = false; - SkipDeserializationForBasicTypes = true; - - PluginRepositories = new List(); - - DisplaySpecialsWithinSeasons = true; - EnableExternalContentInSuggestions = true; - - ImageSavingConvention = ImageSavingConvention.Compatible; - PublicPort = DefaultHttpPort; - PublicHttpsPort = DefaultHttpsPort; - HttpServerPortNumber = DefaultHttpPort; - HttpsPortNumber = DefaultHttpsPort; - EnableMetrics = false; - EnableHttps = false; - EnableDashboardResponseCaching = true; - EnableCaseSensitiveItemIds = true; - EnableNormalizedItemByNameIds = true; - DisableLiveTvChannelUserDataName = true; - EnableNewOmdbSupport = true; - - EnableRemoteAccess = true; - QuickConnectAvailable = false; - - EnableUPnP = false; - MinResumePct = 5; - MaxResumePct = 90; - - // 5 minutes - MinResumeDurationSeconds = 300; - - LibraryMonitorDelay = 60; - - ContentTypes = Array.Empty(); - - PreferredMetadataLanguage = "en"; - MetadataCountryCode = "US"; - - SortReplaceCharacters = new[] { ".", "+", "%" }; - SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" }; - SortRemoveWords = new[] { "the", "a", "an" }; - - BaseUrl = string.Empty; - UICulture = "en-US"; - - MetadataOptions = new[] - { - new MetadataOptions() - { - ItemType = "Book" - }, - new MetadataOptions() - { - ItemType = "Movie" - }, - new MetadataOptions - { - ItemType = "MusicVideo", - DisabledMetadataFetchers = new[] { "The Open Movie Database" }, - DisabledImageFetchers = new[] { "The Open Movie Database" } - }, - new MetadataOptions - { - ItemType = "Series", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - DisabledImageFetchers = new[] { "TheMovieDb" } - }, - new MetadataOptions - { - ItemType = "MusicAlbum", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "MusicArtist", - DisabledMetadataFetchers = new[] { "TheAudioDB" } - }, - new MetadataOptions - { - ItemType = "BoxSet" - }, - new MetadataOptions - { - ItemType = "Season", - DisabledMetadataFetchers = new[] { "TheMovieDb" }, - }, - new MetadataOptions - { - ItemType = "Episode", - DisabledMetadataFetchers = new[] { "The Open Movie Database", "TheMovieDb" }, - DisabledImageFetchers = new[] { "The Open Movie Database", "TheMovieDb" } - } - }; - - EnableSlowResponseWarning = true; - SlowResponseThresholdMs = 500; - CorsHosts = new[] { "*" }; - KnownProxies = Array.Empty(); - ActivityLogRetentionDays = 30; - } - } - - public class PathSubstitution - { - public string From { get; set; } - - public string To { get; set; } + public int? ActivityLogRetentionDays { get; set; } = 30; } } 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