Remove RSSDP

pull/10558/head
Patrick Barron 6 months ago
parent f1aba6b952
commit a8c55ae8fe

@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}"
@ -135,10 +133,6 @@ Global
{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.Build.0 = Release|Any CPU
{21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21002819-C39A-4D3E-BE83-2A276A77FB1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{21002819-C39A-4D3E-BE83-2A276A77FB1F}.Release|Any CPU.Build.0 = Release|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}.Release|Any CPU.ActiveCfg = Release|Any CPU

@ -1,50 +0,0 @@
using System;
using System.Net;
namespace Rssdp
{
/// <summary>
/// Event arguments for the <see cref="Infrastructure.SsdpDeviceLocator.DeviceAvailable"/> event.
/// </summary>
public sealed class DeviceAvailableEventArgs : EventArgs
{
public IPAddress RemoteIPAddress { get; set; }
private readonly DiscoveredSsdpDevice _DiscoveredDevice;
private readonly bool _IsNewlyDiscovered;
/// <summary>
/// Full constructor.
/// </summary>
/// <param name="discoveredDevice">A <see cref="DiscoveredSsdpDevice"/> instance representing the available device.</param>
/// <param name="isNewlyDiscovered">A boolean value indicating whether or not this device came from the cache. See <see cref="IsNewlyDiscovered"/> for more detail.</param>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="discoveredDevice"/> parameter is null.</exception>
public DeviceAvailableEventArgs(DiscoveredSsdpDevice discoveredDevice, bool isNewlyDiscovered)
{
if (discoveredDevice == null)
{
throw new ArgumentNullException(nameof(discoveredDevice));
}
_DiscoveredDevice = discoveredDevice;
_IsNewlyDiscovered = isNewlyDiscovered;
}
/// <summary>
/// Returns true if the device was discovered due to an alive notification, or a search and was not already in the cache. Returns false if the item came from the cache but matched the current search request.
/// </summary>
public bool IsNewlyDiscovered
{
get { return _IsNewlyDiscovered; }
}
/// <summary>
/// A reference to a <see cref="DiscoveredSsdpDevice"/> instance containing the discovered details and allowing access to the full device description.
/// </summary>
public DiscoveredSsdpDevice DiscoveredDevice
{
get { return _DiscoveredDevice; }
}
}
}

@ -1,35 +0,0 @@
using System;
namespace Rssdp
{
/// <summary>
/// Event arguments for the <see cref="SsdpDevice.DeviceAdded"/> and <see cref="SsdpDevice.DeviceRemoved"/> events.
/// </summary>
public sealed class DeviceEventArgs : EventArgs
{
private readonly SsdpDevice _Device;
/// <summary>
/// Constructs a new instance for the specified <see cref="SsdpDevice"/>.
/// </summary>
/// <param name="device">The <see cref="SsdpDevice"/> associated with the event this argument class is being used for.</param>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
public DeviceEventArgs(SsdpDevice device)
{
if (device == null)
{
throw new ArgumentNullException(nameof(device));
}
_Device = device;
}
/// <summary>
/// Returns the <see cref="SsdpDevice"/> instance the event being raised for.
/// </summary>
public SsdpDevice Device
{
get { return _Device; }
}
}
}

@ -1,47 +0,0 @@
using System;
namespace Rssdp
{
/// <summary>
/// Event arguments for the <see cref="Infrastructure.SsdpDeviceLocator.DeviceUnavailable"/> event.
/// </summary>
public sealed class DeviceUnavailableEventArgs : EventArgs
{
private readonly DiscoveredSsdpDevice _DiscoveredDevice;
private readonly bool _Expired;
/// <summary>
/// Full constructor.
/// </summary>
/// <param name="discoveredDevice">A <see cref="DiscoveredSsdpDevice"/> instance representing the device that has become unavailable.</param>
/// <param name="expired">A boolean value indicating whether this device is unavailable because it expired, or because it explicitly sent a byebye notification.. See <see cref="Expired"/> for more detail.</param>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="discoveredDevice"/> parameter is null.</exception>
public DeviceUnavailableEventArgs(DiscoveredSsdpDevice discoveredDevice, bool expired)
{
if (discoveredDevice == null)
{
throw new ArgumentNullException(nameof(discoveredDevice));
}
_DiscoveredDevice = discoveredDevice;
_Expired = expired;
}
/// <summary>
/// Returns true if the device is considered unavailable because it's cached information expired before a new alive notification or search result was received. Returns false if the device is unavailable because it sent an explicit notification of it's unavailability.
/// </summary>
public bool Expired
{
get { return _Expired; }
}
/// <summary>
/// A reference to a <see cref="DiscoveredSsdpDevice"/> instance containing the discovery details of the removed device.
/// </summary>
public DiscoveredSsdpDevice DiscoveredDevice
{
get { return _DiscoveredDevice; }
}
}
}

@ -1,74 +0,0 @@
using System;
using System.Net.Http.Headers;
namespace Rssdp
{
/// <summary>
/// Represents a discovered device, containing basic information about the device and the location of it's full device description document. Also provides convenience methods for retrieving the device description document.
/// </summary>
/// <seealso cref="SsdpDevice"/>
/// <seealso cref="Infrastructure.ISsdpDeviceLocator"/>
public sealed class DiscoveredSsdpDevice
{
private DateTimeOffset _AsAt;
/// <summary>
/// Sets or returns the type of notification, being either a uuid, device type, service type or upnp:rootdevice.
/// </summary>
public string NotificationType { get; set; }
/// <summary>
/// Sets or returns the universal service name (USN) of the device.
/// </summary>
public string Usn { get; set; }
/// <summary>
/// Sets or returns a URL pointing to the device description document for this device.
/// </summary>
public Uri DescriptionLocation { get; set; }
/// <summary>
/// Sets or returns the length of time this information is valid for (from the <see cref="AsAt"/> time).
/// </summary>
public TimeSpan CacheLifetime { get; set; }
/// <summary>
/// Sets or returns the date and time this information was received.
/// </summary>
public DateTimeOffset AsAt
{
get { return _AsAt; }
set
{
if (_AsAt != value)
{
_AsAt = value;
}
}
}
/// <summary>
/// Returns the headers from the SSDP device response message.
/// </summary>
public HttpHeaders ResponseHeaders { get; set; }
/// <summary>
/// Returns true if this device information has expired, based on the current date/time, and the <see cref="CacheLifetime"/> &amp; <see cref="AsAt"/> properties.
/// </summary>
/// <returns></returns>
public bool IsExpired()
{
return this.CacheLifetime == TimeSpan.Zero || this.AsAt.Add(this.CacheLifetime) <= DateTimeOffset.Now;
}
/// <summary>
/// Returns the device's <see cref="Usn"/> value.
/// </summary>
/// <returns>A string containing the device's universal service name.</returns>
public override string ToString()
{
return this.Usn;
}
}
}

@ -1,76 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Correctly implements the <see cref="IDisposable"/> interface and pattern for an object containing only managed resources, and adds a few common niceties not on the interface such as an <see cref="IsDisposed"/> property.
/// </summary>
public abstract class DisposableManagedObjectBase : IDisposable
{
/// <summary>
/// Override this method and dispose any objects you own the lifetime of if disposing is true;
/// </summary>
/// <param name="disposing">True if managed objects should be disposed, if false, only unmanaged resources should be released.</param>
protected abstract void Dispose(bool disposing);
/// <summary>
/// Throws and <see cref="ObjectDisposedException"/> if the <see cref="IsDisposed"/> property is true.
/// </summary>
/// <seealso cref="IsDisposed"/>
/// <exception cref="ObjectDisposedException">Thrown if the <see cref="IsDisposed"/> property is true.</exception>
/// <seealso cref="Dispose()"/>
protected virtual void ThrowIfDisposed()
{
if (this.IsDisposed)
{
throw new ObjectDisposedException(this.GetType().FullName);
}
}
/// <summary>
/// Sets or returns a boolean indicating whether or not this instance has been disposed.
/// </summary>
/// <seealso cref="Dispose()"/>
public bool IsDisposed
{
get;
private set;
}
public string BuildMessage(string header, Dictionary<string, string> values)
{
var builder = new StringBuilder();
const string ArgFormat = "{0}: {1}\r\n";
builder.AppendFormat(CultureInfo.InvariantCulture, "{0}\r\n", header);
foreach (var pair in values)
{
builder.AppendFormat(CultureInfo.InvariantCulture, ArgFormat, pair.Key, pair.Value);
}
builder.Append("\r\n");
return builder.ToString();
}
/// <summary>
/// Disposes this object instance and all internally managed resources.
/// </summary>
/// <remarks>
/// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behavior of derived classes.</para>
/// </remarks>
/// <seealso cref="IsDisposed"/>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "We do exactly as asked, but CA doesn't seem to like us also setting the IsDisposed property. Too bad, it's a good idea and shouldn't cause an exception or anything likely to interfere with the dispose process.")]
public void Dispose()
{
IsDisposed = true;
Dispose(true);
}
}
}

