From 99ea6059c7493ac4ee65980abe631df4969112e9 Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Tue, 6 Feb 2024 14:45:44 -0500 Subject: Use IHostedService for UPnP port forwarding --- src/Jellyfin.Networking/ExternalPortForwarding.cs | 195 ---------------------- src/Jellyfin.Networking/PortForwardingHost.cs | 192 +++++++++++++++++++++ 2 files changed, 192 insertions(+), 195 deletions(-) delete mode 100644 src/Jellyfin.Networking/ExternalPortForwarding.cs create mode 100644 src/Jellyfin.Networking/PortForwardingHost.cs (limited to 'src') diff --git a/src/Jellyfin.Networking/ExternalPortForwarding.cs b/src/Jellyfin.Networking/ExternalPortForwarding.cs deleted file mode 100644 index df9e43ca9..000000000 --- a/src/Jellyfin.Networking/ExternalPortForwarding.cs +++ /dev/null @@ -1,195 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.Logging; -using Mono.Nat; - -namespace Jellyfin.Networking; - -/// -/// Server entrypoint handling external port forwarding. -/// -public sealed class ExternalPortForwarding : IServerEntryPoint -{ - private readonly IServerApplicationHost _appHost; - private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; - - private readonly ConcurrentDictionary _createdRules = new ConcurrentDictionary(); - - private Timer _timer; - private string _configIdentifier; - - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The logger. - /// The application host. - /// The configuration manager. - public ExternalPortForwarding( - ILogger logger, - IServerApplicationHost appHost, - IServerConfigurationManager config) - { - _logger = logger; - _appHost = appHost; - _config = config; - } - - private string GetConfigIdentifier() - { - const char Separator = '|'; - var config = _config.GetNetworkConfiguration(); - - return new StringBuilder(32) - .Append(config.EnableUPnP).Append(Separator) - .Append(config.PublicHttpPort).Append(Separator) - .Append(config.PublicHttpsPort).Append(Separator) - .Append(_appHost.HttpPort).Append(Separator) - .Append(_appHost.HttpsPort).Append(Separator) - .Append(_appHost.ListenWithHttps).Append(Separator) - .Append(config.EnableRemoteAccess).Append(Separator) - .ToString(); - } - - private void OnConfigurationUpdated(object sender, EventArgs e) - { - var oldConfigIdentifier = _configIdentifier; - _configIdentifier = GetConfigIdentifier(); - - if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase)) - { - Stop(); - Start(); - } - } - - /// - public Task RunAsync() - { - Start(); - - _config.ConfigurationUpdated += OnConfigurationUpdated; - - return Task.CompletedTask; - } - - private void Start() - { - var config = _config.GetNetworkConfiguration(); - if (!config.EnableUPnP || !config.EnableRemoteAccess) - { - return; - } - - _logger.LogInformation("Starting NAT discovery"); - - NatUtility.DeviceFound += OnNatUtilityDeviceFound; - NatUtility.StartDiscovery(); - - _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); - } - - private void Stop() - { - _logger.LogInformation("Stopping NAT discovery"); - - NatUtility.StopDiscovery(); - NatUtility.DeviceFound -= OnNatUtilityDeviceFound; - - _timer?.Dispose(); - } - - private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) - { - try - { - await CreateRules(e.Device).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating port forwarding rules"); - } - } - - private Task CreateRules(INatDevice device) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - // On some systems the device discovered event seems to fire repeatedly - // This check will help ensure we're not trying to port map the same device over and over - if (!_createdRules.TryAdd(device.DeviceEndpoint, 0)) - { - return Task.CompletedTask; - } - - return Task.WhenAll(CreatePortMaps(device)); - } - - private IEnumerable CreatePortMaps(INatDevice device) - { - var config = _config.GetNetworkConfiguration(); - yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort); - - if (_appHost.ListenWithHttps) - { - yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort); - } - } - - private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort) - { - _logger.LogDebug( - "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}", - privatePort, - publicPort, - device.DeviceEndpoint); - - try - { - var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name); - await device.CreatePortMapAsync(mapping).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.", - privatePort, - publicPort, - device.DeviceEndpoint); - } - } - - /// - public void Dispose() - { - if (_disposed) - { - return; - } - - _config.ConfigurationUpdated -= OnConfigurationUpdated; - - Stop(); - - _timer?.Dispose(); - _timer = null; - - _disposed = true; - } -} diff --git a/src/Jellyfin.Networking/PortForwardingHost.cs b/src/Jellyfin.Networking/PortForwardingHost.cs new file mode 100644 index 000000000..d01343624 --- /dev/null +++ b/src/Jellyfin.Networking/PortForwardingHost.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Mono.Nat; + +namespace Jellyfin.Networking; + +/// +/// responsible for UPnP port forwarding. +/// +public sealed class PortForwardingHost : IHostedService, IDisposable +{ + private readonly IServerApplicationHost _appHost; + private readonly ILogger _logger; + private readonly IServerConfigurationManager _config; + private readonly ConcurrentDictionary _createdRules = new(); + + private Timer? _timer; + private string? _configIdentifier; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The application host. + /// The configuration manager. + public PortForwardingHost( + ILogger logger, + IServerApplicationHost appHost, + IServerConfigurationManager config) + { + _logger = logger; + _appHost = appHost; + _config = config; + } + + private string GetConfigIdentifier() + { + const char Separator = '|'; + var config = _config.GetNetworkConfiguration(); + + return new StringBuilder(32) + .Append(config.EnableUPnP).Append(Separator) + .Append(config.PublicHttpPort).Append(Separator) + .Append(config.PublicHttpsPort).Append(Separator) + .Append(_appHost.HttpPort).Append(Separator) + .Append(_appHost.HttpsPort).Append(Separator) + .Append(_appHost.ListenWithHttps).Append(Separator) + .Append(config.EnableRemoteAccess).Append(Separator) + .ToString(); + } + + private void OnConfigurationUpdated(object? sender, EventArgs e) + { + var oldConfigIdentifier = _configIdentifier; + _configIdentifier = GetConfigIdentifier(); + + if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase)) + { + Stop(); + Start(); + } + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + Start(); + + _config.ConfigurationUpdated += OnConfigurationUpdated; + + return Task.CompletedTask; + } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + Stop(); + + return Task.CompletedTask; + } + + private void Start() + { + var config = _config.GetNetworkConfiguration(); + if (!config.EnableUPnP || !config.EnableRemoteAccess) + { + return; + } + + _logger.LogInformation("Starting NAT discovery"); + + NatUtility.DeviceFound += OnNatUtilityDeviceFound; + NatUtility.StartDiscovery(); + + _timer?.Dispose(); + _timer = new Timer(_ => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); + } + + private void Stop() + { + _logger.LogInformation("Stopping NAT discovery"); + + NatUtility.StopDiscovery(); + NatUtility.DeviceFound -= OnNatUtilityDeviceFound; + + _timer?.Dispose(); + _timer = null; + } + + private async void OnNatUtilityDeviceFound(object? sender, DeviceEventArgs e) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + try + { + // On some systems the device discovered event seems to fire repeatedly + // This check will help ensure we're not trying to port map the same device over and over + if (!_createdRules.TryAdd(e.Device.DeviceEndpoint, 0)) + { + return; + } + + await Task.WhenAll(CreatePortMaps(e.Device)).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating port forwarding rules"); + } + } + + private IEnumerable CreatePortMaps(INatDevice device) + { + var config = _config.GetNetworkConfiguration(); + yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort); + + if (_appHost.ListenWithHttps) + { + yield return CreatePortMap(device, _appHost.HttpsPort, config.PublicHttpsPort); + } + } + + private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort) + { + _logger.LogDebug( + "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}", + privatePort, + publicPort, + device.DeviceEndpoint); + + try + { + var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name); + await device.CreatePortMapAsync(mapping).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.", + privatePort, + publicPort, + device.DeviceEndpoint); + } + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _config.ConfigurationUpdated -= OnConfigurationUpdated; + + _timer?.Dispose(); + _timer = null; + + _disposed = true; + } +} -- cgit v1.2.3