2019-01-13 15:03:10 -05:00
using System ;
2016-10-29 18:22:20 -04:00
using System.Collections.Generic ;
2018-09-12 13:26:21 -04:00
using System.Collections.ObjectModel ;
2021-02-14 09:11:46 -05:00
using System.Globalization ;
2018-09-12 13:26:21 -04:00
using System.Linq ;
2019-07-07 15:03:26 -04:00
using System.Net ;
2023-02-16 12:15:12 -05:00
using System.Text ;
2018-09-12 13:26:21 -04:00
using System.Threading ;
using System.Threading.Tasks ;
2016-10-29 18:22:20 -04:00
2018-09-12 13:26:21 -04:00
namespace Rssdp.Infrastructure
2016-10-29 18:22:20 -04:00
{
2018-09-12 13:26:21 -04:00
/// <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 ;
2019-02-21 23:06:49 -05:00
private bool _sendOnlyMatchedHost ;
2018-09-12 13:26:21 -04:00
private bool _SupportPnpRootDevice ;
private IList < SsdpRootDevice > _Devices ;
private IReadOnlyList < SsdpRootDevice > _ReadOnlyDevices ;
2019-02-05 03:49:46 -05:00
private Timer _RebroadcastAliveNotificationsTimer ;
2018-09-12 13:26:21 -04:00
private IDictionary < string , SearchRequest > _RecentSearchRequests ;
private Random _Random ;
/// <summary>
/// Default constructor.
/// </summary>
2021-11-16 06:24:17 -05:00
public SsdpDevicePublisher (
ISsdpCommunicationsServer communicationsServer ,
string osName ,
string osVersion ,
bool sendOnlyMatchedHost )
2018-09-12 13:26:21 -04:00
{
2023-02-20 10:58:22 -05:00
ArgumentNullException . ThrowIfNull ( communicationsServer ) ;
ArgumentNullException . ThrowIfNullOrEmpty ( osName ) ;
ArgumentNullException . ThrowIfNullOrEmpty ( osVersion ) ;
2018-09-12 13:26:21 -04:00
_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 ;
2019-02-21 23:06:49 -05:00
_sendOnlyMatchedHost = sendOnlyMatchedHost ;
2018-09-12 13:26:21 -04:00
2023-02-16 12:15:12 -05:00
_CommsServer . BeginListeningForMulticast ( ) ;
// Send alive notification once on creation
SendAllAliveNotifications ( null ) ;
2018-09-12 13:26:21 -04:00
}
2023-02-16 12:15:12 -05:00
public void StartSendingAliveNotifications ( TimeSpan interval )
2018-09-12 13:26:21 -04:00
{
2019-02-05 03:49:46 -05:00
_RebroadcastAliveNotificationsTimer = new Timer ( SendAllAliveNotifications , null , TimeSpan . FromSeconds ( 5 ) , interval ) ;
2018-09-12 13:26:21 -04:00
}
/// <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>
2019-01-13 15:37:13 -05:00
/// <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>
2018-09-12 13:26:21 -04:00
public void AddDevice ( SsdpRootDevice device )
{
2023-02-16 12:15:12 -05:00
if ( device is null )
2020-06-20 04:35:29 -04:00
{
throw new ArgumentNullException ( nameof ( device ) ) ;
}
2018-09-12 13:26:21 -04:00
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>
2019-01-13 15:37:13 -05:00
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
2018-09-12 13:26:21 -04:00
public async Task RemoveDevice ( SsdpRootDevice device )
{
2023-02-16 12:15:12 -05:00
if ( device is null )
2020-06-20 04:35:29 -04:00
{
throw new ArgumentNullException ( nameof ( device ) ) ;
}
2018-09-12 13:26:21 -04:00
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>
2020-11-18 08:46:14 -05:00
/// <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>
2018-09-12 13:26:21 -04:00
/// </remarks>
public bool SupportPnpRootDevice
{
get { return _SupportPnpRootDevice ; }
2020-06-15 17:43:52 -04:00
2018-09-12 13:26:21 -04:00
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 ;
2023-02-16 12:15:12 -05:00
if ( commsServer is not null )
2018-09-12 13:26:21 -04:00
{
commsServer . RequestReceived - = this . CommsServer_RequestReceived ;
}
var tasks = Devices . ToList ( ) . Select ( RemoveDevice ) . ToArray ( ) ;
Task . WaitAll ( tasks ) ;
_CommsServer = null ;
2023-02-16 12:15:12 -05:00
if ( commsServer is not null )
2018-09-12 13:26:21 -04:00
{
if ( ! commsServer . IsShared )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
commsServer . Dispose ( ) ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
}
_RecentSearchRequests = null ;
}
}
2019-07-07 15:03:26 -04:00
private void ProcessSearchRequest (
string mx ,
string searchTarget ,
IPEndPoint remoteEndPoint ,
2023-02-17 13:27:36 -05:00
IPAddress receivedOnlocalIPAddress ,
2019-07-07 15:03:26 -04:00
CancellationToken cancellationToken )
2018-09-12 13:26:21 -04:00
{
2023-10-10 18:02:37 -04:00
if ( string . IsNullOrEmpty ( searchTarget ) )
2018-09-12 13:26:21 -04:00
{
2023-10-10 18:02:37 -04:00
WriteTrace ( string . Format ( CultureInfo . InvariantCulture , "Invalid search request received From {0}, Target is null/empty." , remoteEndPoint . ToString ( ) ) ) ;
2018-09-12 13:26:21 -04:00
return ;
}
2020-06-14 05:11:11 -04:00
// WriteTrace(String.Format("Search Request Received From {0}, Target = {1}", remoteEndPoint.ToString(), searchTarget));
2018-09-12 13:26:21 -04:00
if ( IsDuplicateSearchRequest ( searchTarget , remoteEndPoint ) )
{
2020-06-14 05:11:11 -04:00
// WriteTrace("Search Request is Duplicate, ignoring.");
2018-09-12 13:26:21 -04:00
return ;
}
2020-06-14 05:11:11 -04:00
// 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.
2018-09-12 13:26:21 -04:00
if ( String . IsNullOrEmpty ( mx ) )
{
2020-06-14 05:11:11 -04:00
// Windows Explorer is poorly behaved and doesn't supply an MX header value.
// if (this.SupportPnpRootDevice)
2018-09-12 13:26:21 -04:00
mx = "1" ;
2020-06-14 05:11:11 -04:00
// else
// return;
2018-09-12 13:26:21 -04:00
}
2023-10-10 18:02:37 -04:00
if ( ! int . TryParse ( mx , out var maxWaitInterval ) | | maxWaitInterval < = 0 )
2020-06-20 04:35:29 -04:00
{
return ;
}
2018-09-12 13:26:21 -04:00
if ( maxWaitInterval > 120 )
2020-06-20 04:35:29 -04:00
{
2018-09-12 13:26:21 -04:00
maxWaitInterval = _Random . Next ( 0 , 120 ) ;
2020-06-20 04:35:29 -04:00
}
2018-09-12 13:26:21 -04:00
2020-06-14 05:11:11 -04:00
// Do not block synchronously as that may tie up a threadpool thread for several seconds.
2023-10-10 18:02:37 -04:00
Task . Delay ( _Random . Next ( 16 , maxWaitInterval * 1000 ) ) . ContinueWith ( ( parentTask ) = >
2018-09-12 13:26:21 -04:00
{
2020-06-14 05:11:11 -04:00
// Copying devices to local array here to avoid threading issues/enumerator exceptions.
2018-09-12 13:26:21 -04:00
IEnumerable < SsdpDevice > devices = null ;
lock ( _Devices )
{
2023-10-10 18:02:37 -04:00
if ( string . Compare ( SsdpConstants . SsdpDiscoverAllSTHeader , searchTarget , StringComparison . OrdinalIgnoreCase ) = = 0 )
2020-06-20 04:35:29 -04:00
{
2018-09-12 13:26:21 -04:00
devices = GetAllDevicesAsFlatEnumerable ( ) . ToArray ( ) ;
2020-06-20 04:35:29 -04:00
}
2023-10-10 18:02:37 -04:00
else if ( string . Compare ( SsdpConstants . UpnpDeviceTypeRootDevice , searchTarget , StringComparison . OrdinalIgnoreCase ) = = 0 | | ( SupportPnpRootDevice & & String . Compare ( SsdpConstants . PnpDeviceTypeRootDevice , searchTarget , StringComparison . OrdinalIgnoreCase ) = = 0 ) )
2020-06-20 04:35:29 -04:00
{
2018-09-12 13:26:21 -04:00
devices = _Devices . ToArray ( ) ;
2020-06-20 04:35:29 -04:00
}
2018-09-12 13:26:21 -04:00
else if ( searchTarget . Trim ( ) . StartsWith ( "uuid:" , StringComparison . OrdinalIgnoreCase ) )
2020-06-20 04:35:29 -04:00
{
2023-10-10 18:02:37 -04:00
devices = GetAllDevicesAsFlatEnumerable ( ) . Where ( d = > string . Compare ( d . Uuid , searchTarget . Substring ( 5 ) , StringComparison . OrdinalIgnoreCase ) = = 0 ) . ToArray ( ) ;
2020-06-20 04:35:29 -04:00
}
2018-09-12 13:26:21 -04:00
else if ( searchTarget . StartsWith ( "urn:" , StringComparison . OrdinalIgnoreCase ) )
2020-06-20 04:35:29 -04:00
{
2023-10-10 18:02:37 -04:00
devices = GetAllDevicesAsFlatEnumerable ( ) . Where ( d = > string . Compare ( d . FullDeviceType , searchTarget , StringComparison . OrdinalIgnoreCase ) = = 0 ) . ToArray ( ) ;
2020-06-20 04:35:29 -04:00
}
2018-09-12 13:26:21 -04:00
}
2023-02-16 12:15:12 -05:00
if ( devices is not null )
2018-09-12 13:26:21 -04:00
{
2020-06-14 05:11:11 -04:00
// WriteTrace(String.Format("Sending {0} search responses", deviceList.Count));
2018-09-12 13:26:21 -04:00
2023-02-16 12:15:12 -05:00
foreach ( var device in devices )
2018-09-12 13:26:21 -04:00
{
2020-10-04 04:56:33 -04:00
var root = device . ToRootDevice ( ) ;
2023-02-16 12:15:12 -05:00
2023-02-17 13:27:36 -05:00
if ( ! _sendOnlyMatchedHost | | root . Address . Equals ( receivedOnlocalIPAddress ) )
2019-02-21 23:06:49 -05:00
{
2023-02-17 13:27:36 -05:00
SendDeviceSearchResponses ( device , remoteEndPoint , receivedOnlocalIPAddress , cancellationToken ) ;
2019-02-21 23:06:49 -05:00
}
2018-09-12 13:26:21 -04:00
}
}
} ) ;
}
private IEnumerable < SsdpDevice > GetAllDevicesAsFlatEnumerable ( )
{
return _Devices . Union ( _Devices . SelectManyRecursive < SsdpDevice > ( ( d ) = > d . Devices ) ) ;
}
2019-07-07 15:03:26 -04:00
private void SendDeviceSearchResponses (
SsdpDevice device ,
IPEndPoint endPoint ,
2023-02-17 13:27:36 -05:00
IPAddress receivedOnlocalIPAddress ,
2019-07-07 15:03:26 -04:00
CancellationToken cancellationToken )
2018-09-12 13:26:21 -04:00
{
2023-02-16 12:15:12 -05:00
bool isRootDevice = ( device as SsdpRootDevice ) is not null ;
2018-09-12 13:26:21 -04:00
if ( isRootDevice )
{
2023-02-17 13:27:36 -05:00
SendSearchResponse ( SsdpConstants . UpnpDeviceTypeRootDevice , device , GetUsn ( device . Udn , SsdpConstants . UpnpDeviceTypeRootDevice ) , endPoint , receivedOnlocalIPAddress , cancellationToken ) ;
2023-10-10 18:02:37 -04:00
if ( SupportPnpRootDevice )
2020-06-20 05:12:36 -04:00
{
2023-02-17 13:27:36 -05:00
SendSearchResponse ( SsdpConstants . PnpDeviceTypeRootDevice , device , GetUsn ( device . Udn , SsdpConstants . PnpDeviceTypeRootDevice ) , endPoint , receivedOnlocalIPAddress , cancellationToken ) ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
}
2023-02-17 13:27:36 -05:00
SendSearchResponse ( device . Udn , device , device . Udn , endPoint , receivedOnlocalIPAddress , cancellationToken ) ;
2018-09-12 13:26:21 -04:00
2023-02-17 13:27:36 -05:00
SendSearchResponse ( device . FullDeviceType , device , GetUsn ( device . Udn , device . FullDeviceType ) , endPoint , receivedOnlocalIPAddress , cancellationToken ) ;
2018-09-12 13:26:21 -04:00
}
private string GetUsn ( string udn , string fullDeviceType )
{
2023-10-10 18:02:37 -04:00
return string . Format ( CultureInfo . InvariantCulture , "{0}::{1}" , udn , fullDeviceType ) ;
2018-09-12 13:26:21 -04:00
}
2019-07-07 15:03:26 -04:00
private async void SendSearchResponse (
string searchTarget ,
SsdpDevice device ,
string uniqueServiceName ,
IPEndPoint endPoint ,
2023-02-17 13:27:36 -05:00
IPAddress receivedOnlocalIPAddress ,
2019-07-07 15:03:26 -04:00
CancellationToken cancellationToken )
2018-09-12 13:26:21 -04:00
{
const string header = "HTTP/1.1 200 OK" ;
2023-02-16 12:15:12 -05:00
var rootDevice = device . ToRootDevice ( ) ;
2023-10-10 18:02:37 -04:00
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 ( )
} ;
2018-09-12 13:26:21 -04:00
var message = BuildMessage ( header , values ) ;
try
{
2018-12-15 18:48:40 -05:00
await _CommsServer . SendMessage (
2023-02-16 12:15:12 -05:00
Encoding . UTF8 . GetBytes ( message ) ,
2018-12-15 18:48:40 -05:00
endPoint ,
2023-02-17 13:27:36 -05:00
receivedOnlocalIPAddress ,
2018-12-15 18:48:40 -05:00
cancellationToken )
. ConfigureAwait ( false ) ;
2018-09-12 13:26:21 -04:00
}
2018-12-15 13:53:09 -05:00
catch ( Exception )
2018-09-12 13:26:21 -04:00
{
}
2020-06-14 05:11:11 -04:00
// WriteTrace(String.Format("Sent search response to " + endPoint.ToString()), device);
2018-09-12 13:26:21 -04:00
}
2019-07-07 15:03:26 -04:00
private bool IsDuplicateSearchRequest ( string searchTarget , IPEndPoint endPoint )
2018-09-12 13:26:21 -04:00
{
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 ( ) )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
_RecentSearchRequests [ newRequest . Key ] = newRequest ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
else
2020-06-20 05:19:16 -04:00
{
2018-09-12 13:26:21 -04:00
isDuplicateRequest = true ;
2020-06-20 05:19:16 -04:00
}
2018-09-12 13:26:21 -04:00
}
else
{
_RecentSearchRequests . Add ( newRequest . Key , newRequest ) ;
if ( _RecentSearchRequests . Count > 10 )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
CleanUpRecentSearchRequestsAsync ( ) ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
}
}
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
{
2020-06-20 04:35:29 -04:00
if ( IsDisposed )
{
return ;
}
2018-09-12 13:26:21 -04:00
2020-06-14 05:11:11 -04:00
// WriteTrace("Begin Sending Alive Notifications For All Devices");
2018-09-12 13:26:21 -04:00
SsdpRootDevice [ ] devices ;
lock ( _Devices )
{
devices = _Devices . ToArray ( ) ;
}
foreach ( var device in devices )
{
2020-06-20 04:35:29 -04:00
if ( IsDisposed )
{
return ;
}
2018-09-12 13:26:21 -04:00
SendAliveNotifications ( device , true , CancellationToken . None ) ;
}
2020-06-14 05:11:11 -04:00
// WriteTrace("Completed Sending Alive Notifications For All Devices");
2018-09-12 13:26:21 -04:00
}
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 ) ;
2023-10-10 18:02:37 -04:00
if ( SupportPnpRootDevice )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
SendAliveNotification ( device , SsdpConstants . PnpDeviceTypeRootDevice , GetUsn ( device . Udn , SsdpConstants . PnpDeviceTypeRootDevice ) , cancellationToken ) ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
}
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" ;
2023-10-10 18:02:37 -04:00
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
} ;
2018-09-12 13:26:21 -04:00
var message = BuildMessage ( header , values ) ;
2019-02-22 21:15:38 -05:00
_CommsServer . SendMulticastMessage ( message , _sendOnlyMatchedHost ? rootDevice . Address : null , cancellationToken ) ;
2018-09-12 13:26:21 -04:00
2020-06-14 05:11:11 -04:00
// WriteTrace(String.Format("Sent alive notification"), device);
2018-09-12 13:26:21 -04:00
}
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 ) ) ;
2023-10-10 18:02:37 -04:00
if ( SupportPnpRootDevice )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
tasks . Add ( SendByeByeNotification ( device , "pnp:rootdevice" , GetUsn ( device . Udn , "pnp:rootdevice" ) , cancellationToken ) ) ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
}
tasks . Add ( SendByeByeNotification ( device , device . Udn , device . Udn , cancellationToken ) ) ;
2021-02-14 09:11:46 -05:00
tasks . Add ( SendByeByeNotification ( device , String . Format ( CultureInfo . InvariantCulture , "urn:{0}" , device . FullDeviceType ) , GetUsn ( device . Udn , device . FullDeviceType ) , cancellationToken ) ) ;
2018-09-12 13:26:21 -04:00
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" ;
2023-10-10 18:02:37 -04:00
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
} ;
2018-09-12 13:26:21 -04:00
var message = BuildMessage ( header , values ) ;
var sendCount = IsDisposed ? 1 : 3 ;
2023-10-10 18:02:37 -04:00
WriteTrace ( string . Format ( CultureInfo . InvariantCulture , "Sent byebye notification" ) , device ) ;
2019-02-22 21:15:38 -05:00
return _CommsServer . SendMulticastMessage ( message , sendCount , _sendOnlyMatchedHost ? device . ToRootDevice ( ) . Address : null , cancellationToken ) ;
2018-09-12 13:26:21 -04:00
}
private void DisposeRebroadcastTimer ( )
{
var timer = _RebroadcastAliveNotificationsTimer ;
_RebroadcastAliveNotificationsTimer = null ;
2023-02-16 12:15:12 -05:00
if ( timer is not null )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
timer . Dispose ( ) ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
}
private TimeSpan GetMinimumNonZeroCacheLifetime ( )
{
2020-06-20 05:12:36 -04:00
var nonzeroCacheLifetimesQuery = (
from device
in _Devices
where device . CacheLifetime ! = TimeSpan . Zero
select device . CacheLifetime ) . ToList ( ) ;
2018-09-12 13:26:21 -04:00
if ( nonzeroCacheLifetimesQuery . Any ( ) )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
return nonzeroCacheLifetimesQuery . Min ( ) ;
2020-06-20 05:12:36 -04:00
}
2023-04-06 13:38:34 -04:00
return TimeSpan . Zero ;
2018-09-12 13:26:21 -04:00
}
private string GetFirstHeaderValue ( System . Net . Http . Headers . HttpRequestHeaders httpRequestHeaders , string headerName )
{
string retVal = null ;
2023-05-09 09:25:41 -04:00
if ( httpRequestHeaders . TryGetValues ( headerName , out var values ) & & values is not null )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
retVal = values . FirstOrDefault ( ) ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
return retVal ;
}
public Action < string > LogFunction { get ; set ; }
private void WriteTrace ( string text )
{
2023-02-16 12:15:12 -05:00
if ( LogFunction is not null )
2018-09-12 13:26:21 -04:00
{
LogFunction ( text ) ;
}
2020-06-14 05:11:11 -04:00
// System.Diagnostics.Debug.WriteLine(text, "SSDP Publisher");
2018-09-12 13:26:21 -04:00
}
private void WriteTrace ( string text , SsdpDevice device )
{
var rootDevice = device as SsdpRootDevice ;
2023-02-16 12:15:12 -05:00
if ( rootDevice is not null )
2020-06-20 05:12:36 -04:00
{
2018-09-12 13:26:21 -04:00
WriteTrace ( text + " " + device . DeviceType + " - " + device . Uuid + " - " + rootDevice . Location ) ;
2020-06-20 05:12:36 -04:00
}
2018-09-12 13:26:21 -04:00
else
2020-06-20 05:19:16 -04:00
{
2018-09-12 13:26:21 -04:00
WriteTrace ( text + " " + device . DeviceType + " - " + device . Uuid ) ;
2020-06-20 05:19:16 -04:00
}
2018-09-12 13:26:21 -04:00
}
private void CommsServer_RequestReceived ( object sender , RequestReceivedEventArgs e )
{
2020-06-20 04:35:29 -04:00
if ( this . IsDisposed )
{
return ;
}
2018-09-12 13:26:21 -04:00
if ( string . Equals ( e . Message . Method . Method , SsdpConstants . MSearchMethod , StringComparison . OrdinalIgnoreCase ) )
{
2020-06-14 05:11:11 -04:00
// According to SSDP/UPnP spec, ignore message if missing these headers.
2018-09-12 13:26:21 -04:00
// Edit: But some devices do it anyway
2020-06-14 05:11:11 -04:00
// if (!e.Message.Headers.Contains("MX"))
2019-01-07 18:24:34 -05:00
// WriteTrace("Ignoring search request - missing MX header.");
2020-06-14 05:11:11 -04:00
// else if (!e.Message.Headers.Contains("MAN"))
2019-01-07 18:24:34 -05:00
// WriteTrace("Ignoring search request - missing MAN header.");
2020-06-14 05:11:11 -04:00
// else
2023-02-17 13:27:36 -05:00
ProcessSearchRequest ( GetFirstHeaderValue ( e . Message . Headers , "MX" ) , GetFirstHeaderValue ( e . Message . Headers , "ST" ) , e . ReceivedFrom , e . LocalIPAddress , CancellationToken . None ) ;
2018-09-12 13:26:21 -04:00
}
}
private class SearchRequest
{
2019-07-07 15:03:26 -04:00
public IPEndPoint EndPoint { get ; set ; }
2020-06-15 17:43:52 -04:00
2018-09-12 13:26:21 -04:00
public DateTime Received { get ; set ; }
2020-06-15 17:43:52 -04:00
2018-09-12 13:26:21 -04:00
public string SearchTarget { get ; set ; }
public string Key
{
2023-04-06 13:11:45 -04:00
get { return this . SearchTarget + ":" + this . EndPoint ; }
2018-09-12 13:26:21 -04:00
}
public bool IsOld ( )
{
return DateTime . UtcNow . Subtract ( this . Received ) . TotalMilliseconds > 500 ;
}
}
2016-10-29 18:22:20 -04:00
}
2018-12-15 13:53:09 -05:00
}