@ -1,228 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
namespace Rssdp.Infrastructure
{
/// <summary>
/// A base class for the <see cref="HttpResponseParser"/> and <see cref="HttpRequestParser"/> classes. Not intended for direct use.
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class HttpParserBase<T> where T : new()
{
private readonly string[] LineTerminators = new string[] { "\r\n", "\n" };
private readonly char[] SeparatorCharacters = new char[] { ',', ';' };
/// <summary>
/// Parses the <paramref name="data"/> provided into either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.
/// </summary>
/// <param name="data">A string containing the HTTP message to parse.</param>
/// <returns>Either a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object containing the parsed data.</returns>
public abstract T Parse(string data);
/// <summary>
/// Parses a string containing either an HTTP request or response into a <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object.
/// </summary>
/// <param name="message">A <see cref="HttpRequestMessage"/> or <see cref="HttpResponseMessage"/> object representing the parsed message.</param>
/// <param name="headers">A reference to the <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the <paramref name="message"/> object.</param>
/// <param name="data">A string containing the data to be parsed.</param>
/// <returns>An <see cref="HttpContent"/> object containing the content of the parsed message.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Honestly, it's fine. MemoryStream doesn't mind.")]
protected virtual void Parse(T message, System.Net.Http.Headers.HttpHeaders headers, string data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
if (data.Length == 0)
{
throw new ArgumentException("data cannot be an empty string.", nameof(data));
}
if (!LineTerminators.Any(data.Contains))
{
throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", nameof(data));
}
using (var retVal = new ByteArrayContent(Array.Empty<byte>()))
{
var lines = data.Split(LineTerminators, StringSplitOptions.None);
// First line is the 'request' line containing http protocol details like method, uri, http version etc.
ParseStatusLine(lines[0], message);
ParseHeaders(headers, retVal.Headers, lines);
}
}
/// <summary>
/// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>.
/// </summary>
/// <param name="data">The first line of the HTTP message to be parsed.</param>
/// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param>
protected abstract void ParseStatusLine(string data, T message);
/// <summary>
/// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false).
/// </summary>
/// <param name="headerName">A string containing the name of the header to return the type of.</param>
protected abstract bool IsContentHeader(string headerName);
/// <summary>
/// Parses the HTTP version text from an HTTP request or response status line and returns a <see cref="Version"/> object representing the parsed values.
/// </summary>
/// <param name="versionData">A string containing the HTTP version, from the message status line.</param>
/// <returns>A <see cref="Version"/> object containing the parsed version data.</returns>
protected Version ParseHttpVersion(string versionData)
{
if (versionData == null)
{
throw new ArgumentNullException(nameof(versionData));
}
var versionSeparatorIndex = versionData.IndexOf('/', StringComparison.Ordinal);
if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length)
{
throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", nameof(versionData));
}
return Version.Parse(versionData.Substring(versionSeparatorIndex + 1));
}
/// <summary>
/// Parses a line from an HTTP request or response message containing a header name and value pair.
/// </summary>
/// <param name="line">A string containing the data to be parsed.</param>
/// <param name="headers">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection to which the parsed header will be added.</param>
/// <param name="contentHeaders">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the message content, to which the parsed header will be added.</param>
private void ParseHeader(string line, System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders)
{
// Header format is
// name: value
var headerKeySeparatorIndex = line.IndexOf(':', StringComparison.Ordinal);
var headerName = line.Substring(0, headerKeySeparatorIndex).Trim();
var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim();
// Not sure how to determine where request headers and content headers begin,
// at least not without a known set of headers (general headers first the content headers)
// which seems like a bad way of doing it. So we'll assume if it's a known content header put it there
// else use request headers.
var values = ParseValues(headerValue);
var headersToAddTo = IsContentHeader(headerName) ? contentHeaders : headers;
if (values.Count > 1)
{
headersToAddTo.TryAddWithoutValidation(headerName, values);
}
else
{
headersToAddTo.TryAddWithoutValidation(headerName, values[0]);
}
}
private int ParseHeaders(System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders, string[] lines)
{
// Blank line separates headers from content, so read headers until we find blank line.
int lineIndex = 1;
string line = null, nextLine = null;
while (lineIndex + 1 < lines.Length && !String.IsNullOrEmpty((line = lines[lineIndex++])))
{
// If the following line starts with space or tab (or any whitespace), it is really part of this header but split for human readability.
// Combine these lines into a single comma separated style header for easier parsing.
while (lineIndex < lines.Length && !String.IsNullOrEmpty((nextLine = lines[lineIndex])))
{
if (nextLine.Length > 0 && Char.IsWhiteSpace(nextLine[0]))
{
line += "," + nextLine.TrimStart();
lineIndex++;
}
else
{
break;
}
}
ParseHeader(line, headers, contentHeaders);
}
return lineIndex;
}
private List<string> ParseValues(string headerValue)
{
// This really should be better and match the HTTP 1.1 spec,
// but this should actually be good enough for SSDP implementations
// I think.
var values = new List<string>();
if (headerValue == "\"\"")
{
values.Add(string.Empty);
return values;
}
var indexOfSeparator = headerValue.IndexOfAny(SeparatorCharacters);
if (indexOfSeparator <= 0)
{
values.Add(headerValue);
}
else
{
var segments = headerValue.Split(SeparatorCharacters);
if (headerValue.Contains('"', StringComparison.Ordinal))
{
for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++)
{
var segment = segments[segmentIndex];
if (segment.Trim().StartsWith("\"", StringComparison.OrdinalIgnoreCase))
{
segment = CombineQuotedSegments(segments, ref segmentIndex, segment);
}
values.Add(segment);
}
}
else
{
values.AddRange(segments);
}
}
return values;
}
private string CombineQuotedSegments(string[] segments, ref int segmentIndex, string segment)
{
var trimmedSegment = segment.Trim();
for (int index = segmentIndex; index < segments.Length; index++)
{
if (trimmedSegment == "\"\"" ||
(
trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase)
&& !trimmedSegment.EndsWith("\"\"", StringComparison.OrdinalIgnoreCase)
&& !trimmedSegment.EndsWith("\\\"", StringComparison.OrdinalIgnoreCase))
)
{
segmentIndex = index;
return trimmedSegment.Substring(1, trimmedSegment.Length - 2);
}
if (index + 1 < segments.Length)
{
trimmedSegment += "," + segments[index + 1].TrimEnd();
}
}
segmentIndex = segments.Length;
if (trimmedSegment.StartsWith("\"", StringComparison.OrdinalIgnoreCase) && trimmedSegment.EndsWith("\"", StringComparison.OrdinalIgnoreCase))
{
return trimmedSegment.Substring(1, trimmedSegment.Length - 2);
}
return trimmedSegment;
}
}
}

@ -1,88 +0,0 @@
using System;
using System.Net.Http;
using Jellyfin.Extensions;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Parses a string into a <see cref="HttpRequestMessage"/> or throws an exception.
/// </summary>
public sealed class HttpRequestParser : HttpParserBase<HttpRequestMessage>
{
private readonly string[] ContentHeaderNames = new string[]
{
"Allow", "Content-Disposition", "Content-Encoding", "Content-Language", "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", "Expires", "Last-Modified"
};
/// <summary>
/// Parses the specified data into a <see cref="HttpRequestMessage"/> instance.
/// </summary>
/// <param name="data">A string containing the data to parse.</param>
/// <returns>A <see cref="HttpRequestMessage"/> instance containing the parsed data.</returns>
public override HttpRequestMessage Parse(string data)
{
HttpRequestMessage retVal = null;
try
{
retVal = new HttpRequestMessage();
Parse(retVal, retVal.Headers, data);
return retVal;
}
finally
{
retVal?.Dispose();
}
}
/// <summary>
/// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>.
/// </summary>
/// <param name="data">The first line of the HTTP message to be parsed.</param>
/// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param>
protected override void ParseStatusLine(string data, HttpRequestMessage message)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
var parts = data.Split(' ');
if (parts.Length < 2)
{
throw new ArgumentException("Status line is invalid. Insufficient status parts.", nameof(data));
}
message.Method = new HttpMethod(parts[0].Trim());
if (Uri.TryCreate(parts[1].Trim(), UriKind.RelativeOrAbsolute, out var requestUri))
{
message.RequestUri = requestUri;
}
else
{
System.Diagnostics.Debug.WriteLine(parts[1]);
}
if (parts.Length >= 3)
{
message.Version = ParseHttpVersion(parts[2].Trim());
}
}
/// <summary>
/// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false).
/// </summary>
/// <param name="headerName">A string containing the name of the header to return the type of.</param>
protected override bool IsContentHeader(string headerName)
{
return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase);
}
}
}

@ -1,90 +0,0 @@
using System;
using System.Net;
using System.Net.Http;
using Jellyfin.Extensions;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Parses a string into a <see cref="HttpResponseMessage"/> or throws an exception.
/// </summary>
public sealed class HttpResponseParser : HttpParserBase<HttpResponseMessage>
{
private readonly string[] ContentHeaderNames = new string[]
{
"Allow", "Content-Disposition", "Content-Encoding", "Content-Language", "Content-Length", "Content-Location", "Content-MD5", "Content-Range", "Content-Type", "Expires", "Last-Modified"
};
/// <summary>
/// Parses the specified data into a <see cref="HttpResponseMessage"/> instance.
/// </summary>
/// <param name="data">A string containing the data to parse.</param>
/// <returns>A <see cref="HttpResponseMessage"/> instance containing the parsed data.</returns>
public override HttpResponseMessage Parse(string data)
{
HttpResponseMessage retVal = null;
try
{
retVal = new HttpResponseMessage();
Parse(retVal, retVal.Headers, data);
return retVal;
}
catch
{
retVal?.Dispose();
throw;
}
}
/// <summary>
/// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false).
/// </summary>
/// <param name="headerName">A string containing the name of the header to return the type of.</param>
/// <returns>A boolean, true if th specified header relates to HTTP content, otherwise false.</returns>
protected override bool IsContentHeader(string headerName)
{
return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>.
/// </summary>
/// <param name="data">The first line of the HTTP message to be parsed.</param>
/// <param name="message">Either a <see cref="HttpResponseMessage"/> or <see cref="HttpRequestMessage"/> to assign the parsed values to.</param>
protected override void ParseStatusLine(string data, HttpResponseMessage message)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
var parts = data.Split(' ');
if (parts.Length < 2)
{
throw new ArgumentException("data status line is invalid. Insufficient status parts.", nameof(data));
}
message.Version = ParseHttpVersion(parts[0].Trim());
if (!Int32.TryParse(parts[1].Trim(), out var statusCode))
{
throw new ArgumentException("data status line is invalid. Status code is not a valid integer.", nameof(data));
}
message.StatusCode = (HttpStatusCode)statusCode;
if (parts.Length >= 3)
{
message.ReasonPhrase = parts[2].Trim();
}
}
}
}

