diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index a97a4c7416..edc8b08646 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -7,6 +7,7 @@
- [anthonylavado](https://github.com/anthonylavado)
- [Artiume](https://github.com/Artiume)
- [AThomsen](https://github.com/AThomsen)
+ - [barongreenback](https://github.com/BaronGreenback)
- [barronpm](https://github.com/barronpm)
- [bilde2910](https://github.com/bilde2910)
- [bfayers](https://github.com/bfayers)
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index f8a00efaca..fb4454a343 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -2,12 +2,14 @@
using System;
using System.Globalization;
+using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
+using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -134,20 +136,20 @@ namespace Emby.Dlna.Main
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
- await ReloadComponents().ConfigureAwait(false);
+ ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
}
- private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
+ private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
{
if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
{
- await ReloadComponents().ConfigureAwait(false);
+ ReloadComponents();
}
}
- private async Task ReloadComponents()
+ private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
@@ -155,7 +157,7 @@ namespace Emby.Dlna.Main
if (options.EnableServer)
{
- await StartDevicePublisher(options).ConfigureAwait(false);
+ StartDevicePublisher(options);
}
else
{
@@ -225,7 +227,7 @@ namespace Emby.Dlna.Main
}
}
- public async Task StartDevicePublisher(Configuration.DlnaOptions options)
+ public void StartDevicePublisher(Configuration.DlnaOptions options)
{
if (!options.BlastAliveMessages)
{
@@ -245,7 +247,7 @@ namespace Emby.Dlna.Main
SupportPnpRootDevice = false
};
- await RegisterServerEndpoints().ConfigureAwait(false);
+ RegisterServerEndpoints();
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
}
@@ -255,14 +257,22 @@ namespace Emby.Dlna.Main
}
}
- private async Task RegisterServerEndpoints()
+ private void RegisterServerEndpoints()
{
- var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
-
var udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml";
- foreach (var address in addresses)
+ var bindAddresses = NetworkManager.CreateCollection(
+ _networkManager.GetInternalBindAddresses()
+ .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
+
+ if (bindAddresses.Count == 0)
+ {
+ // No interfaces returned, so use loopback.
+ bindAddresses = _networkManager.GetLoopbacks();
+ }
+
+ foreach (IPNetAddress address in bindAddresses)
{
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
@@ -271,7 +281,7 @@ namespace Emby.Dlna.Main
}
// Limit to LAN addresses only
- if (!_networkManager.IsAddressInSubnets(address, true, true))
+ if (!_networkManager.IsInLocalNetwork(address))
{
continue;
}
@@ -280,14 +290,14 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
- var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
+ var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
var device = new SsdpRootDevice
{
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document.
- Address = address,
- SubnetMask = _networkManager.GetLocalIpSubnetMask(address),
+ Address = address.Address,
+ PrefixLength = address.PrefixLength,
FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server",
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index e93aef3043..854b6a1a2b 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -177,15 +177,7 @@ namespace Emby.Dlna.PlayTo
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
- string serverAddress;
- if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any))
- {
- serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
- }
- else
- {
- serverAddress = _appHost.GetLocalApiUrl(info.LocalIpAddress);
- }
+ string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
controller = new PlayToController(
sessionInfo,
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index bca9e81cd0..09525aae4e 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -40,8 +40,6 @@ namespace Emby.Dlna.Server
_serverId = serverId;
}
- private static bool EnableAbsoluteUrls => false;
-
public string GetXml()
{
var builder = new StringBuilder();
@@ -75,13 +73,6 @@ namespace Emby.Dlna.Server
builder.Append("0");
builder.Append("");
- if (!EnableAbsoluteUrls)
- {
- builder.Append("")
- .Append(SecurityElement.Escape(_serverAddress))
- .Append("");
- }
-
AppendDeviceInfo(builder);
builder.Append("");
@@ -257,14 +248,7 @@ namespace Emby.Dlna.Server
return string.Empty;
}
- url = url.TrimStart('/');
-
- url = "/dlna/" + _serverUdn + "/" + url;
-
- if (EnableAbsoluteUrls)
- {
- url = _serverAddress.TrimEnd('/') + url;
- }
+ url = _serverAddress.TrimEnd('/') + "/dlna/" + _serverUdn + "/" + url.TrimStart('/');
return SecurityElement.Escape(url);
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 5d47d1e401..c695c02315 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -15,6 +15,7 @@ using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using System.Xml.Serialization;
using Emby.Dlna;
using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
@@ -46,6 +47,8 @@ using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Manager;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@@ -97,6 +100,8 @@ using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
+using Microsoft.AspNetCore.DataProtection.Repositories;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -117,7 +122,6 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager;
- private readonly INetworkManager _networkManager;
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
@@ -158,6 +162,11 @@ namespace Emby.Server.Implementations
}
}
+ ///
+ /// Gets the singleton instance.
+ ///
+ public INetworkManager NetManager { get; internal set; }
+
///
/// Occurs when [has pending restart changed].
///
@@ -210,7 +219,7 @@ namespace Emby.Server.Implementations
private readonly List _disposableParts = new List();
///
- /// Gets the configuration manager.
+ /// Gets or sets the configuration manager.
///
/// The configuration manager.
protected IConfigurationManager ConfigurationManager { get; set; }
@@ -243,14 +252,12 @@ namespace Emby.Server.Implementations
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
- /// Instance of the interface.
/// Instance of the interface.
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
- INetworkManager networkManager,
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
@@ -258,14 +265,17 @@ namespace Emby.Server.Implementations
ServiceCollection = serviceCollection;
- _networkManager = networkManager;
- networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
-
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
_fileSystemManager = fileSystem;
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+ // Have to migrate settings here as migration subsystem not yet initialised.
+ MigrateNetworkConfiguration();
+
+ // Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
+ ConfigurationManager.RegisterConfiguration();
+ NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger());
Logger = LoggerFactory.CreateLogger();
@@ -279,8 +289,6 @@ namespace Emby.Server.Implementations
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
- _networkManager.NetworkChanged += OnNetworkChanged;
-
CertificateInfo = new CertificateInfo
{
Path = ServerConfigurationManager.Configuration.CertificatePath,
@@ -293,6 +301,22 @@ namespace Emby.Server.Implementations
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
}
+ ///
+ /// Temporary function to migration network settings out of system.xml and into network.xml.
+ /// TODO: remove at the point when a fixed migration path has been decided upon.
+ ///
+ private void MigrateNetworkConfiguration()
+ {
+ string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
+ if (!File.Exists(path))
+ {
+ var networkSettings = new NetworkConfiguration();
+ ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
+ _xmlSerializer.SerializeToFile(networkSettings, path);
+ Logger?.LogDebug("Successfully migrated network settings.");
+ }
+ }
+
public string ExpandVirtualPath(string path)
{
var appPaths = ApplicationPaths;
@@ -309,16 +333,6 @@ namespace Emby.Server.Implementations
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
}
- private string[] GetConfiguredLocalSubnets()
- {
- return ServerConfigurationManager.Configuration.LocalNetworkSubnets;
- }
-
- private void OnNetworkChanged(object sender, EventArgs e)
- {
- _validAddressResults.Clear();
- }
-
///
public Version ApplicationVersion { get; }
@@ -485,14 +499,15 @@ namespace Emby.Server.Implementations
///
public void Init()
{
- HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
- HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
+ var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
+ HttpPort = networkConfiguration.HttpServerPortNumber;
+ HttpsPort = networkConfiguration.HttpsPortNumber;
// Safeguard against invalid configuration
if (HttpPort == HttpsPort)
{
- HttpPort = ServerConfiguration.DefaultHttpPort;
- HttpsPort = ServerConfiguration.DefaultHttpsPort;
+ HttpPort = NetworkConfiguration.DefaultHttpPort;
+ HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
DiscoverTypes();
@@ -521,7 +536,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton();
- ServiceCollection.AddSingleton(_networkManager);
+ ServiceCollection.AddSingleton(NetManager);
ServiceCollection.AddSingleton();
@@ -902,9 +917,10 @@ namespace Emby.Server.Implementations
// Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0)
{
+ var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Need to restart if ports have changed
- if (ServerConfigurationManager.Configuration.HttpServerPortNumber != HttpPort ||
- ServerConfigurationManager.Configuration.HttpsPortNumber != HttpsPort)
+ if (networkConfiguration.HttpServerPortNumber != HttpPort ||
+ networkConfiguration.HttpsPortNumber != HttpsPort)
{
if (ServerConfigurationManager.Configuration.IsPortAuthorized)
{
@@ -1147,6 +1163,9 @@ namespace Emby.Server.Implementations
// Xbmc
yield return typeof(ArtistNfoProvider).Assembly;
+ // Network
+ yield return typeof(NetworkManager).Assembly;
+
foreach (var i in GetAssembliesWithPartsInternal())
{
yield return i;
@@ -1158,13 +1177,10 @@ namespace Emby.Server.Implementations
///
/// Gets the system status.
///
- /// The cancellation token.
+ /// Where this request originated.
/// SystemInfo.
- public async Task GetSystemInfo(CancellationToken cancellationToken)
+ public SystemInfo GetSystemInfo(IPAddress source)
{
- var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
- var transcodingTempPath = ConfigurationManager.GetTranscodePath();
-
return new SystemInfo
{
HasPendingRestart = HasPendingRestart,
@@ -1184,9 +1200,9 @@ namespace Emby.Server.Implementations
CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser,
HasUpdateAvailable = HasUpdateAvailable,
- TranscodingTempPath = transcodingTempPath,
+ TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
- LocalAddress = localAddress,
+ LocalAddress = GetSmartApiUrl(source),
SupportsLibraryMonitor = true,
EncoderLocation = _mediaEncoder.EncoderLocation,
SystemArchitecture = RuntimeInformation.OSArchitecture,
@@ -1195,14 +1211,12 @@ namespace Emby.Server.Implementations
}
public IEnumerable GetWakeOnLanInfo()
- => _networkManager.GetMacAddresses()
+ => NetManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i))
.ToList();
- public async Task GetPublicSystemInfo(CancellationToken cancellationToken)
+ public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
{
- var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
-
return new PublicSystemInfo
{
Version = ApplicationVersionString,
@@ -1210,193 +1224,98 @@ namespace Emby.Server.Implementations
Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
- LocalAddress = localAddress,
+ LocalAddress = GetSmartApiUrl(source),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
///
- public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
+ public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
///
- public async Task GetLocalApiUrl(CancellationToken cancellationToken)
+ public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{
- try
+ // Published server ends with a /
+ if (_startupOptions.PublishedServerUrl != null)
{
- // Return the first matched address, if found, or the first known local address
- var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
- if (addresses.Count == 0)
- {
- return null;
- }
-
- return GetLocalApiUrl(addresses[0]);
+ // Published server ends with a '/', so we need to remove it.
+ return _startupOptions.PublishedServerUrl.ToString().Trim('/');
}
- catch (Exception ex)
+
+ string smart = NetManager.GetBindInterface(ipAddress, out port);
+ // If the smartAPI doesn't start with http then treat it as a host or ip.
+ if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- Logger.LogError(ex, "Error getting local Ip address information");
+ return smart.Trim('/');
}
- return null;
+ return GetLocalApiUrl(smart.Trim('/'), null, port);
}
- ///
- /// Removes the scope id from IPv6 addresses.
- ///
- /// The IPv6 address.
- /// The IPv6 address without the scope id.
- private ReadOnlySpan RemoveScopeId(ReadOnlySpan address)
+ ///
+ public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
- var index = address.IndexOf('%');
- if (index == -1)
+ // Published server ends with a /
+ if (_startupOptions.PublishedServerUrl != null)
{
- return address;
+ // Published server ends with a '/', so we need to remove it.
+ return _startupOptions.PublishedServerUrl.ToString().Trim('/');
}
- return address.Slice(0, index);
- }
-
- ///
- public string GetLocalApiUrl(IPAddress ipAddress)
- {
- if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
+ string smart = NetManager.GetBindInterface(request, out port);
+ // If the smartAPI doesn't start with http then treat it as a host or ip.
+ if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- var str = RemoveScopeId(ipAddress.ToString());
- Span span = new char[str.Length + 2];
- span[0] = '[';
- str.CopyTo(span.Slice(1));
- span[^1] = ']';
-
- return GetLocalApiUrl(span);
+ return smart.Trim('/');
}
- return GetLocalApiUrl(ipAddress.ToString());
+ return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
}
///
- public string GetLoopbackHttpApiUrl()
- {
- return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
- }
-
- ///
- public string GetLocalApiUrl(ReadOnlySpan host, string scheme = null, int? port = null)
- {
- // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
- // not. For consistency, always trim the trailing slash.
- return new UriBuilder
- {
- Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
- Host = host.ToString(),
- Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
- Path = ServerConfigurationManager.Configuration.BaseUrl
- }.ToString().TrimEnd('/');
- }
-
- public Task> GetLocalIpAddresses(CancellationToken cancellationToken)
+ public string GetSmartApiUrl(string hostname, int? port = null)
{
- return GetLocalIpAddressesInternal(true, 0, cancellationToken);
- }
-
- private async Task> GetLocalIpAddressesInternal(bool allowLoopback, int limit, CancellationToken cancellationToken)
- {
- var addresses = ServerConfigurationManager
- .Configuration
- .LocalNetworkAddresses
- .Select(x => NormalizeConfiguredLocalAddress(x))
- .Where(i => i != null)
- .ToList();
-
- if (addresses.Count == 0)
+ // Published server ends with a /
+ if (_startupOptions.PublishedServerUrl != null)
{
- addresses.AddRange(_networkManager.GetLocalIpAddresses());
+ // Published server ends with a '/', so we need to remove it.
+ return _startupOptions.PublishedServerUrl.ToString().Trim('/');
}
- var resultList = new List();
+ string smart = NetManager.GetBindInterface(hostname, out port);
- foreach (var address in addresses)
+ // If the smartAPI doesn't start with http then treat it as a host or ip.
+ if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
- if (!allowLoopback)
- {
- if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback))
- {
- continue;
- }
- }
-
- if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
- {
- resultList.Add(address);
-
- if (limit > 0 && resultList.Count >= limit)
- {
- return resultList;
- }
- }
+ return smart.Trim('/');
}
- return resultList;
+ return GetLocalApiUrl(smart.Trim('/'), null, port);
}
- public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan address)
+ ///
+ public string GetLoopbackHttpApiUrl()
{
- var index = address.Trim('/').IndexOf('/');
- if (index != -1)
+ if (NetManager.IsIP6Enabled)
{
- address = address.Slice(index + 1);
+ return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
}
- if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
- {
- return result;
- }
-
- return null;
+ return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
}
- private readonly ConcurrentDictionary _validAddressResults = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
-
- private async Task IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
+ ///
+ public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
{
- if (address.Equals(IPAddress.Loopback)
- || address.Equals(IPAddress.IPv6Loopback))
- {
- return true;
- }
-
- var apiUrl = GetLocalApiUrl(address) + "/system/ping";
-
- if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
- {
- return cachedResult;
- }
-
- try
- {
- using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl);
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
- .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
-
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var result = await System.Text.Json.JsonSerializer.DeserializeAsync(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false);
- var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
-
- _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
- Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid);
- return valid;
- }
- catch (OperationCanceledException)
- {
- Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, "Cancelled");
- throw;
- }
- catch (Exception ex)
+ // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
+ // not. For consistency, always trim the trailing slash.
+ return new UriBuilder
{
- Logger.LogDebug(ex, "Ping test result to {0}. Success: {1}", apiUrl, false);
-
- _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false);
- return false;
- }
+ Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
+ Host = host,
+ Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
+ Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
+ }.ToString().TrimEnd('/');
}
public string FriendlyName =>
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index d360bb00f2..dab4ec5a69 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -22,7 +22,6 @@
-
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 2e8cc76d23..14201ead29 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -8,6 +8,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
+using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;
@@ -56,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints
private string GetConfigIdentifier()
{
const char Separator = '|';
- var config = _config.Configuration;
+ var config = _config.GetNetworkConfiguration();
return new StringBuilder(32)
.Append(config.EnableUPnP).Append(Separator)
@@ -93,7 +94,8 @@ namespace Emby.Server.Implementations.EntryPoints
private void Start()
{
- if (!_config.Configuration.EnableUPnP || !_config.Configuration.EnableRemoteAccess)
+ var config = _config.GetNetworkConfiguration();
+ if (!config.EnableUPnP || !config.EnableRemoteAccess)
{
return;
}
@@ -156,11 +158,12 @@ namespace Emby.Server.Implementations.EntryPoints
private IEnumerable CreatePortMaps(INatDevice device)
{
- yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
+ var config = _config.GetNetworkConfiguration();
+ yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort);
if (_appHost.ListenWithHttps)
{
- yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
+ yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort);
}
}
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index 8a0c0043a9..3a738fd5d2 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -76,7 +76,6 @@ namespace Emby.Server.Implementations.LiveTv
}
var list = sources.ToList();
- var serverUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
foreach (var source in list)
{
@@ -103,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv
// Dummy this up so that direct play checks can still run
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
{
- source.Path = serverUrl;
+ source.Path = _appHost.GetSmartApiUrl(string.Empty);
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 63d41ec83a..cf653f87d0 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Net;
+using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
@@ -50,6 +52,26 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
EnableStreamSharing = true;
}
+ ///
+ /// Returns an unused UDP port number in the range specified.
+ /// Temporarily placed here until future network PR merged.
+ ///
+ /// Upper and Lower boundary of ports to select.
+ /// System.Int32.
+ private static int GetUdpPortFromRange((int Min, int Max) range)
+ {
+ var properties = IPGlobalProperties.GetIPGlobalProperties();
+
+ // Get active udp listeners.
+ var udpListenerPorts = properties.GetActiveUdpListeners()
+ .Where(n => n.Port >= range.Min && n.Port <= range.Max)
+ .Select(n => n.Port);
+
+ return Enumerable
+ .Range(range.Min, range.Max)
+ .FirstOrDefault(i => !udpListenerPorts.Contains(i));
+ }
+
public override async Task Open(CancellationToken openCancellationToken)
{
LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
@@ -57,7 +79,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var mediaSource = OriginalMediaSource;
var uri = new Uri(mediaSource.Path);
- var localPort = _networkManager.GetRandomUnusedUdpPort();
+ // Temporary code to reduce PR size. This will be updated by a future network pr.
+ var localPort = GetUdpPortFromRange((49152, 65535));
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs
deleted file mode 100644
index ff0a2a3617..0000000000
--- a/Emby.Server.Implementations/Networking/NetworkManager.cs
+++ /dev/null
@@ -1,566 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Net.NetworkInformation;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Networking
-{
- ///
- /// Class to take care of network interface management.
- ///
- public class NetworkManager : INetworkManager
- {
- private readonly ILogger _logger;
- private 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;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Logger to use for messages.
- public NetworkManager(ILogger logger)
- {
- _logger = logger;
-
- NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
- NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
- }
-
- ///
- public event EventHandler NetworkChanged;
-
- ///
- public Func LocalSubnetsFn { get; set; }
-
- private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
- {
- _logger.LogDebug("NetworkAvailabilityChanged");
- OnNetworkChanged();
- }
-
- private void OnNetworkAddressChanged(object sender, EventArgs e)
- {
- _logger.LogDebug("NetworkAddressChanged");
- OnNetworkChanged();
- }
-
- private void OnNetworkChanged()
- {
- lock (_localIpAddressSyncLock)
- {
- _localIpAddresses = null;
- _macAddresses = null;
- }
-
- NetworkChanged?.Invoke(this, EventArgs.Empty);
- }
-
- ///
- public IPAddress[] GetLocalIpAddresses()
- {
- lock (_localIpAddressSyncLock)
- {
- if (_localIpAddresses == null)
- {
- var addresses = GetLocalIpAddressesInternal().ToArray();
-
- _localIpAddresses = addresses;
- }
-
- return _localIpAddresses;
- }
- }
-
- private List GetLocalIpAddressesInternal()
- {
- var list = GetIPsDefault().ToList();
-
- if (list.Count == 0)
- {
- list = GetLocalIpAddressesFallback().GetAwaiter().GetResult().ToList();
- }
-
- var listClone = new List();
-
- var subnets = LocalSubnetsFn();
-
- foreach (var i in list)
- {
- if (i.IsIPv6LinkLocal || i.ToString().StartsWith("169.254.", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- if (Array.IndexOf(subnets, $"[{i}]") == -1)
- {
- listClone.Add(i);
- }
- }
-
- return listClone
- .OrderBy(i => i.AddressFamily == AddressFamily.InterNetwork ? 0 : 1)
- // .ThenBy(i => listClone.IndexOf(i))
- .GroupBy(i => i.ToString())
- .Select(x => x.First())
- .ToList();
- }
-
- ///
- public bool IsInPrivateAddressSpace(string endpoint)
- {
- return IsInPrivateAddressSpace(endpoint, true);
- }
-
- // Checks if the address in endpoint is an RFC1918, RFC1122, or RFC3927 address
- private bool IsInPrivateAddressSpace(string endpoint, bool checkSubnets)
- {
- if (string.Equals(endpoint, "::1", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
-
- // IPV6
- if (endpoint.Split('.').Length > 4)
- {
- // Handle ipv4 mapped to ipv6
- var originalEndpoint = endpoint;
- endpoint = endpoint.Replace("::ffff:", string.Empty, StringComparison.OrdinalIgnoreCase);
-
- if (string.Equals(endpoint, originalEndpoint, StringComparison.OrdinalIgnoreCase))
- {
- return false;
- }
- }
-
- // Private address space:
-
- if (string.Equals(endpoint, "localhost", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
-
- if (!IPAddress.TryParse(endpoint, out var ipAddress))
- {
- return false;
- }
-
- // 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
- (octet[0] == 192 && octet[1] == 168) || // RFC1918
- (octet[0] == 127) || // RFC1122
- (octet[0] == 169 && octet[1] == 254)) // RFC3927
- {
- return true;
- }
-
- if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
- {
- return true;
- }
-
- return false;
- }
-
- ///
- public bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint)
- {
- if (endpoint.StartsWith("10.", StringComparison.OrdinalIgnoreCase))
- {
- var endpointFirstPart = endpoint.Split('.')[0];
-
- var subnets = GetSubnets(endpointFirstPart);
-
- foreach (var subnet_Match in subnets)
- {
- // logger.LogDebug("subnet_Match:" + subnet_Match);
-
- if (endpoint.StartsWith(subnet_Match + ".", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- }
-
- return false;
- }
-
- // Gives a list of possible subnets from the system whose interface ip starts with endpointFirstPart
- private List GetSubnets(string endpointFirstPart)
- {
- lock (_subnetLookupLock)
- {
- if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets))
- {
- return subnets;
- }
-
- subnets = new List();
-
- foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
- {
- foreach (var unicastIPAddressInformation in adapter.GetIPProperties().UnicastAddresses)
- {
- if (unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetwork && endpointFirstPart == unicastIPAddressInformation.Address.ToString().Split('.')[0])
- {
- int subnet_Test = 0;
- foreach (string part in unicastIPAddressInformation.IPv4Mask.ToString().Split('.'))
- {
- if (part.Equals("0", StringComparison.Ordinal))
- {
- break;
- }
-
- subnet_Test++;
- }
-
- var subnet_Match = string.Join(".", unicastIPAddressInformation.Address.ToString().Split('.').Take(subnet_Test).ToArray());
-
- // TODO: Is this check necessary?
- if (adapter.OperationalStatus == OperationalStatus.Up)
- {
- subnets.Add(subnet_Match);
- }
- }
- }
- }
-
- _subnetLookup[endpointFirstPart] = subnets;
-
- return subnets;
- }
- }
-
- ///
- public bool IsInLocalNetwork(string endpoint)
- {
- return IsInLocalNetworkInternal(endpoint, true);
- }
-
- ///
- public bool IsAddressInSubnets(string addressString, string[] subnets)
- {
- return IsAddressInSubnets(IPAddress.Parse(addressString), addressString, subnets);
- }
-
- ///
- public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC)
- {
- // 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
- {
- // don't use on loopback or 169 interfaces
- return false;
- }
-
- string addressString = address.ToString();
- string excludeAddress = "[" + addressString + "]";
- var subnets = LocalSubnetsFn();
-
- // Include any address if LAN subnets aren't specified
- if (subnets.Length == 0)
- {
- return true;
- }
-
- // Exclude any addresses if they appear in the LAN list in [ ]
- if (Array.IndexOf(subnets, excludeAddress) != -1)
- {
- return false;
- }
-
- return IsAddressInSubnets(address, addressString, subnets);
- }
-
- ///
- /// Checks if the give address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format.
- ///
- /// IPAddress version of the address.
- /// The address to check.
- /// If true, check against addresses in the LAN settings which have [] arroud and return true if it matches the address give in address.
- /// falseif the address isn't in the subnets, true otherwise.
- private static bool IsAddressInSubnets(IPAddress address, string addressString, string[] subnets)
- {
- foreach (var subnet in subnets)
- {
- var normalizedSubnet = subnet.Trim();
- // Is the subnet a host address and does it match the address being passes?
- if (string.Equals(normalizedSubnet, addressString, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
-
- // Parse CIDR subnets and see if address falls within it.
- if (normalizedSubnet.Contains('/', StringComparison.Ordinal))
- {
- try
- {
- var ipNetwork = IPNetwork.Parse(normalizedSubnet);
- if (ipNetwork.Contains(address))
- {
- return true;
- }
- }
- catch
- {
- // Ignoring - invalid subnet passed encountered.
- }
- }
- }
-
- return false;
- }
-
- private bool IsInLocalNetworkInternal(string endpoint, bool resolveHost)
- {
- if (string.IsNullOrEmpty(endpoint))
- {
- throw new ArgumentNullException(nameof(endpoint));
- }
-
- if (IPAddress.TryParse(endpoint, out var address))
- {
- var addressString = address.ToString();
-
- var localSubnetsFn = LocalSubnetsFn;
- if (localSubnetsFn != null)
- {
- var localSubnets = localSubnetsFn();
- foreach (var subnet in localSubnets)
- {
- // Only validate if there's at least one valid entry.
- if (!string.IsNullOrWhiteSpace(subnet))
- {
- return IsAddressInSubnets(address, addressString, localSubnets) || IsInPrivateAddressSpace(addressString, false);
- }
- }
- }
-
- int lengthMatch = 100;
- if (address.AddressFamily == AddressFamily.InterNetwork)
- {
- lengthMatch = 4;
- if (IsInPrivateAddressSpace(addressString, true))
- {
- return true;
- }
- }
- else if (address.AddressFamily == AddressFamily.InterNetworkV6)
- {
- lengthMatch = 9;
- if (IsInPrivateAddressSpace(endpoint, true))
- {
- return true;
- }
- }
-
- // Should be even be doing this with ipv6?
- if (addressString.Length >= lengthMatch)
- {
- var prefix = addressString.Substring(0, lengthMatch);
-
- if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
- {
- return true;
- }
- }
- }
- else if (resolveHost)
- {
- if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out var uri))
- {
- try
- {
- var host = uri.DnsSafeHost;
- _logger.LogDebug("Resolving host {0}", host);
-
- address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault();
-
- if (address != null)
- {
- _logger.LogDebug("{0} resolved to {1}", host, address);
-
- return IsInLocalNetworkInternal(address.ToString(), false);
- }
- }
- catch (InvalidOperationException)
- {
- // Can happen with reverse proxy or IIS url rewriting?
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error resolving hostname");
- }
- }
- }
-
- return false;
- }
-
- private static Task GetIpAddresses(string hostName)
- {
- return Dns.GetHostAddressesAsync(hostName);
- }
-
- private IEnumerable GetIPsDefault()
- {
- IEnumerable interfaces;
-
- try
- {
- interfaces = NetworkInterface.GetAllNetworkInterfaces()
- .Where(x => x.OperationalStatus == OperationalStatus.Up
- || x.OperationalStatus == OperationalStatus.Unknown);
- }
- catch (NetworkInformationException ex)
- {
- _logger.LogError(ex, "Error in GetAllNetworkInterfaces");
- return Enumerable.Empty();
- }
-
- return interfaces.SelectMany(network =>
- {
- var ipProperties = network.GetIPProperties();
-
- // Exclude any addresses if they appear in the LAN list in [ ]
-
- return ipProperties.UnicastAddresses
- .Select(i => i.Address)
- .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6);
- }).GroupBy(i => i.ToString())
- .Select(x => x.First());
- }
-
- private static async Task> GetLocalIpAddressesFallback()
- {
- var host = await Dns.GetHostEntryAsync(Dns.GetHostName()).ConfigureAwait(false);
-
- // Reverse them because the last one is usually the correct one
- // It's not fool-proof so ultimately the consumer will have to examine them and decide
- return host.AddressList
- .Where(i => i.AddressFamily == AddressFamily.InterNetwork || i.AddressFamily == AddressFamily.InterNetworkV6)
- .Reverse();
- }
-
- ///
- /// Gets a random port number that is currently available.
- ///
- /// System.Int32.
- public int GetRandomUnusedTcpPort()
- {
- var listener = new TcpListener(IPAddress.Any, 0);
- listener.Start();
- var port = ((IPEndPoint)listener.LocalEndpoint).Port;
- listener.Stop();
- return port;
- }
-
- ///
- public int GetRandomUnusedUdpPort()
- {
- var localEndPoint = new IPEndPoint(IPAddress.Any, 0);
- using (var udpClient = new UdpClient(localEndPoint))
- {
- return ((IPEndPoint)udpClient.Client.LocalEndPoint).Port;
- }
- }
-
- ///
- public List GetMacAddresses()
- {
- return _macAddresses ??= GetMacAddressesInternal().ToList();
- }
-
- private static IEnumerable GetMacAddressesInternal()
- => NetworkInterface.GetAllNetworkInterfaces()
- .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback)
- .Select(x => x.GetPhysicalAddress())
- .Where(x => !x.Equals(PhysicalAddress.None));
-
- ///
- public bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask)
- {
- IPAddress network1 = GetNetworkAddress(address1, subnetMask);
- IPAddress network2 = GetNetworkAddress(address2, subnetMask);
- return network1.Equals(network2);
- }
-
- private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
- {
- 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 (ipAddressBytes.Length != subnetMaskBytes.Length)
- {
- throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
- }
-
- byte[] broadcastAddress = new byte[ipAddressBytes.Length];
- for (int i = 0; i < broadcastAddress.Length; i++)
- {
- broadcastAddress[i] = (byte)(ipAddressBytes[i] & subnetMaskBytes[i]);
- }
-
- return new IPAddress(broadcastAddress);
- }
-
- ///
- public IPAddress GetLocalIpSubnetMask(IPAddress address)
- {
- NetworkInterface[] interfaces;
-
- try
- {
- var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown };
-
- interfaces = NetworkInterface.GetAllNetworkInterfaces()
- .Where(i => validStatuses.Contains(i.OperationalStatus))
- .ToArray();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in GetAllNetworkInterfaces");
- return null;
- }
-
- foreach (NetworkInterface ni in interfaces)
- {
- foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses)
- {
- if (ip.Address.Equals(address) && ip.IPv4Mask != null)
- {
- return ip.IPv4Mask;
- }
- }
- }
-
- return null;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index b7a59cee2d..4fd7ac0c18 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Udp
{
string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
? _config[AddressOverrideConfigKey]
- : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
+ : _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address);
if (!string.IsNullOrEmpty(localUrl))
{
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 4e6455eaa7..4fd9c2fbf6 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers
private string GetAbsoluteUri()
{
- return $"{Request.Scheme}://{Request.Host}{Request.Path}";
+ return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
}
private Task ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index e59c6e1ddf..d9cb34557b 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -3,6 +3,7 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.StartupDtos;
+using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
@@ -89,9 +90,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
{
- _config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
- _config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
- _config.SaveConfiguration();
+ NetworkConfiguration settings = _config.GetNetworkConfiguration();
+ settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
+ settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
+ _config.SaveConfiguration("network", settings);
return NoContent();
}
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 92875d7357..7784e8a119 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -64,9 +64,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Info")]
[Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> GetSystemInfo()
+ public ActionResult GetSystemInfo()
{
- return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
+ return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
}
///
@@ -76,9 +76,9 @@ namespace Jellyfin.Api.Controllers
/// A with public info about the system.
[HttpGet("Info/Public")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> GetPublicSystemInfo()
+ public ActionResult GetPublicSystemInfo()
{
- return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
+ return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
}
///
diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs
new file mode 100644
index 0000000000..a911a33241
--- /dev/null
+++ b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Reflection;
+
+namespace Jellyfin.Api.Helpers
+{
+ ///
+ /// A static class for copying matching properties from one object to another.
+ /// TODO: remove at the point when a fixed migration path has been decided upon.
+ ///
+ public static class ClassMigrationHelper
+ {
+ ///
+ /// Extension for 'Object' that copies the properties to a destination object.
+ ///
+ /// The source.
+ /// The destination.
+ public static void CopyProperties(this object source, object destination)
+ {
+ // If any this null throw an exception.
+ if (source == null || destination == null)
+ {
+ throw new Exception("Source or/and Destination Objects are null");
+ }
+
+ // Getting the Types of the objects.
+ Type typeDest = destination.GetType();
+ Type typeSrc = source.GetType();
+
+ // Iterate the Properties of the source instance and populate them from their destination counterparts.
+ PropertyInfo[] srcProps = typeSrc.GetProperties();
+ foreach (PropertyInfo srcProp in srcProps)
+ {
+ if (!srcProp.CanRead)
+ {
+ continue;
+ }
+
+ var targetProperty = typeDest.GetProperty(srcProp.Name);
+ if (targetProperty == null)
+ {
+ continue;
+ }
+
+ if (!targetProperty.CanWrite)
+ {
+ continue;
+ }
+
+ var obj = targetProperty.GetSetMethod(true);
+ if (obj != null && obj.IsPrivate)
+ {
+ continue;
+ }
+
+ var target = targetProperty.GetSetMethod();
+ if (target != null && (target.Attributes & MethodAttributes.Static) != 0)
+ {
+ continue;
+ }
+
+ if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType))
+ {
+ continue;
+ }
+
+ // Passed all tests, lets set the value.
+ targetProperty.SetValue(destination, srcProp.GetValue(source, null), null);
+ }
+ }
+ }
+}
diff --git a/Jellyfin.Networking/Manager/INetworkManager.cs b/Jellyfin.Networking/Manager/INetworkManager.cs
deleted file mode 100644
index eababa6a90..0000000000
--- a/Jellyfin.Networking/Manager/INetworkManager.cs
+++ /dev/null
@@ -1,234 +0,0 @@
-#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.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index cb8ae91f56..78f596a5c9 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -38,21 +38,18 @@ namespace Jellyfin.Server
/// The to be used by the .
/// The to be used by the .
/// The to be used by the .
- /// The to be used by the .
/// The to be used by the .
public CoreAppHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
- INetworkManager networkManager,
IServiceCollection collection)
: base(
applicationPaths,
loggerFactory,
options,
fileSystem,
- networkManager,
collection)
{
}
diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
index c7fbfa4d02..6bf6f383fc 100644
--- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Middleware;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
@@ -24,8 +25,8 @@ namespace Jellyfin.Server.Extensions
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
- var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/');
- var apiDocBaseUrl = serverConfigurationManager.Configuration.BaseUrl;
+ var baseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl.Trim('/');
+ var apiDocBaseUrl = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
if (!string.IsNullOrEmpty(baseUrl))
{
baseUrl += '/';
diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
index 9316737bdf..c23da2fd63 100644
--- a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
+++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
@@ -42,7 +43,7 @@ namespace Jellyfin.Server.Middleware
public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager)
{
var localPath = httpContext.Request.Path.ToString();
- var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl;
+ var baseUrlPrefix = serverConfigurationManager.GetNetworkConfiguration().BaseUrl;
if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
|| string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
index 4bda8f2737..525cd9ffe2 100644
--- a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
+++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs
@@ -1,5 +1,6 @@
-using System.Linq;
+using System.Net;
using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -34,40 +35,40 @@ namespace Jellyfin.Server.Middleware
{
if (httpContext.IsLocal())
{
+ // Running locally.
await _next(httpContext).ConfigureAwait(false);
return;
}
- var remoteIp = httpContext.GetNormalizedRemoteIp();
+ var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
- if (serverConfigurationManager.Configuration.EnableRemoteAccess)
+ if (serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
{
- var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
+ // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
+ // If left blank, all remote addresses will be allowed.
+ var remoteAddressFilter = networkManager.RemoteAddressFilter;
- if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp))
+ if (remoteAddressFilter.Count > 0 && !networkManager.IsInLocalNetwork(remoteIp))
{
- if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist)
+ // remoteAddressFilter is a whitelist or blacklist.
+ bool isListed = remoteAddressFilter.ContainsAddress(remoteIp);
+ if (!serverConfigurationManager.GetNetworkConfiguration().IsRemoteIPFilterBlacklist)
{
- if (networkManager.IsAddressInSubnets(remoteIp, addressFilter))
- {
- return;
- }
+ // Black list, so flip over.
+ isListed = !isListed;
}
- else
+
+ if (!isListed)
{
- if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter))
- {
- return;
- }
+ // If your name isn't on the list, you arn't coming in.
+ return;
}
}
}
- else
+ else if (!networkManager.IsInLocalNetwork(remoteIp))
{
- if (!networkManager.IsInLocalNetwork(remoteIp))
- {
- return;
- }
+ // Remote not enabled. So everyone should be LAN.
+ return;
}
await _next(httpContext).ConfigureAwait(false);
diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
index 9d795145aa..8065054a1e 100644
--- a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
+++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs
@@ -1,6 +1,9 @@
using System;
using System.Linq;
+using System.Net;
using System.Threading.Tasks;
+using Jellyfin.Networking.Configuration;
+using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Http;
@@ -32,45 +35,14 @@ namespace Jellyfin.Server.Middleware
/// The async task.
public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager)
{
- var currentHost = httpContext.Request.Host.ToString();
- var hosts = serverConfigurationManager
- .Configuration
- .LocalNetworkAddresses
- .Select(NormalizeConfiguredLocalAddress)
- .ToList();
+ var host = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
- if (hosts.Count == 0)
+ if (!networkManager.IsInLocalNetwork(host) && !serverConfigurationManager.GetNetworkConfiguration().EnableRemoteAccess)
{
- await _next(httpContext).ConfigureAwait(false);
return;
}
- currentHost ??= string.Empty;
-
- if (networkManager.IsInPrivateAddressSpace(currentHost))
- {
- hosts.Add("localhost");
- hosts.Add("127.0.0.1");
-
- if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1))
- {
- return;
- }
- }
-
await _next(httpContext).ConfigureAwait(false);
}
-
- private static string NormalizeConfiguredLocalAddress(string address)
- {
- var add = address.AsSpan().Trim('/');
- int index = add.IndexOf('/');
- if (index != -1)
- {
- add = add.Slice(index + 1);
- }
-
- return add.TrimStart('/').ToString();
- }
}
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 97a51c202b..db67f64708 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -12,9 +12,9 @@ using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
using Emby.Server.Implementations.IO;
-using Emby.Server.Implementations.Networking;
using Jellyfin.Api.Controllers;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -161,7 +161,6 @@ namespace Jellyfin.Server
_loggerFactory,
options,
new ManagedFileSystem(_loggerFactory.CreateLogger(), appPaths),
- new NetworkManager(_loggerFactory.CreateLogger()),
serviceCollection);
try
@@ -272,53 +271,17 @@ namespace Jellyfin.Server
return builder
.UseKestrel((builderContext, options) =>
{
- var addresses = appHost.ServerConfigurationManager
- .Configuration
- .LocalNetworkAddresses
- .Select(x => appHost.NormalizeConfiguredLocalAddress(x))
- .Where(i => i != null)
- .ToHashSet();
- if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any))
- {
- if (!addresses.Contains(IPAddress.Loopback))
- {
- // we must listen on loopback for LiveTV to function regardless of the settings
- addresses.Add(IPAddress.Loopback);
- }
+ var addresses = appHost.NetManager.GetAllBindInterfaces();
- foreach (var address in addresses)
- {
- _logger.LogInformation("Kestrel listening on {IpAddress}", address);
- options.Listen(address, appHost.HttpPort);
-
- if (appHost.ListenWithHttps)
- {
- options.Listen(
- address,
- appHost.HttpsPort,
- listenOptions => listenOptions.UseHttps(appHost.Certificate));
- }
- else if (builderContext.HostingEnvironment.IsDevelopment())
- {
- try
- {
- options.Listen(address, appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
- }
- catch (InvalidOperationException ex)
- {
- _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
- }
- }
- }
- }
- else
+ bool flagged = false;
+ foreach (IPObject netAdd in addresses)
{
- _logger.LogInformation("Kestrel listening on all interfaces");
- options.ListenAnyIP(appHost.HttpPort);
-
+ _logger.LogInformation("Kestrel listening on {0}", netAdd);
+ options.Listen(netAdd.Address, appHost.HttpPort);
if (appHost.ListenWithHttps)
{
- options.ListenAnyIP(
+ options.Listen(
+ netAdd.Address,
appHost.HttpsPort,
listenOptions => listenOptions.UseHttps(appHost.Certificate));
}
@@ -326,11 +289,18 @@ namespace Jellyfin.Server
{
try
{
- options.ListenAnyIP(appHost.HttpsPort, listenOptions => listenOptions.UseHttps());
+ options.Listen(
+ netAdd.Address,
+ appHost.HttpsPort,
+ listenOptions => listenOptions.UseHttps());
}
- catch (InvalidOperationException ex)
+ catch (InvalidOperationException)
{
- _logger.LogError(ex, "Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
+ if (!flagged)
+ {
+ _logger.LogWarning("Failed to listen to HTTPS using the ASP.NET Core HTTPS development certificate. Please ensure it has been installed and set as trusted.");
+ flagged = true;
+ }
}
}
}
diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs
index 6de0dd7ecf..a615e9aeb9 100644
--- a/Jellyfin.Server/Startup.cs
+++ b/Jellyfin.Server/Startup.cs
@@ -1,5 +1,6 @@
using System.Net.Http.Headers;
using System.Net.Mime;
+using Jellyfin.Networking.Configuration;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Middleware;
@@ -51,7 +52,7 @@ namespace Jellyfin.Server
{
options.HttpsPort = _serverApplicationHost.HttpsPort;
});
- services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.Configuration.KnownProxies);
+ services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration().KnownProxies);
services.AddJellyfinApiSwagger();
@@ -103,7 +104,7 @@ namespace Jellyfin.Server
app.UseBaseUrlRedirection();
// Wrap rest of configuration so everything only listens on BaseUrl.
- app.Map(_serverConfigurationManager.Configuration.BaseUrl, mainApp =>
+ app.Map(_serverConfigurationManager.GetNetworkConfiguration().BaseUrl, mainApp =>
{
if (env.IsDevelopment())
{
diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs
index 12966a474f..43562afe38 100644
--- a/MediaBrowser.Common/Net/INetworkManager.cs
+++ b/MediaBrowser.Common/Net/INetworkManager.cs
@@ -1,97 +1,233 @@
-#pragma warning disable CS1591
-
+#nullable enable
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Net;
using System.Net.NetworkInformation;
+using MediaBrowser.Common.Net;
+using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Common.Net
{
+ ///
+ /// Interface for the NetworkManager class.
+ ///
public interface INetworkManager
{
+ ///
+ /// Event triggered on network changes.
+ ///
event EventHandler NetworkChanged;
///
- /// Gets or sets a function to return the list of user defined LAN addresses.
+ /// 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.
///
- Func LocalSubnetsFn { get; set; }
+ /// IP to check. Can be an IPAddress or an IPObject.
+ /// Result of the check.
+ bool IsGatewayInterface(IPAddress? addressObj);
///
- /// Gets a random port TCP number that is currently available.
+ /// Returns true if the address is a private address.
+ /// The config option TrustIP6Interfaces overrides this functions behaviour.
///
- /// System.Int32.
- int GetRandomUnusedTcpPort();
+ /// Address to check.
+ /// True or False.
+ bool IsPrivateAddressRange(IPObject address);
///
- /// Gets a random port UDP number that is currently available.
+ /// Returns true if the address is part of the user defined LAN.
+ /// The config option TrustIP6Interfaces overrides this functions behaviour.
///
- /// System.Int32.
- int GetRandomUnusedUdpPort();
+ /// IP to check.
+ /// True if endpoint is within the LAN range.
+ bool IsInLocalNetwork(string address);
///
- /// Returns the MAC Address from first Network Card in Computer.
+ /// Returns true if the address is part of the user defined LAN.
+ /// The config option TrustIP6Interfaces overrides this functions behaviour.
///
- /// The MAC Address.
- List GetMacAddresses();
+ /// IP to check.
+ /// True if endpoint is within the LAN range.
+ bool IsInLocalNetwork(IPObject address);
///
- /// Determines whether [is in private address space] [the specified endpoint].
+ /// Returns true if the address is part of the user defined LAN.
+ /// The config option TrustIP6Interfaces overrides this functions behaviour.
///
- /// The endpoint.
- /// true if [is in private address space] [the specified endpoint]; otherwise, false.
- bool IsInPrivateAddressSpace(string endpoint);
+ /// IP to check.
+ /// True if endpoint is within the LAN range.
+ bool IsInLocalNetwork(IPAddress address);
///
- /// Determines whether [is in private address space 10.x.x.x] [the specified endpoint] and exists in the subnets returned by GetSubnets().
+ /// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes.
+ /// eg. "eth1", or "TP-LINK Wireless USB Adapter".
///
- /// The endpoint.
- /// true if [is in private address space 10.x.x.x] [the specified endpoint]; otherwise, false.
- bool IsInPrivateAddressSpaceAndLocalSubnet(string endpoint);
+ /// Token to parse.
+ /// Resultant object's ip addresses, if successful.
+ /// Success of the operation.
+ bool TryParseInterface(string token, out Collection? result);
///
- /// Determines whether [is in local network] [the specified endpoint].
+ /// Parses an array of strings into a Collection{IPObject}.
///
- /// The endpoint.
- /// true if [is in local network] [the specified endpoint]; otherwise, false.
- bool IsInLocalNetwork(string endpoint);
+ /// 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);
///
- /// Investigates an caches a list of interface addresses, excluding local link and LAN excluded addresses.
+ /// Returns all the internal Bind interface addresses.
///
- /// The list of ip addresses.
- IPAddress[] GetLocalIpAddresses();
+ /// An internal list of interfaces addresses.
+ Collection GetInternalBindAddresses();
///
- /// Checks if the given address falls within the ranges given in [subnets]. The addresses in subnets can be hosts or subnets in the CIDR format.
+ /// Checks to see if an IP address is still a valid interface address.
///
- /// The address to check.
- /// If true, check against addresses in the LAN settings surrounded by brackets ([]).
- /// trueif the address is in at least one of the given subnets, false otherwise.
- bool IsAddressInSubnets(string addressString, string[] subnets);
+ /// IP address to check.
+ /// True if it is.
+ bool IsValidInterfaceAddress(IPAddress address);
///
- /// Returns true if address is in the LAN list in the config file.
+ /// Returns true if the IP address is in the excluded list.
///
- /// The address to check.
- /// If true, check against addresses in the LAN settings which have [] around and return true if it matches the address give in address.
- /// If true, returns false if address is in the 127.x.x.x or 169.128.x.x range.
- /// falseif the address isn't in the LAN list, true if the address has been defined as a LAN address.
- bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC);
+ /// IP to check.
+ /// True if excluded.
+ bool IsExcluded(IPAddress ip);
///
- /// Checks if address is in the LAN list in the config file.
+ /// Returns true if the IP address is in the excluded list.
///
- /// Source address to check.
- /// Destination address to check against.
- /// Destination subnet to check against.
- /// true/falsedepending on whether address1 is in the same subnet as IPAddress2 with subnetMask.
- bool IsInSameSubnet(IPAddress address1, IPAddress address2, IPAddress subnetMask);
+ /// IP to check.
+ /// True if excluded.
+ bool IsExcluded(EndPoint ip);
///
- /// Returns the subnet mask of an interface with the given address.
+ /// Gets the filtered LAN ip addresses.
///
- /// The address to check.
- /// Returns the subnet mask of an interface with the given address, or null if an interface match cannot be found.
- IPAddress GetLocalIpSubnetMask(IPAddress address);
+ /// Optional filter for the list.
+ /// Returns a filtered list of LAN addresses.
+ Collection GetFilteredLANSubnets(Collection? filter = null);
}
}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index ffbb147b0d..2456da826f 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller
{
@@ -56,41 +57,42 @@ namespace MediaBrowser.Controller
///
/// Gets the system info.
///
- /// A cancellation token that can be used to cancel the task.
+ /// The originator of the request.
/// SystemInfo.
- Task GetSystemInfo(CancellationToken cancellationToken = default);
+ SystemInfo GetSystemInfo(IPAddress source);
- Task GetPublicSystemInfo(CancellationToken cancellationToken = default);
+ PublicSystemInfo GetPublicSystemInfo(IPAddress address);
///
- /// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
- /// to the API that should exist at the address.
+ /// Gets a URL specific for the request.
///
- /// A cancellation token that can be used to cancel the task.
- /// A list containing all the local IP addresses of the server.
- Task> GetLocalIpAddresses(CancellationToken cancellationToken = default);
+ /// The instance.
+ /// Optional port number.
+ /// An accessible URL.
+ string GetSmartApiUrl(HttpRequest request, int? port = null);
///
- /// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
- /// IP address that can be found via . HTTPS will be preferred when available.
+ /// Gets a URL specific for the request.
///
- /// A cancellation token that can be used to cancel the task.
- /// The server URL.
- Task GetLocalApiUrl(CancellationToken cancellationToken = default);
+ /// The remote of the connection.
+ /// Optional port number.
+ /// An accessible URL.
+ string GetSmartApiUrl(IPAddress remoteAddr, int? port = null);
///
- /// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
- /// over HTTP (not HTTPS).
+ /// Gets a URL specific for the request.
///
- /// The API URL.
- string GetLoopbackHttpApiUrl();
+ /// The hostname used in the connection.
+ /// Optional port number.
+ /// An accessible URL.
+ string GetSmartApiUrl(string hostname, int? port = null);
///
- /// Gets a local (LAN) URL that can be used to access the API. HTTPS will be preferred when available.
+ /// Gets a localhost URL that can be used to access the API using the loop-back IP address.
+ /// over HTTP (not HTTPS).
///
- /// The IP address to use as the hostname in the URL.
/// The API URL.
- string GetLocalApiUrl(IPAddress address);
+ string GetLoopbackHttpApiUrl();
///
/// Gets a local (LAN) URL that can be used to access the API.
@@ -106,7 +108,7 @@ namespace MediaBrowser.Controller
/// preferring the HTTPS port, if available.
///
/// The API URL.
- string GetLocalApiUrl(ReadOnlySpan hostname, string scheme = null, int? port = null);
+ string GetLocalApiUrl(string hostname, string scheme = null, int? port = null);
///
/// Open a URL in an external browser window.
diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
index 06985ebf40..830c8bd102 100644
--- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs
+++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs
@@ -233,7 +233,7 @@ namespace MediaBrowser.Model.Configuration
/// Gets or sets a value indicating whether quick connect is available for use on this server.
///
public bool QuickConnectAvailable { get; set; } = false;
-
+
///
/// Gets or sets a value indicating whether access outside of the LAN is permitted.
///
diff --git a/MediaBrowser.sln b/MediaBrowser.sln
index d460c0ab0c..cb204137bd 100644
--- a/MediaBrowser.sln
+++ b/MediaBrowser.sln
@@ -68,6 +68,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking", "Jellyfin.Networking\Jellyfin.Networking.csproj", "{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Networking.Tests", "tests\Jellyfin.Networking.Tests\NetworkTesting\Jellyfin.Networking.Tests.csproj", "{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -182,6 +184,10 @@ Global
{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A3FCC4D-C714-4072-B90F-E374A15F9FF9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -193,6 +199,7 @@ Global
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
{462584F7-5023-4019-9EAC-B98CA458C0A0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE}
diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj
index d0962e82c8..c64ee9389d 100644
--- a/RSSDP/RSSDP.csproj
+++ b/RSSDP/RSSDP.csproj
@@ -6,6 +6,7 @@
+
diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs
index a4be32e7d2..8f1f0fa613 100644
--- a/RSSDP/SsdpCommunicationsServer.cs
+++ b/RSSDP/SsdpCommunicationsServer.cs
@@ -352,7 +352,7 @@ namespace Rssdp.Infrastructure
if (_enableMultiSocketBinding)
{
- foreach (var address in _networkManager.GetLocalIpAddresses())
+ foreach (var address in _networkManager.GetInternalBindAddresses())
{
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
@@ -362,7 +362,7 @@ namespace Rssdp.Infrastructure
try
{
- sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address, _LocalPort));
+ sockets.Add(_SocketFactory.CreateSsdpUdpSocket(address.Address, _LocalPort));
}
catch (Exception ex)
{
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs
index 90925b9e0a..c9e795d565 100644
--- a/RSSDP/SsdpDevicePublisher.cs
+++ b/RSSDP/SsdpDevicePublisher.cs
@@ -300,17 +300,15 @@ namespace Rssdp.Infrastructure
foreach (var device in deviceList)
{
- if (!_sendOnlyMatchedHost ||
- _networkManager.IsInSameSubnet(device.ToRootDevice().Address, remoteEndPoint.Address, device.ToRootDevice().SubnetMask))
+ var root = device.ToRootDevice();
+ var source = new IPNetAddress(root.Address, root.PrefixLength);
+ var destination = new IPNetAddress(remoteEndPoint.Address, root.PrefixLength);
+ if (!_sendOnlyMatchedHost || source.NetworkAddress.Equals(destination.NetworkAddress))
{
SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken);
}
}
}
- else
- {
- // WriteTrace(String.Format("Sending 0 search responses."));
- }
});
}
diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs
index 4084b31ca6..5ecb1f86f6 100644
--- a/RSSDP/SsdpRootDevice.cs
+++ b/RSSDP/SsdpRootDevice.cs
@@ -45,9 +45,9 @@ namespace Rssdp
public IPAddress Address { get; set; }
///
- /// Gets or sets the SubnetMask used to check if the received message from same interface with this device/tree. Required.
+ /// Gets or sets the prefix length used to check if the received message from same interface with this device/tree. Required.
///
- public IPAddress SubnetMask { get; set; }
+ public byte PrefixLength { get; set; }
///
/// The base URL to use for all relative url's provided in other properties (and those of child devices). Optional.
diff --git a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs
index bd3d356870..54f8eb225f 100644
--- a/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Api.Tests/JellyfinApplicationFactory.cs
@@ -3,8 +3,6 @@ using System.Collections.Concurrent;
using System.IO;
using Emby.Server.Implementations;
using Emby.Server.Implementations.IO;
-using Emby.Server.Implementations.Networking;
-using Jellyfin.Drawing.Skia;
using Jellyfin.Server;
using MediaBrowser.Common;
using Microsoft.AspNetCore.Hosting;
@@ -80,7 +78,6 @@ namespace Jellyfin.Api.Tests
loggerFactory,
commandLineOpts,
new ManagedFileSystem(loggerFactory.CreateLogger(), appPaths),
- new NetworkManager(loggerFactory.CreateLogger()),
serviceCollection);
_disposableComponents.Add(appHost);
appHost.Init();
diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
new file mode 100644
index 0000000000..703d43210b
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/Jellyfin.Networking.Tests.csproj
@@ -0,0 +1,39 @@
+
+
+
+
+ {42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}
+
+
+
+ net5.0
+ false
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ../../jellyfin-tests.ruleset
+
+
+ DEBUG
+
+
diff --git a/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs
new file mode 100644
index 0000000000..56d11ef521
--- /dev/null
+++ b/tests/Jellyfin.Networking.Tests/NetworkTesting/NetworkParseTests.cs
@@ -0,0 +1,517 @@
+using System;
+using System.Net;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Manager;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using Moq;
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+using System.Collections.ObjectModel;
+
+namespace Jellyfin.Networking.Tests
+{
+ public class NetworkParseTests
+ {
+ ///
+ /// Tries to identify the string and return an object of that class.
+ ///
+ /// String to parse.
+ /// IPObject to return.
+ /// True if the value parsed successfully.
+ private static bool TryParse(string addr, out IPObject result)
+ {
+ if (!string.IsNullOrEmpty(addr))
+ {
+ // Is it an IP address
+ if (IPNetAddress.TryParse(addr, out IPNetAddress nw))
+ {
+ result = nw;
+ return true;
+ }
+
+ if (IPHost.TryParse(addr, out IPHost h))
+ {
+ result = h;
+ return true;
+ }
+ }
+
+ result = IPNetAddress.None;
+ return false;
+ }
+
+ private static IConfigurationManager GetMockConfig(NetworkConfiguration conf)
+ {
+ var configManager = new Mock
+ {
+ CallBase = true
+ };
+ configManager.Setup(x => x.GetConfiguration(It.IsAny())).Returns(conf);
+ return (IConfigurationManager)configManager.Object;
+ }
+
+ ///
+ /// Checks the ability to ignore interfaces
+ ///
+ /// Mock network setup, in the format (IP address, interface index, interface name) : ....
+ /// LAN addresses.
+ /// Bind addresses that are excluded.
+ [Theory]
+ [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24;200.200.200.0/24", "[192.168.1.208/24,200.200.200.200/24]")]
+ [InlineData("192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
+ [InlineData("192.168.1.208/24,-16,vEthernet1:192.168.1.208/24,-16,vEthernet212;200.200.200.200/24,11,eth11", "192.168.1.0/24", "[192.168.1.208/24]")]
+ public void IgnoreVirtualInterfaces(string interfaces, string lan, string value)
+ {
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV6 = true,
+ EnableIPV4 = true,
+ LocalNetworkSubnets = lan?.Split(';') ?? throw new ArgumentNullException(nameof(lan))
+ };
+
+ NetworkManager.MockNetworkSettings = interfaces;
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ NetworkManager.MockNetworkSettings = string.Empty;
+
+ Assert.Equal(nm.GetInternalBindAddresses().AsString(), value);
+ }
+
+ ///
+ /// Check that the value given is in the network provided.
+ ///
+ /// Network address.
+ /// Value to check.
+ [Theory]
+ [InlineData("192.168.10.0/24, !192.168.10.60/32", "192.168.10.60")]
+ public void IsInNetwork(string network, string value)
+ {
+ if (network == null)
+ {
+ throw new ArgumentNullException(nameof(network));
+ }
+
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV6 = true,
+ EnableIPV4 = true,
+ LocalNetworkSubnets = network.Split(',')
+ };
+
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+
+ Assert.False(nm.IsInLocalNetwork(value));
+ }
+
+ ///
+ /// Checks IP address formats.
+ ///
+ ///
+ [Theory]
+ [InlineData("127.0.0.1")]
+ [InlineData("127.0.0.1:123")]
+ [InlineData("localhost")]
+ [InlineData("localhost:1345")]
+ [InlineData("www.google.co.uk")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")]
+ [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")]
+ [InlineData("fe80::7add:12ff:febb:c67b%16")]
+ [InlineData("[fe80::7add:12ff:febb:c67b%16]:123")]
+ [InlineData("192.168.1.2/255.255.255.0")]
+ [InlineData("192.168.1.2/24")]
+ public void ValidIPStrings(string address)
+ {
+ Assert.True(TryParse(address, out _));
+ }
+
+
+ ///
+ /// All should be invalid address strings.
+ ///
+ /// Invalid address strings.
+ [Theory]
+ [InlineData("256.128.0.0.0.1")]
+ [InlineData("127.0.0.1#")]
+ [InlineData("localhost!")]
+ [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517:1231")]
+ public void InvalidAddressString(string address)
+ {
+ Assert.False(TryParse(address, out _));
+ }
+
+
+ ///
+ /// Test collection parsing.
+ ///
+ /// Collection to parse.
+ /// Included addresses from the collection.
+ /// Included IP4 addresses from the collection.
+ /// Excluded addresses from the collection.
+ /// Excluded IP4 addresses from the collection.
+ /// Network addresses of the collection.
+ [Theory]
+ [InlineData("127.0.0.1#",
+ "[]",
+ "[]",
+ "[]",
+ "[]",
+ "[]")]
+ [InlineData("[127.0.0.1]",
+ "[]",
+ "[]",
+ "[127.0.0.1/32]",
+ "[127.0.0.1/32]",
+ "[]")]
+ [InlineData("",
+ "[]",
+ "[]",
+ "[]",
+ "[]",
+ "[]")]
+ [InlineData("192.158.1.2/255.255.0.0,192.169.1.2/8",
+ "[192.158.1.2/16,192.169.1.2/8]",
+ "[192.158.1.2/16,192.169.1.2/8]",
+ "[]",
+ "[]",
+ "[192.158.0.0/16,192.0.0.0/8]")]
+ [InlineData("192.158.1.2/16, localhost, fd23:184f:2029:0:3139:7386:67d7:d517, [10.10.10.10]",
+ "[192.158.1.2/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]",
+ "[192.158.1.2/16,127.0.0.1/32]",
+ "[10.10.10.10/32]",
+ "[10.10.10.10/32]",
+ "[192.158.0.0/16,127.0.0.1/32,fd23:184f:2029:0:3139:7386:67d7:d517/128]")]
+ public void TestCollections(string settings, string result1, string result2, string result3, string result4, string result5)
+ {
+ if (settings == null)
+ {
+ throw new ArgumentNullException(nameof(settings));
+ }
+
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV6 = true,
+ EnableIPV4 = true,
+ };
+
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+
+ // Test included.
+ Collection nc = nm.CreateIPCollection(settings.Split(","), false);
+ Assert.Equal(nc.AsString(), result1);
+
+ // Test excluded.
+ nc = nm.CreateIPCollection(settings.Split(","), true);
+ Assert.Equal(nc.AsString(), result3);
+
+ conf.EnableIPV6 = false;
+ nm.UpdateSettings(conf);
+
+ // Test IP4 included.
+ nc = nm.CreateIPCollection(settings.Split(","), false);
+ Assert.Equal(nc.AsString(), result2);
+
+ // Test IP4 excluded.
+ nc = nm.CreateIPCollection(settings.Split(","), true);
+ Assert.Equal(nc.AsString(), result4);
+
+ conf.EnableIPV6 = true;
+ nm.UpdateSettings(conf);
+
+ // Test network addresses of collection.
+ nc = nm.CreateIPCollection(settings.Split(","), false);
+ nc = nc.AsNetworks();
+ Assert.Equal(nc.AsString(), result5);
+ }
+
+ ///
+ /// Union two collections.
+ ///
+ /// Source.
+ /// Destination.
+ /// Result.
+ [Theory]
+ [InlineData("127.0.0.1", "fd23:184f:2029:0:3139:7386:67d7:d517/64,fd23:184f:2029:0:c0f0:8a8a:7605:fffa/128,fe80::3139:7386:67d7:d517%16/64,192.168.1.208/24,::1/128,127.0.0.1/8", "[127.0.0.1/32]")]
+ [InlineData("127.0.0.1", "127.0.0.1/8", "[127.0.0.1/32]")]
+ public void UnionCheck(string settings, string compare, string result)
+ {
+ if (settings == null)
+ {
+ throw new ArgumentNullException(nameof(settings));
+ }
+
+ if (compare == null)
+ {
+ throw new ArgumentNullException(nameof(compare));
+ }
+
+ if (result == null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV6 = true,
+ EnableIPV4 = true,
+ };
+
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+
+ Collection nc1 = nm.CreateIPCollection(settings.Split(","), false);
+ Collection nc2 = nm.CreateIPCollection(compare.Split(","), false);
+
+ Assert.Equal(nc1.Union(nc2).AsString(), result);
+ }
+
+ [Theory]
+ [InlineData("192.168.5.85/24", "192.168.5.1")]
+ [InlineData("192.168.5.85/24", "192.168.5.254")]
+ [InlineData("10.128.240.50/30", "10.128.240.48")]
+ [InlineData("10.128.240.50/30", "10.128.240.49")]
+ [InlineData("10.128.240.50/30", "10.128.240.50")]
+ [InlineData("10.128.240.50/30", "10.128.240.51")]
+ [InlineData("127.0.0.1/8", "127.0.0.1")]
+ public void IpV4SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress)
+ {
+ var ipAddressObj = IPNetAddress.Parse(netMask);
+ Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress)));
+ }
+
+ [Theory]
+ [InlineData("192.168.5.85/24", "192.168.4.254")]
+ [InlineData("192.168.5.85/24", "191.168.5.254")]
+ [InlineData("10.128.240.50/30", "10.128.240.47")]
+ [InlineData("10.128.240.50/30", "10.128.240.52")]
+ [InlineData("10.128.240.50/30", "10.128.239.50")]
+ [InlineData("10.128.240.50/30", "10.127.240.51")]
+ public void IpV4SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress)
+ {
+ var ipAddressObj = IPNetAddress.Parse(netMask);
+ Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress)));
+ }
+
+ [Theory]
+ [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")]
+ [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF")]
+ [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:0001:0000:0000:0000")]
+ [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFF0")]
+ [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0000")]
+ public void IpV6SubnetMaskMatchesValidIpAddress(string netMask, string ipAddress)
+ {
+ var ipAddressObj = IPNetAddress.Parse(netMask);
+ Assert.True(ipAddressObj.Contains(IPAddress.Parse(ipAddress)));
+ }
+
+ [Theory]
+ [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFFF")]
+ [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0000:0000:0000:0000")]
+ [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0013:0001:0000:0000:0000")]
+ [InlineData("2001:db8:abcd:0012::0/64", "2001:0DB8:ABCD:0011:FFFF:FFFF:FFFF:FFF0")]
+ [InlineData("2001:db8:abcd:0012::0/128", "2001:0DB8:ABCD:0012:0000:0000:0000:0001")]
+ public void IpV6SubnetMaskDoesNotMatchInvalidIpAddress(string netMask, string ipAddress)
+ {
+ var ipAddressObj = IPNetAddress.Parse(netMask);
+ Assert.False(ipAddressObj.Contains(IPAddress.Parse(ipAddress)));
+ }
+
+ [Theory]
+ [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1/32")]
+ [InlineData("10.0.0.0/8", "10.10.10.1/32")]
+ [InlineData("10.0.0.0/255.0.0.0", "10.10.10.1")]
+
+ [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1/32")]
+ [InlineData("10.10.0.0/16", "10.10.10.1/32")]
+ [InlineData("10.10.0.0/255.255.0.0", "10.10.10.1")]
+
+ [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1/32")]
+ [InlineData("10.10.10.0/24", "10.10.10.1/32")]
+ [InlineData("10.10.10.0/255.255.255.0", "10.10.10.1")]
+
+ public void TestSubnetContains(string network, string ip)
+ {
+ Assert.True(TryParse(network, out IPObject? networkObj));
+ Assert.True(TryParse(ip, out IPObject? ipObj));
+ Assert.True(networkObj.Contains(ipObj));
+ }
+
+ [Theory]
+ [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24", "172.168.1.2/24")]
+ [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "172.168.1.2/24, 10.10.10.1", "172.168.1.2/24,10.10.10.1/24")]
+ [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/255.255.255.0, 10.10.10.1", "192.168.1.2/24,10.10.10.1/24")]
+ [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "192.168.1.2/24, 100.10.10.1", "192.168.1.2/24")]
+ [InlineData("192.168.1.2/24,10.10.10.1/24,172.168.1.2/24", "194.168.1.2/24, 100.10.10.1", "")]
+
+ public void TestCollectionEquality(string source, string dest, string result)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (dest == null)
+ {
+ throw new ArgumentNullException(nameof(dest));
+ }
+
+ if (result == null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ var conf = new NetworkConfiguration()
+ {
+ EnableIPV6 = true,
+ EnableIPV4 = true
+ };
+
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+
+ // Test included, IP6.
+ Collection ncSource = nm.CreateIPCollection(source.Split(","));
+ Collection ncDest = nm.CreateIPCollection(dest.Split(","));
+ Collection ncResult = ncSource.Union(ncDest);
+ Collection resultCollection = nm.CreateIPCollection(result.Split(","));
+ Assert.True(ncResult.Compare(resultCollection));
+ }
+
+
+ [Theory]
+ [InlineData("10.1.1.1/32", "10.1.1.1")]
+ [InlineData("192.168.1.254/32", "192.168.1.254/255.255.255.255")]
+
+ public void TestEquals(string source, string dest)
+ {
+ Assert.True(IPNetAddress.Parse(source).Equals(IPNetAddress.Parse(dest)));
+ Assert.True(IPNetAddress.Parse(dest).Equals(IPNetAddress.Parse(source)));
+ }
+
+ [Theory]
+
+ // Testing bind interfaces.
+ // On my system eth16 is internal, eth11 external (Windows defines the indexes).
+ //
+ // This test is to replicate how DNLA requests work throughout the system.
+
+ // User on internal network, we're bound internal and external - so result is internal.
+ [InlineData("192.168.1.1", "eth16,eth11", false, "eth16")]
+ // User on external network, we're bound internal and external - so result is external.
+ [InlineData("8.8.8.8", "eth16,eth11", false, "eth11")]
+ // User on internal network, we're bound internal only - so result is internal.
+ [InlineData("10.10.10.10", "eth16", false, "eth16")]
+ // User on internal network, no binding specified - so result is the 1st internal.
+ [InlineData("192.168.1.1", "", false, "eth16")]
+ // User on external network, internal binding only - so result is the 1st internal.
+ [InlineData("jellyfin.org", "eth16", false, "eth16")]
+ // User on external network, no binding - so result is the 1st external.
+ [InlineData("jellyfin.org", "", false, "eth11")]
+ // User assumed to be internal, no binding - so result is the 1st internal.
+ [InlineData("", "", false, "eth16")]
+ public void TestBindInterfaces(string source, string bindAddresses, bool ipv6enabled, string result)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (bindAddresses == null)
+ {
+ throw new ArgumentNullException(nameof(bindAddresses));
+ }
+
+ if (result == null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+
+ var conf = new NetworkConfiguration()
+ {
+ LocalNetworkAddresses = bindAddresses.Split(','),
+ EnableIPV6 = ipv6enabled,
+ EnableIPV4 = true
+ };
+
+ NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11";
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ NetworkManager.MockNetworkSettings = string.Empty;
+
+ _ = nm.TryParseInterface(result, out Collection? resultObj);
+
+ if (resultObj != null)
+ {
+ result = ((IPNetAddress)resultObj[0]).ToString(true);
+ var intf = nm.GetBindInterface(source, out int? _);
+
+ Assert.Equal(intf, result);
+ }
+ }
+
+ [Theory]
+
+ // Testing bind interfaces. These are set for my system so won't work elsewhere.
+ // On my system eth16 is internal, eth11 external (Windows defines the indexes).
+ //
+ // This test is to replicate how subnet bound ServerPublisherUri work throughout the system.
+
+ // User on internal network, we're bound internal and external - so result is internal override.
+ [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")]
+
+ // User on external network, we're bound internal and external - so result is override.
+ [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+
+ // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override.
+ [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")]
+
+ // User on internal network, no binding specified - so result is the 1st internal.
+ [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
+
+ // User on external network, internal binding only - so asumption is a proxy forward, return external override.
+ [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")]
+
+ // User on external network, no binding - so result is the 1st external which is overriden.
+ [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0 = http://helloworld.com", "http://helloworld.com")]
+
+ // User assumed to be internal, no binding - so result is the 1st internal.
+ [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")]
+
+ // User is internal, no binding - so result is the 1st internal, which is then overridden.
+ [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")]
+
+ public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result)
+ {
+ if (lan == null)
+ {
+ throw new ArgumentNullException(nameof(lan));
+ }
+
+ if (bindAddresses == null)
+ {
+ throw new ArgumentNullException(nameof(bindAddresses));
+ }
+
+ var conf = new NetworkConfiguration()
+ {
+ LocalNetworkSubnets = lan.Split(','),
+ LocalNetworkAddresses = bindAddresses.Split(','),
+ EnableIPV6 = ipv6enabled,
+ EnableIPV4 = true,
+ PublishedServerUriBySubnet = new string[] { publishedServers }
+ };
+
+ NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16:200.200.200.200/24,11,eth11";
+ using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger());
+ NetworkManager.MockNetworkSettings = string.Empty;
+
+ if (nm.TryParseInterface(result, out Collection? resultObj) && resultObj != null)
+ {
+ // Parse out IPAddresses so we can do a string comparison. (Ignore subnet masks).
+ result = ((IPNetAddress)resultObj[0]).ToString(true);
+ }
+
+ var intf = nm.GetBindInterface(source, out int? _);
+
+ Assert.Equal(intf, result);
+ }
+ }
+}