#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 Jellyfin.Networking.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; using Microsoft.Extensions.Logging; using Mono.Nat; namespace Emby.Server.Implementations.EntryPoints { /// /// Server entrypoint handling external port forwarding. /// public 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 = false; /// /// 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) { if (_disposed) { throw new ObjectDisposedException(GetType().Name); } // 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() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (_disposed) { return; } _config.ConfigurationUpdated -= OnConfigurationUpdated; Stop(); _timer = null; _disposed = true; } } }