@ -1,34 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Rssdp.Infrastructure
{
internal static class IEnumerableExtensions
{
public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return !source.Any() ? source :
source.Concat(
source
.SelectMany(i => selector(i).EmptyIfNull())
.SelectManyRecursive(selector)
);
}
public static IEnumerable<T> EmptyIfNull<T>(this IEnumerable<T> source)
{
return source ?? Enumerable.Empty<T>();
}
}
}

@ -1,52 +0,0 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Interface for a component that manages network communication (sending and receiving HTTPU messages) for the SSDP protocol.
/// </summary>
public interface ISsdpCommunicationsServer : IDisposable
{
/// <summary>
/// Raised when a HTTPU request message is received by a socket (unicast or multicast).
/// </summary>
event EventHandler<RequestReceivedEventArgs> RequestReceived;
/// <summary>
/// Raised when an HTTPU response message is received by a socket (unicast or multicast).
/// </summary>
event EventHandler<ResponseReceivedEventArgs> ResponseReceived;
/// <summary>
/// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications.
/// </summary>
void BeginListeningForMulticast();
/// <summary>
/// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications.
/// </summary>
void StopListeningForMulticast();
/// <summary>
/// Sends a message to a particular address (uni or multicast) and port.
/// </summary>
Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIPAddress, CancellationToken cancellationToken);
/// <summary>
/// Sends a message to the SSDP multicast address and port.
/// </summary>
Task SendMulticastMessage(string message, IPAddress fromLocalIPAddress, CancellationToken cancellationToken);
Task SendMulticastMessage(string message, int sendCount, IPAddress fromLocalIPAddress, CancellationToken cancellationToken);
/// <summary>
/// Gets or sets a boolean value indicating whether or not this instance is shared amongst multiple <see cref="SsdpDeviceLocator"/> and/or <see cref="ISsdpDevicePublisher"/> instances.
/// </summary>
/// <remarks>
/// <para>If true, disposing an instance of a <see cref="SsdpDeviceLocator"/>or a <see cref="ISsdpDevicePublisher"/> will not dispose this comms server instance. The calling code is responsible for managing the lifetime of the server.</para>
/// </remarks>
bool IsShared { get; set; }
}
}

@ -1,123 +0,0 @@
using System;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Interface for components that discover the existence of SSDP devices.
/// </summary>
/// <remarks>
/// <para>Discovering devices includes explicit search requests as well as listening for broadcast status notifications.</para>
/// </remarks>
/// <seealso cref="DiscoveredSsdpDevice"/>
/// <seealso cref="SsdpDevice"/>
/// <seealso cref="ISsdpDevicePublisher"/>
public interface ISsdpDeviceLocator
{
/// <summary>
/// Event raised when a device becomes available or is found by a search request.
/// </summary>
/// <seealso cref="NotificationFilter"/>
/// <seealso cref="DeviceUnavailable"/>
/// <seealso cref="StartListeningForNotifications"/>
/// <seealso cref="StopListeningForNotifications"/>
event EventHandler<DeviceAvailableEventArgs> DeviceAvailable;
/// <summary>
/// Event raised when a device explicitly notifies of shutdown or a device expires from the cache.
/// </summary>
/// <seeseealso cref="NotificationFilter"/>
/// <seealso cref="DeviceAvailable"/>
/// <seealso cref="StartListeningForNotifications"/>
/// <seealso cref="StopListeningForNotifications"/>
event EventHandler<DeviceUnavailableEventArgs> DeviceUnavailable;
/// <summary>
/// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the <see cref="DeviceAvailable"/> or <see cref="DeviceUnavailable"/> events.
/// </summary>
/// <remarks>
/// <para>Device alive/byebye notifications whose NT header does not match this filter value will still be captured and cached internally, but will not raise events about device availability. Usually used with either a device type of uuid NT header value.</para>
/// <para>Example filters follow;</para>
/// <example>upnp:rootdevice</example>
/// <example>urn:schemas-upnp-org:device:WANDevice:1</example>
/// <example>"uuid:9F15356CC-95FA-572E-0E99-85B456BD3012"</example>
/// </remarks>
/// <seealso cref="DeviceAvailable"/>
/// <seealso cref="DeviceUnavailable"/>
/// <seealso cref="StartListeningForNotifications"/>
/// <seealso cref="StopListeningForNotifications"/>
string NotificationFilter
{
get;
set;
}
/// <summary>
/// Asynchronously performs a search for all devices using the default search timeout, and returns an awaitable task that can be used to retrieve the results.
/// </summary>
/// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync();
/// <summary>
/// Performs a search for the specified search target (criteria) and default search timeout.
/// </summary>
/// <param name="searchTarget">The criteria for the search. Value can be;
/// <list type="table">
/// <item><term>Root devices</term><description>upnp:rootdevice</description></item>
/// <item><term>Specific device by UUID</term><description>uuid:&lt;device uuid&gt;</description></item>
/// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item>
/// </list>
/// </param>
/// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync(string searchTarget);
/// <summary>
/// Performs a search for the specified search target (criteria) and search timeout.
/// </summary>
/// <param name="searchTarget">The criteria for the search. Value can be;
/// <list type="table">
/// <item><term>Root devices</term><description>upnp:rootdevice</description></item>
/// <item><term>Specific device by UUID</term><description>uuid:&lt;device uuid&gt;</description></item>
/// <item><term>Device type</term><description>A device namespace and type in format of urn:&lt;device namespace&gt;:device:&lt;device type&gt;:&lt;device version&gt; i.e urn:schemas-upnp-org:device:Basic:1</description></item>
/// <item><term>Service type</term><description>A service namespace and type in format of urn:&lt;service namespace&gt;:service:&lt;servicetype&gt;:&lt;service version&gt; i.e urn:my-namespace:service:MyCustomService:1</description></item>
/// </list>
/// </param>
/// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 is recommended by the UPnP 1.1 specification. Specify TimeSpan.Zero to return only devices already in the cache.</param>
/// <remarks>
/// <para>By design RSSDP does not support 'publishing services' as it is intended for use with non-standard UPnP devices that don't publish UPnP style services. However, it is still possible to use RSSDP to search for devices implementing these services if you know the service type.</para>
/// </remarks>
/// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync(string searchTarget, TimeSpan searchWaitTime);
/// <summary>
/// Performs a search for all devices using the specified search timeout.
/// </summary>
/// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 is recommended by the UPnP 1.1 specification. Specify TimeSpan.Zero to return only devices already in the cache.</param>
/// <returns>A task whose result is an <see cref="System.Collections.Generic.IEnumerable{T}"/> of <see cref="DiscoveredSsdpDevice" /> instances, representing all found devices.</returns>
System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<DiscoveredSsdpDevice>> SearchAsync(TimeSpan searchWaitTime);
/// <summary>
/// Starts listening for broadcast notifications of service availability.
/// </summary>
/// <remarks>
/// <para>When called the system will listen for 'alive' and 'byebye' notifications. This can speed up searching, as well as provide dynamic notification of new devices appearing on the network, and previously discovered devices disappearing.</para>
/// </remarks>
/// <seealso cref="StopListeningForNotifications"/>
/// <seealso cref="DeviceAvailable"/>
/// <seealso cref="DeviceUnavailable"/>
/// <seealso cref="NotificationFilter"/>
void StartListeningForNotifications();
/// <summary>
/// Stops listening for broadcast notifications of service availability.
/// </summary>
/// <remarks>
/// <para>Does nothing if this instance is not already listening for notifications.</para>
/// </remarks>
/// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true.</exception>
/// <seealso cref="StartListeningForNotifications"/>
/// <seealso cref="DeviceAvailable"/>
/// <seealso cref="DeviceUnavailable"/>
/// <seealso cref="NotificationFilter"/>
void StopListeningForNotifications();
}
}

@ -1,35 +0,0 @@
using System.Threading.Tasks;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Interface for components that publish the existence of SSDP devices.
/// </summary>
/// <remarks>
/// <para>Publishing a device includes sending notifications (alive and byebye) as well as responding to search requests when appropriate.</para>
/// </remarks>
/// <seealso cref="SsdpRootDevice"/>
/// <seealso cref="ISsdpDeviceLocator"/>
public interface ISsdpDevicePublisher
{
/// <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>
/// <param name="device">The <see cref="SsdpRootDevice"/> instance to add.</param>
/// <returns>An awaitable <see cref="Task"/>.</returns>
void AddDevice(SsdpRootDevice device);
/// <summary>
/// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable.
/// </summary>
/// <param name="device">The <see cref="SsdpRootDevice"/> instance to add.</param>
/// <returns>An awaitable <see cref="Task"/>.</returns>
Task RemoveDevice(SsdpRootDevice device);
/// <summary>
/// Returns a read only list of devices being published by this instance.
/// </summary>
/// <seealso cref="SsdpDevice"/>
System.Collections.Generic.IEnumerable<SsdpRootDevice> Devices { get; }
}
}

@ -1,4 +0,0 @@
RSSDP
Copyright (c) 2015 Troy Willmot
Copyright (c) 2015-2018 Luke Pulverenti

