diff options
Diffstat (limited to 'src')
17 files changed, 139 insertions, 135 deletions
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index a158e5c86..ede93aaa5 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -219,7 +219,7 @@ public class SkiaEncoder : IImageEncoder return path; } - var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan()))); + var tempPath = Path.Join(_appPaths.TempDirectory, string.Concat("skia_", Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan()))); var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid."); Directory.CreateDirectory(directory); File.Copy(path, tempPath, true); @@ -263,6 +263,11 @@ public class SkiaEncoder : IImageEncoder return null; } + if (codec.FrameCount != 0) + { + throw new ArgumentException("Cannot decode images with multiple frames"); + } + // create the bitmap var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); @@ -554,9 +559,13 @@ public class SkiaEncoder : IImageEncoder /// <inheritdoc /> public void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops) { - var splashBuilder = new SplashscreenBuilder(this); - var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); - splashBuilder.GenerateSplash(posters, backdrops, outputPath); + // Only generate the splash screen if we have at least one poster and at least one backdrop/thumbnail. + if (posters.Count > 0 && backdrops.Count > 0) + { + var splashBuilder = new SplashscreenBuilder(this); + var outputPath = Path.Combine(_appPaths.DataPath, "splashscreen.png"); + splashBuilder.GenerateSplash(posters, backdrops, outputPath); + } } /// <inheritdoc /> diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj index 98b567e30..f786cc3b4 100644 --- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj +++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj @@ -8,12 +8,14 @@ <EmbedUntrackedSources>true</EmbedUntrackedSources> <IncludeSymbols>true</IncludeSymbols> <SymbolPackageFormat>snupkg</SymbolPackageFormat> + <!-- ICU4N.Transliterator only has prerelease versions --> + <NoWarn>NU5104</NoWarn> </PropertyGroup> <PropertyGroup> <Authors>Jellyfin Contributors</Authors> <PackageId>Jellyfin.Extensions</PackageId> - <VersionPrefix>10.9.0</VersionPrefix> + <VersionPrefix>10.10.0</VersionPrefix> <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs deleted file mode 100644 index cd582ced6..000000000 --- a/src/Jellyfin.Extensions/Json/Converters/JsonLowerCaseConverter.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Jellyfin.Extensions.Json.Converters -{ - /// <summary> - /// Converts an object to a lowercase string. - /// </summary> - /// <typeparam name="T">The object type.</typeparam> - public class JsonLowerCaseConverter<T> : JsonConverter<T> - { - /// <inheritdoc /> - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return JsonSerializer.Deserialize<T>(ref reader, options); - } - - /// <inheritdoc /> - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value?.ToString()?.ToLowerInvariant()); - } - } -} diff --git a/src/Jellyfin.Extensions/StreamExtensions.cs b/src/Jellyfin.Extensions/StreamExtensions.cs index 182996852..0cfac384e 100644 --- a/src/Jellyfin.Extensions/StreamExtensions.cs +++ b/src/Jellyfin.Extensions/StreamExtensions.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; namespace Jellyfin.Extensions { @@ -48,11 +50,12 @@ namespace Jellyfin.Extensions /// Reads all lines in the <see cref="TextReader" />. /// </summary> /// <param name="reader">The <see cref="TextReader" /> to read from.</param> + /// <param name="cancellationToken">The token to monitor for cancellation requests.</param> /// <returns>All lines in the stream.</returns> - public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader) + public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default) { string? line; - while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null) + while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null) { yield return line; } diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index cce2911dc..83f68ab50 100644 --- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -1130,7 +1130,7 @@ namespace Jellyfin.LiveTv.Channels { if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase)) { - item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray(); + item.Tags = [..item.Tags, "livestream"]; _logger.LogDebug("Forcing update due to Tags {0}", item.Name); forceUpdate = true; } diff --git a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs index 87f47611e..dfd376092 100644 --- a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs +++ b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs @@ -60,14 +60,13 @@ public class ListingsManager : IListingsManager var config = _config.GetLiveTvConfiguration(); - var list = config.ListingProviders.ToList(); - int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); + var list = config.ListingProviders; + int index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) { info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - list.Add(info); - config.ListingProviders = list.ToArray(); + config.ListingProviders = [..list, info]; } else { @@ -236,13 +235,12 @@ public class ListingsManager : IListingsManager if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)) { - var list = listingsProviderInfo.ChannelMappings.ToList(); - list.Add(new NameValuePair + var newItem = new NameValuePair { Name = tunerChannelNumber, Value = providerChannelNumber - }); - listingsProviderInfo.ChannelMappings = list.ToArray(); + }; + listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem]; } _config.SaveConfiguration("livetv", config); diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index c19d8195c..0c85dc434 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -939,7 +939,7 @@ namespace Jellyfin.LiveTv { var internalChannelId = _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId); var channel = _libraryManager.GetItemById(internalChannelId); - channelName = channel is null ? null : channel.Name; + channelName = channel?.Name; } return _tvDtoService.GetSeriesTimerInfoDto(i.Item1, i.Item2, channelName); diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs index 92605a1eb..2f4caa386 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs @@ -159,7 +159,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable { Locations = [customPath], Name = "Recorded Movies", - CollectionType = CollectionTypeOptions.Movies + CollectionType = CollectionTypeOptions.movies }; } @@ -172,7 +172,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable { Locations = [customPath], Name = "Recorded Shows", - CollectionType = CollectionTypeOptions.TvShows + CollectionType = CollectionTypeOptions.tvshows }; } } diff --git a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs index 18e4810a2..9e7323f5b 100644 --- a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs +++ b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs @@ -115,11 +115,7 @@ namespace Jellyfin.LiveTv.Timers throw new ArgumentException("item already exists", nameof(item)); } - int oldLen = _items.Length; - var newList = new T[oldLen + 1]; - _items.CopyTo(newList, 0); - newList[oldLen] = item; - _items = newList; + _items = [.._items, item]; SaveList(); } @@ -134,11 +130,7 @@ namespace Jellyfin.LiveTv.Timers int index = Array.FindIndex(_items, i => EqualityComparer(i, item)); if (index == -1) { - int oldLen = _items.Length; - var newList = new T[oldLen + 1]; - _items.CopyTo(newList, 0); - newList[oldLen] = item; - _items = newList; + _items = [.._items, item]; } else { diff --git a/src/Jellyfin.LiveTv/Timers/TimerManager.cs b/src/Jellyfin.LiveTv/Timers/TimerManager.cs index da5deea36..1cf335159 100644 --- a/src/Jellyfin.LiveTv/Timers/TimerManager.cs +++ b/src/Jellyfin.LiveTv/Timers/TimerManager.cs @@ -22,7 +22,7 @@ namespace Jellyfin.LiveTv.Timers public TimerManager(ILogger<TimerManager> logger, IConfigurationManager config) : base( logger, - Path.Combine(config.CommonApplicationPaths.DataPath, "livetv"), + Path.Combine(config.CommonApplicationPaths.DataPath, "livetv/timers.json"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) { } diff --git a/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs index afc2e4f9c..aba9627ba 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs @@ -219,7 +219,7 @@ namespace Jellyfin.LiveTv.TunerHosts } } - throw new LiveTvConflictException(); + throw new LiveTvConflictException("Unable to find host to play channel"); } protected virtual bool IsValidChannelId(string channelId) diff --git a/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 861338727..1dd35da41 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -145,7 +145,7 @@ namespace Jellyfin.LiveTv.TunerHosts.HdHomerun } _activeTuner = -1; - throw new LiveTvConflictException(); + throw new LiveTvConflictException("No tuners available"); } public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken) diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs index 8d52151cb..365f0188d 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -28,15 +30,8 @@ namespace Jellyfin.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private static readonly string[] _disallowedMimeTypes = - { - "video/x-matroska", - "video/mp4", - "application/vnd.apple.mpegurl", - "application/mpegurl", - "application/x-mpegurl", - "video/vnd.mpeg.dash.mpd" - }; + private static readonly string[] _mimeTypesCanShareHttpStream = ["video/MP2T"]; + private static readonly string[] _extensionsCanShareHttpStream = [".ts", ".tsv", ".m2t"]; private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; @@ -101,14 +96,31 @@ namespace Jellyfin.LiveTv.TunerHosts if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping) { - using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .SendAsync(message, cancellationToken) - .ConfigureAwait(false); - - response.EnsureSuccessStatusCode(); + var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path); - if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(extension)) + { + try + { + using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(message, cancellationToken) + .ConfigureAwait(false); + + if (response.IsSuccessStatusCode) + { + if (_mimeTypesCanShareHttpStream.Contains(response.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase)) + { + return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); + } + } + } + catch (Exception) + { + Logger.LogWarning("HEAD request to check MIME type failed, shared stream disabled"); + } + } + else if (_extensionsCanShareHttpStream.Contains(extension, StringComparison.OrdinalIgnoreCase)) { return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } diff --git a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs index 5900d1c5b..c8d678e2f 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs @@ -273,12 +273,12 @@ namespace Jellyfin.LiveTv.TunerHosts var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal); if (numberIndex > 0) { - var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); + var numberPart = nameInExtInf.AsSpan(0, numberIndex).Trim(new[] { ' ', '.' }); if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); - nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); + nameInExtInf = nameInExtInf.AsSpan(numberIndex + 1).Trim(new[] { ' ', '-' }).ToString(); } } } diff --git a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs index 60be19c68..473f00153 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs @@ -76,14 +76,13 @@ public class TunerHostManager : ITunerHostManager var config = _config.GetLiveTvConfiguration(); - var list = config.TunerHosts.ToList(); - var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); + var list = config.TunerHosts; + var index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) { info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - list.Add(info); - config.TunerHosts = list.ToArray(); + config.TunerHosts = [..list, info]; } else { diff --git a/src/Jellyfin.Networking/AutoDiscoveryHost.cs b/src/Jellyfin.Networking/AutoDiscoveryHost.cs index 5624c4ed1..ff0d179b9 100644 --- a/src/Jellyfin.Networking/AutoDiscoveryHost.cs +++ b/src/Jellyfin.Networking/AutoDiscoveryHost.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; @@ -59,65 +57,58 @@ public sealed class AutoDiscoveryHost : BackgroundService return; } - var udpServers = new List<Task>(); - // Linux needs to bind to the broadcast addresses to receive broadcast traffic - if (OperatingSystem.IsLinux() && networkConfig.EnableIPv4) - { - udpServers.Add(ListenForAutoDiscoveryMessage(IPAddress.Broadcast, stoppingToken)); - } - - udpServers.AddRange(_networkManager.GetInternalBindAddresses() - .Select(intf => ListenForAutoDiscoveryMessage( - OperatingSystem.IsLinux() && intf.AddressFamily == AddressFamily.InterNetwork - ? NetworkUtils.GetBroadcastAddress(intf.Subnet) - : intf.Address, - stoppingToken))); - - await Task.WhenAll(udpServers).ConfigureAwait(false); + await ListenForAutoDiscoveryMessage(IPAddress.Any, stoppingToken).ConfigureAwait(false); } - private async Task ListenForAutoDiscoveryMessage(IPAddress address, CancellationToken cancellationToken) + private async Task ListenForAutoDiscoveryMessage(IPAddress listenAddress, CancellationToken cancellationToken) { - using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber)); - udpClient.MulticastLoopback = false; - - while (!cancellationToken.IsCancellationRequested) + try { - try + using var udpClient = new UdpClient(new IPEndPoint(listenAddress, PortNumber)); + udpClient.MulticastLoopback = false; + + while (!cancellationToken.IsCancellationRequested) { - var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); - var text = Encoding.UTF8.GetString(result.Buffer); - if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) + try { - await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + var text = Encoding.UTF8.GetString(result.Buffer); + if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) + { + await RespondToV2Message(result.RemoteEndPoint, udpClient, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException ex) + { + _logger.LogError(ex, "Failed to receive data from socket"); } - } - catch (SocketException ex) - { - _logger.LogError(ex, "Failed to receive data from socket"); - } - catch (OperationCanceledException) - { - _logger.LogDebug("Broadcast socket operation cancelled"); } } + catch (OperationCanceledException) + { + _logger.LogDebug("Broadcast socket operation cancelled"); + } + catch (Exception ex) + { + // Exception in this function will prevent the background service from restarting in-process. + _logger.LogError(ex, "Unable to bind to {Address}:{Port}", listenAddress, PortNumber); + } } - private async Task RespondToV2Message(UdpClient udpClient, IPEndPoint endpoint, CancellationToken cancellationToken) + private async Task RespondToV2Message(IPEndPoint endpoint, UdpClient broadCastUdpClient, CancellationToken cancellationToken) { var localUrl = _appHost.GetSmartApiUrl(endpoint.Address); if (string.IsNullOrEmpty(localUrl)) { - _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined."); + _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined"); return; } var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); - try { _logger.LogDebug("Sending AutoDiscovery response"); - await udpClient + await broadCastUdpClient .SendAsync(JsonSerializer.SerializeToUtf8Bytes(response).AsMemory(), endpoint, cancellationToken) .ConfigureAwait(false); } diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 1da44b048..cf6a2cc55 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -11,7 +11,6 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -237,7 +236,7 @@ public class NetworkManager : INetworkManager, IDisposable var mac = adapter.GetPhysicalAddress(); // Populate MAC list - if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac)) + if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && !PhysicalAddress.None.Equals(mac)) { macAddresses.Add(mac); } @@ -412,7 +411,9 @@ public class NetworkManager : INetworkManager, IDisposable interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } - _interfaces = interfaces; + // Users may have complex networking configuration that multiple interfaces sharing the same IP address + // Only return one IP for binding, and let the OS handle the rest + _interfaces = interfaces.DistinctBy(iface => iface.Address).ToList(); } } @@ -737,7 +738,9 @@ public class NetworkManager : INetworkManager, IDisposable /// <inheritdoc/> public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) { - if (_interfaces.Count > 0 || individualInterfaces) + var config = _configurationManager.GetNetworkConfiguration(); + var localNetworkAddresses = config.LocalNetworkAddresses; + if ((localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]) && _interfaces.Count > 0) || individualInterfaces) { return _interfaces; } @@ -900,15 +903,30 @@ public class NetworkManager : INetworkManager, IDisposable return false; } + /// <summary> + /// Get if the IPAddress is Link-local. + /// </summary> + /// <param name="address">The IP Address.</param> + /// <returns>Bool indicates if the address is link-local.</returns> + public bool IsLinkLocalAddress(IPAddress address) + { + ArgumentNullException.ThrowIfNull(address); + return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal; + } + /// <inheritdoc/> public bool IsInLocalNetwork(IPAddress address) { ArgumentNullException.ThrowIfNull(address); - // See conversation at https://github.com/jellyfin/jellyfin/pull/3515. + // Map IPv6 mapped IPv4 back to IPv4 (happens if Kestrel runs in dual-socket mode) + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) - || address.Equals(IPAddress.Loopback) - || address.Equals(IPAddress.IPv6Loopback)) + || IPAddress.IsLoopback(address)) { return true; } @@ -1017,7 +1035,7 @@ public class NetworkManager : INetworkManager, IDisposable result = string.Empty; int count = _interfaces.Count; - if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any))) + if (count == 1 && (_interfaces[0].Address.Equals(IPAddress.Any) || _interfaces[0].Address.Equals(IPAddress.IPv6Any))) { // Ignore IPAny addresses. count = 0; @@ -1049,7 +1067,7 @@ public class NetworkManager : INetworkManager, IDisposable return true; } - _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source); + _logger.LogDebug("{Source}: External request received, no matching external bind address found, trying internal addresses", source); } else { @@ -1081,13 +1099,17 @@ public class NetworkManager : INetworkManager, IDisposable private bool MatchesExternalInterface(IPAddress source, out string result) { // Get the first external interface address that isn't a loopback. - var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray(); + var extResult = _interfaces + .Where(p => !IsInLocalNetwork(p.Address)) + .Where(p => p.Address.AddressFamily.Equals(source.AddressFamily)) + .Where(p => !IsLinkLocalAddress(p.Address)) + .OrderBy(x => x.Index).ToArray(); // No external interface found if (extResult.Length == 0) { result = string.Empty; - _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source); + _logger.LogDebug("{Source}: External request received, but no external interface found. Need to route through internal network", source); return false; } @@ -1114,12 +1136,13 @@ public class NetworkManager : INetworkManager, IDisposable var logLevel = debug ? LogLevel.Debug : LogLevel.Information; if (_logger.IsEnabled(logLevel)) { - _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); - _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); - _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN subnets: {Subnets}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN exclusions: {Subnets}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Used LAN subnets: {Subnets}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Filtered interface addresses: {Addresses}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); + _logger.Log(logLevel, "Bind Addresses {Addresses}", GetAllBindInterfaces(false).OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); + _logger.Log(logLevel, "Remote IP filter is {Type}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); + _logger.Log(logLevel, "Filtered subnets: {Subnets}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); } } } |
