diff options
| author | Patrick Barron <barronpm@gmail.com> | 2023-11-30 11:36:16 -0500 |
|---|---|---|
| committer | Patrick Barron <barronpm@gmail.com> | 2023-11-30 11:57:09 -0500 |
| commit | f1ca1dd7cc14e59938a73a34c2561856d706312b (patch) | |
| tree | d590cb1272e6cb2e3cf0f52ca8d64c7ac362adf5 /src | |
| parent | 57a22673049e34f2c314cc20df96ab91da0de314 (diff) | |
Move UdpServerEntryPoint to Jellyfin.Networking
Diffstat (limited to 'src')
| -rw-r--r-- | src/Jellyfin.Networking/Udp/UdpServer.cs | 137 | ||||
| -rw-r--r-- | src/Jellyfin.Networking/UdpServerEntryPoint.cs | 144 |
2 files changed, 281 insertions, 0 deletions
diff --git a/src/Jellyfin.Networking/Udp/UdpServer.cs b/src/Jellyfin.Networking/Udp/UdpServer.cs new file mode 100644 index 000000000..978eb7853 --- /dev/null +++ b/src/Jellyfin.Networking/Udp/UdpServer.cs @@ -0,0 +1,137 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Model.ApiClient; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; + +namespace Jellyfin.Networking.Udp +{ + /// <summary> + /// Provides a Udp Server. + /// </summary> + public sealed class UdpServer : IDisposable + { + /// <summary> + /// The _logger. + /// </summary> + private readonly ILogger _logger; + private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _config; + + private readonly byte[] _receiveBuffer = new byte[8192]; + + private readonly Socket _udpSocket; + private readonly IPEndPoint _endpoint; + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="UdpServer" /> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="appHost">The application host.</param> + /// <param name="configuration">The configuration manager.</param> + /// <param name="bindAddress"> The bind address.</param> + /// <param name="port">The port.</param> + public UdpServer( + ILogger logger, + IServerApplicationHost appHost, + IConfiguration configuration, + IPAddress bindAddress, + int port) + { + _logger = logger; + _appHost = appHost; + _config = configuration; + + _endpoint = new IPEndPoint(bindAddress, port); + + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) + { + MulticastLoopback = false, + }; + _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + } + + private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken) + { + string? localUrl = _config[AddressOverrideKey]; + if (string.IsNullOrEmpty(localUrl)) + { + localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address); + } + + if (string.IsNullOrEmpty(localUrl)) + { + _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined."); + return; + } + + var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); + + try + { + _logger.LogDebug("Sending AutoDiscovery response"); + await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); + } + catch (SocketException ex) + { + _logger.LogError(ex, "Error sending response message"); + } + } + + /// <summary> + /// Starts the specified port. + /// </summary> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + public void Start(CancellationToken cancellationToken) + { + _udpSocket.Bind(_endpoint); + + _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); + } + + private async Task BeginReceiveAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + 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)) + { + await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException ex) + { + _logger.LogError(ex, "Failed to receive data from socket"); + } + catch (OperationCanceledException) + { + _logger.LogDebug("Broadcast socket operation cancelled"); + } + } + } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + _udpSocket.Dispose(); + _disposed = true; + } + } +} diff --git a/src/Jellyfin.Networking/UdpServerEntryPoint.cs b/src/Jellyfin.Networking/UdpServerEntryPoint.cs new file mode 100644 index 000000000..ced5a5ce1 --- /dev/null +++ b/src/Jellyfin.Networking/UdpServerEntryPoint.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Networking.Udp; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; + +namespace Jellyfin.Networking +{ + /// <summary> + /// Class responsible for registering all UDP broadcast endpoints and their handlers. + /// </summary> + public sealed class UdpServerEntryPoint : IServerEntryPoint + { + /// <summary> + /// The port of the UDP server. + /// </summary> + public const int PortNumber = 7359; + + /// <summary> + /// The logger. + /// </summary> + private readonly ILogger<UdpServerEntryPoint> _logger; + private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _config; + private readonly IConfigurationManager _configurationManager; + private readonly INetworkManager _networkManager; + + /// <summary> + /// The UDP server. + /// </summary> + private readonly List<UdpServer> _udpServers; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private bool _disposed; + + /// <summary> + /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param> + /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param> + /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> + /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> + /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + public UdpServerEntryPoint( + ILogger<UdpServerEntryPoint> logger, + IServerApplicationHost appHost, + IConfiguration configuration, + IConfigurationManager configurationManager, + INetworkManager networkManager) + { + _logger = logger; + _appHost = appHost; + _config = configuration; + _configurationManager = configurationManager; + _networkManager = networkManager; + _udpServers = new List<UdpServer>(); + } + + /// <inheritdoc /> + public Task RunAsync() + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery) + { + return Task.CompletedTask; + } + + try + { + // 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 listener + var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, 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 broadcastAddress = NetworkUtils.GetBroadcastAddress(intf.Subnet); + _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber); + + server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); + } + } + else + { + // 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) + { + _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber); + } + + return Task.CompletedTask; + } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + foreach (var server in _udpServers) + { + server.Dispose(); + } + + _udpServers.Clear(); + _disposed = true; + } + } +} |