@ -1,24 +0,0 @@
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("RSSDP")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")]
[assembly: AssemblyCopyright("Copyright © 2015 Troy Willmot. Code released under the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
[assembly: AssemblyVersion("1.0.3.0")]
[assembly: AssemblyFileVersion("2019.1.20.3")]

@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{21002819-C39A-4D3E-BE83-2A276A77FB1F}</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Jellyfin.Networking\Jellyfin.Networking.csproj" />
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
<Nullable>disable</Nullable>
<NoWarn>CA2016</NoWarn>
</PropertyGroup>
</Project>

@ -1,44 +0,0 @@
using System;
using System.Net;
using System.Net.Http;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Provides arguments for the <see cref="ISsdpCommunicationsServer.RequestReceived"/> event.
/// </summary>
public sealed class RequestReceivedEventArgs : EventArgs
{
private readonly HttpRequestMessage _Message;
private readonly IPEndPoint _ReceivedFrom;
public IPAddress LocalIPAddress { get; private set; }
/// <summary>
/// Full constructor.
/// </summary>
public RequestReceivedEventArgs(HttpRequestMessage message, IPEndPoint receivedFrom, IPAddress localIPAddress)
{
_Message = message;
_ReceivedFrom = receivedFrom;
LocalIPAddress = localIPAddress;
}
/// <summary>
/// The <see cref="HttpRequestMessage"/> that was received.
/// </summary>
public HttpRequestMessage Message
{
get { return _Message; }
}
/// <summary>
/// The <see cref="IPEndPoint"/> the request came from.
/// </summary>
public IPEndPoint ReceivedFrom
{
get { return _ReceivedFrom; }
}
}
}

@ -1,43 +0,0 @@
using System;
using System.Net;
using System.Net.Http;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Provides arguments for the <see cref="ISsdpCommunicationsServer.ResponseReceived"/> event.
/// </summary>
public sealed class ResponseReceivedEventArgs : EventArgs
{
public IPAddress LocalIPAddress { get; set; }
private readonly HttpResponseMessage _Message;
private readonly IPEndPoint _ReceivedFrom;
/// <summary>
/// Full constructor.
/// </summary>
public ResponseReceivedEventArgs(HttpResponseMessage message, IPEndPoint receivedFrom)
{
_Message = message;
_ReceivedFrom = receivedFrom;
}
/// <summary>
/// The <see cref="HttpResponseMessage"/> that was received.
/// </summary>
public HttpResponseMessage Message
{
get { return _Message; }
}
/// <summary>
/// The <see cref="IPEndPoint"/> the response came from.
/// </summary>
public IPEndPoint ReceivedFrom
{
get { return _ReceivedFrom; }
}
}
}

@ -1,523 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Provides the platform independent logic for publishing device existence and responding to search requests.
/// </summary>
public sealed class SsdpCommunicationsServer : DisposableManagedObjectBase, ISsdpCommunicationsServer
{
/* We could technically use one socket listening on port 1900 for everything.
* This should get both multicast (notifications) and unicast (search response) messages, however
* this often doesn't work under Windows because the MS SSDP service is running. If that service
* is running then it will steal the unicast messages and we will never see search responses.
* Since stopping the service would be a bad idea (might not be allowed security wise and might
* break other apps running on the system) the only other work around is to use two sockets.
*
* We use one group of sockets to listen for/receive notifications and search requests (_MulticastListenSockets).
* We use a second group, bound to a different local port, to send search requests and listen for
* responses (_SendSockets). The responses are sent to the local ports these sockets are bound to,
* which aren't port 1900 so the MS service doesn't steal them. While the caller can specify a local
* port to use, we will default to 0 which allows the underlying system to auto-assign a free port.
*/
private object _BroadcastListenSocketSynchroniser = new();
private List<Socket> _MulticastListenSockets;
private object _SendSocketSynchroniser = new();
private List<Socket> _sendSockets;
private HttpRequestParser _RequestParser;
private HttpResponseParser _ResponseParser;
private readonly ILogger _logger;
private ISocketFactory _SocketFactory;
private readonly INetworkManager _networkManager;
private int _LocalPort;
private int _MulticastTtl;
private bool _IsShared;
/// <summary>
/// Raised when a HTTPU request message is received by a socket (unicast or multicast).
/// </summary>
public event EventHandler<RequestReceivedEventArgs> RequestReceived;
/// <summary>
/// Raised when an HTTPU response message is received by a socket (unicast or multicast).
/// </summary>
public event EventHandler<ResponseReceivedEventArgs> ResponseReceived;
/// <summary>
/// Minimum constructor.
/// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
public SsdpCommunicationsServer(
ISocketFactory socketFactory,
INetworkManager networkManager,
ILogger logger)
: this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger)
{
}
/// <summary>
/// Full constructor.
/// </summary>
/// <exception cref="ArgumentNullException">The <paramref name="socketFactory"/> argument is null.</exception>
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="multicastTimeToLive"/> argument is less than or equal to zero.</exception>
public SsdpCommunicationsServer(
ISocketFactory socketFactory,
int localPort,
int multicastTimeToLive,
INetworkManager networkManager,
ILogger logger)
{
if (socketFactory is null)
{
throw new ArgumentNullException(nameof(socketFactory));
}
if (multicastTimeToLive <= 0)
{
throw new ArgumentOutOfRangeException(nameof(multicastTimeToLive), "multicastTimeToLive must be greater than zero.");
}
_BroadcastListenSocketSynchroniser = new();
_SendSocketSynchroniser = new();
_LocalPort = localPort;
_SocketFactory = socketFactory;
_RequestParser = new();
_ResponseParser = new();
_MulticastTtl = multicastTimeToLive;
_networkManager = networkManager;
_logger = logger;
}
/// <summary>
/// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications.
/// </summary>
/// <exception cref="ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception>
public void BeginListeningForMulticast()
{
ThrowIfDisposed();
lock (_BroadcastListenSocketSynchroniser)
{
if (_MulticastListenSockets is null)
{
try
{
_MulticastListenSockets = CreateMulticastSocketsAndListen();
}
catch (SocketException ex)
{
_logger.LogError("Failed to bind to multicast address: {Message}. DLNA will be unavailable", ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in BeginListeningForMulticast");
}
}
}
}
/// <summary>
/// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications.
/// </summary>
/// <exception cref="ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception>
public void StopListeningForMulticast()
{
lock (_BroadcastListenSocketSynchroniser)
{
if (_MulticastListenSockets is not null)
{
_logger.LogInformation("{0} disposing _BroadcastListenSocket", GetType().Name);
foreach (var socket in _MulticastListenSockets)
{
socket.Dispose();
}
_MulticastListenSockets = null;
}
}
}
/// <summary>
/// Sends a message to a particular address (uni or multicast) and port.
/// </summary>
public async Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromlocalIPAddress, CancellationToken cancellationToken)
{
if (messageData is null)
{
throw new ArgumentNullException(nameof(messageData));
}
ThrowIfDisposed();
var sockets = GetSendSockets(fromlocalIPAddress, destination);
if (sockets.Count == 0)
{
return;
}
// SSDP spec recommends sending messages multiple times (not more than 3) to account for possible packet loss over UDP.
for (var i = 0; i < SsdpConstants.UdpResendCount; i++)
{
var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken)).ToArray();
await Task.WhenAll(tasks).ConfigureAwait(false);
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
}
private async Task SendFromSocket(Socket socket, byte[] messageData, IPEndPoint destination, CancellationToken cancellationToken)
{
try
{
await socket.SendToAsync(messageData, destination, cancellationToken).ConfigureAwait(false);
}
catch (ObjectDisposedException)
{
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
var localIP = ((IPEndPoint)socket.LocalEndPoint).Address;
_logger.LogError(ex, "Error sending socket message from {0} to {1}", localIP.ToString(), destination.ToString());
}
}
private List<Socket> GetSendSockets(IPAddress fromlocalIPAddress, IPEndPoint destination)
{
EnsureSendSocketCreated();
lock (_SendSocketSynchroniser)
{
var sockets = _sendSockets.Where(s => s.AddressFamily == fromlocalIPAddress.AddressFamily);
// Send from the Any socket and the socket with the matching address
if (fromlocalIPAddress.AddressFamily == AddressFamily.InterNetwork)
{
sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Any)
|| ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromlocalIPAddress));
// If sending to the loopback address, filter the socket list as well
if (destination.Address.Equals(IPAddress.Loopback))
{
sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Any)
|| ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.Loopback));
}
}
else if (fromlocalIPAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Any)
|| ((IPEndPoint)s.LocalEndPoint).Address.Equals(fromlocalIPAddress));
// If sending to the loopback address, filter the socket list as well
if (destination.Address.Equals(IPAddress.IPv6Loopback))
{
sockets = sockets.Where(s => ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Any)
|| ((IPEndPoint)s.LocalEndPoint).Address.Equals(IPAddress.IPv6Loopback));
}
}
return sockets.ToList();
}
}
public Task SendMulticastMessage(string message, IPAddress fromlocalIPAddress, CancellationToken cancellationToken)
{
return SendMulticastMessage(message, SsdpConstants.UdpResendCount, fromlocalIPAddress, cancellationToken);
}
/// <summary>
/// Sends a message to the SSDP multicast address and port.
/// </summary>
public async Task SendMulticastMessage(string message, int sendCount, IPAddress fromlocalIPAddress, CancellationToken cancellationToken)
{
if (message is null)
{
throw new ArgumentNullException(nameof(message));
}
byte[] messageData = Encoding.UTF8.GetBytes(message);
ThrowIfDisposed();
cancellationToken.ThrowIfCancellationRequested();
EnsureSendSocketCreated();
// SSDP spec recommends sending messages multiple times (not more than 3) to account for possible packet loss over UDP.
for (var i = 0; i < sendCount; i++)
{
await SendMessageIfSocketNotDisposed(
messageData,
new IPEndPoint(
IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress),
SsdpConstants.MulticastPort),
fromlocalIPAddress,
cancellationToken).ConfigureAwait(false);
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Stops listening for search responses on the local, unicast socket.
/// </summary>
/// <exception cref="ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception>
public void StopListeningForResponses()
{
lock (_SendSocketSynchroniser)
{
if (_sendSockets is not null)
{
var sockets = _sendSockets.ToList();
_sendSockets = null;
_logger.LogInformation("{0} Disposing {1} sendSockets", GetType().Name, sockets.Count);
foreach (var socket in sockets)
{
var socketAddress = ((IPEndPoint)socket.LocalEndPoint).Address;
_logger.LogInformation("{0} disposing sendSocket from {1}", GetType().Name, socketAddress);
socket.Dispose();
}
}
}
}
/// <summary>
/// Gets or sets a boolean value indicating whether or not this instance is shared amongst multiple <see cref="SsdpDeviceLocator"/> and/or <see cref="ISsdpDevicePublisher"/> instances.
/// </summary>
/// <remarks>
/// <para>If true, disposing an instance of a <see cref="SsdpDeviceLocator"/>or a <see cref="ISsdpDevicePublisher"/> will not dispose this comms server instance. The calling code is responsible for managing the lifetime of the server.</para>
/// </remarks>
public bool IsShared
{
get { return _IsShared; }
set { _IsShared = value; }
}
/// <summary>
/// Stops listening for requests, disposes this instance and all internal resources.
/// </summary>
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
StopListeningForMulticast();
StopListeningForResponses();
}
}
private Task SendMessageIfSocketNotDisposed(byte[] messageData, IPEndPoint destination, IPAddress fromlocalIPAddress, CancellationToken cancellationToken)
{
var sockets = _sendSockets;
if (sockets is not null)
{
sockets = sockets.ToList();
var tasks = sockets.Where(s => fromlocalIPAddress is null || fromlocalIPAddress.Equals(((IPEndPoint)s.LocalEndPoint).Address))
.Select(s => SendFromSocket(s, messageData, destination, cancellationToken));
return Task.WhenAll(tasks);
}
return Task.CompletedTask;
}
private List<Socket> CreateMulticastSocketsAndListen()
{
var sockets = new List<Socket>();
var multicastGroupAddress = IPAddress.Parse(SsdpConstants.MulticastLocalAdminAddress);
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses()
.Where(x => x.Address is not null)
.Where(x => x.SupportsMulticast)
.Where(x => x.AddressFamily == AddressFamily.InterNetwork)
.DistinctBy(x => x.Index);
foreach (var intf in validInterfaces)
{
try
{
var socket = _SocketFactory.CreateUdpMulticastSocket(multicastGroupAddress, intf, _MulticastTtl, SsdpConstants.MulticastPort);
_ = ListenToSocketInternal(socket);
sockets.Add(socket);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create SSDP UDP multicast socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
}
}
return sockets;
}
private List<Socket> CreateSendSockets()
{
var sockets = new List<Socket>();
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses()
.Where(x => x.Address is not null)
.Where(x => x.SupportsMulticast)
.Where(x => x.AddressFamily == AddressFamily.InterNetwork);
if (OperatingSystem.IsMacOS())
{
// Manually remove loopback on macOS due to https://github.com/dotnet/runtime/issues/24340
validInterfaces = validInterfaces.Where(x => !x.Address.Equals(IPAddress.Loopback));
}
foreach (var intf in validInterfaces)
{
try
{
var socket = _SocketFactory.CreateSsdpUdpSocket(intf, _LocalPort);
_ = ListenToSocketInternal(socket);
sockets.Add(socket);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create SSDP UDP sender socket for {0} on interface {1} (index {2})", intf.Address, intf.Name, intf.Index);
}
}
return sockets;
}
private async Task ListenToSocketInternal(Socket socket)
{
var cancelled = false;
var receiveBuffer = new byte[8192];
while (!cancelled && !IsDisposed)
{
try
{
var result = await socket.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, _LocalPort), CancellationToken.None).ConfigureAwait(false);
if (result.ReceivedBytes > 0)
{
var remoteEndpoint = (IPEndPoint)result.RemoteEndPoint;
var localEndpointAdapter = _networkManager.GetAllBindInterfaces().First(a => a.Index == result.PacketInformation.Interface);
ProcessMessage(
Encoding.UTF8.GetString(receiveBuffer, 0, result.ReceivedBytes),
remoteEndpoint,
localEndpointAdapter.Address);
}
}
catch (ObjectDisposedException)
{
cancelled = true;
}
catch (TaskCanceledException)
{
cancelled = true;
}
}
}
private void EnsureSendSocketCreated()
{
if (_sendSockets is null)
{
lock (_SendSocketSynchroniser)
{
_sendSockets ??= CreateSendSockets();
}
}
}
private void ProcessMessage(string data, IPEndPoint endPoint, IPAddress receivedOnlocalIPAddress)
{
// Responses start with the HTTP version, prefixed with HTTP/ while
// requests start with a method which can vary and might be one we haven't
// seen/don't know. We'll check if this message is a request or a response
// by checking for the HTTP/ prefix on the start of the message.
_logger.LogDebug("Received data from {From} on {Port} at {Address}:\n{Data}", endPoint.Address, endPoint.Port, receivedOnlocalIPAddress, data);
if (data.StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase))
{
HttpResponseMessage responseMessage = null;
try
{
responseMessage = _ResponseParser.Parse(data);
}
catch (ArgumentException)
{
// Ignore invalid packets.
}
if (responseMessage is not null)
{
OnResponseReceived(responseMessage, endPoint, receivedOnlocalIPAddress);
}
}
else
{
HttpRequestMessage requestMessage = null;
try
{
requestMessage = _RequestParser.Parse(data);
}
catch (ArgumentException)
{
// Ignore invalid packets.
}
if (requestMessage is not null)
{
OnRequestReceived(requestMessage, endPoint, receivedOnlocalIPAddress);
}
}
}
private void OnRequestReceived(HttpRequestMessage data, IPEndPoint remoteEndPoint, IPAddress receivedOnlocalIPAddress)
{
// SSDP specification says only * is currently used but other uri's might
// be implemented in the future and should be ignored unless understood.
// Section 4.2 - http://tools.ietf.org/html/draft-cai-ssdp-v1-03#page-11
if (data.RequestUri.ToString() != "*")
{
return;
}
var handlers = RequestReceived;
handlers?.Invoke(this, new RequestReceivedEventArgs(data, remoteEndPoint, receivedOnlocalIPAddress));
}
private void OnResponseReceived(HttpResponseMessage data, IPEndPoint endPoint, IPAddress localIPAddress)
{
var handlers = ResponseReceived;
handlers?.Invoke(this, new ResponseReceivedEventArgs(data, endPoint)
{
LocalIPAddress = localIPAddress
});
}
}
}

