diff options
| author | Bond-009 <bond.009@outlook.com> | 2023-11-30 17:40:06 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-30 17:40:06 +0100 |
| commit | cc276838b4edbb67356b805952262c38e9c9cd19 (patch) | |
| tree | 8cb27ed0ba34fdd2d941f43c09ccc2be70c10abb /RSSDP/SsdpDevicePublisher.cs | |
| parent | cf80ea25413b75bbeddaef136fbeee33aa882a60 (diff) | |
| parent | e46e3be667c76ff9a242d7499aff83d2d10881ed (diff) | |
Merge pull request #10558 from barronpm/dlna-plugin2
Move DLNA to Plugin (Part 2)
Diffstat (limited to 'RSSDP/SsdpDevicePublisher.cs')
| -rw-r--r-- | RSSDP/SsdpDevicePublisher.cs | 623 |
1 files changed, 0 insertions, 623 deletions
diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs deleted file mode 100644 index 0ac9cc9a1..000000000 --- a/RSSDP/SsdpDevicePublisher.cs +++ /dev/null @@ -1,623 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Provides the platform independent logic for publishing SSDP devices (notifications and search responses). - /// </summary> - public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher - { - private ISsdpCommunicationsServer _CommsServer; - private string _OSName; - private string _OSVersion; - private bool _sendOnlyMatchedHost; - - private bool _SupportPnpRootDevice; - - private IList<SsdpRootDevice> _Devices; - private IReadOnlyList<SsdpRootDevice> _ReadOnlyDevices; - - private Timer _RebroadcastAliveNotificationsTimer; - - private IDictionary<string, SearchRequest> _RecentSearchRequests; - - private Random _Random; - - /// <summary> - /// Default constructor. - /// </summary> - public SsdpDevicePublisher( - ISsdpCommunicationsServer communicationsServer, - string osName, - string osVersion, - bool sendOnlyMatchedHost) - { - ArgumentNullException.ThrowIfNull(communicationsServer); - ArgumentNullException.ThrowIfNullOrEmpty(osName); - ArgumentNullException.ThrowIfNullOrEmpty(osVersion); - - _SupportPnpRootDevice = true; - _Devices = new List<SsdpRootDevice>(); - _ReadOnlyDevices = new ReadOnlyCollection<SsdpRootDevice>(_Devices); - _RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase); - _Random = new Random(); - - _CommsServer = communicationsServer; - _CommsServer.RequestReceived += CommsServer_RequestReceived; - _OSName = osName; - _OSVersion = osVersion; - _sendOnlyMatchedHost = sendOnlyMatchedHost; - - _CommsServer.BeginListeningForMulticast(); - - // Send alive notification once on creation - SendAllAliveNotifications(null); - } - - public void StartSendingAliveNotifications(TimeSpan interval) - { - _RebroadcastAliveNotificationsTimer = new Timer(SendAllAliveNotifications, null, TimeSpan.FromSeconds(5), interval); - } - - /// <summary> - /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients. - /// </summary> - /// <remarks> - /// <para>Adding a device causes "alive" notification messages to be sent immediately, or very soon after. Ensure your device/description service is running before adding the device object here.</para> - /// <para>Devices added here with a non-zero cache life time will also have notifications broadcast periodically.</para> - /// <para>This method ignores duplicate device adds (if the same device instance is added multiple times, the second and subsequent add calls do nothing).</para> - /// </remarks> - /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param> - /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - /// <exception cref="InvalidOperationException">Thrown if the <paramref name="device"/> contains property values that are not acceptable to the UPnP 1.0 specification.</exception> - public void AddDevice(SsdpRootDevice device) - { - if (device is null) - { - throw new ArgumentNullException(nameof(device)); - } - - ThrowIfDisposed(); - - bool wasAdded = false; - lock (_Devices) - { - if (!_Devices.Contains(device)) - { - _Devices.Add(device); - wasAdded = true; - } - } - - if (wasAdded) - { - WriteTrace("Device Added", device); - - SendAliveNotifications(device, true, CancellationToken.None); - } - } - - /// <summary> - /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable. - /// </summary> - /// <remarks> - /// <para>Removing a device causes "byebye" notification messages to be sent immediately, advising clients of the device/service becoming unavailable. We recommend removing the device from the published list before shutting down the actual device/service, if possible.</para> - /// <para>This method does nothing if the device was not found in the collection.</para> - /// </remarks> - /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param> - /// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - public async Task RemoveDevice(SsdpRootDevice device) - { - if (device is null) - { - throw new ArgumentNullException(nameof(device)); - } - - bool wasRemoved = false; - lock (_Devices) - { - if (_Devices.Contains(device)) - { - _Devices.Remove(device); - wasRemoved = true; - } - } - - if (wasRemoved) - { - WriteTrace("Device Removed", device); - - await SendByeByeNotifications(device, true, CancellationToken.None).ConfigureAwait(false); - } - } - - /// <summary> - /// Returns a read only list of devices being published by this instance. - /// </summary> - public IEnumerable<SsdpRootDevice> Devices - { - get - { - return _ReadOnlyDevices; - } - } - - /// <summary> - /// If true (default) treats root devices as both upnp:rootdevice and pnp:rootdevice types. - /// </summary> - /// <remarks> - /// <para>Enabling this option will cause devices to show up in Microsoft Windows Explorer's network screens (if discovery is enabled etc.). Windows Explorer appears to search only for pnp:rootdeivce and not upnp:rootdevice.</para> - /// <para>If false, the system will only use upnp:rootdevice for notification broadcasts and and search responses, which is correct according to the UPnP/SSDP spec.</para> - /// </remarks> - public bool SupportPnpRootDevice - { - get { return _SupportPnpRootDevice; } - - set - { - _SupportPnpRootDevice = value; - } - } - - /// <summary> - /// Stops listening for requests, stops sending periodic broadcasts, disposes all internal resources. - /// </summary> - /// <param name="disposing"></param> - protected override void Dispose(bool disposing) - { - if (disposing) - { - DisposeRebroadcastTimer(); - - var commsServer = _CommsServer; - if (commsServer is not null) - { - commsServer.RequestReceived -= this.CommsServer_RequestReceived; - } - - var tasks = Devices.ToList().Select(RemoveDevice).ToArray(); - Task.WaitAll(tasks); - - _CommsServer = null; - if (commsServer is not null) - { - if (!commsServer.IsShared) - { - commsServer.Dispose(); - } - } - - _RecentSearchRequests = null; - } - } - - private void ProcessSearchRequest( - string mx, - string searchTarget, - IPEndPoint remoteEndPoint, - IPAddress receivedOnlocalIPAddress, - CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(searchTarget)) - { - WriteTrace(string.Format(CultureInfo.InvariantCulture, "Invalid search request received From {0}, Target is null/empty.", remoteEndPoint.ToString())); - return; - } - - // WriteTrace(String.Format("Search Request Received From {0}, Target = {1}", remoteEndPoint.ToString(), searchTarget)); - - if (IsDuplicateSearchRequest(searchTarget, remoteEndPoint)) - { - // WriteTrace("Search Request is Duplicate, ignoring."); - return; - } - - // Wait on random interval up to MX, as per SSDP spec. - // Also, as per UPnP 1.1/SSDP spec ignore missing/bank MX header. If over 120, assume random value between 0 and 120. - // Using 16 as minimum as that's often the minimum system clock frequency anyway. - if (String.IsNullOrEmpty(mx)) - { - // Windows Explorer is poorly behaved and doesn't supply an MX header value. - // if (this.SupportPnpRootDevice) - mx = "1"; - // else - // return; - } - - if (!int.TryParse(mx, out var maxWaitInterval) || maxWaitInterval <= 0) - { - return; - } - - if (maxWaitInterval > 120) - { - maxWaitInterval = _Random.Next(0, 120); - } - - // Do not block synchronously as that may tie up a threadpool thread for several seconds. - Task.Delay(_Random.Next(16, maxWaitInterval * 1000), cancellationToken).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) - { - devices = GetAllDevicesAsFlatEnumerable().ToArray(); - } - 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(); - } - else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) - { - devices = GetAllDevicesAsFlatEnumerable().Where(d => string.Compare(d.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0).ToArray(); - } - } - - if (devices is not null) - { - // WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); - - foreach (var device in devices) - { - var root = device.ToRootDevice(); - - if (!_sendOnlyMatchedHost || root.Address.Equals(receivedOnlocalIPAddress)) - { - SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIPAddress, cancellationToken); - } - } - } - }, cancellationToken); - } - - private IEnumerable<SsdpDevice> GetAllDevicesAsFlatEnumerable() - { - return _Devices.Union(_Devices.SelectManyRecursive<SsdpDevice>((d) => d.Devices)); - } - - private void SendDeviceSearchResponses( - SsdpDevice device, - IPEndPoint endPoint, - IPAddress receivedOnlocalIPAddress, - CancellationToken cancellationToken) - { - bool isRootDevice = (device as SsdpRootDevice) is not null; - if (isRootDevice) - { - SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); - if (SupportPnpRootDevice) - { - SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint, receivedOnlocalIPAddress, cancellationToken); - } - } - - SendSearchResponse(device.Udn, device, device.Udn, endPoint, receivedOnlocalIPAddress, cancellationToken); - - SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint, receivedOnlocalIPAddress, cancellationToken); - } - - private string GetUsn(string udn, string fullDeviceType) - { - return string.Format(CultureInfo.InvariantCulture, "{0}::{1}", udn, fullDeviceType); - } - - private async void SendSearchResponse( - string searchTarget, - SsdpDevice device, - string uniqueServiceName, - IPEndPoint endPoint, - IPAddress receivedOnlocalIPAddress, - CancellationToken cancellationToken) - { - const string header = "HTTP/1.1 200 OK"; - - var rootDevice = device.ToRootDevice(); - 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); - - try - { - await _CommsServer.SendMessage( - Encoding.UTF8.GetBytes(message), - endPoint, - receivedOnlocalIPAddress, - cancellationToken) - .ConfigureAwait(false); - } - catch (Exception) - { - } - - // WriteTrace(String.Format("Sent search response to " + endPoint.ToString()), device); - } - - private bool IsDuplicateSearchRequest(string searchTarget, IPEndPoint endPoint) - { - var isDuplicateRequest = false; - - var newRequest = new SearchRequest() { EndPoint = endPoint, SearchTarget = searchTarget, Received = DateTime.UtcNow }; - lock (_RecentSearchRequests) - { - if (_RecentSearchRequests.ContainsKey(newRequest.Key)) - { - var lastRequest = _RecentSearchRequests[newRequest.Key]; - if (lastRequest.IsOld()) - { - _RecentSearchRequests[newRequest.Key] = newRequest; - } - else - { - isDuplicateRequest = true; - } - } - else - { - _RecentSearchRequests.Add(newRequest.Key, newRequest); - if (_RecentSearchRequests.Count > 10) - { - CleanUpRecentSearchRequestsAsync(); - } - } - } - - return isDuplicateRequest; - } - - private void CleanUpRecentSearchRequestsAsync() - { - lock (_RecentSearchRequests) - { - foreach (var requestKey in (from r in _RecentSearchRequests where r.Value.IsOld() select r.Key).ToArray()) - { - _RecentSearchRequests.Remove(requestKey); - } - } - } - - private void SendAllAliveNotifications(object state) - { - try - { - if (IsDisposed) - { - return; - } - - // WriteTrace("Begin Sending Alive Notifications For All Devices"); - - SsdpRootDevice[] devices; - lock (_Devices) - { - devices = _Devices.ToArray(); - } - - foreach (var device in devices) - { - if (IsDisposed) - { - return; - } - - SendAliveNotifications(device, true, CancellationToken.None); - } - - // WriteTrace("Completed Sending Alive Notifications For All Devices"); - } - catch (ObjectDisposedException ex) - { - WriteTrace("Publisher stopped, exception " + ex.Message); - Dispose(); - } - } - - private void SendAliveNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken) - { - if (isRoot) - { - SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken); - if (SupportPnpRootDevice) - { - SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); - } - } - - SendAliveNotification(device, device.Udn, device.Udn, cancellationToken); - SendAliveNotification(device, device.FullDeviceType, GetUsn(device.Udn, device.FullDeviceType), cancellationToken); - - foreach (var childDevice in device.Devices) - { - SendAliveNotifications(childDevice, false, cancellationToken); - } - } - - private void SendAliveNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) - { - var rootDevice = device.ToRootDevice(); - - 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 - ["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); - - _CommsServer.SendMulticastMessage(message, _sendOnlyMatchedHost ? rootDevice.Address : null, cancellationToken); - - // WriteTrace(String.Format("Sent alive notification"), device); - } - - private Task SendByeByeNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken) - { - var tasks = new List<Task>(); - if (isRoot) - { - tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken)); - if (SupportPnpRootDevice) - { - tasks.Add(SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken)); - } - } - - tasks.Add(SendByeByeNotification(device, device.Udn, device.Udn, cancellationToken)); - tasks.Add(SendByeByeNotification(device, String.Format(CultureInfo.InvariantCulture, "urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken)); - - foreach (var childDevice in device.Devices) - { - tasks.Add(SendByeByeNotifications(childDevice, false, cancellationToken)); - } - - return Task.WhenAll(tasks); - } - - private Task SendByeByeNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) - { - 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 - ["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); - return _CommsServer.SendMulticastMessage(message, sendCount, _sendOnlyMatchedHost ? device.ToRootDevice().Address : null, cancellationToken); - } - - private void DisposeRebroadcastTimer() - { - var timer = _RebroadcastAliveNotificationsTimer; - _RebroadcastAliveNotificationsTimer = null; - timer?.Dispose(); - } - - private TimeSpan GetMinimumNonZeroCacheLifetime() - { - var nonzeroCacheLifetimesQuery = ( - from device - in _Devices - where device.CacheLifetime != TimeSpan.Zero - select device.CacheLifetime).ToList(); - - if (nonzeroCacheLifetimesQuery.Any()) - { - return nonzeroCacheLifetimesQuery.Min(); - } - - return TimeSpan.Zero; - } - - private string GetFirstHeaderValue(System.Net.Http.Headers.HttpRequestHeaders httpRequestHeaders, string headerName) - { - string retVal = null; - if (httpRequestHeaders.TryGetValues(headerName, out var values) && values is not null) - { - retVal = values.FirstOrDefault(); - } - - return retVal; - } - - public Action<string> LogFunction { get; set; } - - private void WriteTrace(string text) - { - LogFunction?.Invoke(text); - // System.Diagnostics.Debug.WriteLine(text, "SSDP Publisher"); - } - - private void WriteTrace(string text, SsdpDevice device) - { - var rootDevice = device as SsdpRootDevice; - if (rootDevice is not null) - { - WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid + " - " + rootDevice.Location); - } - else - { - WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid); - } - } - - private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e) - { - if (this.IsDisposed) - { - return; - } - - if (string.Equals(e.Message.Method.Method, SsdpConstants.MSearchMethod, StringComparison.OrdinalIgnoreCase)) - { - // According to SSDP/UPnP spec, ignore message if missing these headers. - // Edit: But some devices do it anyway - // if (!e.Message.Headers.Contains("MX")) - // WriteTrace("Ignoring search request - missing MX header."); - // else if (!e.Message.Headers.Contains("MAN")) - // WriteTrace("Ignoring search request - missing MAN header."); - // else - ProcessSearchRequest(GetFirstHeaderValue(e.Message.Headers, "MX"), GetFirstHeaderValue(e.Message.Headers, "ST"), e.ReceivedFrom, e.LocalIPAddress, CancellationToken.None); - } - } - - private class SearchRequest - { - public IPEndPoint EndPoint { get; set; } - - public DateTime Received { get; set; } - - public string SearchTarget { get; set; } - - public string Key - { - get { return this.SearchTarget + ":" + this.EndPoint; } - } - - public bool IsOld() - { - return DateTime.UtcNow.Subtract(this.Received).TotalMilliseconds > 500; - } - } - } -} |
