diff options
62 files changed, 813 insertions, 706 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4c9d68d11..8385e84bf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '7.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1 + uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3 diff --git a/Directory.Packages.props b/Directory.Packages.props index 55b03cdce..29bf42328 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,13 +27,13 @@ <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> @@ -42,8 +42,8 @@ <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.11" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> @@ -86,8 +86,8 @@ <PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> - <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.1" /> + <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.5.1" /> + <PackageVersion Include="xunit" Version="2.5.3" /> </ItemGroup> </Project> diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs new file mode 100644 index 000000000..87ec14d95 --- /dev/null +++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Text; +using Emby.Dlna.ConnectionManager; +using Emby.Dlna.ContentDirectory; +using Emby.Dlna.MediaReceiverRegistrar; +using Emby.Dlna.Ssdp; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Rssdp.Infrastructure; + +namespace Emby.Dlna.Extensions; + +/// <summary> +/// Extension methods for adding DLNA services. +/// </summary> +public static class DlnaServiceCollectionExtensions +{ + /// <summary> + /// Adds DLNA services to the provided <see cref="IServiceCollection"/>. + /// </summary> + /// <param name="services">The <see cref="IServiceCollection"/>.</param> + /// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param> + public static void AddDlnaServices( + this IServiceCollection services, + IServerApplicationHost applicationHost) + { + services.AddHttpClient(NamedClient.Dlna, c => + { + c.DefaultRequestHeaders.UserAgent.ParseAdd( + string.Format( + CultureInfo.InvariantCulture, + "{0}/{1} UPnP/1.0 {2}/{3}", + Environment.OSVersion.Platform, + Environment.OSVersion, + applicationHost.Name, + applicationHost.ApplicationVersionString)); + + c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0 + c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from? + }) + .ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8 + }); + + services.AddSingleton<IDlnaManager, DlnaManager>(); + services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); + services.AddSingleton<IContentDirectory, ContentDirectoryService>(); + services.AddSingleton<IConnectionManager, ConnectionManagerService>(); + services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>(); + + services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer( + provider.GetRequiredService<ISocketFactory>(), + provider.GetRequiredService<INetworkManager>(), + provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>()) + { + IsShared = true + }); + } +} diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 1a4bec398..aa7012487 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.TV; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; using Rssdp; using Rssdp.Infrastructure; @@ -49,14 +47,13 @@ namespace Emby.Dlna.Main private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; private readonly IDeviceDiscovery _deviceDiscovery; - private readonly ISocketFactory _socketFactory; + private readonly ISsdpCommunicationsServer _communicationsServer; private readonly INetworkManager _networkManager; - private readonly object _syncLock = new object(); + private readonly object _syncLock = new(); private readonly bool _disabled; private PlayToManager _manager; private SsdpDevicePublisher _publisher; - private ISsdpCommunicationsServer _communicationsServer; private bool _disposed; @@ -75,10 +72,8 @@ namespace Emby.Dlna.Main IMediaSourceManager mediaSourceManager, IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder, - ISocketFactory socketFactory, - INetworkManager networkManager, - IUserViewManager userViewManager, - ITVSeriesManager tvSeriesManager) + ISsdpCommunicationsServer communicationsServer, + INetworkManager networkManager) { _config = config; _appHost = appHost; @@ -93,37 +88,10 @@ namespace Emby.Dlna.Main _mediaSourceManager = mediaSourceManager; _deviceDiscovery = deviceDiscovery; _mediaEncoder = mediaEncoder; - _socketFactory = socketFactory; + _communicationsServer = communicationsServer; _networkManager = networkManager; _logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); - ContentDirectory = new ContentDirectory.ContentDirectoryService( - dlnaManager, - userDataManager, - imageProcessor, - libraryManager, - config, - userManager, - loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(), - httpClientFactory, - localizationManager, - mediaSourceManager, - userViewManager, - mediaEncoder, - tvSeriesManager); - - ConnectionManager = new ConnectionManager.ConnectionManagerService( - dlnaManager, - config, - loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(), - httpClientFactory); - - MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService( - loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(), - httpClientFactory, - config); - Current = this; - var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); _disabled = appHost.ListenWithHttps && netConfig.RequireHttps; @@ -133,19 +101,6 @@ namespace Emby.Dlna.Main } } - public static DlnaEntryPoint Current { get; private set; } - - /// <summary> - /// Gets a value indicating whether the dlna server is enabled. - /// </summary> - public static bool Enabled { get; private set; } - - public IContentDirectory ContentDirectory { get; private set; } - - public IConnectionManager ConnectionManager { get; private set; } - - public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; } - public async Task RunAsync() { await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); @@ -172,9 +127,7 @@ namespace Emby.Dlna.Main private void ReloadComponents() { var options = _config.GetDlnaConfiguration(); - Enabled = options.EnableServer; - - StartSsdpHandler(); + StartDeviceDiscovery(); if (options.EnableServer) { @@ -195,36 +148,11 @@ namespace Emby.Dlna.Main } } - private void StartSsdpHandler() - { - try - { - if (_communicationsServer is null) - { - var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); - - _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) - { - IsShared = true - }; - - StartDeviceDiscovery(_communicationsServer); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting ssdp handlers"); - } - } - - private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer) + private void StartDeviceDiscovery() { try { - if (communicationsServer is not null) - { - ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); - } + ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer); } catch (Exception ex) { @@ -232,19 +160,6 @@ namespace Emby.Dlna.Main } } - private void DisposeDeviceDiscovery() - { - try - { - _logger.LogInformation("Disposing DeviceDiscovery"); - ((DeviceDiscovery)_deviceDiscovery).Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error stopping device discovery"); - } - } - public void StartDevicePublisher(Configuration.DlnaOptions options) { if (_publisher is not null) @@ -317,7 +232,7 @@ namespace Emby.Dlna.Main // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. }; - SetProperies(device, fullService); + SetProperties(device, fullService); _publisher.AddDevice(device); var embeddedDevices = new[] @@ -338,13 +253,13 @@ namespace Emby.Dlna.Main // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. }; - SetProperies(embeddedDevice, subDevice); + SetProperties(embeddedDevice, subDevice); device.AddDevice(embeddedDevice); } } } - private string CreateUuid(string text) + private static string CreateUuid(string text) { if (!Guid.TryParse(text, out var guid)) { @@ -354,15 +269,14 @@ namespace Emby.Dlna.Main return guid.ToString("D", CultureInfo.InvariantCulture); } - private void SetProperies(SsdpDevice device, string fullDeviceType) + private static void SetProperties(SsdpDevice device, string fullDeviceType) { - var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase); - - var serviceParts = service.Split(':'); - - var deviceTypeNamespace = serviceParts[0].Replace('.', '-'); + var serviceParts = fullDeviceType + .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase) + .Split(':'); - device.DeviceTypeNamespace = deviceTypeNamespace; + device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-'); device.DeviceClass = serviceParts[1]; device.DeviceType = serviceParts[2]; } @@ -443,20 +357,6 @@ namespace Emby.Dlna.Main DisposeDevicePublisher(); DisposePlayToManager(); - DisposeDeviceDiscovery(); - - if (_communicationsServer is not null) - { - _logger.LogInformation("Disposing SsdpCommunicationsServer"); - _communicationsServer.Dispose(); - _communicationsServer = null; - } - - ContentDirectory = null; - ConnectionManager = null; - MediaReceiverRegistrar = null; - Current = null; - _disposed = true; } } diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs index 220aa1a8d..255c51f19 100644 --- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs +++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs @@ -55,41 +55,42 @@ namespace Emby.Dlna.PlayTo var client = _httpClientFactory.CreateClient(NamedClient.Dlna); using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using MemoryStream ms = new MemoryStream(); - await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); - ms.Position = 0; - try + Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { - return await XDocument.LoadAsync( - ms, - LoadOptions.None, - cancellationToken).ConfigureAwait(false); - } - catch (XmlException) - { - // try correcting the Xml response with common errors - ms.Position = 0; - using StreamReader sr = new StreamReader(ms); - var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - - // find and replace unescaped ampersands (&) - xmlString = EscapeAmpersandRegex().Replace(xmlString, "&"); - try { - // retry reading Xml - using var xmlReader = new StringReader(xmlString); return await XDocument.LoadAsync( - xmlReader, + stream, LoadOptions.None, cancellationToken).ConfigureAwait(false); } - catch (XmlException ex) + catch (XmlException) { - _logger.LogError(ex, "Failed to parse response"); - _logger.LogDebug("Malformed response: {Content}\n", xmlString); - - return null; + // try correcting the Xml response with common errors + stream.Position = 0; + using StreamReader sr = new StreamReader(stream); + var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + + // find and replace unescaped ampersands (&) + xmlString = EscapeAmpersandRegex().Replace(xmlString, "&"); + + try + { + // retry reading Xml + using var xmlReader = new StringReader(xmlString); + return await XDocument.LoadAsync( + xmlReader, + LoadOptions.None, + cancellationToken).ConfigureAwait(false); + } + catch (XmlException ex) + { + _logger.LogError(ex, "Failed to parse response"); + _logger.LogDebug("Malformed response: {Content}\n", xmlString); + + return null; + } } } } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 3253ea026..c9bf7f085 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -13,9 +13,7 @@ using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using Emby.Dlna; using Emby.Dlna.Main; -using Emby.Dlna.Ssdp; using Emby.Naming.Common; using Emby.Photos; using Emby.Server.Implementations.Channels; @@ -58,7 +56,6 @@ using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -82,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -450,7 +446,7 @@ namespace Emby.Server.Implementations ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); - NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); + NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>()); // Initialize runtime stat collection if (ConfigurationManager.Configuration.EnableMetrics) @@ -563,8 +559,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ISessionManager, SessionManager>(); - serviceCollection.AddSingleton<IDlnaManager, DlnaManager>(); - serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); @@ -576,8 +570,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); - serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); - serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); @@ -913,7 +905,7 @@ namespace Emby.Server.Implementations /// <inheritdoc/> public string GetSmartApiUrl(HttpRequest request) { - // Return the host in the HTTP request as the API url + // Return the host in the HTTP request as the API URL if not configured otherwise if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; @@ -948,7 +940,7 @@ namespace Emby.Server.Implementations public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) { // With an empty source, the port will be null - var smart = NetManager.GetBindAddress(ipAddress, out _, true); + var smart = NetManager.GetBindAddress(ipAddress, out _, false); var scheme = !allowHttps ? Uri.UriSchemeHttp : null; int? port = !allowHttps ? HttpPort : null; return GetLocalApiUrl(smart, scheme, port); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 961e225e9..3036cbcf7 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels Directory.CreateDirectory(Path.GetDirectoryName(path)); - await using FileStream createStream = File.Create(path); - await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); + FileStream createStream = File.Create(path); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); + } } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 3aab0a5e9..80263c139 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -43,8 +43,6 @@ <TargetFramework>net7.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> - <NoWarn>AD0001</NoWarn> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 54191649d..2c03f9ffd 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { /// <summary> - /// Class UdpServerEntryPoint. + /// Class responsible for registering all UDP broadcast endpoints and their handlers. /// </summary> public sealed class UdpServerEntryPoint : IServerEntryPoint { @@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; private readonly INetworkManager _networkManager; - private readonly bool _enableMultiSocketBinding; /// <summary> /// The UDP server. @@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints _configurationManager = configurationManager; _networkManager = networkManager; _udpServers = new List<UdpServer>(); - _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } /// <inheritdoc /> @@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints try { - if (_enableMultiSocketBinding) + // Linux needs to bind to the broadcast addresses to get broadcast traffic + // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses + if (OperatingSystem.IsLinux()) { - // Add global broadcast socket + // Add global broadcast listener var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); server.Start(_cancellationTokenSource.Token); _udpServers.Add(server); - // Add bind address specific broadcast sockets + // Add bind address specific broadcast listeners // IPv6 is currently unsupported var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); foreach (var intf in validInterfaces) @@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints } else { - var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); + // Add bind address specific broadcast listeners + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); + foreach (var intf in validInterfaces) + { + var intfAddress = intf.Address; + _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber); + + var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); + } } } catch (SocketException ex) @@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints { if (_disposed) { - throw new ObjectDisposedException(this.GetType().Name); + throw new ObjectDisposedException(GetType().Name); } } diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 936a08da8..59d705ace 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library if (!string.IsNullOrEmpty(cacheKey)) { + FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); try { - await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); } - catch + catch (Exception ex) { + _logger.LogError(ex, "Error deserializing mediainfo cache"); + } + finally + { + await jsonStream.DisposeAsync().ConfigureAwait(false); } } @@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); - await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + } - // _logger.LogDebug("Saved media info to {0}", cacheFilePath); + _logger.LogDebug("Saved media info to {0}", cacheFilePath); } } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c9a26a30f..91469dba9 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library if (!string.IsNullOrEmpty(cacheKey)) { + FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); try { - await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - - // _logger.LogDebug("Found cached media info"); } catch (Exception ex) { _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); } + finally + { + await jsonStream.DisposeAsync().ConfigureAwait(false); + } } if (mediaInfo is null) @@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - await using FileStream createStream = File.Create(cacheFilePath); - await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + FileStream createStream = File.Create(cacheFilePath); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + } // _logger.LogDebug("Saved media info to {0}", cacheFilePath); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index b9d0f170a..74b62ca3f 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using (stream.ConfigureAwait(false)) { var settings = new XmlWriterSettings { @@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Async = true }; - await using (var writer = XmlWriter.Create(stream, settings)) + var writer = XmlWriter.Create(stream, settings); + await using (writer.ConfigureAwait(false)) { await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); @@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using (stream.ConfigureAwait(false)) { var settings = new XmlWriterSettings { @@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var isSeriesEpisode = timer.IsProgramSeries; - await using (var writer = XmlWriter.Create(stream, settings)) + var writer = XmlWriter.Create(stream, settings); + await using (writer.ConfigureAwait(false)) { await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); @@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } else { - await writer.WriteStartElementAsync(null, "movie", null); + await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(item.Name)) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 7645c6c52..6b0520ad0 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings options.Content = JsonContent.Create(requestList, options: _jsonOptions); options.Headers.TryAddWithoutValidation("token", token); using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (dailySchedules is null) { return Array.Empty<ProgramInfo>(); @@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); - await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (programDetails is null) { return Array.Empty<ProgramInfo>(); @@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); - await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); + return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); - await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - - var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); - + var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (root is not null) { foreach (HeadendsDto headend in root) @@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (string.Equals(root?.Message, "OK", StringComparison.Ordinal)) { _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token); @@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); httpResponse.EnsureSuccessStatusCode(); - await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var response = httpResponse.Content; - var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - + var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false; } catch (HttpRequestException ex) @@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings options.Headers.TryAddWithoutValidation("token", token); using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); - await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (root is null) { return new List<ChannelInfo>(); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 7e588f681..4f96dde44 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -76,13 +77,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken) - .ConfigureAwait(false) ?? new List<Channels>(); - + var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>(); if (info.ImportFavoritesOnly) { - lineup = lineup.Where(i => i.Favorite).ToList(); + lineup = lineup.Where(i => i.Favorite); } return lineup.Where(i => !i.DRM).ToList(); @@ -129,9 +127,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken) - .ConfigureAwait(false); + var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(cacheKey)) { @@ -175,34 +171,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun using var response = await _httpClientFactory.CreateClient(NamedClient.Default) .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); var tuners = new List<LiveTvTunerInfo>(); - await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { - string stripedLine = StripXML(line); - if (stripedLine.Contains("Channel", StringComparison.Ordinal)) + using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); + await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) { - LiveTvTunerStatus status; - var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = stripedLine.Substring(0, index - 1); - var currentChannel = stripedLine.Substring(index + 7); - if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) + string stripedLine = StripXML(line); + if (stripedLine.Contains("Channel", StringComparison.Ordinal)) { - status = LiveTvTunerStatus.LiveTv; - } - else - { - status = LiveTvTunerStatus.Available; - } + LiveTvTunerStatus status; + var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = stripedLine.Substring(0, index - 1); + var currentChannel = stripedLine.Substring(index + 7); + if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) + { + status = LiveTvTunerStatus.LiveTv; + } + else + { + status = LiveTvTunerStatus.Available; + } - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); + tuners.Add(new LiveTvTunerInfo + { + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); + } } } diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json new file mode 100644 index 000000000..40aa5f71a --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/fo.json @@ -0,0 +1,18 @@ +{ + "Artists": "Listafólk", + "Collections": "Søvn", + "Default": "Sjálvgildi", + "DeviceOfflineWithName": "{0} hevur slitið sambandið", + "External": "Ytri", + "Genres": "Greinar", + "Albums": "Album", + "AppDeviceValues": "App: {0}, Eind: {1}", + "Application": "Nýtsluskipan", + "Books": "Bøkur", + "Channels": "Rásir", + "ChapterNameValue": "Kapittul {0}", + "DeviceOnlineWithName": "{0} er sambundið", + "Favorites": "Yndis", + "Folders": "Mappur", + "Forced": "Kravt" +} diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index a40f49506..0f1f0b3d2 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -13,8 +13,8 @@ "HeaderFavoriteArtists": "Uppáhalds Listamenn", "HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderContinueWatching": "Halda áfram að horfa", - "HeaderAlbumArtists": "Höfundur plötu", - "Genres": "Tegundir", + "HeaderAlbumArtists": "Listamaður á umslagi", + "Genres": "Stefnur", "Folders": "Möppur", "Favorites": "Uppáhalds", "FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig", @@ -22,32 +22,32 @@ "DeviceOfflineWithName": "{0} hefur aftengst", "Collections": "Söfn", "ChapterNameValue": "Kafli {0}", - "Channels": "Stöðvar", - "CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}", + "Channels": "Rásir", + "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni", "Books": "Bækur", - "AuthenticationSucceededWithUserName": "{0} auðkenning tókst", - "Artists": "Listamaður", + "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst", + "Artists": "Listamenn", "Application": "Forrit", "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}", "Albums": "Plötur", - "Plugin": "Viðbót", - "Photos": "Myndir", - "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð", - "NotificationOptionVideoPlayback": "Myndbandafspilun hafin", + "Plugin": "Viðbótarvirkni", + "Photos": "Ljósmyndir", + "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð", + "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin", "NotificationOptionUserLockedOut": "Notandi læstur úti", - "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg", - "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett", - "NotificationOptionPluginUninstalled": "Viðbót fjarlægð", - "NotificationOptionPluginInstalled": "Viðbót sett upp", + "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg", + "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið", + "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð", + "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp", "NotificationOptionPluginError": "Bilun í viðbót", "NotificationOptionInstallationFailed": "Uppsetning tókst ekki", - "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp", + "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp", "NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð", "NotificationOptionAudioPlayback": "Hljóðafspilun hafin", "NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett", "NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði", - "NameSeasonUnknown": "Sería óþekkt", - "NameSeasonNumber": "Sería {0}", + "NameSeasonUnknown": "Þáttaröð óþekkt", + "NameSeasonNumber": "Þáttaröð {0}", "MixedContent": "Blandað efni", "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar", "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}", @@ -57,24 +57,24 @@ "User": "Notandi", "System": "Kerfi", "NotificationOptionNewLibraryContent": "Nýju efni bætt við", - "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.", + "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.", "NameInstallFailed": "{0} uppsetning mistókst", "MusicVideos": "Tónlistarmyndbönd", "Music": "Tónlist", "Movies": "Kvikmyndir", "UserDeletedWithName": "Notanda {0} hefur verið eytt", "UserCreatedWithName": "Notandi {0} hefur verið stofnaður", - "TvShows": "Þættir", + "TvShows": "Sjónvarpsþættir", "Sync": "Samstilla", "Songs": "Lög", - "ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa", + "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur", "ScheduledTaskStartedWithName": "{0} hafin", "ScheduledTaskFailedWithName": "{0} mistókst", "PluginUpdatedWithName": "{0} var uppfært", "PluginUninstalledWithName": "{0} var fjarlægt", "PluginInstalledWithName": "{0} var sett upp", "NotificationOptionTaskFailed": "Tímasett verkefni mistókst", - "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.", + "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.", "VersionNumber": "Útgáfa {0}", "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", @@ -83,14 +83,14 @@ "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt", "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", "UserOfflineFromDevice": "{0} hefur aftengst frá {1}", - "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur", - "UserDownloadingItemWithValues": "{0} Hleður niður {1}", + "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti", + "UserDownloadingItemWithValues": "{0} hleður niður {1}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", - "ProviderValue": "Veitandi: {0}", + "ProviderValue": "Efnisveita: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", - "ValueSpecialEpisodeName": "Sérstakt - {0}", - "Shows": "Sýningar", - "Playlists": "Spilunarlisti", + "ValueSpecialEpisodeName": "Sérstaktur - {0}", + "Shows": "Þættir", + "Playlists": "Efnisskrár", "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.", "TaskRefreshChannels": "Endurhlaða Rásir", "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.", @@ -116,5 +116,12 @@ "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.", "TaskCleanLogs": "Hreinsa færslu skrá", "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.", - "HearingImpaired": "Heyrnarskertur" + "HearingImpaired": "Heyrnarskertur", + "TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.", + "TaskKeyframeExtractor": "Lykilrammaplokkari", + "TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.", + "TaskRefreshChapterImages": "Plokka kafla-myndir", + "TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.", + "Forced": "Þvingað", + "External": "Útvær" } diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index f7b24412a..d558cdfe0 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -75,7 +75,7 @@ "HeaderAlbumArtists": "Albumu Izpildītāji", "Genres": "Žanri", "Folders": "Mapes", - "Favorites": "Favorīti", + "Favorites": "Izlase", "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}", "DeviceOnlineWithName": "{0} ir pievienojies", "DeviceOfflineWithName": "{0} ir atvienojies", @@ -113,7 +113,7 @@ "TaskCleanCache": "Iztīrīt Kešošanas Mapi", "TasksChannelsCategory": "Interneta Kanāli", "TasksMaintenanceCategory": "Apkope", - "Forced": "Piespiests", + "Forced": "Piespiedu", "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.", "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu", "Undefined": "Nenoteikts", diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 858cc40dd..c231d76fe 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -124,5 +124,5 @@ "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", "TaskKeyframeExtractor": "Extraktor kľúčových snímkov", "External": "Externé", - "HearingImpaired": "Sluchovo Postihnutý" + "HearingImpaired": "Sluchovo postihnutí" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 96f435399..16776b6bd 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization string countryCode = resource.Substring(RatingsPath.Length, 2); var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase); - await using var stream = _assembly.GetManifestResourceStream(resource); - using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + var stream = _assembly.GetManifestResourceStream(resource); + await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() { - if (string.IsNullOrWhiteSpace(line)) + using var reader = new StreamReader(stream!); + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - continue; - } - - string[] parts = line.Split(','); - if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - var name = parts[0]; - dict.Add(name, new ParentalRating(name, value)); - } - else - { - _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + string[] parts = line.Split(','); + if (parts.Length == 2 + && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + var name = parts[0]; + dict.Add(name, new ParentalRating(name, value)); + } + else + { + _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); + } } } diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 51e92953d..505984cfb 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,12 +1,15 @@ -#pragma warning disable CS1591 - using System; +using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.Net { + /// <summary> + /// Factory class to create different kinds of sockets. + /// </summary> public class SocketFactory : ISocketFactory { /// <inheritdoc /> @@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net /// <inheritdoc /> public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) { - ArgumentNullException.ThrowIfNull(bindInterface.Address); + var interfaceAddress = bindInterface.Address; + ArgumentNullException.ThrowIfNull(interfaceAddress); if (localPort < 0) { @@ -49,7 +53,7 @@ namespace Emby.Server.Implementations.Net try { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(new IPEndPoint(bindInterface.Address, localPort)); + socket.Bind(new IPEndPoint(interfaceAddress, localPort)); return socket; } @@ -82,16 +86,25 @@ namespace Emby.Server.Implementations.Net try { - var interfaceIndex = bindInterface.Index; - var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex); - socket.MulticastLoopback = false; socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); - socket.Bind(new IPEndPoint(multicastAddress, localPort)); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress)); + socket.Bind(new IPEndPoint(multicastAddress, localPort)); + } + else + { + // Only create socket if interface supports multicast + var interfaceIndex = bindInterface.Index; + var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex); + + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); + socket.Bind(new IPEndPoint(bindIPAddress, localPort)); + } return socket; } diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index d7189ef0c..20793ee39 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -386,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins var url = new Uri(packageInfo.ImageUrl); imagePath = Path.Join(path, url.Segments[^1]); - await using var fileStream = AsyncFile.OpenWrite(imagePath); - + var fileStream = AsyncFile.OpenWrite(imagePath); + Stream? downloadStream = null; try { - await using var downloadStream = await HttpClientFactory + downloadStream = await HttpClientFactory .CreateClient(NamedClient.Default) .GetStreamAsync(url) .ConfigureAwait(false); @@ -402,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath); imagePath = string.Empty; } + finally + { + await fileStream.DisposeAsync().ConfigureAwait(false); + if (downloadStream is not null) + { + await downloadStream.DisposeAsync().ConfigureAwait(false); + } + } } var manifest = new PluginManifest @@ -421,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins ImagePath = imagePath }; - if (!await ReconcileManifest(manifest, path)) + if (!await ReconcileManifest(manifest, path).ConfigureAwait(false)) { // An error occurred during reconciliation and saving could be undesirable. return false; @@ -458,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins } using var metaStream = File.OpenRead(metafile); - var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions); + var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions).ConfigureAwait(false); localManifest ??= new PluginManifest(); if (!Equals(localManifest.Id, manifest.Id)) diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index 0fdfe66ca..2c477218f 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -46,10 +46,6 @@ public class SystemManager : ISystemManager _installationManager = installationManager; } - private bool CanLaunchWebBrowser => Environment.UserInteractive - && !_startupOptions.IsService - && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); - /// <inheritdoc /> public SystemInfo GetSystemInfo(HttpRequest request) { @@ -67,7 +63,6 @@ public class SystemManager : ISystemManager ItemsByNamePath = _applicationPaths.InternalMetadataPath, InternalMetadataPath = _applicationPaths.InternalMetadataPath, CachePath = _applicationPaths.CachePath, - CanLaunchWebBrowser = CanLaunchWebBrowser, TranscodingTempPath = _configurationManager.GetTranscodePath(), ServerName = _applicationHost.FriendlyName, LocalAddress = _applicationHost.GetSmartApiUrl(request), diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index a3bbd6df0..10376ed76 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp _endpoint = new IPEndPoint(bindAddress, port); - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) + { + MulticastLoopback = false, + }; _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } @@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp try { + _logger.LogDebug("Sending AutoDiscovery response"); await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); } catch (SocketException ex) @@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp { try { - var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false); + var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false); var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) { @@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp } catch (OperationCanceledException) { - // Don't throw + _logger.LogDebug("Broadcast socket operation cancelled"); } } } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 77d385ba1..c717744b1 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -520,10 +520,9 @@ namespace Emby.Server.Implementations.Updates // CA5351: Do Not Use Broken Cryptographic Algorithms #pragma warning disable CA5351 - using var md5 = MD5.Create(); cancellationToken.ThrowIfCancellationRequested(); - var hash = Convert.ToHexString(md5.ComputeHash(stream)); + var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false)); if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) { _logger.LogError( @@ -556,7 +555,7 @@ namespace Emby.Server.Implementations.Updates reader.ExtractToDirectory(targetDir, true); // Ensure we create one or populate existing ones with missing data. - await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status); + await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); _pluginManager.ImportPluginFrom(targetDir); } diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index 95b296fae..42576934b 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -5,7 +5,6 @@ using System.IO; using System.Net.Mime; using System.Threading.Tasks; using Emby.Dlna; -using Emby.Dlna.Main; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Dlna; @@ -33,12 +32,19 @@ public class DlnaServerController : BaseJellyfinApiController /// Initializes a new instance of the <see cref="DlnaServerController"/> class. /// </summary> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> - public DlnaServerController(IDlnaManager dlnaManager) + /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param> + /// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param> + /// <param name="mediaReceiverRegistrar">Instance of the <see cref="IMediaReceiverRegistrar"/> interface.</param> + public DlnaServerController( + IDlnaManager dlnaManager, + IContentDirectory contentDirectory, + IConnectionManager connectionManager, + IMediaReceiverRegistrar mediaReceiverRegistrar) { _dlnaManager = dlnaManager; - _contentDirectory = DlnaEntryPoint.Current.ContentDirectory; - _connectionManager = DlnaEntryPoint.Current.ConnectionManager; - _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar; + _contentDirectory = contentDirectory; + _connectionManager = connectionManager; + _mediaReceiverRegistrar = mediaReceiverRegistrar; } /// <summary> diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index a653c5795..11f6bcf6b 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -421,13 +421,12 @@ public static class StreamingHelpers /// <param name="state">The state.</param> /// <param name="mediaSource">The mediaSource.</param> /// <returns>System.String.</returns> - private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) + private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) { - var ext = Path.GetExtension(state.RequestedUrl.AsSpan()); - - if (ext.IsEmpty) + var ext = Path.GetExtension(state.RequestedUrl); + if (!string.IsNullOrEmpty(ext)) { - return null; + return ext; } // Try to infer based on the desired video codec @@ -463,10 +462,9 @@ public static class StreamingHelpers return ".asf"; } } - - // Try to infer based on the desired audio codec - if (!state.IsVideoRequest) + else { + // Try to infer based on the desired audio codec var audioCodec = state.Request.AudioCodec; if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) @@ -497,7 +495,7 @@ public static class StreamingHelpers return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim(); } - return null; + throw new InvalidOperationException("Failed to find an appropriate file extension"); } /// <summary> @@ -509,12 +507,12 @@ public static class StreamingHelpers /// <param name="deviceId">The device id.</param> /// <param name="playSessionId">The play session id.</param> /// <returns>The complete file path, including the folder, for the transcoding file.</returns> - private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) + private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) { var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); - var ext = outputFileExtension?.ToLowerInvariant(); + var ext = outputFileExtension.ToLowerInvariant(); var folder = serverConfigurationManager.GetTranscodePath(); return Path.Combine(folder, filename + ext); diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 6a0a4706b..7ac231885 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -8,8 +8,6 @@ <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> - <NoWarn>AD0001</NoWarn> </PropertyGroup> <ItemGroup> diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs index e45fa3bcb..2d921a482 100644 --- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs +++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs @@ -231,12 +231,12 @@ public static partial class NetworkExtensions } else if (address.AddressFamily == AddressFamily.InterNetwork) { - result = new IPNetwork(address, Network.MinimumIPv4PrefixSize); + result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize); return true; } else if (address.AddressFamily == AddressFamily.InterNetworkV6) { - result = new IPNetwork(address, Network.MinimumIPv6PrefixSize); + result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize); return true; } } @@ -284,12 +284,15 @@ public static partial class NetworkExtensions if (hosts.Count <= 2) { + var firstPart = hosts[0]; + // Is hostname or hostname:port - if (FqdnGeneratedRegex().IsMatch(hosts[0])) + if (FqdnGeneratedRegex().IsMatch(firstPart)) { try { - addresses = Dns.GetHostAddresses(hosts[0]); + // .NET automatically filters only supported returned addresses based on OS support. + addresses = Dns.GetHostAddresses(firstPart); return true; } catch (SocketException) @@ -299,7 +302,7 @@ public static partial class NetworkExtensions } // Is an IPv4 or IPv4:port - if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) + if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address)) { if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs index f20e28526..9c59500d7 100644 --- a/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/Jellyfin.Networking/Manager/NetworkManager.cs @@ -15,7 +15,9 @@ using MediaBrowser.Common.Net; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Jellyfin.Networking.Manager { @@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager private readonly IConfigurationManager _configurationManager; + private readonly IConfiguration _startupConfig; + private readonly object _networkEventLock; /// <summary> /// Holds the published server URLs and the IPs to use them on. /// </summary> - private IReadOnlyDictionary<IPData, string> _publishedServerUrls; + private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls; private IReadOnlyList<IPNetwork> _remoteAddressFilter; @@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager /// <summary> /// Initializes a new instance of the <see cref="NetworkManager"/> class. /// </summary> - /// <param name="configurationManager">IServerConfigurationManager instance.</param> + /// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param> + /// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param> /// <param name="logger">Logger to use for messages.</param> #pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this. - public NetworkManager(IConfigurationManager configurationManager, ILogger<NetworkManager> logger) + public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger) { ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(configurationManager); _logger = logger; _configurationManager = configurationManager; + _startupConfig = startupConfig; _initLock = new(); _interfaces = new List<IPData>(); _macAddresses = new List<PhysicalAddress>(); - _publishedServerUrls = new Dictionary<IPData, string>(); + _publishedServerUrls = new List<PublishedServerUriOverride>(); _networkEventLock = new object(); _remoteAddressFilter = new List<IPNetwork>(); @@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager /// <summary> /// Gets the Published server override list. /// </summary> - public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls; + public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls; /// <inheritdoc/> public void Dispose() @@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager { if (!_eventfire) { - _logger.LogDebug("Network Address Change Event."); // As network events tend to fire one after the other only fire once every second. _eventfire = true; OnNetworkChange(); @@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager } else { - InitialiseInterfaces(); - InitialiseLan(networkConfig); + InitializeInterfaces(); + InitializeLan(networkConfig); EnforceBindSettings(networkConfig); } + PrintNetworkInformation(networkConfig); NetworkChanged?.Invoke(this, EventArgs.Empty); } finally @@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. /// Generate a list of all active mac addresses that aren't loopback addresses. /// </summary> - private void InitialiseInterfaces() + private void InitializeInterfaces() { lock (_initLock) { @@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager try { var nics = NetworkInterface.GetAllNetworkInterfaces() - .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); + .Where(i => i.OperationalStatus == OperationalStatus.Up); foreach (NetworkInterface adapter in nics) { @@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager { if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); - interfaceObject.Index = ipProperties.GetIPv4Properties().Index; - interfaceObject.Name = adapter.Name; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) + { + Index = ipProperties.GetIPv4Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; interfaces.Add(interfaceObject); } else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) { - var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); - interfaceObject.Index = ipProperties.GetIPv6Properties().Index; - interfaceObject.Name = adapter.Name; + var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name) + { + Index = ipProperties.GetIPv6Properties().Index, + Name = adapter.Name, + SupportsMulticast = adapter.SupportsMulticast + }; interfaces.Add(interfaceObject); } } } -#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types { // Ignore error, and attempt to continue. _logger.LogError(ex, "Error encountered parsing interfaces."); } } } -#pragma warning disable CA1031 // Do not catch general exception types catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types { _logger.LogError(ex, "Error obtaining interfaces."); } @@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager { _logger.LogWarning("No interface information available. Using loopback interface(s)."); - if (IsIPv4Enabled && !IsIPv6Enabled) + if (IsIPv4Enabled) { - interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); + interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } - if (!IsIPv4Enabled && IsIPv6Enabled) + if (IsIPv6Enabled) { - interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); + interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } } @@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager } /// <summary> - /// Initialises internal LAN cache. + /// Initializes internal LAN cache. /// </summary> - private void InitialiseLan(NetworkConfiguration config) + private void InitializeLan(NetworkConfiguration config) { lock (_initLock) { @@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) ? excludedSubnets : new List<IPNetwork>(); - - _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); - _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); } } @@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager .ToHashSet(); interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); - if (bindAddresses.Contains(IPAddress.Loopback)) + if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback))) { interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); } - if (bindAddresses.Contains(IPAddress.IPv6Loopback)) + if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback))) { interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); } @@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); } - _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); _interfaces = interfaces; } } /// <summary> - /// Initialises the remote address values. + /// Initializes the remote address values. /// </summary> - private void InitialiseRemote(NetworkConfiguration config) + private void InitializeRemote(NetworkConfiguration config) { lock (_initLock) { @@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager /// format is subnet=ipaddress|host|uri /// when subnet = 0.0.0.0, any external address matches. /// </summary> - private void InitialiseOverrides(NetworkConfiguration config) + private void InitializeOverrides(NetworkConfiguration config) { lock (_initLock) { - var publishedServerUrls = new Dictionary<IPData, string>(); - var overrides = config.PublishedServerUriBySubnet; + var publishedServerUrls = new List<PublishedServerUriOverride>(); + + // Prefer startup configuration. + var startupOverrideKey = _startupConfig[AddressOverrideKey]; + if (!string.IsNullOrEmpty(startupOverrideKey)) + { + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + startupOverrideKey, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + startupOverrideKey, + true, + true)); + _publishedServerUrls = publishedServerUrls; + return; + } + var overrides = config.PublishedServerUriBySubnet; foreach (var entry in overrides) { var parts = entry.Split('='); @@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager var identifier = parts[0]; if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; + // Drop any other overrides in case an "all" override exists + publishedServerUrls.Clear(); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + replacement, + true, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + replacement, + true, + true)); + break; } else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) { - publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement; - publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.Any, Network.IPv4Any), + replacement, + false, + true)); + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(IPAddress.IPv6Any, Network.IPv6Any), + replacement, + false, + true)); } else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) { foreach (var lan in _lanSubnets) { var lanPrefix = lan.Prefix; - publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)), + replacement, + true, + false)); } } else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) { var data = new IPData(result.Prefix, result); - publishedServerUrls[data] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + data, + replacement, + true, + true)); } else if (TryParseInterface(identifier, out var ifaces)) { foreach (var iface in ifaces) { - publishedServerUrls[iface] = replacement; + publishedServerUrls.Add( + new PublishedServerUriOverride( + iface, + replacement, + true, + true)); } } else @@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager } /// <summary> - /// Reloads all settings and re-initialises the instance. + /// Reloads all settings and re-Initializes the instance. /// </summary> /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param> public void UpdateSettings(object configuration) @@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager var config = (NetworkConfiguration)configuration; HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6; - InitialiseLan(config); - InitialiseRemote(config); + InitializeLan(config); + InitializeRemote(config); if (string.IsNullOrEmpty(MockNetworkSettings)) { - InitialiseInterfaces(); + InitializeInterfaces(); } else // Used in testing only. { @@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager var index = int.Parse(parts[1], CultureInfo.InvariantCulture); if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) { - var data = new IPData(address, subnet, parts[2]); - data.Index = index; + var data = new IPData(address, subnet, parts[2]) + { + Index = index + }; interfaces.Add(data); } } @@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager } EnforceBindSettings(config); - InitialiseOverrides(config); + InitializeOverrides(config); + + PrintNetworkInformation(config, false); } /// <summary> @@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager /// <inheritdoc/> public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) { - if (_interfaces.Count != 0) + if (_interfaces.Count > 0 || individualInterfaces) { return _interfaces; } // No bind address and no exclusions, so listen on all interfaces. var result = new List<IPData>(); - - if (individualInterfaces) - { - result.AddRange(_interfaces); - return result; - } - if (IsIPv4Enabled && IsIPv6Enabled) { // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default @@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager bindPreference = string.Empty; int? port = null; - var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any) - || x.Key.Subnet.Contains(source)) - .DistinctBy(x => x.Key) - .OrderBy(x => x.Key.Address.Equals(IPAddress.Any) - || x.Key.Address.Equals(IPAddress.IPv6Any)) + // Only consider subnets including the source IP, prefering specific overrides + List<PublishedServerUriOverride> validPublishedServerUrls; + if (!isInExternalSubnet) + { + // Only use matching internal subnets + // Prefer more specific (bigger subnet prefix) overrides + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source)) + .OrderByDescending(x => x.Data.Subnet.PrefixLength) + .ToList(); + } + else + { + // Only use matching external subnets + // Prefer more specific (bigger subnet prefix) overrides + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source)) + .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); + } - // Check for user override. foreach (var data in validPublishedServerUrls) { - if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any))) - { - // External. - bindPreference = data.Value; - break; - } - - // Get address interface. - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); + // Get interface matching override subnet + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address)); if (intf?.Address is not null) { - // Match IP address. - bindPreference = data.Value; + // If matching interface is found, use override + bindPreference = data.OverrideUri; break; } } @@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager return false; } - // Has it got a port defined? + // Handle override specifying port var parts = bindPreference.Split(':'); if (parts.Length > 1) { @@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager { bindPreference = parts[0]; port = p; + _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); + return true; } } - if (port is not null) - { - _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); - } - else - { - _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); - } - + _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); return true; } @@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); return true; } + + private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true) + { + var logLevel = debug ? LogLevel.Debug : LogLevel.Information; + if (_logger.IsEnabled(logLevel)) + { + _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength)); + _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address)); + _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist"); + _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength)); + } + } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 89dbbdd2f..cb1680558 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); } } - else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) + else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6)) { foreach (var address in addresses) { diff --git a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs index 3cb791b57..c9d5b54de 100644 --- a/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs @@ -3,7 +3,6 @@ using System.IO; using System.Net; using Jellyfin.Server.Helpers; using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions return builder .UseKestrel((builderContext, options) => { - var addresses = appHost.NetManager.GetAllBindInterfaces(); + var addresses = appHost.NetManager.GetAllBindInterfaces(true); bool flagged = false; foreach (var netAdd in addresses) diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index b759b6bca..2acddb243 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,10 +1,10 @@ using System; -using System.Globalization; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; using System.Text; +using Emby.Dlna.Extensions; using Jellyfin.Api.Middleware; using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.Networking.Configuration; @@ -27,7 +27,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; -using Microsoft.VisualBasic; using Prometheus; namespace Jellyfin.Server @@ -120,26 +119,11 @@ namespace Jellyfin.Server }) .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); - services.AddHttpClient(NamedClient.Dlna, c => - { - c.DefaultRequestHeaders.UserAgent.ParseAdd( - string.Format( - CultureInfo.InvariantCulture, - "{0}/{1} UPnP/1.0 {2}/{3}", - Environment.OSVersion.Platform, - Environment.OSVersion, - _serverApplicationHost.Name, - _serverApplicationHost.ApplicationVersionString)); - - c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", _serverApplicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0 - c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", _serverApplicationHost.FriendlyName); // REVIEW: where does this come from? - }) - .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); - services.AddHealthChecks() .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext)); services.AddHlsPlaylistGenerator(); + services.AddDlnaServices(_serverApplicationHost); } /// <summary> diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 3f1a098e4..7015d991f 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -38,10 +38,6 @@ <SymbolPackageFormat>snupkg</SymbolPackageFormat> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> - <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors> - </PropertyGroup> - <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> diff --git a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs index dea1c2f32..2a7e6be0f 100644 --- a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs +++ b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs @@ -23,9 +23,12 @@ namespace MediaBrowser.Controller.ClientEvent { var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log"; var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName); - await using var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None); - await fileContents.CopyToAsync(fileStream).ConfigureAwait(false); - return fileName; + var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using (fileStream.ConfigureAwait(false)) + { + await fileContents.CopyToAsync(fileStream).ConfigureAwait(false); + return fileName; + } } } } diff --git a/MediaBrowser.Model/Net/IPData.cs b/MediaBrowser.Model/Net/IPData.cs index 985b16c6e..e9fcd6797 100644 --- a/MediaBrowser.Model/Net/IPData.cs +++ b/MediaBrowser.Model/Net/IPData.cs @@ -48,6 +48,11 @@ public class IPData public int Index { get; set; } /// <summary> + /// Gets or sets a value indicating whether the network supports multicast. + /// </summary> + public bool SupportsMulticast { get; set; } = false; + + /// <summary> /// Gets or sets the interface name. /// </summary> public string Name { get; set; } diff --git a/MediaBrowser.Model/Net/PublishedServerUriOverride.cs b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs new file mode 100644 index 000000000..476d1ba38 --- /dev/null +++ b/MediaBrowser.Model/Net/PublishedServerUriOverride.cs @@ -0,0 +1,42 @@ +namespace MediaBrowser.Model.Net; + +/// <summary> +/// Class holding information for a published server URI override. +/// </summary> +public class PublishedServerUriOverride +{ + /// <summary> + /// Initializes a new instance of the <see cref="PublishedServerUriOverride"/> class. + /// </summary> + /// <param name="data">The <see cref="IPData"/>.</param> + /// <param name="overrideUri">The override.</param> + /// <param name="internalOverride">A value indicating whether the override is for internal requests.</param> + /// <param name="externalOverride">A value indicating whether the override is for external requests.</param> + public PublishedServerUriOverride(IPData data, string overrideUri, bool internalOverride, bool externalOverride) + { + Data = data; + OverrideUri = overrideUri; + IsInternalOverride = internalOverride; + IsExternalOverride = externalOverride; + } + + /// <summary> + /// Gets or sets the object's IP address. + /// </summary> + public IPData Data { get; set; } + + /// <summary> + /// Gets or sets the override URI. + /// </summary> + public string OverrideUri { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the override should be applied to internal requests. + /// </summary> + public bool IsInternalOverride { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the override should be applied to external requests. + /// </summary> + public bool IsExternalOverride { get; set; } +} diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index d9544b40f..aa7c03ebd 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -85,7 +85,8 @@ namespace MediaBrowser.Model.System [Obsolete("This is always true")] public bool CanSelfRestart { get; set; } = true; - public bool CanLaunchWebBrowser { get; set; } + [Obsolete("This is always false")] + public bool CanLaunchWebBrowser { get; set; } = false; /// <summary> /// Gets or sets the program data path. diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f3211ba45..d0bb34d52 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -943,6 +943,12 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority) { + ArgumentNullException.ThrowIfNull(itemId); + if (itemId.Equals(default)) + { + throw new ArgumentException("Guid can't be empty", nameof(itemId)); + } + if (_disposed) { return; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index 55e2474a5..daad9706c 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -176,17 +176,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb Directory.CreateDirectory(Path.GetDirectoryName(path)); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) + var fileStreamOptions = AsyncFile.WriteOptions; + fileStreamOptions.Mode = FileMode.Create; + var fs = new FileStream(path, fileStreamOptions); + await using (fs.ConfigureAwait(false)) { - var fileStreamOptions = AsyncFile.WriteOptions; - fileStreamOptions.Mode = FileMode.Create; - fileStreamOptions.PreallocationSize = stream.Length; - var xmlFileStream = new FileStream(path, fileStreamOptions); - await using (xmlFileStream.ConfigureAwait(false)) - { - await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); - } + await response.Content.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index f3385b3a9..92742b1aa 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -154,20 +154,15 @@ namespace MediaBrowser.Providers.Plugins.AudioDb using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) + var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + var fileStreamOptions = AsyncFile.WriteOptions; + fileStreamOptions.Mode = FileMode.Create; + var xmlFileStream = new FileStream(path, fileStreamOptions); + await using (xmlFileStream.ConfigureAwait(false)) { - var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - var fileStreamOptions = AsyncFile.WriteOptions; - fileStreamOptions.Mode = FileMode.Create; - fileStreamOptions.PreallocationSize = stream.Length; - var xmlFileStream = new FileStream(path, fileStreamOptions); - await using (xmlFileStream.ConfigureAwait(false)) - { - await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); - } + await response.Content.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false); } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index e4bb4eaea..e84f1359b 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Threading; @@ -137,31 +138,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb var url = OmdbProvider.GetOmdbUrl(urlQuery.ToString()); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false); - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) + if (isSearch) { - if (isSearch) + var searchResultList = await response.Content.ReadFromJsonAsync<SearchResultList>(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (searchResultList?.Search is not null) { - var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (searchResultList?.Search is not null) + var resultCount = searchResultList.Search.Count; + var result = new RemoteSearchResult[resultCount]; + for (var i = 0; i < resultCount; i++) { - var resultCount = searchResultList.Search.Count; - var result = new RemoteSearchResult[resultCount]; - for (var i = 0; i < resultCount; i++) - { - result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd); - } - - return result; + result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd); } + + return result; } - else + } + else + { + var result = await response.Content.ReadFromJsonAsync<SearchResult>(_jsonOptions, cancellationToken).ConfigureAwait(false); + if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase)) { - var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase)) - { - return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) }; - } + return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) }; } } diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 0dce6c3bf..a3f30c174 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -32,10 +32,10 @@ namespace Rssdp.Infrastructure * port to use, we will default to 0 which allows the underlying system to auto-assign a free port. */ - private object _BroadcastListenSocketSynchroniser = new object(); + private object _BroadcastListenSocketSynchroniser = new(); private List<Socket> _MulticastListenSockets; - private object _SendSocketSynchroniser = new object(); + private object _SendSocketSynchroniser = new(); private List<Socket> _sendSockets; private HttpRequestParser _RequestParser; @@ -48,7 +48,6 @@ namespace Rssdp.Infrastructure private int _MulticastTtl; private bool _IsShared; - private readonly bool _enableMultiSocketBinding; /// <summary> /// Raised when a HTTPU request message is received by a socket (unicast or multicast). @@ -64,9 +63,11 @@ namespace Rssdp.Infrastructure /// Minimum constructor. /// </summary> /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception> - public SsdpCommunicationsServer(ISocketFactory socketFactory, - INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) - : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger, enableMultiSocketBinding) + public SsdpCommunicationsServer( + ISocketFactory socketFactory, + INetworkManager networkManager, + ILogger logger) + : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger) { } @@ -76,7 +77,12 @@ namespace Rssdp.Infrastructure /// </summary> /// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception> - public SsdpCommunicationsServer(ISocketFactory socketFactory, int localPort, int multicastTimeToLive, INetworkManager networkManager, ILogger logger, bool enableMultiSocketBinding) + public SsdpCommunicationsServer( + ISocketFactory socketFactory, + int localPort, + int multicastTimeToLive, + INetworkManager networkManager, + ILogger logger) { if (socketFactory is null) { @@ -88,19 +94,18 @@ namespace Rssdp.Infrastructure throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero."); } - _BroadcastListenSocketSynchroniser = new object(); - _SendSocketSynchroniser = new object(); + _BroadcastListenSocketSynchroniser = new(); + _SendSocketSynchroniser = new(); _LocalPort = localPort; _SocketFactory = socketFactory; - _RequestParser = new HttpRequestParser(); - _ResponseParser = new HttpResponseParser(); + _RequestParser = new(); + _ResponseParser = new(); _MulticastTtl = multicastTimeToLive; _networkManager = networkManager; _logger = logger; - _enableMultiSocketBinding = enableMultiSocketBinding; } /// <summary> @@ -335,7 +340,7 @@ namespace Rssdp.Infrastructure { sockets = sockets.ToList(); - var tasks = sockets.Where(s => (fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))) + var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address)) .Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); return Task.WhenAll(tasks); } @@ -347,33 +352,26 @@ namespace Rssdp.Infrastructure { var sockets = new List<Socket>(); var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress); - if (_enableMultiSocketBinding) - { - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork) - .DistinctBy(x => x.Index); - foreach (var intf in validInterfaces) + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.SupportsMulticast) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork) + .DistinctBy(x => x.Index); + + foreach (var intf in validInterfaces) + { + try { - try - { - var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreateMulticastSocketsAndListen. IP address: {0}", intf.Address); - } + var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index); } - } - else - { - var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, new IPData(IPAddress.Any, null), _MulticastTtl, SsdpConstants.MulticastPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); } return sockets; @@ -382,34 +380,32 @@ namespace Rssdp.Infrastructure private List<Socket> CreateSendSockets() { var sockets = new List<Socket>(); - if (_enableMultiSocketBinding) + + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses() + .Where(x => x.Address is not null) + .Where(x => x.SupportsMulticast) + .Where(x => x.AddressFamily == AddressFamily.InterNetwork); + + if (OperatingSystem.IsMacOS()) { - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses() - .Where(x => x.Address is not null) - .Where(x => x.AddressFamily == AddressFamily.InterNetwork); + // Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340 + validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback)); + } - foreach (var intf in validInterfaces) + foreach (var intf in validInterfaces) + { + try { - try - { - var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreateSsdpUdpSocket. IPAddress: {0}", intf.Address); - } + var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort); + _ = ListenToSocketInternal(socket); + sockets.Add(socket); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index); } } - else - { - var socket = _SocketFactory.CreateSsdpUdpSocket(new IPData(IPAddress.Any, null), _LocalPort); - _ = ListenToSocketInternal(socket); - sockets.Add(socket); - } - return sockets; } @@ -423,7 +419,7 @@ namespace Rssdp.Infrastructure { try { - var result = await socket.ReceiveMessageFromAsync(receiveBuffer, SocketFlags.None, new IPEndPoint(IPAddress.Any, 0), CancellationToken.None).ConfigureAwait(false);; + var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);; if (result.ReceivedBytes > 0) { @@ -431,7 +427,7 @@ namespace Rssdp.Infrastructure var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface); ProcessMessage( - UTF8Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), + Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes), remoteEndpoint, localEndpointAdapter.Address); } @@ -511,7 +507,7 @@ namespace Rssdp.Infrastructure return; } - var handlers = this.RequestReceived; + var handlers = RequestReceived; if (handlers is not null) { handlers(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress)); @@ -520,7 +516,7 @@ namespace Rssdp.Infrastructure private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress) { - var handlers = this.ResponseReceived; + var handlers = ResponseReceived; if (handlers is not null) { handlers(this, new ResponseReceivedEventArgs(data, endPoint) diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 59f4c5070..82b09c4b4 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -17,7 +17,7 @@ namespace Rssdp.Infrastructure private ISsdpCommunicationsServer _CommunicationsServer; private Timer _BroadcastTimer; - private object _timerLock = new object(); + private object _timerLock = new(); private string _OSName; @@ -221,12 +221,12 @@ namespace Rssdp.Infrastructure /// <seealso cref="DeviceAvailable"/> protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress) { - if (this.IsDisposed) + if (IsDisposed) { return; } - var handlers = this.DeviceAvailable; + var handlers = DeviceAvailable; if (handlers is not null) { handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) @@ -244,12 +244,12 @@ namespace Rssdp.Infrastructure /// <seealso cref="DeviceUnavailable"/> protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) { - if (this.IsDisposed) + if (IsDisposed) { return; } - var handlers = this.DeviceUnavailable; + var handlers = DeviceUnavailable; if (handlers is not null) { handlers(this, new DeviceUnavailableEventArgs(device, expired)); @@ -291,8 +291,8 @@ namespace Rssdp.Infrastructure _CommunicationsServer = null; if (commsServer is not null) { - commsServer.ResponseReceived -= this.CommsServer_ResponseReceived; - commsServer.RequestReceived -= this.CommsServer_RequestReceived; + commsServer.ResponseReceived -= CommsServer_ResponseReceived; + commsServer.RequestReceived -= CommsServer_RequestReceived; } } } @@ -341,7 +341,7 @@ namespace Rssdp.Infrastructure var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - values["HOST"] = "239.255.255.250:1900"; + values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort); values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); values["MAN"] = "\"ssdp:discover\""; @@ -382,17 +382,17 @@ namespace Rssdp.Infrastructure private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress) { - if (String.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) + if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0) { return; } var notificationType = GetFirstHeaderStringValue("NTS", message); - if (String.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0) { ProcessAliveNotification(message, IPAddress); } - else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) + else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) { ProcessByeByeNotification(message); } @@ -420,7 +420,7 @@ namespace Rssdp.Infrastructure private void ProcessByeByeNotification(HttpRequestMessage message) { var notficationType = GetFirstHeaderStringValue("NT", message); - if (!String.IsNullOrEmpty(notficationType)) + if (!string.IsNullOrEmpty(notficationType)) { var usn = GetFirstHeaderStringValue("USN", message); @@ -447,10 +447,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) { string retVal = null; - IEnumerable<string> values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -463,10 +462,9 @@ namespace Rssdp.Infrastructure private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) { string retVal = null; - IEnumerable<string> values; if (message.Headers.Contains(headerName)) { - message.Headers.TryGetValues(headerName, out values); + message.Headers.TryGetValues(headerName, out var values); if (values is not null) { retVal = values.FirstOrDefault(); @@ -479,10 +477,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) { string value = null; - IEnumerable<string> values; if (request.Headers.Contains(headerName)) { - request.Headers.TryGetValues(headerName, out values); + request.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -496,10 +493,9 @@ namespace Rssdp.Infrastructure private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) { string value = null; - IEnumerable<string> values; if (response.Headers.Contains(headerName)) { - response.Headers.TryGetValues(headerName, out values); + response.Headers.TryGetValues(headerName, out var values); if (values is not null) { value = values.FirstOrDefault(); @@ -529,7 +525,7 @@ namespace Rssdp.Infrastructure foreach (var device in expiredDevices) { - if (this.IsDisposed) + if (IsDisposed) { return; } @@ -543,7 +539,7 @@ namespace Rssdp.Infrastructure // problems. foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct()) { - if (this.IsDisposed) + if (IsDisposed) { return; } @@ -560,7 +556,7 @@ namespace Rssdp.Infrastructure existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn); foreach (var existingDevice in existingDevices) { - if (this.IsDisposed) + if (IsDisposed) { return true; } diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 950e6fec8..65ae658a4 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -206,9 +206,9 @@ namespace Rssdp.Infrastructure IPAddress receivedOnlocalIPAddress, CancellationToken cancellationToken) { - if (String.IsNullOrEmpty(searchTarget)) + if (string.IsNullOrEmpty(searchTarget)) { - WriteTrace(String.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); + WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); return; } @@ -232,7 +232,7 @@ namespace Rssdp.Infrastructure // return; } - if (!Int32.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) + if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) { return; } @@ -243,27 +243,27 @@ namespace Rssdp.Infrastructure } // Do not block synchronously as that may tie up a threadpool thread for several seconds. - Task.Delay(_Random.Next(16, (maxWaitInterval * 1000))).ContinueWith((parentTask) => + Task.Delay(_Random.Next(16, maxWaitInterval * 1000)).ContinueWith((parentTask) => { // Copying devices to local array here to avoid threading issues/enumerator exceptions. IEnumerable<SsdpDevice> devices = null; lock (_Devices) { - if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) + if (string.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) { devices = GetAllDevicesAsFlatEnumerable().ToArray(); } - else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) + else if (string.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) { devices = _Devices.ToArray(); } else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) { - devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) { - devices = GetAllDevicesAsFlatEnumerable().Where(d => String.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); + devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); } } @@ -299,7 +299,7 @@ namespace Rssdp.Infrastructure if (isRootDevice) { SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); } @@ -312,7 +312,7 @@ namespace Rssdp.Infrastructure private string GetUsn(string udn, string fullDeviceType) { - return String.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); + return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); } private async void SendSearchResponse( @@ -326,16 +326,17 @@ namespace Rssdp.Infrastructure const string header = "HTTP/1.1 200 OK"; var rootDevice = device.ToRootDevice(); - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - values["EXT"] = ""; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["HOST"] = "239.255.255.250:1900"; - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["ST"] = searchTarget; - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["USN"] = uniqueServiceName; - values["LOCATION"] = rootDevice.Location.ToString(); + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + { + ["EXT"] = "", + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds, + ["ST"] = searchTarget, + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["USN"] = uniqueServiceName, + ["LOCATION"] = rootDevice.Location.ToString() + }; var message = BuildMessage(header, values); @@ -439,7 +440,7 @@ namespace Rssdp.Infrastructure if (isRoot) { SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); } @@ -460,17 +461,18 @@ namespace Rssdp.Infrastructure const string header = "NOTIFY * HTTP/1.1"; - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["LOCATION"] = rootDevice.Location.ToString(); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["NTS"] = "ssdp:alive"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + { + // If needed later for non-server devices, these headers will need to be dynamic + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds, + ["LOCATION"] = rootDevice.Location.ToString(), + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["NTS"] = "ssdp:alive", + ["NT"] = notificationType, + ["USN"] = uniqueServiceName + }; var message = BuildMessage(header, values); @@ -485,7 +487,7 @@ namespace Rssdp.Infrastructure if (isRoot) { tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken)); - if (this.SupportPnpRootDevice) + if (SupportPnpRootDevice) { tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken)); } @@ -506,20 +508,21 @@ namespace Rssdp.Infrastructure { const string header = "NOTIFY * HTTP/1.1"; - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion); - values["NTS"] = "ssdp:byebye"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + { + // If needed later for non-server devices, these headers will need to be dynamic + ["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort), + ["DATE"] = DateTime.UtcNow.ToString("r"), + ["SERVER"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion), + ["NTS"] = "ssdp:byebye", + ["NT"] = notificationType, + ["USN"] = uniqueServiceName + }; var message = BuildMessage(header, values); var sendCount = IsDisposed ? 1 : 3; - WriteTrace(String.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); + WriteTrace(string.Format(CultureInfo.InvariantCulture, "Sent byebye notification"), device); return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); } diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 28cf585e9..6c2086bee 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 0e8166f46..16002f790 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 04c748d09..2befb4b55 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 5bc197679..497a7a3dd 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index fab869a6b..fae7b209f 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/61f29db0-10a5-4816-8fd8-ca2f71beaea3/e15fb7288eb5bc0053b91ea7b0bfd580/dotnet-sdk-7.0.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c4b5aad8-a416-436b-927c-3ebd5a9793ad/38efd1b64c8edc7c5f13699dd0be54e1/dotnet-sdk-7.0.402-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs index 2ff7c7de4..072e0a8c5 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkExtensionsTests.cs @@ -16,7 +16,6 @@ namespace Jellyfin.Networking.Tests [InlineData("127.0.0.1:123")] [InlineData("localhost")] [InlineData("localhost:1345")] - [InlineData("www.google.co.uk")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517")] [InlineData("fd23:184f:2029:0:3139:7386:67d7:d517/56")] [InlineData("[fd23:184f:2029:0:3139:7386:67d7:d517]:124")] diff --git a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs index 0b07a3c53..2302f90b8 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkManagerTests.cs @@ -1,7 +1,9 @@ using System.Net; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Xunit; namespace Jellyfin.Networking.Tests @@ -28,7 +30,8 @@ namespace Jellyfin.Networking.Tests LocalNetworkSubnets = network.Split(',') }; - using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); Assert.True(networkManager.IsInLocalNetwork(ip)); } @@ -56,9 +59,10 @@ namespace Jellyfin.Networking.Tests LocalNetworkSubnets = network.Split(',') }; - using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var networkManager = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); - Assert.False(nm.IsInLocalNetwork(ip)); + Assert.False(networkManager.IsInLocalNetwork(ip)); } } } diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 731cbbafb..022b8a3d0 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -7,6 +7,7 @@ using Jellyfin.Networking.Extensions; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Net; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -54,7 +55,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; Assert.Equal(value, "[" + string.Join(",", nm.GetInternalBindAddresses().Select(x => x.Address + "/" + x.Subnet.PrefixLength)) + "]"); @@ -200,7 +202,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; // Check to see if DNS resolution is working. If not, skip test. @@ -229,24 +232,24 @@ namespace Jellyfin.Networking.Tests [InlineData("192.168.1.1", "192.168.1.0/24", "eth16,eth11", false, "192.168.1.0/24=internal.jellyfin", "internal.jellyfin")] // User on external network, we're bound internal and external - so result is override. - [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + [InlineData("8.8.8.8", "192.168.1.0/24", "eth16,eth11", false, "all=http://helloworld.com", "http://helloworld.com")] // User on internal network, we're bound internal only, but the address isn't in the LAN - so return the override. - [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] + [InlineData("10.10.10.10", "192.168.1.0/24", "eth16", false, "external=http://internalButNotDefinedAsLan.com", "http://internalButNotDefinedAsLan.com")] // User on internal network, no binding specified - so result is the 1st internal. - [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "external=http://helloworld.com", "eth16")] // User on external network, internal binding only - so assumption is a proxy forward, return external override. - [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + [InlineData("jellyfin.org", "192.168.1.0/24", "eth16", false, "external=http://helloworld.com", "http://helloworld.com")] // User on external network, no binding - so result is the 1st external which is overriden. - [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "http://helloworld.com")] + [InlineData("jellyfin.org", "192.168.1.0/24", "", false, "external=http://helloworld.com", "http://helloworld.com")] - // User assumed to be internal, no binding - so result is the 1st internal. - [InlineData("", "192.168.1.0/24", "", false, "0.0.0.0=http://helloworld.com", "eth16")] + // User assumed to be internal, no binding - so result is the 1st matching interface. + [InlineData("", "192.168.1.0/24", "", false, "all=http://helloworld.com", "eth16")] - // User is internal, no binding - so result is the 1st internal, which is then overridden. + // User is internal, no binding - so result is the 1st internal interface, which is then overridden. [InlineData("192.168.1.1", "192.168.1.0/24", "", false, "eth16=http://helloworld.com", "http://helloworld.com")] public void TestBindInterfaceOverrides(string source, string lan, string bindAddresses, bool ipv6enabled, string publishedServers, string result) { @@ -264,7 +267,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = "192.168.1.208/24,-16,eth16|200.200.200.200/24,11,eth11"; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); NetworkManager.MockNetworkSettings = string.Empty; if (nm.TryParseInterface(result, out IReadOnlyList<IPData>? resultObj) && resultObj is not null) @@ -293,7 +297,9 @@ namespace Jellyfin.Networking.Tests RemoteIPFilter = addresses.Split(','), IsRemoteIPFilterBlacklist = false }; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } @@ -314,7 +320,8 @@ namespace Jellyfin.Networking.Tests IsRemoteIPFilterBlacklist = true }; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); Assert.NotEqual(nm.HasRemoteAccess(IPAddress.Parse(remoteIP)), denied); } @@ -334,7 +341,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); var interfaceToUse = nm.GetBindAddress(string.Empty, out _); @@ -358,7 +366,8 @@ namespace Jellyfin.Networking.Tests }; NetworkManager.MockNetworkSettings = interfaces; - using var nm = new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + using var nm = new NetworkManager(NetworkParseTests.GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); var interfaceToUse = nm.GetBindAddress(source, out _); diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 5ddbd30d1..4e8aec9f1 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -40,9 +40,7 @@ namespace Jellyfin.Server.Integration.Tests using var authResponse = await client.SendAsync(httpRequest); authResponse.EnsureSuccessStatusCode(); - var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>( - await authResponse.Content.ReadAsStreamAsync(), - jsonOptions); + var auth = await authResponse.Content.ReadFromJsonAsync<AuthenticationResultDto>(jsonOptions); return auth!.AccessToken; } @@ -51,8 +49,7 @@ namespace Jellyfin.Server.Integration.Tests { using var response = await client.GetAsync("Users/Me"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var userDto = await JsonSerializer.DeserializeAsync<UserDto>( - await response.Content.ReadAsStreamAsync(), JsonDefaults.Options); + var userDto = await response.Content.ReadFromJsonAsync<UserDto>(JsonDefaults.Options); Assert.NotNull(userDto); return userDto; } @@ -67,9 +64,7 @@ namespace Jellyfin.Server.Integration.Tests var response = await client.GetAsync($"Users/{userId}/Items/Root"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( - await response.Content.ReadAsStreamAsync(), - JsonDefaults.Options); + var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(JsonDefaults.Options); Assert.NotNull(rootDto); return rootDto; } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs index 87136dfc8..8761cf69b 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/BrandingControllerTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http.Json; using System.Net.Mime; using System.Text; using System.Text.Json; @@ -30,8 +31,7 @@ namespace Jellyfin.Server.Integration.Tests Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var responseBody = await response.Content.ReadAsStreamAsync(); - _ = await JsonSerializer.DeserializeAsync<BrandingOptions>(responseBody); + await response.Content.ReadFromJsonAsync<BrandingOptions>(); } [Theory] diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index bd6e1b690..39d449e27 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -1,5 +1,6 @@ using System.IO; using System.Net; +using System.Net.Http.Json; using System.Net.Mime; using System.Text; using System.Text.Json; @@ -64,8 +65,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var res = await response.Content.ReadAsStreamAsync(); - _ = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions); + _ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOpions); // TODO: check content } @@ -81,8 +81,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var res = await response.Content.ReadAsStreamAsync(); - var data = await JsonSerializer.DeserializeAsync<ConfigurationPageInfo[]>(res, _jsonOpions); + var data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOpions); Assert.NotNull(data); Assert.Empty(data); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs index 65e70caa0..e5d5e785c 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DlnaControllerTests.cs @@ -93,9 +93,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions); var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal)); Assert.NotNull(newProfile); @@ -124,9 +122,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions); Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsNew", StringComparison.Ordinal))); var newProfile = profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal)); @@ -150,9 +146,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet); - var profiles = await JsonSerializer.DeserializeAsync<DeviceProfileInfo[]>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var profiles = await response.Content.ReadFromJsonAsync<DeviceProfileInfo[]>(_jsonOptions); Assert.Null(profiles?.FirstOrDefault(x => string.Equals(x.Name, "ThisProfileIsUpdated", StringComparison.Ordinal))); } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs index a12e7ca0d..23de2489e 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Net; +using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Extensions.Json; @@ -56,9 +57,7 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var items = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var items = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions); Assert.NotNull(items); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs index 2d3879bdb..36861294b 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/StartupControllerTests.cs @@ -43,8 +43,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); - using var responseStream = await getResponse.Content.ReadAsStreamAsync(); - var newConfig = await JsonSerializer.DeserializeAsync<StartupConfigurationDto>(responseStream, _jsonOptions); + var newConfig = await getResponse.Content.ReadFromJsonAsync<StartupConfigurationDto>(_jsonOptions); Assert.Equal(config.UICulture, newConfig!.UICulture); Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode); Assert.Equal(config.PreferredMetadataLanguage, newConfig.PreferredMetadataLanguage); @@ -60,8 +59,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType); - using var contentStream = await response.Content.ReadAsStreamAsync(); - var user = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions); + var user = await response.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions); Assert.NotNull(user); Assert.NotNull(user.Name); Assert.NotEmpty(user.Name); @@ -87,8 +85,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType); - var contentStream = await getResponse.Content.ReadAsStreamAsync(); - var newUser = await JsonSerializer.DeserializeAsync<StartupUserDto>(contentStream, _jsonOptions); + var newUser = await getResponse.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions); Assert.NotNull(newUser); Assert.Equal(user.Name, newUser.Name); Assert.NotNull(newUser.Password); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs index 79d03d539..4fcacd2ca 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserControllerTests.cs @@ -43,8 +43,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await client.GetAsync("Users/Public"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await response.Content.ReadAsStreamAsync(), _jsonOpions); + var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOpions); // User are hidden by default Assert.NotNull(users); Assert.Empty(users); @@ -59,8 +58,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await client.GetAsync("Users"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var users = await JsonSerializer.DeserializeAsync<UserDto[]>( - await response.Content.ReadAsStreamAsync(), _jsonOpions); + var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOpions); Assert.NotNull(users); Assert.Single(users); Assert.False(users![0].HasConfiguredPassword); @@ -92,8 +90,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers using var response = await CreateUserByName(client, createRequest); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var user = await JsonSerializer.DeserializeAsync<UserDto>( - await response.Content.ReadAsStreamAsync(), _jsonOpions); + var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOpions); Assert.Equal(TestUsername, user!.Name); Assert.False(user.HasPassword); Assert.False(user.HasConfiguredPassword); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs index 826a0a69d..130281c6d 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/UserLibraryControllerTests.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Net; +using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Extensions.Json; @@ -85,9 +86,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(_jsonOptions); Assert.NotNull(rootDto); } @@ -102,9 +101,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await JsonSerializer.DeserializeAsync<QueryResult<BaseItemDto>>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions); Assert.NotNull(rootDto); } @@ -121,9 +118,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id)); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var rootDto = await JsonSerializer.DeserializeAsync<BaseItemDto[]>( - await response.Content.ReadAsStreamAsync(), - _jsonOptions); + var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto[]>(_jsonOptions); Assert.NotNull(rootDto); } } diff --git a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs index 0ade345a1..98195a294 100644 --- a/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/OpenApiSpecTests.cs @@ -31,10 +31,10 @@ namespace Jellyfin.Server.Integration.Tests Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString()); // Write out for publishing - var responseBody = await response.Content.ReadAsStringAsync(); string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json")); _outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath); - File.WriteAllText(outputPath, responseBody); + await using var fs = File.Create(outputPath); + await response.Content.CopyToAsync(fs); } } } diff --git a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs index 49516cccc..288102037 100644 --- a/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs +++ b/tests/Jellyfin.Server.Tests/ParseNetworkTests.cs @@ -7,6 +7,7 @@ using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; @@ -119,8 +120,8 @@ namespace Jellyfin.Server.Tests EnableIPv6 = true, EnableIPv4 = true, }; - - return new NetworkManager(GetMockConfig(conf), new NullLogger<NetworkManager>()); + var startupConf = new Mock<IConfiguration>(); + return new NetworkManager(GetMockConfig(conf), startupConf.Object, new NullLogger<NetworkManager>()); } } } |