@ -1,63 +0,0 @@
namespace Rssdp.Infrastructure
{
/// <summary>
/// Provides constants for common values related to the SSDP protocols.
/// </summary>
public static class SsdpConstants
{
/// <summary>
/// Multicast IP Address used for SSDP multicast messages. Values is 239.255.255.250.
/// </summary>
public const string MulticastLocalAdminAddress = "239.255.255.250";
/// <summary>
/// The UDP port used for SSDP multicast messages. Values is 1900.
/// </summary>
public const int MulticastPort = 1900;
/// <summary>
/// The default multicase TTL for SSDP multicast messages. Value is 4.
/// </summary>
public const int SsdpDefaultMulticastTimeToLive = 4;
internal const string MSearchMethod = "M-SEARCH";
internal const string SsdpDiscoverMessage = "ssdp:discover";
internal const string SsdpDiscoverAllSTHeader = "ssdp:all";
internal const string SsdpDeviceDescriptionXmlNamespace = "urn:schemas-upnp-org:device-1-0";
internal const string ServerVersion = "1.0";
/// <summary>
/// Default buffer size for receiving SSDP broadcasts. Value is 8192 (bytes).
/// </summary>
public const int DefaultUdpSocketBufferSize = 8192;
/// <summary>
/// The maximum possible buffer size for a UDP message. Value is 65507 (bytes).
/// </summary>
public const int MaxUdpSocketBufferSize = 65507; // Max possible UDP packet size on IPv4 without using 'jumbograms'.
/// <summary>
/// Namespace/prefix for UPnP device types. Values is schemas-upnp-org.
/// </summary>
public const string UpnpDeviceTypeNamespace = "schemas-upnp-org";
/// <summary>
/// UPnP Root Device type. Value is upnp:rootdevice.
/// </summary>
public const string UpnpDeviceTypeRootDevice = "upnp:rootdevice";
/// <summary>
/// The value is used by Windows Explorer for device searches instead of the UPNPDeviceTypeRootDevice constant.
/// Not sure why (different spec, bug, alternate protocol etc). Used to enable Windows Explorer support.
/// </summary>
public const string PnpDeviceTypeRootDevice = "pnp:rootdevice";
/// <summary>
/// UPnP Basic Device type. Value is Basic.
/// </summary>
public const string UpnpDeviceTypeBasicDevice = "Basic";
internal const string SsdpKeepAliveNotification = "ssdp:alive";
internal const string SsdpByeByeNotification = "ssdp:byebye";
internal const int UdpResendCount = 3;
}
}

