From a8c55ae8fec58b3aacd1cf311a8ac9c2dda503ff Mon Sep 17 00:00:00 2001 From: Patrick Barron Date: Thu, 9 Nov 2023 14:46:13 -0500 Subject: [PATCH] Remove RSSDP --- Jellyfin.sln | 6 - RSSDP/DeviceAvailableEventArgs.cs | 50 --- RSSDP/DeviceEventArgs.cs | 35 -- RSSDP/DeviceUnavailableEventArgs.cs | 47 -- RSSDP/DiscoveredSsdpDevice.cs | 74 ---- RSSDP/DisposableManagedObjectBase.cs | 76 ---- RSSDP/HttpParserBase.cs | 228 ---------- RSSDP/HttpRequestParser.cs | 88 ---- RSSDP/HttpResponseParser.cs | 90 ---- RSSDP/IEnumerableExtensions.cs | 34 -- RSSDP/ISsdpCommunicationsServer.cs | 52 --- RSSDP/ISsdpDeviceLocator.cs | 123 ------ RSSDP/ISsdpDevicePublisher.cs | 35 -- RSSDP/LICENSE | 4 - RSSDP/Properties/AssemblyInfo.cs | 24 - RSSDP/RSSDP.csproj | 21 - RSSDP/RequestReceivedEventArgs.cs | 44 -- RSSDP/ResponseReceivedEventArgs.cs | 43 -- RSSDP/SsdpCommunicationsServer.cs | 523 ---------------------- RSSDP/SsdpConstants.cs | 63 --- RSSDP/SsdpDevice.cs | 355 --------------- RSSDP/SsdpDeviceLocator.cs | 626 --------------------------- RSSDP/SsdpDevicePublisher.cs | 623 -------------------------- RSSDP/SsdpEmbeddedDevice.cs | 40 -- RSSDP/SsdpRootDevice.cs | 71 --- 25 files changed, 3375 deletions(-) delete mode 100644 RSSDP/DeviceAvailableEventArgs.cs delete mode 100644 RSSDP/DeviceEventArgs.cs delete mode 100644 RSSDP/DeviceUnavailableEventArgs.cs delete mode 100644 RSSDP/DiscoveredSsdpDevice.cs delete mode 100644 RSSDP/DisposableManagedObjectBase.cs delete mode 100644 RSSDP/HttpParserBase.cs delete mode 100644 RSSDP/HttpRequestParser.cs delete mode 100644 RSSDP/HttpResponseParser.cs delete mode 100644 RSSDP/IEnumerableExtensions.cs delete mode 100644 RSSDP/ISsdpCommunicationsServer.cs delete mode 100644 RSSDP/ISsdpDeviceLocator.cs delete mode 100644 RSSDP/ISsdpDevicePublisher.cs delete mode 100644 RSSDP/LICENSE delete mode 100644 RSSDP/Properties/AssemblyInfo.cs delete mode 100644 RSSDP/RSSDP.csproj delete mode 100644 RSSDP/RequestReceivedEventArgs.cs delete mode 100644 RSSDP/ResponseReceivedEventArgs.cs delete mode 100644 RSSDP/SsdpCommunicationsServer.cs delete mode 100644 RSSDP/SsdpConstants.cs delete mode 100644 RSSDP/SsdpDevice.cs delete mode 100644 RSSDP/SsdpDeviceLocator.cs delete mode 100644 RSSDP/SsdpDevicePublisher.cs delete mode 100644 RSSDP/SsdpEmbeddedDevice.cs delete mode 100644 RSSDP/SsdpRootDevice.cs diff --git a/Jellyfin.sln b/Jellyfin.sln index cf656bcba6..60a857a2ea 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -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 diff --git a/RSSDP/DeviceAvailableEventArgs.cs b/RSSDP/DeviceAvailableEventArgs.cs deleted file mode 100644 index f933f258be..0000000000 --- a/RSSDP/DeviceAvailableEventArgs.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Net; - -namespace Rssdp -{ - /// - /// Event arguments for the event. - /// - public sealed class DeviceAvailableEventArgs : EventArgs - { - public IPAddress RemoteIPAddress { get; set; } - - private readonly DiscoveredSsdpDevice _DiscoveredDevice; - - private readonly bool _IsNewlyDiscovered; - - /// - /// Full constructor. - /// - /// A instance representing the available device. - /// A boolean value indicating whether or not this device came from the cache. See for more detail. - /// Thrown if the parameter is null. - public DeviceAvailableEventArgs(DiscoveredSsdpDevice discoveredDevice, bool isNewlyDiscovered) - { - if (discoveredDevice == null) - { - throw new ArgumentNullException(nameof(discoveredDevice)); - } - - _DiscoveredDevice = discoveredDevice; - _IsNewlyDiscovered = isNewlyDiscovered; - } - - /// - /// 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. - /// - public bool IsNewlyDiscovered - { - get { return _IsNewlyDiscovered; } - } - - /// - /// A reference to a instance containing the discovered details and allowing access to the full device description. - /// - public DiscoveredSsdpDevice DiscoveredDevice - { - get { return _DiscoveredDevice; } - } - } -} diff --git a/RSSDP/DeviceEventArgs.cs b/RSSDP/DeviceEventArgs.cs deleted file mode 100644 index 2455ccbfad..0000000000 --- a/RSSDP/DeviceEventArgs.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Rssdp -{ - /// - /// Event arguments for the and events. - /// - public sealed class DeviceEventArgs : EventArgs - { - private readonly SsdpDevice _Device; - - /// - /// Constructs a new instance for the specified . - /// - /// The associated with the event this argument class is being used for. - /// Thrown if the argument is null. - public DeviceEventArgs(SsdpDevice device) - { - if (device == null) - { - throw new ArgumentNullException(nameof(device)); - } - - _Device = device; - } - - /// - /// Returns the instance the event being raised for. - /// - public SsdpDevice Device - { - get { return _Device; } - } - } -} diff --git a/RSSDP/DeviceUnavailableEventArgs.cs b/RSSDP/DeviceUnavailableEventArgs.cs deleted file mode 100644 index ca25152027..0000000000 --- a/RSSDP/DeviceUnavailableEventArgs.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace Rssdp -{ - /// - /// Event arguments for the event. - /// - public sealed class DeviceUnavailableEventArgs : EventArgs - { - private readonly DiscoveredSsdpDevice _DiscoveredDevice; - - private readonly bool _Expired; - - /// - /// Full constructor. - /// - /// A instance representing the device that has become unavailable. - /// A boolean value indicating whether this device is unavailable because it expired, or because it explicitly sent a byebye notification.. See for more detail. - /// Thrown if the parameter is null. - public DeviceUnavailableEventArgs(DiscoveredSsdpDevice discoveredDevice, bool expired) - { - if (discoveredDevice == null) - { - throw new ArgumentNullException(nameof(discoveredDevice)); - } - - _DiscoveredDevice = discoveredDevice; - _Expired = expired; - } - - /// - /// 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. - /// - public bool Expired - { - get { return _Expired; } - } - - /// - /// A reference to a instance containing the discovery details of the removed device. - /// - public DiscoveredSsdpDevice DiscoveredDevice - { - get { return _DiscoveredDevice; } - } - } -} diff --git a/RSSDP/DiscoveredSsdpDevice.cs b/RSSDP/DiscoveredSsdpDevice.cs deleted file mode 100644 index 322bd55e57..0000000000 --- a/RSSDP/DiscoveredSsdpDevice.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Net.Http.Headers; - -namespace Rssdp -{ - /// - /// 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. - /// - /// - /// - public sealed class DiscoveredSsdpDevice - { - private DateTimeOffset _AsAt; - - /// - /// Sets or returns the type of notification, being either a uuid, device type, service type or upnp:rootdevice. - /// - public string NotificationType { get; set; } - - /// - /// Sets or returns the universal service name (USN) of the device. - /// - public string Usn { get; set; } - - /// - /// Sets or returns a URL pointing to the device description document for this device. - /// - public Uri DescriptionLocation { get; set; } - - /// - /// Sets or returns the length of time this information is valid for (from the time). - /// - public TimeSpan CacheLifetime { get; set; } - - /// - /// Sets or returns the date and time this information was received. - /// - public DateTimeOffset AsAt - { - get { return _AsAt; } - - set - { - if (_AsAt != value) - { - _AsAt = value; - } - } - } - - /// - /// Returns the headers from the SSDP device response message. - /// - public HttpHeaders ResponseHeaders { get; set; } - - /// - /// Returns true if this device information has expired, based on the current date/time, and the & properties. - /// - /// - public bool IsExpired() - { - return this.CacheLifetime == TimeSpan.Zero || this.AsAt.Add(this.CacheLifetime) <= DateTimeOffset.Now; - } - - /// - /// Returns the device's value. - /// - /// A string containing the device's universal service name. - public override string ToString() - { - return this.Usn; - } - } -} diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs deleted file mode 100644 index 5d7da4124e..0000000000 --- a/RSSDP/DisposableManagedObjectBase.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; - -namespace Rssdp.Infrastructure -{ - /// - /// Correctly implements the interface and pattern for an object containing only managed resources, and adds a few common niceties not on the interface such as an property. - /// - public abstract class DisposableManagedObjectBase : IDisposable - { - /// - /// Override this method and dispose any objects you own the lifetime of if disposing is true; - /// - /// True if managed objects should be disposed, if false, only unmanaged resources should be released. - protected abstract void Dispose(bool disposing); - - /// - /// Throws and if the property is true. - /// - /// - /// Thrown if the property is true. - /// - protected virtual void ThrowIfDisposed() - { - if (this.IsDisposed) - { - throw new ObjectDisposedException(this.GetType().FullName); - } - } - - /// - /// Sets or returns a boolean indicating whether or not this instance has been disposed. - /// - /// - public bool IsDisposed - { - get; - private set; - } - - public string BuildMessage(string header, Dictionary 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(); - } - - /// - /// Disposes this object instance and all internally managed resources. - /// - /// - /// Sets the property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behavior of derived classes. - /// - /// - [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); - } - } -} diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs deleted file mode 100644 index 1949a9df33..0000000000 --- a/RSSDP/HttpParserBase.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; - -namespace Rssdp.Infrastructure -{ - /// - /// A base class for the and classes. Not intended for direct use. - /// - /// - public abstract class HttpParserBase where T : new() - { - private readonly string[] LineTerminators = new string[] { "\r\n", "\n" }; - private readonly char[] SeparatorCharacters = new char[] { ',', ';' }; - - /// - /// Parses the provided into either a or object. - /// - /// A string containing the HTTP message to parse. - /// Either a or object containing the parsed data. - public abstract T Parse(string data); - - /// - /// Parses a string containing either an HTTP request or response into a or object. - /// - /// A or object representing the parsed message. - /// A reference to the collection for the object. - /// A string containing the data to be parsed. - /// An object containing the content of the parsed message. - [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())) - { - 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); - } - } - - /// - /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the . - /// - /// The first line of the HTTP message to be parsed. - /// Either a or to assign the parsed values to. - protected abstract void ParseStatusLine(string data, T message); - - /// - /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). - /// - /// A string containing the name of the header to return the type of. - protected abstract bool IsContentHeader(string headerName); - - /// - /// Parses the HTTP version text from an HTTP request or response status line and returns a object representing the parsed values. - /// - /// A string containing the HTTP version, from the message status line. - /// A object containing the parsed version data. - 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)); - } - - /// - /// Parses a line from an HTTP request or response message containing a header name and value pair. - /// - /// A string containing the data to be parsed. - /// A reference to a collection to which the parsed header will be added. - /// A reference to a collection for the message content, to which the parsed header will be added. - 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 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(); - - 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; - } - } -} diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs deleted file mode 100644 index fab70eae2c..0000000000 --- a/RSSDP/HttpRequestParser.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Net.Http; -using Jellyfin.Extensions; - -namespace Rssdp.Infrastructure -{ - /// - /// Parses a string into a or throws an exception. - /// - public sealed class HttpRequestParser : HttpParserBase - { - 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" - }; - - /// - /// Parses the specified data into a instance. - /// - /// A string containing the data to parse. - /// A instance containing the parsed data. - public override HttpRequestMessage Parse(string data) - { - HttpRequestMessage retVal = null; - - try - { - retVal = new HttpRequestMessage(); - - Parse(retVal, retVal.Headers, data); - - return retVal; - } - finally - { - retVal?.Dispose(); - } - } - - /// - /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the . - /// - /// The first line of the HTTP message to be parsed. - /// Either a or to assign the parsed values to. - 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()); - } - } - - /// - /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). - /// - /// A string containing the name of the header to return the type of. - protected override bool IsContentHeader(string headerName) - { - return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase); - } - } -} diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs deleted file mode 100644 index c570c84cbb..0000000000 --- a/RSSDP/HttpResponseParser.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using Jellyfin.Extensions; - -namespace Rssdp.Infrastructure -{ - /// - /// Parses a string into a or throws an exception. - /// - public sealed class HttpResponseParser : HttpParserBase - { - 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" - }; - - /// - /// Parses the specified data into a instance. - /// - /// A string containing the data to parse. - /// A instance containing the parsed data. - public override HttpResponseMessage Parse(string data) - { - HttpResponseMessage retVal = null; - try - { - retVal = new HttpResponseMessage(); - - Parse(retVal, retVal.Headers, data); - - return retVal; - } - catch - { - retVal?.Dispose(); - - throw; - } - } - - /// - /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). - /// - /// A string containing the name of the header to return the type of. - /// A boolean, true if th specified header relates to HTTP content, otherwise false. - protected override bool IsContentHeader(string headerName) - { - return ContentHeaderNames.Contains(headerName, StringComparison.OrdinalIgnoreCase); - } - - /// - /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the . - /// - /// The first line of the HTTP message to be parsed. - /// Either a or to assign the parsed values to. - 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(); - } - } - } -} diff --git a/RSSDP/IEnumerableExtensions.cs b/RSSDP/IEnumerableExtensions.cs deleted file mode 100644 index 1f0daad3e1..0000000000 --- a/RSSDP/IEnumerableExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Rssdp.Infrastructure -{ - internal static class IEnumerableExtensions - { - public static IEnumerable SelectManyRecursive(this IEnumerable source, Func> 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 EmptyIfNull(this IEnumerable source) - { - return source ?? Enumerable.Empty(); - } - } -} diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs deleted file mode 100644 index 95b0a1c704..0000000000 --- a/RSSDP/ISsdpCommunicationsServer.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; - -namespace Rssdp.Infrastructure -{ - /// - /// Interface for a component that manages network communication (sending and receiving HTTPU messages) for the SSDP protocol. - /// - public interface ISsdpCommunicationsServer : IDisposable - { - /// - /// Raised when a HTTPU request message is received by a socket (unicast or multicast). - /// - event EventHandler RequestReceived; - - /// - /// Raised when an HTTPU response message is received by a socket (unicast or multicast). - /// - event EventHandler ResponseReceived; - - /// - /// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications. - /// - void BeginListeningForMulticast(); - - /// - /// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications. - /// - void StopListeningForMulticast(); - - /// - /// Sends a message to a particular address (uni or multicast) and port. - /// - Task SendMessage(byte[] messageData, IPEndPoint destination, IPAddress fromLocalIPAddress, CancellationToken cancellationToken); - - /// - /// Sends a message to the SSDP multicast address and port. - /// - Task SendMulticastMessage(string message, IPAddress fromLocalIPAddress, CancellationToken cancellationToken); - Task SendMulticastMessage(string message, int sendCount, IPAddress fromLocalIPAddress, CancellationToken cancellationToken); - - /// - /// Gets or sets a boolean value indicating whether or not this instance is shared amongst multiple and/or instances. - /// - /// - /// If true, disposing an instance of a or a will not dispose this comms server instance. The calling code is responsible for managing the lifetime of the server. - /// - bool IsShared { get; set; } - } -} diff --git a/RSSDP/ISsdpDeviceLocator.cs b/RSSDP/ISsdpDeviceLocator.cs deleted file mode 100644 index 4df166cd26..0000000000 --- a/RSSDP/ISsdpDeviceLocator.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; - -namespace Rssdp.Infrastructure -{ - /// - /// Interface for components that discover the existence of SSDP devices. - /// - /// - /// Discovering devices includes explicit search requests as well as listening for broadcast status notifications. - /// - /// - /// - /// - public interface ISsdpDeviceLocator - { - /// - /// Event raised when a device becomes available or is found by a search request. - /// - /// - /// - /// - /// - event EventHandler DeviceAvailable; - - /// - /// Event raised when a device explicitly notifies of shutdown or a device expires from the cache. - /// - /// - /// - /// - /// - event EventHandler DeviceUnavailable; - - /// - /// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the or events. - /// - /// - /// 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. - /// Example filters follow; - /// upnp:rootdevice - /// urn:schemas-upnp-org:device:WANDevice:1 - /// "uuid:9F15356CC-95FA-572E-0E99-85B456BD3012" - /// - /// - /// - /// - /// - string NotificationFilter - { - get; - set; - } - - /// - /// 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. - /// - /// A task whose result is an of instances, representing all found devices. - System.Threading.Tasks.Task> SearchAsync(); - - /// - /// Performs a search for the specified search target (criteria) and default search timeout. - /// - /// The criteria for the search. Value can be; - /// - /// Root devicesupnp:rootdevice - /// Specific device by UUIDuuid:<device uuid> - /// Device typeFully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1 - /// - /// - /// A task whose result is an of instances, representing all found devices. - System.Threading.Tasks.Task> SearchAsync(string searchTarget); - - /// - /// Performs a search for the specified search target (criteria) and search timeout. - /// - /// The criteria for the search. Value can be; - /// - /// Root devicesupnp:rootdevice - /// Specific device by UUIDuuid:<device uuid> - /// Device typeA device namespace and type in format of urn:<device namespace>:device:<device type>:<device version> i.e urn:schemas-upnp-org:device:Basic:1 - /// Service typeA service namespace and type in format of urn:<service namespace>:service:<servicetype>:<service version> i.e urn:my-namespace:service:MyCustomService:1 - /// - /// - /// 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. - /// - /// 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. - /// - /// A task whose result is an of instances, representing all found devices. - System.Threading.Tasks.Task> SearchAsync(string searchTarget, TimeSpan searchWaitTime); - - /// - /// Performs a search for all devices using the specified search timeout. - /// - /// 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. - /// A task whose result is an of instances, representing all found devices. - System.Threading.Tasks.Task> SearchAsync(TimeSpan searchWaitTime); - - /// - /// Starts listening for broadcast notifications of service availability. - /// - /// - /// 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. - /// - /// - /// - /// - /// - void StartListeningForNotifications(); - - /// - /// Stops listening for broadcast notifications of service availability. - /// - /// - /// Does nothing if this instance is not already listening for notifications. - /// - /// Throw if the property is true. - /// - /// - /// - /// - void StopListeningForNotifications(); - } -} diff --git a/RSSDP/ISsdpDevicePublisher.cs b/RSSDP/ISsdpDevicePublisher.cs deleted file mode 100644 index 96c15443d4..0000000000 --- a/RSSDP/ISsdpDevicePublisher.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Threading.Tasks; - -namespace Rssdp.Infrastructure -{ - /// - /// Interface for components that publish the existence of SSDP devices. - /// - /// - /// Publishing a device includes sending notifications (alive and byebye) as well as responding to search requests when appropriate. - /// - /// - /// - public interface ISsdpDevicePublisher - { - /// - /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients. - /// - /// The instance to add. - /// An awaitable . - void AddDevice(SsdpRootDevice device); - - /// - /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable. - /// - /// The instance to add. - /// An awaitable . - Task RemoveDevice(SsdpRootDevice device); - - /// - /// Returns a read only list of devices being published by this instance. - /// - /// - System.Collections.Generic.IEnumerable Devices { get; } - } -} diff --git a/RSSDP/LICENSE b/RSSDP/LICENSE deleted file mode 100644 index aabeb93af0..0000000000 --- a/RSSDP/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -RSSDP - -Copyright (c) 2015 Troy Willmot -Copyright (c) 2015-2018 Luke Pulverenti diff --git a/RSSDP/Properties/AssemblyInfo.cs b/RSSDP/Properties/AssemblyInfo.cs deleted file mode 100644 index 55f7b6a834..0000000000 --- a/RSSDP/Properties/AssemblyInfo.cs +++ /dev/null @@ -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")] diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj deleted file mode 100644 index 3f24de4e65..0000000000 --- a/RSSDP/RSSDP.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - - {21002819-C39A-4D3E-BE83-2A276A77FB1F} - - - - - - - - - net8.0 - false - AllDisabledByDefault - disable - CA2016 - - - diff --git a/RSSDP/RequestReceivedEventArgs.cs b/RSSDP/RequestReceivedEventArgs.cs deleted file mode 100644 index b8b2249e42..0000000000 --- a/RSSDP/RequestReceivedEventArgs.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; - -namespace Rssdp.Infrastructure -{ - /// - /// Provides arguments for the event. - /// - public sealed class RequestReceivedEventArgs : EventArgs - { - private readonly HttpRequestMessage _Message; - - private readonly IPEndPoint _ReceivedFrom; - - public IPAddress LocalIPAddress { get; private set; } - - /// - /// Full constructor. - /// - public RequestReceivedEventArgs(HttpRequestMessage message, IPEndPoint receivedFrom, IPAddress localIPAddress) - { - _Message = message; - _ReceivedFrom = receivedFrom; - LocalIPAddress = localIPAddress; - } - - /// - /// The that was received. - /// - public HttpRequestMessage Message - { - get { return _Message; } - } - - /// - /// The the request came from. - /// - public IPEndPoint ReceivedFrom - { - get { return _ReceivedFrom; } - } - } -} diff --git a/RSSDP/ResponseReceivedEventArgs.cs b/RSSDP/ResponseReceivedEventArgs.cs deleted file mode 100644 index e87ba14524..0000000000 --- a/RSSDP/ResponseReceivedEventArgs.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; - -namespace Rssdp.Infrastructure -{ - /// - /// Provides arguments for the event. - /// - public sealed class ResponseReceivedEventArgs : EventArgs - { - public IPAddress LocalIPAddress { get; set; } - - private readonly HttpResponseMessage _Message; - - private readonly IPEndPoint _ReceivedFrom; - - /// - /// Full constructor. - /// - public ResponseReceivedEventArgs(HttpResponseMessage message, IPEndPoint receivedFrom) - { - _Message = message; - _ReceivedFrom = receivedFrom; - } - - /// - /// The that was received. - /// - public HttpResponseMessage Message - { - get { return _Message; } - } - - /// - /// The the response came from. - /// - public IPEndPoint ReceivedFrom - { - get { return _ReceivedFrom; } - } - } -} diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs deleted file mode 100644 index 42563e2edb..0000000000 --- a/RSSDP/SsdpCommunicationsServer.cs +++ /dev/null @@ -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 -{ - /// - /// Provides the platform independent logic for publishing device existence and responding to search requests. - /// - 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 _MulticastListenSockets; - - private object _SendSocketSynchroniser = new(); - private List _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; - - /// - /// Raised when a HTTPU request message is received by a socket (unicast or multicast). - /// - public event EventHandler RequestReceived; - - /// - /// Raised when an HTTPU response message is received by a socket (unicast or multicast). - /// - public event EventHandler ResponseReceived; - - /// - /// Minimum constructor. - /// - /// The argument is null. - public SsdpCommunicationsServer( - ISocketFactory socketFactory, - INetworkManager networkManager, - ILogger logger) - : this(socketFactory, 0, SsdpConstants.SsdpDefaultMulticastTimeToLive, networkManager, logger) - { - - } - - /// - /// Full constructor. - /// - /// The argument is null. - /// The argument is less than or equal to zero. - 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; - } - - /// - /// Causes the server to begin listening for multicast messages, being SSDP search requests and notifications. - /// - /// Thrown if the property is true (because has been called previously). - 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"); - } - } - } - } - - /// - /// Causes the server to stop listening for multicast messages, being SSDP search requests and notifications. - /// - /// Thrown if the property is true (because has been called previously). - 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; - } - } - } - - /// - /// Sends a message to a particular address (uni or multicast) and port. - /// - 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 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); - } - - /// - /// Sends a message to the SSDP multicast address and port. - /// - 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); - } - } - - /// - /// Stops listening for search responses on the local, unicast socket. - /// - /// Thrown if the property is true (because has been called previously). - 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(); - } - } - } - } - - /// - /// Gets or sets a boolean value indicating whether or not this instance is shared amongst multiple and/or instances. - /// - /// - /// If true, disposing an instance of a or a will not dispose this comms server instance. The calling code is responsible for managing the lifetime of the server. - /// - public bool IsShared - { - get { return _IsShared; } - - set { _IsShared = value; } - } - - /// - /// Stops listening for requests, disposes this instance and all internal resources. - /// - /// - 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 CreateMulticastSocketsAndListen() - { - var sockets = new List(); - 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 CreateSendSockets() - { - var sockets = new List(); - - // 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 - }); - } - } -} diff --git a/RSSDP/SsdpConstants.cs b/RSSDP/SsdpConstants.cs deleted file mode 100644 index 442f2b8f84..0000000000 --- a/RSSDP/SsdpConstants.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Rssdp.Infrastructure -{ - /// - /// Provides constants for common values related to the SSDP protocols. - /// - public static class SsdpConstants - { - - /// - /// Multicast IP Address used for SSDP multicast messages. Values is 239.255.255.250. - /// - public const string MulticastLocalAdminAddress = "239.255.255.250"; - /// - /// The UDP port used for SSDP multicast messages. Values is 1900. - /// - public const int MulticastPort = 1900; - /// - /// The default multicase TTL for SSDP multicast messages. Value is 4. - /// - 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"; - - /// - /// Default buffer size for receiving SSDP broadcasts. Value is 8192 (bytes). - /// - public const int DefaultUdpSocketBufferSize = 8192; - /// - /// The maximum possible buffer size for a UDP message. Value is 65507 (bytes). - /// - public const int MaxUdpSocketBufferSize = 65507; // Max possible UDP packet size on IPv4 without using 'jumbograms'. - - /// - /// Namespace/prefix for UPnP device types. Values is schemas-upnp-org. - /// - public const string UpnpDeviceTypeNamespace = "schemas-upnp-org"; - /// - /// UPnP Root Device type. Value is upnp:rootdevice. - /// - public const string UpnpDeviceTypeRootDevice = "upnp:rootdevice"; - /// - /// 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. - /// - public const string PnpDeviceTypeRootDevice = "pnp:rootdevice"; - /// - /// UPnP Basic Device type. Value is Basic. - /// - public const string UpnpDeviceTypeBasicDevice = "Basic"; - - internal const string SsdpKeepAliveNotification = "ssdp:alive"; - internal const string SsdpByeByeNotification = "ssdp:byebye"; - - internal const int UdpResendCount = 3; - } -} diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs deleted file mode 100644 index 569d733ea0..0000000000 --- a/RSSDP/SsdpDevice.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using Rssdp.Infrastructure; - -namespace Rssdp -{ - /// - /// Base class representing the common details of a (root or embedded) device, either to be published or that has been located. - /// - /// - /// Do not derive new types directly from this class. New device classes should derive from either or . - /// - /// - /// - public abstract class SsdpDevice - { - private string _Udn; - private string _DeviceType; - private string _DeviceTypeNamespace; - private int _DeviceVersion; - - private IList _Devices; - - /// - /// Raised when a new child device is added. - /// - /// - /// - public event EventHandler DeviceAdded; - - /// - /// Raised when a child device is removed. - /// - /// - /// - public event EventHandler DeviceRemoved; - - /// - /// Derived type constructor, allows constructing a device with no parent. Should only be used from derived types that are or inherit from . - /// - protected SsdpDevice() - { - _DeviceTypeNamespace = SsdpConstants.UpnpDeviceTypeNamespace; - _DeviceType = SsdpConstants.UpnpDeviceTypeBasicDevice; - _DeviceVersion = 1; - - _Devices = new List(); - this.Devices = new ReadOnlyCollection(_Devices); - } - - public SsdpRootDevice ToRootDevice() - { - var device = this; - - var rootDevice = device as SsdpRootDevice; - if (rootDevice == null) - { - rootDevice = ((SsdpEmbeddedDevice)device).RootDevice; - } - - return rootDevice; - } - - /// - /// Sets or returns the core device type (not including namespace, version etc.). Required. - /// - /// Defaults to the UPnP basic device type. - /// - /// - /// - public string DeviceType - { - get - { - return _DeviceType; - } - - set - { - _DeviceType = value; - } - } - - public string DeviceClass { get; set; } - - /// - /// Sets or returns the namespace for the of this device. Optional, but defaults to UPnP schema so should be changed if is not a UPnP device type. - /// - /// Defaults to the UPnP standard namespace. - /// - /// - /// - public string DeviceTypeNamespace - { - get - { - return _DeviceTypeNamespace; - } - - set - { - _DeviceTypeNamespace = value; - } - } - - /// - /// Sets or returns the version of the device type. Optional, defaults to 1. - /// - /// Defaults to a value of 1. - /// - /// - /// - public int DeviceVersion - { - get - { - return _DeviceVersion; - } - - set - { - _DeviceVersion = value; - } - } - - /// - /// Returns the full device type string. - /// - /// - /// The format used is urn::device:: - /// - 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"); - } - } - - /// - /// Sets or returns the universally unique identifier for this device (without the uuid: prefix). Required. - /// - /// - /// Must be the same over time for a specific device instance (i.e. must survive reboots). - /// 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. - /// 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. - /// - public string Uuid { get; set; } - - /// - /// Returns (or sets*) a unique device name for this device. Optional, not recommended to be explicitly set. - /// - /// - /// * 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. - /// 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. - /// If a value is explicitly set, it is used verbatim, and so any prefix (such as uuid:) must be provided in the value. - /// - public string Udn - { - get - { - if (String.IsNullOrEmpty(_Udn) && !String.IsNullOrEmpty(this.Uuid)) - { - return "uuid:" + this.Uuid; - } - - return _Udn; - } - - set - { - _Udn = value; - } - } - - /// - /// 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. - /// - /// A short description for the end user. - public string FriendlyName { get; set; } - - /// - /// Sets or returns the name of the manufacturer of this device. Required. - /// - public string Manufacturer { get; set; } - - /// - /// Sets or returns a URL to the manufacturers web site. Optional. - /// - public Uri ManufacturerUrl { get; set; } - - /// - /// Sets or returns a description of this device model. Recommended. - /// - /// A long description for the end user. - public string ModelDescription { get; set; } - - /// - /// Sets or returns the name of this model. Required. - /// - public string ModelName { get; set; } - - /// - /// Sets or returns the number of this model. Recommended. - /// - public string ModelNumber { get; set; } - - /// - /// Sets or returns a URL to a web page with details of this device model. Optional. - /// - /// - /// Optional. May be relative to base URL. - /// - public Uri ModelUrl { get; set; } - - /// - /// Sets or returns the serial number for this device. Recommended. - /// - public string SerialNumber { get; set; } - - /// - /// Sets or returns the universal product code of the device, if any. Optional. - /// - /// - /// If not blank, must be exactly 12 numeric digits. - /// - public string Upc { get; set; } - - /// - /// Sets or returns the URL to a web page that can be used to configure/manager/use the device. Recommended. - /// - /// - /// May be relative to base URL. - /// - public Uri PresentationUrl { get; set; } - - /// - /// Returns a read-only enumerable set of objects representing children of this device. Child devices are optional. - /// - /// - /// - public IList Devices - { - get; - private set; - } - - /// - /// Adds a child device to the collection. - /// - /// The instance to add. - /// - /// If the device is already a member of the collection, this method does nothing. - /// Also sets the property of the added device and all descendant devices to the relevant instance. - /// - /// Thrown if the argument is null. - /// Thrown if the is already associated with a different 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. - /// - 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); - } - } - - /// - /// Removes a child device from the collection. - /// - /// The instance to remove. - /// - /// If the device is not a member of the collection, this method does nothing. - /// Also sets the property to null for the removed device and all descendant devices. - /// - /// Thrown if the argument is null. - /// - 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); - } - } - - /// - /// Raises the event. - /// - /// The instance added to the collection. - /// - /// - protected virtual void OnDeviceAdded(SsdpEmbeddedDevice device) - { - var handlers = this.DeviceAdded; - handlers?.Invoke(this, new DeviceEventArgs(device)); - } - - /// - /// Raises the event. - /// - /// The instance removed from the collection. - /// - /// - protected virtual void OnDeviceRemoved(SsdpEmbeddedDevice device) - { - var handlers = this.DeviceRemoved; - handlers?.Invoke(this, new DeviceEventArgs(device)); - } - } -} diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs deleted file mode 100644 index d6fad4b9d4..0000000000 --- a/RSSDP/SsdpDeviceLocator.cs +++ /dev/null @@ -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 -{ - /// - /// 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. - /// - public class SsdpDeviceLocator : DisposableManagedObjectBase - { - private List _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); - - /// - /// Default constructor. - /// - 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(); - } - - /// - /// Raised for when - /// - /// 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. - /// For each item found during a device (cached or not), allowing clients to respond to found devices before the entire search is complete. - /// Only if the notification type matches the property. By default the filter is null, meaning all notifications raise events (regardless of ant - /// - /// 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. - /// - /// - /// - /// - /// - public event EventHandler DeviceAvailable; - - /// - /// Raised when a notification is received that indicates a device has shutdown or otherwise become unavailable. - /// - /// - /// 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. - /// This event is only raised if the notification type matches the property. A null or empty string for the will be treated as no filter and raise the event for all notifications. - /// The property may contain either a fully complete 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. - /// 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. - /// - /// - /// - /// - /// - public event EventHandler 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) - { - } - } - - /// - /// Performs a search for all devices using the default search timeout. - /// - private Task SearchAsync(CancellationToken cancellationToken) - { - return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, DefaultSearchWaitTime, cancellationToken); - } - - /// - /// Performs a search for the specified search target (criteria) and default search timeout. - /// - /// The criteria for the search. Value can be; - /// - /// Root devicesupnp:rootdevice - /// Specific device by UUIDuuid:<device uuid> - /// Device typeFully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1 - /// - /// - private Task SearchAsync(string searchTarget) - { - return SearchAsync(searchTarget, DefaultSearchWaitTime, CancellationToken.None); - } - - /// - /// Performs a search for all devices using the specified search timeout. - /// - /// 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. - 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); - } - - /// - /// Starts listening for broadcast notifications of service availability. - /// - /// - /// 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. - /// - /// - /// - /// - /// Throw if the ty is true. - public void StartListeningForNotifications() - { - _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; - _CommunicationsServer.RequestReceived += CommsServer_RequestReceived; - _CommunicationsServer.BeginListeningForMulticast(); - } - - /// - /// Stops listening for broadcast notifications of service availability. - /// - /// - /// Does nothing if this instance is not already listening for notifications. - /// - /// - /// - /// - /// Throw if the property is true. - public void StopListeningForNotifications() - { - ThrowIfDisposed(); - - _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; - } - - /// - /// Raises the event. - /// - /// - 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 - }); - } - - /// - /// Raises the event. - /// - /// A representing the device that is no longer available. - /// True if the device expired from the cache without being renewed, otherwise false to indicate the device explicitly notified us it was being shutdown. - /// - protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) - { - if (IsDisposed) - { - return; - } - - var handlers = DeviceUnavailable; - handlers?.Invoke(this, new DeviceUnavailableEventArgs(device, expired)); - } - - /// - /// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the or events. - /// - /// - /// 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. - /// If the value is null or empty string then, all notifications are reported. - /// Example filters follow; - /// upnp:rootdevice - /// urn:schemas-upnp-org:device:WANDevice:1 - /// uuid:9F15356CC-95FA-572E-0E99-85B456BD3012 - /// - /// - /// - /// - /// - public string NotificationFilter - { - get; - set; - } - - /// - /// Disposes this object and all internal resources. Stops listening for all network messages. - /// - /// True if managed resources should be disposed, or false is only unmanaged resources should be cleaned up. - 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(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 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 devices, string notificationType, string usn) - { - foreach (var d in devices) - { - if (d.NotificationType == notificationType && d.Usn == usn) - { - return d; - } - } - - return null; - } - - private List FindExistingDeviceNotifications(IList devices, string usn) - { - var list = new List(); - - 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); - } - } -} diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs deleted file mode 100644 index 0ac9cc9a13..0000000000 --- a/RSSDP/SsdpDevicePublisher.cs +++ /dev/null @@ -1,623 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Rssdp.Infrastructure -{ - /// - /// Provides the platform independent logic for publishing SSDP devices (notifications and search responses). - /// - public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher - { - private ISsdpCommunicationsServer _CommsServer; - private string _OSName; - private string _OSVersion; - private bool _sendOnlyMatchedHost; - - private bool _SupportPnpRootDevice; - - private IList _Devices; - private IReadOnlyList _ReadOnlyDevices; - - private Timer _RebroadcastAliveNotificationsTimer; - - private IDictionary _RecentSearchRequests; - - private Random _Random; - - /// - /// Default constructor. - /// - public SsdpDevicePublisher( - ISsdpCommunicationsServer communicationsServer, - string osName, - string osVersion, - bool sendOnlyMatchedHost) - { - ArgumentNullException.ThrowIfNull(communicationsServer); - ArgumentNullException.ThrowIfNullOrEmpty(osName); - ArgumentNullException.ThrowIfNullOrEmpty(osVersion); - - _SupportPnpRootDevice = true; - _Devices = new List(); - _ReadOnlyDevices = new ReadOnlyCollection(_Devices); - _RecentSearchRequests = new Dictionary(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); - } - - /// - /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients. - /// - /// - /// 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. - /// Devices added here with a non-zero cache life time will also have notifications broadcast periodically. - /// This method ignores duplicate device adds (if the same device instance is added multiple times, the second and subsequent add calls do nothing). - /// - /// The instance to add. - /// Thrown if the argument is null. - /// Thrown if the contains property values that are not acceptable to the UPnP 1.0 specification. - 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); - } - } - - /// - /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable. - /// - /// - /// 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. - /// This method does nothing if the device was not found in the collection. - /// - /// The instance to add. - /// Thrown if the argument is null. - 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); - } - } - - /// - /// Returns a read only list of devices being published by this instance. - /// - public IEnumerable Devices - { - get - { - return _ReadOnlyDevices; - } - } - - /// - /// If true (default) treats root devices as both upnp:rootdevice and pnp:rootdevice types. - /// - /// - /// 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. - /// 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. - /// - public bool SupportPnpRootDevice - { - get { return _SupportPnpRootDevice; } - - set - { - _SupportPnpRootDevice = value; - } - } - - /// - /// Stops listening for requests, stops sending periodic broadcasts, disposes all internal resources. - /// - /// - 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 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 GetAllDevicesAsFlatEnumerable() - { - return _Devices.Union(_Devices.SelectManyRecursive((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(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(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(); - 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(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 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; - } - } - } -} diff --git a/RSSDP/SsdpEmbeddedDevice.cs b/RSSDP/SsdpEmbeddedDevice.cs deleted file mode 100644 index f1a5981118..0000000000 --- a/RSSDP/SsdpEmbeddedDevice.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Rssdp -{ - /// - /// Represents a device that is a descendant of a instance. - /// - public class SsdpEmbeddedDevice : SsdpDevice - { - private SsdpRootDevice _RootDevice; - - /// - /// Default constructor. - /// - public SsdpEmbeddedDevice() - { - } - - /// - /// Returns the that is this device's first ancestor. If this device is itself an , then returns a reference to itself. - /// - public SsdpRootDevice RootDevice - { - get - { - return _RootDevice; - } - - internal set - { - _RootDevice = value; - lock (this.Devices) - { - foreach (var embeddedDevice in this.Devices) - { - ((SsdpEmbeddedDevice)embeddedDevice).RootDevice = _RootDevice; - } - } - } - } - } -} diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs deleted file mode 100644 index 5ecb1f86f6..0000000000 --- a/RSSDP/SsdpRootDevice.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Net; - -namespace Rssdp -{ - /// - /// 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. - /// - /// - /// Child (embedded) devices are represented by the in the property. - /// Root devices contain some information that applies to the whole device tree and is therefore not present on child devices, such as and . - /// - public class SsdpRootDevice : SsdpDevice - { - private Uri _UrlBase; - - /// - /// Default constructor. - /// - public SsdpRootDevice() : base() - { - } - - /// - /// Specifies how long clients can cache this device's details for. Optional but defaults to which means no-caching. Recommended value is half an hour. - /// - /// - /// Specify to indicate no caching allowed. - /// Also used to specify how often to rebroadcast alive notifications. - /// The UPnP/SSDP specifications indicate this should not be less than 1800 seconds (half an hour), but this is not enforced by this library. - /// - public TimeSpan CacheLifetime - { - get; set; - } - - /// - /// Gets or sets the URL used to retrieve the description document for this device/tree. Required. - /// - public Uri Location { get; set; } - - /// - /// Gets or sets the Address used to check if the received message from same interface with this device/tree. Required. - /// - public IPAddress Address { get; set; } - - /// - /// Gets or sets the prefix length used to check if the received message from same interface with this device/tree. Required. - /// - public byte PrefixLength { get; set; } - - /// - /// The base URL to use for all relative url's provided in other properties (and those of child devices). Optional. - /// - /// - /// 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. - /// - public Uri UrlBase - { - get - { - return _UrlBase ?? this.Location; - } - - set - { - _UrlBase = value; - } - } - } -}