@ -1,355 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using Rssdp.Infrastructure;
namespace Rssdp
{
/// <summary>
/// Base class representing the common details of a (root or embedded) device, either to be published or that has been located.
/// </summary>
/// <remarks>
/// <para>Do not derive new types directly from this class. New device classes should derive from either <see cref="SsdpRootDevice"/> or <see cref="SsdpEmbeddedDevice"/>.</para>
/// </remarks>
/// <seealso cref="SsdpRootDevice"/>
/// <seealso cref="SsdpEmbeddedDevice"/>
public abstract class SsdpDevice
{
private string _Udn;
private string _DeviceType;
private string _DeviceTypeNamespace;
private int _DeviceVersion;
private IList<SsdpDevice> _Devices;
/// <summary>
/// Raised when a new child device is added.
/// </summary>
/// <seealso cref="AddDevice"/>
/// <seealso cref="DeviceAdded"/>
public event EventHandler<DeviceEventArgs> DeviceAdded;
/// <summary>
/// Raised when a child device is removed.
/// </summary>
/// <seealso cref="RemoveDevice"/>
/// <seealso cref="DeviceRemoved"/>
public event EventHandler<DeviceEventArgs> DeviceRemoved;
/// <summary>
/// Derived type constructor, allows constructing a device with no parent. Should only be used from derived types that are or inherit from <see cref="SsdpRootDevice"/>.
/// </summary>
protected SsdpDevice()
{
_DeviceTypeNamespace = SsdpConstants.UpnpDeviceTypeNamespace;
_DeviceType = SsdpConstants.UpnpDeviceTypeBasicDevice;
_DeviceVersion = 1;
_Devices = new List<SsdpDevice>();
this.Devices = new ReadOnlyCollection<SsdpDevice>(_Devices);
}
public SsdpRootDevice ToRootDevice()
{
var device = this;
var rootDevice = device as SsdpRootDevice;
if (rootDevice == null)
{
rootDevice = ((SsdpEmbeddedDevice)device).RootDevice;
}
return rootDevice;
}
/// <summary>
/// Sets or returns the core device type (not including namespace, version etc.). Required.
/// </summary>
/// <remarks><para>Defaults to the UPnP basic device type.</para></remarks>
/// <seealso cref="DeviceTypeNamespace"/>
/// <seealso cref="DeviceVersion"/>
/// <seealso cref="FullDeviceType"/>
public string DeviceType
{
get
{
return _DeviceType;
}
set
{
_DeviceType = value;
}
}
public string DeviceClass { get; set; }
/// <summary>
/// Sets or returns the namespace for the <see cref="DeviceType"/> of this device. Optional, but defaults to UPnP schema so should be changed if <see cref="DeviceType"/> is not a UPnP device type.
/// </summary>
/// <remarks><para>Defaults to the UPnP standard namespace.</para></remarks>
/// <seealso cref="DeviceType"/>
/// <seealso cref="DeviceVersion"/>
/// <seealso cref="FullDeviceType"/>
public string DeviceTypeNamespace
{
get
{
return _DeviceTypeNamespace;
}
set
{
_DeviceTypeNamespace = value;
}
}
/// <summary>
/// Sets or returns the version of the device type. Optional, defaults to 1.
/// </summary>
/// <remarks><para>Defaults to a value of 1.</para></remarks>
/// <seealso cref="DeviceType"/>
/// <seealso cref="DeviceTypeNamespace"/>
/// <seealso cref="FullDeviceType"/>
public int DeviceVersion
{
get
{
return _DeviceVersion;
}
set
{
_DeviceVersion = value;
}
}
/// <summary>
/// Returns the full device type string.
/// </summary>
/// <remarks>
/// <para>The format used is urn:<see cref="DeviceTypeNamespace"/>:device:<see cref="DeviceType"/>:<see cref="DeviceVersion"/></para>
/// </remarks>
public string FullDeviceType
{
get
{
return String.Format(
CultureInfo.InvariantCulture,
"urn:{0}:{3}:{1}:{2}",
this.DeviceTypeNamespace ?? String.Empty,
this.DeviceType ?? String.Empty,
this.DeviceVersion,
this.DeviceClass ?? "device");
}
}
/// <summary>
/// Sets or returns the universally unique identifier for this device (without the uuid: prefix). Required.
/// </summary>
/// <remarks>
/// <para>Must be the same over time for a specific device instance (i.e. must survive reboots).</para>
/// <para>For UPnP 1.0 this can be any unique string. For UPnP 1.1 this should be a 128 bit number formatted in a specific way, preferably generated using the time and MAC based algorithm. See section 1.1.4 of http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf for details.</para>
/// <para>Technically this library implements UPnP 1.0, so any value is allowed, but we advise using UPnP 1.1 compatible values for good behaviour and forward compatibility with future versions.</para>
/// </remarks>
public string Uuid { get; set; }
/// <summary>
/// Returns (or sets*) a unique device name for this device. Optional, not recommended to be explicitly set.
/// </summary>
/// <remarks>
/// <para>* In general you should not explicitly set this property. If it is not set (or set to null/empty string) the property will return a UDN value that is correct as per the UPnP specification, based on the other device properties.</para>
/// <para>The setter is provided to allow for devices that do not correctly follow the specification (when we discover them), rather than to intentionally deviate from the specification.</para>
/// <para>If a value is explicitly set, it is used verbatim, and so any prefix (such as uuid:) must be provided in the value.</para>
/// </remarks>
public string Udn
{
get
{
if (String.IsNullOrEmpty(_Udn) && !String.IsNullOrEmpty(this.Uuid))
{
return "uuid:" + this.Uuid;
}
return _Udn;
}
set
{
_Udn = value;
}
}
/// <summary>
/// Sets or returns a friendly/display name for this device on the network. Something the user can identify the device/instance by, i.e Lounge Main Light. Required.
/// </summary>
/// <remarks><para>A short description for the end user. </para></remarks>
public string FriendlyName { get; set; }
/// <summary>
/// Sets or returns the name of the manufacturer of this device. Required.
/// </summary>
public string Manufacturer { get; set; }
/// <summary>
/// Sets or returns a URL to the manufacturers web site. Optional.
/// </summary>
public Uri ManufacturerUrl { get; set; }
/// <summary>
/// Sets or returns a description of this device model. Recommended.
/// </summary>
/// <remarks><para>A long description for the end user.</para></remarks>
public string ModelDescription { get; set; }
/// <summary>
/// Sets or returns the name of this model. Required.
/// </summary>
public string ModelName { get; set; }
/// <summary>
/// Sets or returns the number of this model. Recommended.
/// </summary>
public string ModelNumber { get; set; }
/// <summary>
/// Sets or returns a URL to a web page with details of this device model. Optional.
/// </summary>
/// <remarks>
/// <para>Optional. May be relative to base URL.</para>
/// </remarks>
public Uri ModelUrl { get; set; }
/// <summary>
/// Sets or returns the serial number for this device. Recommended.
/// </summary>
public string SerialNumber { get; set; }
/// <summary>
/// Sets or returns the universal product code of the device, if any. Optional.
/// </summary>
/// <remarks>
/// <para>If not blank, must be exactly 12 numeric digits.</para>
/// </remarks>
public string Upc { get; set; }
/// <summary>
/// Sets or returns the URL to a web page that can be used to configure/manager/use the device. Recommended.
/// </summary>
/// <remarks>
/// <para>May be relative to base URL. </para>
/// </remarks>
public Uri PresentationUrl { get; set; }
/// <summary>
/// Returns a read-only enumerable set of <see cref="SsdpDevice"/> objects representing children of this device. Child devices are optional.
/// </summary>
/// <seealso cref="AddDevice"/>
/// <seealso cref="RemoveDevice"/>
public IList<SsdpDevice> Devices
{
get;
private set;
}
/// <summary>
/// Adds a child device to the <see cref="Devices"/> collection.
/// </summary>
/// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance to add.</param>
/// <remarks>
/// <para>If the device is already a member of the <see cref="Devices"/> collection, this method does nothing.</para>
/// <para>Also sets the <see cref="SsdpEmbeddedDevice.RootDevice"/> property of the added device and all descendant devices to the relevant <see cref="SsdpRootDevice"/> instance.</para>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
/// <exception cref="InvalidOperationException">Thrown if the <paramref name="device"/> is already associated with a different <see cref="SsdpRootDevice"/> instance than used in this tree. Can occur if you try to add the same device instance to more than one tree. Also thrown if you try to add a device to itself.</exception>
/// <seealso cref="DeviceAdded"/>
public void AddDevice(SsdpEmbeddedDevice device)
{
if (device == null)
{
throw new ArgumentNullException(nameof(device));
}
if (device.RootDevice != null && device.RootDevice != this.ToRootDevice())
{
throw new InvalidOperationException("This device is already associated with a different root device (has been added as a child in another branch).");
}
if (device == this)
{
throw new InvalidOperationException("Can't add device to itself.");
}
bool wasAdded = false;
lock (_Devices)
{
device.RootDevice = this.ToRootDevice();
_Devices.Add(device);
wasAdded = true;
}
if (wasAdded)
{
OnDeviceAdded(device);
}
}
/// <summary>
/// Removes a child device from the <see cref="Devices"/> collection.
/// </summary>
/// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance to remove.</param>
/// <remarks>
/// <para>If the device is not a member of the <see cref="Devices"/> collection, this method does nothing.</para>
/// <para>Also sets the <see cref="SsdpEmbeddedDevice.RootDevice"/> property to null for the removed device and all descendant devices.</para>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception>
/// <seealso cref="DeviceRemoved"/>
public void RemoveDevice(SsdpEmbeddedDevice device)
{
if (device == null)
{
throw new ArgumentNullException(nameof(device));
}
bool wasRemoved = false;
lock (_Devices)
{
wasRemoved = _Devices.Remove(device);
if (wasRemoved)
{
device.RootDevice = null;
}
}
if (wasRemoved)
{
OnDeviceRemoved(device);
}
}
/// <summary>
/// Raises the <see cref="DeviceAdded"/> event.
/// </summary>
/// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance added to the <see cref="Devices"/> collection.</param>
/// <seealso cref="AddDevice"/>
/// <seealso cref="DeviceAdded"/>
protected virtual void OnDeviceAdded(SsdpEmbeddedDevice device)
{
var handlers = this.DeviceAdded;
handlers?.Invoke(this, new DeviceEventArgs(device));
}
/// <summary>
/// Raises the <see cref="DeviceRemoved"/> event.
/// </summary>
/// <param name="device">The <see cref="SsdpEmbeddedDevice"/> instance removed from the <see cref="Devices"/> collection.</param>
/// <seealso cref="RemoveDevice"/>
/// <see cref="DeviceRemoved"/>
protected virtual void OnDeviceRemoved(SsdpEmbeddedDevice device)
{
var handlers = this.DeviceRemoved;
handlers?.Invoke(this, new DeviceEventArgs(device));
}
}
}

@ -1,626 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Rssdp.Infrastructure
{
/// <summary>
/// Allows you to search the network for a particular device, device types, or UPnP service types. Also listenings for broadcast notifications of device availability and raises events to indicate changes in status.
/// </summary>
public class SsdpDeviceLocator : DisposableManagedObjectBase
{
private List<DiscoveredSsdpDevice> _Devices;
private ISsdpCommunicationsServer _CommunicationsServer;
private Timer _BroadcastTimer;
private object _timerLock = new();
private string _OSName;
private string _OSVersion;
private readonly TimeSpan DefaultSearchWaitTime = TimeSpan.FromSeconds(4);
private readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1);
/// <summary>
/// Default constructor.
/// </summary>
public SsdpDeviceLocator(
ISsdpCommunicationsServer communicationsServer,
string osName,
string osVersion)
{
ArgumentNullException.ThrowIfNull(communicationsServer);
ArgumentNullException.ThrowIfNullOrEmpty(osName);
ArgumentNullException.ThrowIfNullOrEmpty(osVersion);
_OSName = osName;
_OSVersion = osVersion;
_CommunicationsServer = communicationsServer;
_CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived;
_Devices = new List<DiscoveredSsdpDevice>();
}
/// <summary>
/// Raised for when
/// <list type="bullet">
/// <item>An 'alive' notification is received that a device, regardless of whether or not that device is not already in the cache or has previously raised this event.</item>
/// <item>For each item found during a device <see cref="SearchAsync(System.Threading.CancellationToken)"/> (cached or not), allowing clients to respond to found devices before the entire search is complete.</item>
/// <item>Only if the notification type matches the <see cref="NotificationFilter"/> property. By default the filter is null, meaning all notifications raise events (regardless of ant </item>
/// </list>
/// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para>
/// </summary>
/// <seealso cref="NotificationFilter"/>
/// <seealso cref="DeviceUnavailable"/>
/// <seealso cref="StartListeningForNotifications"/>
/// <seealso cref="StopListeningForNotifications"/>
public event EventHandler<DeviceAvailableEventArgs> DeviceAvailable;
/// <summary>
/// Raised when a notification is received that indicates a device has shutdown or otherwise become unavailable.
/// </summary>
/// <remarks>
/// <para>Devices *should* broadcast these types of notifications, but not all devices do and sometimes (in the event of power loss for example) it might not be possible for a device to do so. You should also implement error handling when trying to contact a device, even if RSSDP is reporting that device as available.</para>
/// <para>This event is only raised if the notification type matches the <see cref="NotificationFilter"/> property. A null or empty string for the <see cref="NotificationFilter"/> will be treated as no filter and raise the event for all notifications.</para>
/// <para>The <see cref="DeviceUnavailableEventArgs.DiscoveredDevice"/> property may contain either a fully complete <see cref="DiscoveredSsdpDevice"/> instance, or one containing just a USN and NotificationType property. Full information is available if the device was previously discovered and cached, but only partial information if a byebye notification was received for a previously unseen or expired device.</para>
/// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para>
/// </remarks>
/// <seealso cref="NotificationFilter"/>
/// <seealso cref="DeviceAvailable"/>
/// <seealso cref="StartListeningForNotifications"/>
/// <seealso cref="StopListeningForNotifications"/>
public event EventHandler<DeviceUnavailableEventArgs> DeviceUnavailable;
public void RestartBroadcastTimer(TimeSpan dueTime, TimeSpan period)
{
lock (_timerLock)
{
if (_BroadcastTimer is null)
{
_BroadcastTimer = new Timer(OnBroadcastTimerCallback, null, dueTime, period);
}
else
{
_BroadcastTimer.Change(dueTime, period);
}
}
}
public void DisposeBroadcastTimer()
{
lock (_timerLock)
{
if (_BroadcastTimer is not null)
{
_BroadcastTimer.Dispose();
_BroadcastTimer = null;
}
}
}
private async void OnBroadcastTimerCallback(object state)
{
if (IsDisposed)
{
return;
}
StartListeningForNotifications();
RemoveExpiredDevicesFromCache();
try
{
await SearchAsync(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception)
{
}
}
/// <summary>
/// Performs a search for all devices using the default search timeout.
/// </summary>
private Task SearchAsync(CancellationToken cancellationToken)
{
return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, DefaultSearchWaitTime, cancellationToken);
}
/// <summary>
/// Performs a search for the specified search target (criteria) and default search timeout.
/// </summary>
/// <param name="searchTarget">The criteria for the search. Value can be;
/// <list type="table">
/// <item><term>Root devices</term><description>upnp:rootdevice</description></item>
/// <item><term>Specific device by UUID</term><description>uuid:&lt;device uuid&gt;</description></item>
/// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item>
/// </list>
/// </param>
private Task SearchAsync(string searchTarget)
{
return SearchAsync(searchTarget, DefaultSearchWaitTime, CancellationToken.None);
}
/// <summary>
/// Performs a search for all devices using the specified search timeout.
/// </summary>
/// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 seconds is recommended by the UPnP 1.1 specification, this method requires the value be greater 1 second if it is not zero. Specify TimeSpan.Zero to return only devices already in the cache.</param>
private Task SearchAsync(TimeSpan searchWaitTime)
{
return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, searchWaitTime, CancellationToken.None);
}
private Task SearchAsync(string searchTarget, TimeSpan searchWaitTime, CancellationToken cancellationToken)
{
if (searchTarget is null)
{
throw new ArgumentNullException(nameof(searchTarget));
}
if (searchTarget.Length == 0)
{
throw new ArgumentException("searchTarget cannot be an empty string.", nameof(searchTarget));
}
if (searchWaitTime.TotalSeconds < 0)
{
throw new ArgumentException("searchWaitTime must be a positive time.");
}
if (searchWaitTime.TotalSeconds > 0 && searchWaitTime.TotalSeconds <= 1)
{
throw new ArgumentException("searchWaitTime must be zero (if you are not using the result and relying entirely in the events), or greater than one second.");
}
ThrowIfDisposed();
return BroadcastDiscoverMessage(searchTarget, SearchTimeToMXValue(searchWaitTime), cancellationToken);
}
/// <summary>
/// Starts listening for broadcast notifications of service availability.
/// </summary>
/// <remarks>
/// <para>When called the system will listen for 'alive' and 'byebye' notifications. This can speed up searching, as well as provide dynamic notification of new devices appearing on the network, and previously discovered devices disappearing.</para>
/// </remarks>
/// <seealso cref="StopListeningForNotifications"/>
/// <seealso cref="DeviceAvailable"/>
/// <seealso cref="DeviceUnavailable"/>
/// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> ty is true.</exception>
public void StartListeningForNotifications()
{
_CommunicationsServer.RequestReceived -= CommsServer_RequestReceived;
_CommunicationsServer.RequestReceived += CommsServer_RequestReceived;
_CommunicationsServer.BeginListeningForMulticast();
}
/// <summary>
/// Stops listening for broadcast notifications of service availability.
/// </summary>
/// <remarks>
/// <para>Does nothing if this instance is not already listening for notifications.</para>
/// </remarks>
/// <seealso cref="StartListeningForNotifications"/>
/// <seealso cref="DeviceAvailable"/>
/// <seealso cref="DeviceUnavailable"/>
/// <exception cref="ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true.</exception>
public void StopListeningForNotifications()
{
ThrowIfDisposed();
_CommunicationsServer.RequestReceived -= CommsServer_RequestReceived;
}
/// <summary>
/// Raises the <see cref="DeviceAvailable"/> event.
/// </summary>
/// <seealso cref="DeviceAvailable"/>
protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
{
if (IsDisposed)
{
return;
}
var handlers = DeviceAvailable;
handlers?.Invoke(this, new DeviceAvailableEventArgs(device, isNewDevice)
{
RemoteIPAddress = IPAddress
});
}
/// <summary>
/// Raises the <see cref="DeviceUnavailable"/> event.
/// </summary>
/// <param name="device">A <see cref="DiscoveredSsdpDevice"/> representing the device that is no longer available.</param>
/// <param name="expired">True if the device expired from the cache without being renewed, otherwise false to indicate the device explicitly notified us it was being shutdown.</param>
/// <seealso cref="DeviceUnavailable"/>
protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired)
{
if (IsDisposed)
{
return;
}
var handlers = DeviceUnavailable;
handlers?.Invoke(this, new DeviceUnavailableEventArgs(device, expired));
}
/// <summary>
/// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the <see cref="ISsdpDeviceLocator.DeviceAvailable"/> or <see cref="ISsdpDeviceLocator.DeviceUnavailable"/> events.
/// </summary>
/// <remarks>
/// <para>Device alive/byebye notifications whose NT header does not match this filter value will still be captured and cached internally, but will not raise events about device availability. Usually used with either a device type of uuid NT header value.</para>
/// <para>If the value is null or empty string then, all notifications are reported.</para>
/// <para>Example filters follow;</para>
/// <example>upnp:rootdevice</example>
/// <example>urn:schemas-upnp-org:device:WANDevice:1</example>
/// <example>uuid:9F15356CC-95FA-572E-0E99-85B456BD3012</example>
/// </remarks>
/// <seealso cref="ISsdpDeviceLocator.DeviceAvailable"/>
/// <seealso cref="ISsdpDeviceLocator.DeviceUnavailable"/>
/// <seealso cref="ISsdpDeviceLocator.StartListeningForNotifications"/>
/// <seealso cref="ISsdpDeviceLocator.StopListeningForNotifications"/>
public string NotificationFilter
{
get;
set;
}
/// <summary>
/// Disposes this object and all internal resources. Stops listening for all network messages.
/// </summary>
/// <param name="disposing">True if managed resources should be disposed, or false is only unmanaged resources should be cleaned up.</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
DisposeBroadcastTimer();
var commsServer = _CommunicationsServer;
_CommunicationsServer = null;
if (commsServer is not null)
{
commsServer.ResponseReceived -= CommsServer_ResponseReceived;
commsServer.RequestReceived -= CommsServer_RequestReceived;
}
}
}
private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IPAddress IPAddress)
{
bool isNewDevice = false;
lock (_Devices)
{
var existingDevice = FindExistingDeviceNotification(_Devices, device.NotificationType, device.Usn);
if (existingDevice is null)
{
_Devices.Add(device);
isNewDevice = true;
}
else
{
_Devices.Remove(existingDevice);
_Devices.Add(device);
}
}
DeviceFound(device, isNewDevice, IPAddress);
}
private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IPAddress IPAddress)
{
if (!NotificationTypeMatchesFilter(device))
{
return;
}
OnDeviceAvailable(device, isNewDevice, IPAddress);
}
private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device)
{
return String.IsNullOrEmpty(this.NotificationFilter)
|| this.NotificationFilter == SsdpConstants.SsdpDiscoverAllSTHeader
|| device.NotificationType == this.NotificationFilter;
}
private Task BroadcastDiscoverMessage(string serviceType, TimeSpan mxValue, CancellationToken cancellationToken)
{
const string header = "M-SEARCH * HTTP/1.1";
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
values["HOST"] = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", SsdpConstants.MulticastLocalAdminAddress, SsdpConstants.MulticastPort);
values["USER-AGENT"] = string.Format(CultureInfo.InvariantCulture, "{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, SsdpConstants.ServerVersion);
values["MAN"] = "\"ssdp:discover\"";
// Search target
values["ST"] = "ssdp:all";
// Seconds to delay response
values["MX"] = "3";
var message = BuildMessage(header, values);
return _CommunicationsServer.SendMulticastMessage(message, null, cancellationToken);
}
private void ProcessSearchResponseMessage(HttpResponseMessage message, IPAddress IPAddress)
{
if (!message.IsSuccessStatusCode)
{
return;
}
var location = GetFirstHeaderUriValue("Location", message);
if (location is not null)
{
var device = new DiscoveredSsdpDevice()
{
DescriptionLocation = location,
Usn = GetFirstHeaderStringValue("USN", message),
NotificationType = GetFirstHeaderStringValue("ST", message),
CacheLifetime = CacheAgeFromHeader(message.Headers.CacheControl),
AsAt = DateTimeOffset.Now,
ResponseHeaders = message.Headers
};
AddOrUpdateDiscoveredDevice(device, IPAddress);
}
}
private void ProcessNotificationMessage(HttpRequestMessage message, IPAddress IPAddress)
{
if (string.Compare(message.Method.Method, "Notify", StringComparison.OrdinalIgnoreCase) != 0)
{
return;
}
var notificationType = GetFirstHeaderStringValue("NTS", message);
if (string.Compare(notificationType, SsdpConstants.SsdpKeepAliveNotification, StringComparison.OrdinalIgnoreCase) == 0)
{
ProcessAliveNotification(message, IPAddress);
}
else if (string.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0)
{
ProcessByeByeNotification(message);
}
}
private void ProcessAliveNotification(HttpRequestMessage message, IPAddress IPAddress)
{
var location = GetFirstHeaderUriValue("Location", message);
if (location is not null)
{
var device = new DiscoveredSsdpDevice()
{
DescriptionLocation = location,
Usn = GetFirstHeaderStringValue("USN", message),
NotificationType = GetFirstHeaderStringValue("NT", message),
CacheLifetime = CacheAgeFromHeader(message.Headers.CacheControl),
AsAt = DateTimeOffset.Now,
ResponseHeaders = message.Headers
};
AddOrUpdateDiscoveredDevice(device, IPAddress);
}
}
private void ProcessByeByeNotification(HttpRequestMessage message)
{
var notficationType = GetFirstHeaderStringValue("NT", message);
if (!string.IsNullOrEmpty(notficationType))
{
var usn = GetFirstHeaderStringValue("USN", message);
if (!DeviceDied(usn, false))
{
var deadDevice = new DiscoveredSsdpDevice()
{
AsAt = DateTime.UtcNow,
CacheLifetime = TimeSpan.Zero,
DescriptionLocation = null,
NotificationType = GetFirstHeaderStringValue("NT", message),
Usn = usn,
ResponseHeaders = message.Headers
};
if (NotificationTypeMatchesFilter(deadDevice))
{
OnDeviceUnavailable(deadDevice, false);
}
}
}
}
private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message)
{
string retVal = null;
if (message.Headers.Contains(headerName))
{
message.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
retVal = values.FirstOrDefault();
}
}
return retVal;
}
private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message)
{
string retVal = null;
if (message.Headers.Contains(headerName))
{
message.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
retVal = values.FirstOrDefault();
}
}
return retVal;
}
private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request)
{
string value = null;
if (request.Headers.Contains(headerName))
{
request.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
value = values.FirstOrDefault();
}
}
Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var retVal);
return retVal;
}
private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response)
{
string value = null;
if (response.Headers.Contains(headerName))
{
response.Headers.TryGetValues(headerName, out var values);
if (values is not null)
{
value = values.FirstOrDefault();
}
}
Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var retVal);
return retVal;
}
private TimeSpan CacheAgeFromHeader(System.Net.Http.Headers.CacheControlHeaderValue headerValue)
{
if (headerValue is null)
{
return TimeSpan.Zero;
}
return headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero;
}
private void RemoveExpiredDevicesFromCache()
{
DiscoveredSsdpDevice[] expiredDevices = null;
lock (_Devices)
{
expiredDevices = (from device in _Devices where device.IsExpired() select device).ToArray();
foreach (var device in expiredDevices)
{
if (IsDisposed)
{
return;
}
_Devices.Remove(device);
}
}
// Don't do this inside lock because DeviceDied raises an event
// which means public code may execute during lock and cause
// problems.
foreach (var expiredUsn in (from expiredDevice in expiredDevices select expiredDevice.Usn).Distinct())
{
if (IsDisposed)
{
return;
}
DeviceDied(expiredUsn, true);
}
}
private bool DeviceDied(string deviceUsn, bool expired)
{
List<DiscoveredSsdpDevice> existingDevices = null;
lock (_Devices)
{
existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn);
foreach (var existingDevice in existingDevices)
{
if (IsDisposed)
{
return true;
}
_Devices.Remove(existingDevice);
}
}
if (existingDevices is not null && existingDevices.Count > 0)
{
foreach (var removedDevice in existingDevices)
{
if (NotificationTypeMatchesFilter(removedDevice))
{
OnDeviceUnavailable(removedDevice, expired);
}
}
return true;
}
return false;
}
private TimeSpan SearchTimeToMXValue(TimeSpan searchWaitTime)
{
if (searchWaitTime.TotalSeconds < 2 || searchWaitTime == TimeSpan.Zero)
{
return OneSecond;
}
return searchWaitTime.Subtract(OneSecond);
}
private DiscoveredSsdpDevice FindExistingDeviceNotification(IEnumerable<DiscoveredSsdpDevice> devices, string notificationType, string usn)
{
foreach (var d in devices)
{
if (d.NotificationType == notificationType && d.Usn == usn)
{
return d;
}
}
return null;
}
private List<DiscoveredSsdpDevice> FindExistingDeviceNotifications(IList<DiscoveredSsdpDevice> devices, string usn)
{
var list = new List<DiscoveredSsdpDevice>();
foreach (var d in devices)
{
if (d.Usn == usn)
{
list.Add(d);
}
}
return list;
}
private void CommsServer_ResponseReceived(object sender, ResponseReceivedEventArgs e)
{
ProcessSearchResponseMessage(e.Message, e.LocalIPAddress);
}
private void CommsServer_RequestReceived(object sender, RequestReceivedEventArgs e)
{
ProcessNotificationMessage(e.Message, e.ReceivedFrom.Address);
}
}
}

@ -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;
}
}
}
}

@ -1,40 +0,0 @@
namespace Rssdp
{
/// <summary>
/// Represents a device that is a descendant of a <see cref="SsdpRootDevice"/> instance.
/// </summary>
public class SsdpEmbeddedDevice : SsdpDevice
{
private SsdpRootDevice _RootDevice;
/// <summary>
/// Default constructor.
/// </summary>
public SsdpEmbeddedDevice()
{
}
/// <summary>
/// Returns the <see cref="SsdpRootDevice"/> that is this device's first ancestor. If this device is itself an <see cref="SsdpRootDevice"/>, then returns a reference to itself.
/// </summary>
public SsdpRootDevice RootDevice
{
get
{
return _RootDevice;
}
internal set
{
_RootDevice = value;
lock (this.Devices)
{
foreach (var embeddedDevice in this.Devices)
{
((SsdpEmbeddedDevice)embeddedDevice).RootDevice = _RootDevice;
}
}
}
}
}
}

@ -1,71 +0,0 @@
using System;
using System.Net;
namespace Rssdp
{
/// <summary>
/// Represents a 'root' device, a device that has no parent. Used for publishing devices and for the root device in a tree of discovered devices.
/// </summary>
/// <remarks>
/// <para>Child (embedded) devices are represented by the <see cref="SsdpDevice"/> in the <see cref="SsdpDevice.Devices"/> property.</para>
/// <para>Root devices contain some information that applies to the whole device tree and is therefore not present on child devices, such as <see cref="CacheLifetime"/> and <see cref="Location"/>.</para>
/// </remarks>
public class SsdpRootDevice : SsdpDevice
{
private Uri _UrlBase;
/// <summary>
/// Default constructor.
/// </summary>
public SsdpRootDevice() : base()
{
}
/// <summary>
/// Specifies how long clients can cache this device's details for. Optional but defaults to <see cref="TimeSpan.Zero"/> which means no-caching. Recommended value is half an hour.
/// </summary>
/// <remarks>
/// <para>Specify <see cref="TimeSpan.Zero"/> to indicate no caching allowed.</para>
/// <para>Also used to specify how often to rebroadcast alive notifications.</para>
/// <para>The UPnP/SSDP specifications indicate this should not be less than 1800 seconds (half an hour), but this is not enforced by this library.</para>
/// </remarks>
public TimeSpan CacheLifetime
{
get; set;
}
/// <summary>
/// Gets or sets the URL used to retrieve the description document for this device/tree. Required.
/// </summary>
public Uri Location { get; set; }
/// <summary>
/// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required.
/// </summary>
public IPAddress Address { get; set; }
/// <summary>
/// Gets or sets the prefix length used to check if the received message from same interface with this device/tree. Required.
/// </summary>
public byte PrefixLength { get; set; }
/// <summary>
/// The base URL to use for all relative url's provided in other properties (and those of child devices). Optional.
/// </summary>
/// <remarks>
/// <para>Defines the base URL. Used to construct fully-qualified URLs. All relative URLs that appear elsewhere in the description are combined with this base URL. If URLBase is empty or not given, the base URL is the URL from which the device description was retrieved (which is the preferred implementation; use of URLBase is no longer recommended). Specified by UPnP vendor. Single URL.</para>
/// </remarks>
public Uri UrlBase
{
get
{
return _UrlBase ?? this.Location;
}
set
{
_UrlBase = value;
}
}
}
}
Loading…
Cancel
Save