diff options
| author | stefan <stefan@hegedues.at> | 2018-09-12 19:26:21 +0200 |
|---|---|---|
| committer | stefan <stefan@hegedues.at> | 2018-09-12 19:26:21 +0200 |
| commit | 48facb797ed912e4ea6b04b17d1ff190ac2daac4 (patch) | |
| tree | 8dae77a31670a888d733484cb17dd4077d5444e8 /RSSDP | |
| parent | c32d8656382a0eacb301692e0084377fc433ae9b (diff) | |
Update to 3.5.2 and .net core 2.1
Diffstat (limited to 'RSSDP')
| -rw-r--r-- | RSSDP/CustomHttpHeaders.cs | 294 | ||||
| -rw-r--r-- | RSSDP/DiscoveredSsdpDevice.cs | 74 | ||||
| -rw-r--r-- | RSSDP/DisposableManagedObjectBase.cs | 57 | ||||
| -rw-r--r-- | RSSDP/GlobalSuppressions.cs | bin | 7686 -> 0 bytes | |||
| -rw-r--r-- | RSSDP/HttpParserBase.cs | 438 | ||||
| -rw-r--r-- | RSSDP/HttpRequestParser.cs | 2 | ||||
| -rw-r--r-- | RSSDP/HttpResponseParser.cs | 4 | ||||
| -rw-r--r-- | RSSDP/ISsdpCommunicationsServer.cs | 6 | ||||
| -rw-r--r-- | RSSDP/IUPnPDeviceValidator.cs | 27 | ||||
| -rw-r--r-- | RSSDP/RSSDP.csproj | 107 | ||||
| -rw-r--r-- | RSSDP/SsdpCommunicationsServer.cs | 112 | ||||
| -rw-r--r-- | RSSDP/SsdpDevice.cs | 451 | ||||
| -rw-r--r-- | RSSDP/SsdpDeviceExtensions.cs | 36 | ||||
| -rw-r--r-- | RSSDP/SsdpDeviceIcon.cs | 50 | ||||
| -rw-r--r-- | RSSDP/SsdpDeviceLocator.cs | 645 | ||||
| -rw-r--r-- | RSSDP/SsdpDeviceLocatorBase.cs | 628 | ||||
| -rw-r--r-- | RSSDP/SsdpDeviceProperties.cs | 205 | ||||
| -rw-r--r-- | RSSDP/SsdpDeviceProperty.cs | 35 | ||||
| -rw-r--r-- | RSSDP/SsdpDevicePublisher.cs | 580 | ||||
| -rw-r--r-- | RSSDP/SsdpDevicePublisherBase.cs | 709 | ||||
| -rw-r--r-- | RSSDP/SsdpEmbeddedDevice.cs | 11 | ||||
| -rw-r--r-- | RSSDP/SsdpHelper.cs | 88 | ||||
| -rw-r--r-- | RSSDP/SsdpRootDevice.cs | 96 | ||||
| -rw-r--r-- | RSSDP/UPnP10DeviceValidator.cs | 194 |
24 files changed, 1486 insertions, 3363 deletions
diff --git a/RSSDP/CustomHttpHeaders.cs b/RSSDP/CustomHttpHeaders.cs deleted file mode 100644 index f2412d2e7..000000000 --- a/RSSDP/CustomHttpHeaders.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; - -namespace Rssdp -{ - /// <summary> - /// Represents a custom HTTP header sent on device search response or notification messages. - /// </summary> - public sealed class CustomHttpHeader - { - - #region Fields - - private string _Name; - private string _Value; - - #endregion - - #region Constructors - - /// <summary> - /// Full constructor. - /// </summary> - /// <param name="name">The field name of the header.</param> - /// <param name="value">The value of the header</param> - /// <remarks> - /// <para>As per RFC 822 and 2616, the name must contain only printable ASCII characters (33-126) excluding colon (:). The value may contain any ASCII characters except carriage return or line feed.</para> - /// </remarks> - /// <exception cref="System.ArgumentNullException">Thrown if the name is null.</exception> - /// <exception cref="System.ArgumentException">Thrown if the name is an empty value, or contains an invalid character. Also thrown if the value contains a \r or \n character.</exception> - public CustomHttpHeader(string name, string value) - { - Name = name; - Value = value; - } - - #endregion - - #region Public Properties - - /// <summary> - /// Return the name of this header. - /// </summary> - public string Name - { - get { return _Name; } - private set - { - EnsureValidName(value); - _Name = value; - } - } - - /// <summary> - /// Returns the value of this header. - /// </summary> - public string Value - { - get { return _Value; } - private set - { - EnsureValidValue(value); - _Value = value; - } - } - - #endregion - - #region Overrides - - /// <summary> - /// Returns the header formatted for use in an HTTP message. - /// </summary> - /// <returns>A string representing this header in the format of 'name: value'.</returns> - public override string ToString() - { - return this.Name + ": " + this.Value; - } - - #endregion - - #region Private Methods - - private static void EnsureValidName(string name) - { - if (name == null) throw new ArgumentNullException(nameof(name), "Name cannot be null."); - if (name.Length == 0) throw new ArgumentException("Name cannot be blank.", nameof(name)); - - foreach (var c in name) - { - var b = (byte)c; - if (c == ':' || b < 33 || b > 126) throw new ArgumentException("Name contains illegal characters.", nameof(name)); - } - } - - private static void EnsureValidValue(string value) - { - if (String.IsNullOrEmpty(value)) return; - - if (value.Contains("\r") || value.Contains("\n")) throw new ArgumentException("Invalid value.", nameof(value)); - } - - #endregion - - } - - /// <summary> - /// Represents a collection of custom HTTP headers, keyed by name. - /// </summary> - public class CustomHttpHeadersCollection : IEnumerable<CustomHttpHeader> - { - #region Fields - - private IDictionary<string, CustomHttpHeader> _Headers; - - #endregion - - #region Constructors - - /// <summary> - /// Default constructor. - /// </summary> - public CustomHttpHeadersCollection() - { - _Headers = new Dictionary<string, CustomHttpHeader>(); - } - - /// <summary> - /// Full constructor. - /// </summary> - /// <param name="capacity">Specifies the initial capacity of the collection.</param> - public CustomHttpHeadersCollection(int capacity) - { - _Headers = new Dictionary<string, CustomHttpHeader>(capacity); - } - - #endregion - - #region Public Methpds - - /// <summary> - /// Adds a <see cref="CustomHttpHeader"/> instance to the collection. - /// </summary> - /// <param name="header">The <see cref="CustomHttpHeader"/> instance to add to the collection.</param> - /// <remarks> - /// <para></para> - /// </remarks> - /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="header"/> is null.</exception> - public void Add(CustomHttpHeader header) - { - if (header == null) throw new ArgumentNullException(nameof(header)); - - lock (_Headers) - { - _Headers.Add(header.Name, header); - } - } - - #region Remove Overloads - - /// <summary> - /// Removes the specified header instance from the collection. - /// </summary> - /// <param name="header">The <see cref="CustomHttpHeader"/> instance to remove from the collection.</param> - /// <remarks> - /// <para>Only removes the specified header if that instance was in the collection, if another header with the same name exists in the collection it is not removed.</para> - /// </remarks> - /// <returns>True if an item was removed from the collection, otherwise false (because it did not exist or was not the same instance).</returns> - /// <seealso cref="Remove(string)"/> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="header"/> is null.</exception> - public bool Remove(CustomHttpHeader header) - { - if (header == null) throw new ArgumentNullException(nameof(header)); - - lock (_Headers) - { - if (_Headers.ContainsKey(header.Name) && _Headers[header.Name] == header) - return _Headers.Remove(header.Name); - } - - return false; - } - - /// <summary> - /// Removes the property with the specified key (<see cref="CustomHttpHeader.Name"/> from the collection. - /// </summary> - /// <param name="headerName">The name of the <see cref="CustomHttpHeader"/> instance to remove from the collection.</param> - /// <returns>True if an item was removed from the collection, otherwise false (because no item exists in the collection with that key).</returns> - /// <exception cref="System.ArgumentException">Thrown if the <paramref name="headerName"/> argument is null or empty string.</exception> - public bool Remove(string headerName) - { - if (String.IsNullOrEmpty(headerName)) throw new ArgumentException("headerName cannot be null or empty.", nameof(headerName)); - - lock (_Headers) - { - return _Headers.Remove(headerName); - } - } - - #endregion - - /// <summary> - /// Returns a boolean indicating whether or not the specified <see cref="CustomHttpHeader"/> instance is in the collection. - /// </summary> - /// <param name="header">An <see cref="CustomHttpHeader"/> instance to check the collection for.</param> - /// <returns>True if the specified instance exists in the collection, otherwise false.</returns> - public bool Contains(CustomHttpHeader header) - { - if (header == null) throw new ArgumentNullException(nameof(header)); - - lock (_Headers) - { - if (_Headers.ContainsKey(header.Name)) - return _Headers[header.Name] == header; - } - - return false; - } - - /// <summary> - /// Returns a boolean indicating whether or not a <see cref="CustomHttpHeader"/> instance with the specified full name value exists in the collection. - /// </summary> - /// <param name="headerName">A string containing the full name of the <see cref="CustomHttpHeader"/> instance to check for.</param> - /// <returns>True if an item with the specified full name exists in the collection, otherwise false.</returns> - public bool Contains(string headerName) - { - if (String.IsNullOrEmpty(headerName)) throw new ArgumentException("headerName cannot be null or empty.", nameof(headerName)); - - lock (_Headers) - { - return _Headers.ContainsKey(headerName); - } - } - - #endregion - - #region Public Properties - - /// <summary> - /// Returns the number of items in the collection. - /// </summary> - public int Count - { - get { return _Headers.Count; } - } - - /// <summary> - /// Returns the <see cref="CustomHttpHeader"/> instance from the collection that has the specified <see cref="CustomHttpHeader.Name"/> value. - /// </summary> - /// <param name="name">The full name of the property to return.</param> - /// <returns>A <see cref="CustomHttpHeader"/> instance from the collection.</returns> - /// <exception cref="System.Collections.Generic.KeyNotFoundException">Thrown if no item exists in the collection with the specified <paramref name="name"/> value.</exception> - public CustomHttpHeader this[string name] - { - get - { - return _Headers[name]; - } - } - - #endregion - - #region IEnumerable Members - - /// <summary> - /// Returns an enumerator of <see cref="CustomHttpHeader"/> instances in this collection. - /// </summary> - /// <returns>An enumerator of <see cref="CustomHttpHeader"/> instances in this collection.</returns> - public IEnumerator<CustomHttpHeader> GetEnumerator() - { - lock (_Headers) - { - return _Headers.Values.GetEnumerator(); - } - } - - /// <summary> - /// Returns an enumerator of <see cref="CustomHttpHeader"/> instances in this collection. - /// </summary> - /// <returns>An enumerator of <see cref="CustomHttpHeader"/> instances in this collection.</returns> - IEnumerator IEnumerable.GetEnumerator() - { - lock (_Headers) - { - return _Headers.Values.GetEnumerator(); - } - } - - #endregion - - } -}
\ No newline at end of file diff --git a/RSSDP/DiscoveredSsdpDevice.cs b/RSSDP/DiscoveredSsdpDevice.cs index 54701890c..66b8bea36 100644 --- a/RSSDP/DiscoveredSsdpDevice.cs +++ b/RSSDP/DiscoveredSsdpDevice.cs @@ -20,8 +20,6 @@ namespace Rssdp private SsdpRootDevice _Device; private DateTimeOffset _AsAt; - private static HttpClient s_DefaultHttpClient; - #endregion #region Public Properties @@ -80,47 +78,6 @@ namespace Rssdp return this.CacheLifetime == TimeSpan.Zero || this.AsAt.Add(this.CacheLifetime) <= DateTimeOffset.Now; } - /// <summary> - /// Retrieves the device description document specified by the <see cref="DescriptionLocation"/> property. - /// </summary> - /// <remarks> - /// <para>This method may choose to cache (or return cached) information if called multiple times within the <see cref="CacheLifetime"/> period.</para> - /// </remarks> - /// <returns>An <see cref="SsdpDevice"/> instance describing the full device details.</returns> - public async Task<SsdpDevice> GetDeviceInfo() - { - var device = _Device; - if (device == null || this.IsExpired()) - return await GetDeviceInfo(GetDefaultClient()); - else - return device; - } - - /// <summary> - /// Retrieves the device description document specified by the <see cref="DescriptionLocation"/> property using the provided <see cref="System.Net.Http.HttpClient"/> instance. - /// </summary> - /// <remarks> - /// <para>This method may choose to cache (or return cached) information if called multiple times within the <see cref="CacheLifetime"/> period.</para> - /// <para>This method performs no error handling, if an exception occurs downloading or parsing the document it will be thrown to the calling code. Ensure you setup correct error handling for these scenarios.</para> - /// </remarks> - /// <param name="downloadHttpClient">A <see cref="System.Net.Http.HttpClient"/> to use when downloading the document data.</param> - /// <returns>An <see cref="SsdpDevice"/> instance describing the full device details.</returns> - public async Task<SsdpRootDevice> GetDeviceInfo(HttpClient downloadHttpClient) - { - if (_Device == null || this.IsExpired()) - { - var rawDescriptionDocument = await downloadHttpClient.GetAsync(this.DescriptionLocation); - rawDescriptionDocument.EnsureSuccessStatusCode(); - - // Not using ReadAsStringAsync() here as some devices return the content type as utf-8 not UTF-8, - // which causes an (unneccesary) exception. - var data = await rawDescriptionDocument.Content.ReadAsByteArrayAsync(); - _Device = new SsdpRootDevice(this.DescriptionLocation, this.CacheLifetime, System.Text.UTF8Encoding.UTF8.GetString(data, 0, data.Length)); - } - - return _Device; - } - #endregion #region Overrides @@ -135,36 +92,5 @@ namespace Rssdp } #endregion - - #region Private Methods - - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Can't call dispose on the handler since we pass it to the HttpClient, which outlives the scope of this method.")] - private static HttpClient GetDefaultClient() - { - if (s_DefaultHttpClient == null) - { - var handler = new System.Net.Http.HttpClientHandler(); - try - { - if (handler.SupportsAutomaticDecompression) - handler.AutomaticDecompression = System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.GZip; - - s_DefaultHttpClient = new HttpClient(handler); - } - catch - { - if (handler != null) - handler.Dispose(); - - throw; - } - } - - return s_DefaultHttpClient; - } - - #endregion - } }
\ No newline at end of file diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs index 1288ba721..7a0fdd45a 100644 --- a/RSSDP/DisposableManagedObjectBase.cs +++ b/RSSDP/DisposableManagedObjectBase.cs @@ -44,32 +44,43 @@ namespace Rssdp.Infrastructure private set; } - #endregion + #endregion - #region IDisposable Members + public string BuildMessage(string header, Dictionary<string, string> values) + { + var builder = new StringBuilder(); - /// <summary> - /// Disposes this object instance and all internally managed resources. - /// </summary> - /// <remarks> - /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behaviour of derived classes.</para> - /// </remarks> - /// <seealso cref="IsDisposed"/> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification="We do exactly as asked, but CA doesn't seem to like us also setting the IsDisposed property. Too bad, it's a good idea and shouldn't cause an exception or anything likely to interfer with the dispose process.")] + const string argFormat = "{0}: {1}\r\n"; + + builder.AppendFormat("{0}\r\n", header); + + foreach (var pair in values) + { + builder.AppendFormat(argFormat, pair.Key, pair.Value); + } + + builder.Append("\r\n"); + + return builder.ToString(); + } + + #region IDisposable Members + + /// <summary> + /// Disposes this object instance and all internally managed resources. + /// </summary> + /// <remarks> + /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behaviour of derived classes.</para> + /// </remarks> + /// <seealso cref="IsDisposed"/> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification="We do exactly as asked, but CA doesn't seem to like us also setting the IsDisposed property. Too bad, it's a good idea and shouldn't cause an exception or anything likely to interfer with the dispose process.")] public void Dispose() { - try - { - IsDisposed = true; - - Dispose(true); - } - finally - { - GC.SuppressFinalize(this); - } - } + IsDisposed = true; - #endregion - } + Dispose(true); + } + + #endregion + } }
\ No newline at end of file diff --git a/RSSDP/GlobalSuppressions.cs b/RSSDP/GlobalSuppressions.cs Binary files differdeleted file mode 100644 index b9aa0c369..000000000 --- a/RSSDP/GlobalSuppressions.cs +++ /dev/null diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs index 7934419b0..e841feab3 100644 --- a/RSSDP/HttpParserBase.cs +++ b/RSSDP/HttpParserBase.cs @@ -3,242 +3,210 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; +using System.IO; namespace Rssdp.Infrastructure { - /// <summary> - /// A base class for the <see cref="HttpResponseParser"/> and <see cref="HttpRequestParser"/> classes. Not intended for direct use. - /// </summary> - /// <typeparam name="T"></typeparam> - public abstract class HttpParserBase<T> where T : new() - { - - #region Fields - - private static readonly string[] LineTerminators = new string[] { "\r\n", "\n" }; - private static readonly char[] SeparatorCharacters = new char[] { ',', ';' }; - - #endregion - - #region Public Methods - - /// <summary> - /// Parses the <paramref name="data"/> provided into either a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object. - /// </summary> - /// <param name="data">A string containing the HTTP message to parse.</param> - /// <returns>Either a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object containing the parsed data.</returns> - public abstract T Parse(string data); - - /// <summary> - /// Parses a string containing either an HTTP request or response into a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object. - /// </summary> - /// <param name="message">A <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object representing the parsed message.</param> - /// <param name="headers">A reference to the <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the <paramref name="message"/> object.</param> - /// <param name="data">A string containing the data to be parsed.</param> - /// <returns>An <see cref="System.Net.Http.HttpContent"/> object containing the content of the parsed message.</returns> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification="Honestly, it's fine. MemoryStream doesn't mind.")] - protected virtual HttpContent Parse(T message, System.Net.Http.Headers.HttpHeaders headers, string data) - { - if (data == null) throw new ArgumentNullException("data"); - if (data.Length == 0) throw new ArgumentException("data cannot be an empty string.", "data"); - if (!LineTerminators.Any(data.Contains)) throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", "data"); - - HttpContent retVal = null; - try - { - var contentStream = new System.IO.MemoryStream(); - try - { - retVal = new StreamContent(contentStream); - - 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); - - int lineIndex = ParseHeaders(headers, retVal.Headers, lines); - - if (lineIndex < lines.Length - 1) - { - //Read rest of any remaining data as content. - if (lineIndex < lines.Length - 1) - { - //This is inefficient in multiple ways, but not sure of a good way of correcting. Revisit. - var body = System.Text.UTF8Encoding.UTF8.GetBytes(String.Join(null, lines, lineIndex, lines.Length - lineIndex)); - contentStream.Write(body, 0, body.Length); - contentStream.Seek(0, System.IO.SeekOrigin.Begin); - } - } - } - catch - { - if (contentStream != null) - contentStream.Dispose(); - - throw; - } - } - catch - { - if (retVal != null) - retVal.Dispose(); - - throw; - } - - return retVal; - } - - /// <summary> - /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>. - /// </summary> - /// <param name="data">The first line of the HTTP message to be parsed.</param> - /// <param name="message">Either a <see cref="System.Net.Http.HttpResponseMessage"/> or <see cref="System.Net.Http.HttpRequestMessage"/> to assign the parsed values to.</param> - protected abstract void ParseStatusLine(string data, T message); - - /// <summary> - /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). - /// </summary> - /// <param name="headerName">A string containing the name of the header to return the type of.</param> - protected abstract bool IsContentHeader(string headerName); - - /// <summary> - /// Parses the HTTP version text from an HTTP request or response status line and returns a <see cref="Version"/> object representing the parsed values. - /// </summary> - /// <param name="versionData">A string containing the HTTP version, from the message status line.</param> - /// <returns>A <see cref="Version"/> object containing the parsed version data.</returns> - protected static Version ParseHttpVersion(string versionData) - { - if (versionData == null) throw new ArgumentNullException("versionData"); - - var versionSeparatorIndex = versionData.IndexOf('/'); - if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length) throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", "versionData"); - - return Version.Parse(versionData.Substring(versionSeparatorIndex + 1)); - } - - #endregion - - #region Private Methods - - /// <summary> - /// Parses a line from an HTTP request or response message containing a header name and value pair. - /// </summary> - /// <param name="line">A string containing the data to be parsed.</param> - /// <param name="headers">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection to which the parsed header will be added.</param> - /// <param name="contentHeaders">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the message content, to which the parsed header will be added.</param> - private void ParseHeader(string line, System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders) - { - //Header format is - //name: value - var headerKeySeparatorIndex = line.IndexOf(":", StringComparison.OrdinalIgnoreCase); - var headerName = line.Substring(0, headerKeySeparatorIndex).Trim(); - var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim(); - - //Not sure how to determine where request headers and 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.First()); - } - - 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 static IList<string> ParseValues(string headerValue) - { - // This really should be better and match the HTTP 1.1 spec, - // but this should actually be good enough for SSDP implementations - // I think. - var values = new List<string>(); - - if (headerValue == "\"\"") - { - values.Add(String.Empty); - return values; - } - - var indexOfSeparator = headerValue.IndexOfAny(SeparatorCharacters); - if (indexOfSeparator <= 0) - values.Add(headerValue); - else - { - var segments = headerValue.Split(SeparatorCharacters); - if (headerValue.Contains("\"")) - { - 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 static 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); - else - return trimmedSegment; - } - - #endregion - - } + /// <summary> + /// A base class for the <see cref="HttpResponseParser"/> and <see cref="HttpRequestParser"/> classes. Not intended for direct use. + /// </summary> + /// <typeparam name="T"></typeparam> + public abstract class HttpParserBase<T> where T : new() + { + + #region Fields + + private readonly string[] LineTerminators = new string[] { "\r\n", "\n" }; + private readonly char[] SeparatorCharacters = new char[] { ',', ';' }; + + #endregion + + #region Public Methods + + private static byte[] EmptyByteArray = new byte[]{}; + + /// <summary> + /// Parses the <paramref name="data"/> provided into either a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object. + /// </summary> + /// <param name="data">A string containing the HTTP message to parse.</param> + /// <returns>Either a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object containing the parsed data.</returns> + public abstract T Parse(string data); + + /// <summary> + /// Parses a string containing either an HTTP request or response into a <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object. + /// </summary> + /// <param name="message">A <see cref="System.Net.Http.HttpRequestMessage"/> or <see cref="System.Net.Http.HttpResponseMessage"/> object representing the parsed message.</param> + /// <param name="headers">A reference to the <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the <paramref name="message"/> object.</param> + /// <param name="data">A string containing the data to be parsed.</param> + /// <returns>An <see cref="System.Net.Http.HttpContent"/> object containing the content of the parsed message.</returns> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Honestly, it's fine. MemoryStream doesn't mind.")] + protected virtual void Parse(T message, System.Net.Http.Headers.HttpHeaders headers, string data) + { + if (data == null) throw new ArgumentNullException("data"); + if (data.Length == 0) throw new ArgumentException("data cannot be an empty string.", "data"); + if (!LineTerminators.Any(data.Contains)) throw new ArgumentException("data is not a valid request, it does not contain any CRLF/LF terminators.", "data"); + + using (var retVal = new ByteArrayContent(EmptyByteArray)) + { + var lines = data.Split(LineTerminators, StringSplitOptions.None); + + //First line is the 'request' line containing http protocol details like method, uri, http version etc. + ParseStatusLine(lines[0], message); + + ParseHeaders(headers, retVal.Headers, lines); + } + } + + /// <summary> + /// Used to parse the first line of an HTTP request or response and assign the values to the appropriate properties on the <paramref name="message"/>. + /// </summary> + /// <param name="data">The first line of the HTTP message to be parsed.</param> + /// <param name="message">Either a <see cref="System.Net.Http.HttpResponseMessage"/> or <see cref="System.Net.Http.HttpRequestMessage"/> to assign the parsed values to.</param> + protected abstract void ParseStatusLine(string data, T message); + + /// <summary> + /// Returns a boolean indicating whether the specified HTTP header name represents a content header (true), or a message header (false). + /// </summary> + /// <param name="headerName">A string containing the name of the header to return the type of.</param> + protected abstract bool IsContentHeader(string headerName); + + /// <summary> + /// Parses the HTTP version text from an HTTP request or response status line and returns a <see cref="Version"/> object representing the parsed values. + /// </summary> + /// <param name="versionData">A string containing the HTTP version, from the message status line.</param> + /// <returns>A <see cref="Version"/> object containing the parsed version data.</returns> + protected Version ParseHttpVersion(string versionData) + { + if (versionData == null) throw new ArgumentNullException("versionData"); + + var versionSeparatorIndex = versionData.IndexOf('/'); + if (versionSeparatorIndex <= 0 || versionSeparatorIndex == versionData.Length) throw new ArgumentException("request header line is invalid. Http Version not supplied or incorrect format.", "versionData"); + + return Version.Parse(versionData.Substring(versionSeparatorIndex + 1)); + } + + #endregion + + #region Private Methods + + /// <summary> + /// Parses a line from an HTTP request or response message containing a header name and value pair. + /// </summary> + /// <param name="line">A string containing the data to be parsed.</param> + /// <param name="headers">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection to which the parsed header will be added.</param> + /// <param name="contentHeaders">A reference to a <see cref="System.Net.Http.Headers.HttpHeaders"/> collection for the message content, to which the parsed header will be added.</param> + private void ParseHeader(string line, System.Net.Http.Headers.HttpHeaders headers, System.Net.Http.Headers.HttpHeaders contentHeaders) + { + //Header format is + //name: value + var headerKeySeparatorIndex = line.IndexOf(":", StringComparison.OrdinalIgnoreCase); + var headerName = line.Substring(0, headerKeySeparatorIndex).Trim(); + var headerValue = line.Substring(headerKeySeparatorIndex + 1).Trim(); + + //Not sure how to determine where request headers and 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.First()); + } + + 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 IList<string> ParseValues(string headerValue) + { + // This really should be better and match the HTTP 1.1 spec, + // but this should actually be good enough for SSDP implementations + // I think. + var values = new List<string>(); + + if (headerValue == "\"\"") + { + values.Add(String.Empty); + return values; + } + + var indexOfSeparator = headerValue.IndexOfAny(SeparatorCharacters); + if (indexOfSeparator <= 0) + values.Add(headerValue); + else + { + var segments = headerValue.Split(SeparatorCharacters); + if (headerValue.Contains("\"")) + { + 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); + else + return trimmedSegment; + } + + #endregion + + } }
\ No newline at end of file diff --git a/RSSDP/HttpRequestParser.cs b/RSSDP/HttpRequestParser.cs index bedbbe675..8460e1ca5 100644 --- a/RSSDP/HttpRequestParser.cs +++ b/RSSDP/HttpRequestParser.cs @@ -38,7 +38,7 @@ namespace Rssdp.Infrastructure { retVal = new System.Net.Http.HttpRequestMessage(); - retVal.Content = Parse(retVal, retVal.Headers, data); + Parse(retVal, retVal.Headers, data); return retVal; } diff --git a/RSSDP/HttpResponseParser.cs b/RSSDP/HttpResponseParser.cs index 5f297ca9a..8ecb944c2 100644 --- a/RSSDP/HttpResponseParser.cs +++ b/RSSDP/HttpResponseParser.cs @@ -16,7 +16,7 @@ namespace Rssdp.Infrastructure #region Fields & Constants - private static readonly string[] ContentHeaderNames = new string[] + 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" }; @@ -37,7 +37,7 @@ namespace Rssdp.Infrastructure { retVal = new System.Net.Http.HttpResponseMessage(); - retVal.Content = Parse(retVal, retVal.Headers, data); + Parse(retVal, retVal.Headers, data); return retVal; } diff --git a/RSSDP/ISsdpCommunicationsServer.cs b/RSSDP/ISsdpCommunicationsServer.cs index 0e47974e2..b6329c1b3 100644 --- a/RSSDP/ISsdpCommunicationsServer.cs +++ b/RSSDP/ISsdpCommunicationsServer.cs @@ -38,11 +38,6 @@ namespace Rssdp.Infrastructure void StopListeningForBroadcasts(); /// <summary> - /// Stops listening for search responses on the local, unicast socket. - /// </summary> - void StopListeningForResponses(); - - /// <summary> /// Sends a message to a particular address (uni or multicast) and port. /// </summary> Task SendMessage(byte[] messageData, IpEndPointInfo destination, IpAddressInfo fromLocalIpAddress, CancellationToken cancellationToken); @@ -51,6 +46,7 @@ namespace Rssdp.Infrastructure /// Sends a message to the SSDP multicast address and port. /// </summary> Task SendMulticastMessage(string message, CancellationToken cancellationToken); + Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken); #endregion diff --git a/RSSDP/IUPnPDeviceValidator.cs b/RSSDP/IUPnPDeviceValidator.cs deleted file mode 100644 index 8fa48df7b..000000000 --- a/RSSDP/IUPnPDeviceValidator.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Interface for components that check an <see cref="SsdpDevice"/> object's properties meet the UPnP specification for a particular version. - /// </summary> - public interface IUpnpDeviceValidator - { - /// <summary> - /// Returns an enumerable set of strings, each one being a description of an invalid property on the specified root device. - /// </summary> - /// <param name="device">The <see cref="SsdpRootDevice"/> to validate.</param> - System.Collections.Generic.List<string> GetValidationErrors(SsdpRootDevice device); - - /// <summary> - /// Returns an enumerable set of strings, each one being a description of an invalid property on the specified device. - /// </summary> - /// <param name="device">The <see cref="SsdpDevice"/> to validate.</param> - System.Collections.Generic.List<string> GetValidationErrors(SsdpDevice device); - - /// <summary> - /// Validates the specified device and throws an <see cref="System.InvalidOperationException"/> if there are any validation errors. - /// </summary> - void ThrowIfDeviceInvalid(SsdpDevice device); - } -} diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index aea144a5a..994e74158 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -1,98 +1,13 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> - <PropertyGroup> - <MinimumVisualStudioVersion>11.0</MinimumVisualStudioVersion> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> - <ProjectGuid>{21002819-C39A-4D3E-BE83-2A276A77FB1F}</ProjectGuid> - <OutputType>Library</OutputType> - <AppDesignerFolder>Properties</AppDesignerFolder> - <RootNamespace>RSSDP</RootNamespace> - <AssemblyName>RSSDP</AssemblyName> - <DefaultLanguage>en-US</DefaultLanguage> - <FileAlignment>512</FileAlignment> - <TargetFrameworkProfile /> - <TargetFrameworkVersion>v4.6</TargetFrameworkVersion> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>full</DebugType> - <Optimize>false</Optimize> - <OutputPath>bin\Debug\</OutputPath> - <DefineConstants>DEBUG;TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <DebugType>pdbonly</DebugType> - <Optimize>true</Optimize> - <OutputPath>bin\Release\</OutputPath> - <DefineConstants>TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - </PropertyGroup> +<Project Sdk="Microsoft.NET.Sdk"> + <ItemGroup> - <Compile Include="CustomHttpHeaders.cs" /> - <Compile Include="DeviceAvailableEventArgs.cs" /> - <Compile Include="DeviceEventArgs.cs" /> - <Compile Include="DeviceUnavailableEventArgs.cs" /> - <Compile Include="DiscoveredSsdpDevice.cs" /> - <Compile Include="DisposableManagedObjectBase.cs" /> - <Compile Include="GlobalSuppressions.cs" /> - <Compile Include="HttpParserBase.cs" /> - <Compile Include="HttpRequestParser.cs" /> - <Compile Include="HttpResponseParser.cs" /> - <Compile Include="IEnumerableExtensions.cs" /> - <Compile Include="ISsdpCommunicationsServer.cs" /> - <Compile Include="ISsdpDeviceLocator.cs" /> - <Compile Include="ISsdpDevicePublisher.cs" /> - <Compile Include="IUPnPDeviceValidator.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="RequestReceivedEventArgs.cs" /> - <Compile Include="ResponseReceivedEventArgs.cs" /> - <Compile Include="SsdpCommunicationsServer.cs" /> - <Compile Include="SsdpConstants.cs" /> - <Compile Include="SsdpDevice.cs" /> - <Compile Include="SsdpDeviceExtensions.cs" /> - <Compile Include="SsdpDeviceIcon.cs" /> - <Compile Include="SsdpDeviceLocator.cs" /> - <Compile Include="SsdpDeviceLocatorBase.cs" /> - <Compile Include="SsdpDeviceProperties.cs" /> - <Compile Include="SsdpDeviceProperty.cs" /> - <Compile Include="SsdpDevicePublisher.cs" /> - <Compile Include="SsdpDevicePublisherBase.cs" /> - <Compile Include="SsdpEmbeddedDevice.cs" /> - <Compile Include="SsdpHelper.cs" /> - <Compile Include="SsdpRootDevice.cs" /> - <Compile Include="UPnP10DeviceValidator.cs" /> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> - <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> - <Name>MediaBrowser.Common</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj"> - <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> - <Name>MediaBrowser.Model</Name> - </ProjectReference> - <Reference Include="System" /> - <Reference Include="System.Configuration" /> - <Reference Include="System.Core" /> - <Reference Include="System.Runtime.Serialization" /> - <Reference Include="System.Xml.Linq" /> - <Reference Include="System.Data.DataSetExtensions" /> - <Reference Include="Microsoft.CSharp" /> - <Reference Include="System.Data" /> - <Reference Include="System.Net.Http" /> - <Reference Include="System.Xml" /> - </ItemGroup> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <!-- To modify your build process, add your task inside one of the targets below and uncomment it. - Other similar extension points exist, see Microsoft.Common.targets. - <Target Name="BeforeBuild"> - </Target> - <Target Name="AfterBuild"> - </Target> - --> -</Project>
\ No newline at end of file + + <PropertyGroup> + <TargetFramework>netcoreapp2.1</TargetFramework> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + </PropertyGroup> + +</Project> diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 6b4f67b0b..bc8594649 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -124,7 +124,16 @@ namespace Rssdp.Infrastructure lock (_BroadcastListenSocketSynchroniser) { if (_BroadcastListenSocket == null) - _BroadcastListenSocket = ListenForBroadcastsAsync(); + { + try + { + _BroadcastListenSocket = ListenForBroadcastsAsync(); + } + catch (Exception ex) + { + _logger.ErrorException("Error in BeginListeningForBroadcasts", ex); + } + } } } } @@ -135,12 +144,11 @@ namespace Rssdp.Infrastructure /// <exception cref="System.ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception> public void StopListeningForBroadcasts() { - ThrowIfDisposed(); - lock (_BroadcastListenSocketSynchroniser) { if (_BroadcastListenSocket != null) { + _logger.Info("{0} disposing _BroadcastListenSocket.", GetType().Name); _BroadcastListenSocket.Dispose(); _BroadcastListenSocket = null; } @@ -227,10 +235,15 @@ namespace Rssdp.Infrastructure } } + public Task SendMulticastMessage(string message, CancellationToken cancellationToken) + { + return SendMulticastMessage(message, SsdpConstants.UdpResendCount, cancellationToken); + } + /// <summary> /// Sends a message to the SSDP multicast address and port. /// </summary> - public async Task SendMulticastMessage(string message, CancellationToken cancellationToken) + public async Task SendMulticastMessage(string message, int sendCount, CancellationToken cancellationToken) { if (message == null) throw new ArgumentNullException("messageData"); @@ -243,7 +256,7 @@ namespace Rssdp.Infrastructure 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 < SsdpConstants.UdpResendCount; i++) + for (var i = 0; i < sendCount; i++) { await SendMessageIfSocketNotDisposed(messageData, new IpEndPointInfo { @@ -262,8 +275,6 @@ namespace Rssdp.Infrastructure /// <exception cref="System.ObjectDisposedException">Thrown if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true (because <seealso cref="DisposableManagedObjectBase.Dispose()" /> has been called previously).</exception> public void StopListeningForResponses() { - ThrowIfDisposed(); - lock (_SendSocketSynchroniser) { if (_sendSockets != null) @@ -271,8 +282,11 @@ namespace Rssdp.Infrastructure var sockets = _sendSockets.ToList(); _sendSockets = null; + _logger.Info("{0} Disposing {1} sendSockets", GetType().Name, sockets.Count); + foreach (var socket in sockets) { + _logger.Info("{0} disposing sendSocket from {1}", GetType().Name, socket.LocalIPAddress); socket.Dispose(); } } @@ -307,25 +321,9 @@ namespace Rssdp.Infrastructure { if (disposing) { - lock (_BroadcastListenSocketSynchroniser) - { - if (_BroadcastListenSocket != null) - _BroadcastListenSocket.Dispose(); - } + StopListeningForBroadcasts(); - lock (_SendSocketSynchroniser) - { - if (_sendSockets != null) - { - var sockets = _sendSockets.ToList(); - _sendSockets = null; - - foreach (var socket in sockets) - { - socket.Dispose(); - } - } - } + StopListeningForResponses(); } } @@ -333,18 +331,18 @@ namespace Rssdp.Infrastructure #region Private Methods - private async Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, CancellationToken cancellationToken) + private Task SendMessageIfSocketNotDisposed(byte[] messageData, IpEndPointInfo destination, CancellationToken cancellationToken) { var sockets = _sendSockets; if (sockets != null) { sockets = sockets.ToList(); - foreach (var socket in sockets) - { - await SendFromSocket(socket, messageData, destination, cancellationToken).ConfigureAwait(false); - } + var tasks = sockets.Select(s => SendFromSocket(s, messageData, destination, cancellationToken)); + return Task.WhenAll(tasks); } + + return Task.CompletedTask; } private ISocket ListenForBroadcastsAsync() @@ -395,35 +393,37 @@ namespace Rssdp.Infrastructure private void ListenToSocket(ISocket socket) { // Tasks are captured to local variables even if we don't use them just to avoid compiler warnings. - var t = Task.Run(async () => - { - var cancelled = false; - var receiveBuffer = new byte[8192]; + var t = Task.Run(() => ListenToSocketInternal(socket)); + } + + private async Task ListenToSocketInternal(ISocket socket) + { + var cancelled = false; + var receiveBuffer = new byte[8192]; - while (!cancelled) + while (!cancelled && !IsDisposed) + { + try { - try - { - var result = await socket.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); + var result = await socket.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); - if (result.ReceivedBytes > 0) - { - // Strange cannot convert compiler error here if I don't explicitly - // assign or cast to Action first. Assignment is easier to read, - // so went with that. - ProcessMessage(System.Text.UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint, result.LocalIPAddress); - } - } - catch (ObjectDisposedException) - { - cancelled = true; - } - catch (TaskCanceledException) + if (result.ReceivedBytes > 0) { - cancelled = true; + // Strange cannot convert compiler error here if I don't explicitly + // assign or cast to Action first. Assignment is easier to read, + // so went with that. + ProcessMessage(System.Text.UTF8Encoding.UTF8.GetString(result.Buffer, 0, result.ReceivedBytes), result.RemoteEndPoint, result.LocalIPAddress); } } - }); + catch (ObjectDisposedException) + { + cancelled = true; + } + catch (TaskCanceledException) + { + cancelled = true; + } + } } private void EnsureSendSocketCreated() @@ -445,7 +445,7 @@ namespace Rssdp.Infrastructure //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 static HTTP/ prefix on the start of the message. + //by checking for the HTTP/ prefix on the start of the message. if (data.StartsWith("HTTP/", StringComparison.OrdinalIgnoreCase)) { HttpResponseMessage responseMessage = null; @@ -453,7 +453,7 @@ namespace Rssdp.Infrastructure { responseMessage = _ResponseParser.Parse(data); } - catch (ArgumentException ex) + catch (ArgumentException) { // Ignore invalid packets. } @@ -468,7 +468,7 @@ namespace Rssdp.Infrastructure { requestMessage = _RequestParser.Parse(data); } - catch (ArgumentException ex) + catch (ArgumentException) { // Ignore invalid packets. } diff --git a/RSSDP/SsdpDevice.cs b/RSSDP/SsdpDevice.cs index 65d9be139..ef6869c8b 100644 --- a/RSSDP/SsdpDevice.cs +++ b/RSSDP/SsdpDevice.cs @@ -25,8 +25,6 @@ namespace Rssdp private string _DeviceType; private string _DeviceTypeNamespace; private int _DeviceVersion; - private SsdpDevicePropertiesCollection _CustomProperties; - private CustomHttpHeadersCollection _CustomResponseHeaders; private IList<SsdpDevice> _Devices; @@ -61,36 +59,23 @@ namespace Rssdp _DeviceType = SsdpConstants.UpnpDeviceTypeBasicDevice; _DeviceVersion = 1; - this.Icons = new List<SsdpDeviceIcon>(); _Devices = new List<SsdpDevice>(); this.Devices = new ReadOnlyCollection<SsdpDevice>(_Devices); - _CustomResponseHeaders = new CustomHttpHeadersCollection(); - _CustomProperties = new SsdpDevicePropertiesCollection(); } - /// <summary> - /// Deserialisation constructor. - /// </summary> - /// <remarks><para>Uses the provided XML string and parent device properties to set the properties of the object. The XML provided must be a valid UPnP device description document.</para></remarks> - /// <param name="deviceDescriptionXml">A UPnP device description XML document.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="deviceDescriptionXml"/> argument is null.</exception> - /// <exception cref="System.ArgumentException">Thrown if the <paramref name="deviceDescriptionXml"/> argument is empty.</exception> - protected SsdpDevice(string deviceDescriptionXml) - : this() + #endregion + + public SsdpRootDevice ToRootDevice() { - if (deviceDescriptionXml == null) throw new ArgumentNullException("deviceDescriptionXml"); - if (deviceDescriptionXml.Length == 0) throw new ArgumentException("deviceDescriptionXml cannot be an empty string.", "deviceDescriptionXml"); + var device = this; - using (var ms = new System.IO.MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(deviceDescriptionXml))) - { - var reader = XmlReader.Create(ms); + var rootDevice = device as SsdpRootDevice; + if (rootDevice == null) + rootDevice = ((SsdpEmbeddedDevice)device).RootDevice; - LoadDeviceProperties(reader, this); - } + return rootDevice; } - - #endregion - + #region Public Properties #region UPnP Device Description Properties @@ -269,15 +254,6 @@ namespace Rssdp #endregion /// <summary> - /// Returns a list of icons (images) that can be used to display this device. Optional, but recommended you provide at least one at 48x48 pixels. - /// </summary> - public IList<SsdpDeviceIcon> Icons - { - get; - private set; - } - - /// <summary> /// Returns a read-only enumerable set of <see cref="SsdpDevice"/> objects representing children of this device. Child devices are optional. /// </summary> /// <seealso cref="AddDevice"/> @@ -288,32 +264,6 @@ namespace Rssdp private set; } - /// <summary> - /// Returns a dictionary of <see cref="SsdpDeviceProperty"/> objects keyed by <see cref="SsdpDeviceProperty.FullName"/>. Each value represents a custom property in the device description document. - /// </summary> - public SsdpDevicePropertiesCollection CustomProperties - { - get - { - return _CustomProperties; - } - } - - /// <summary> - /// Provides a list of additional information to provide about this device in search response and notification messages. - /// </summary> - /// <remarks> - /// <para>The headers included here are included in the (HTTP headers) for search response and alive notifications sent in relation to this device.</para> - /// <para>Only values specified directly on this <see cref="SsdpDevice"/> instance will be included, headers from ancestors are not automatically included.</para> - /// </remarks> - public CustomHttpHeadersCollection CustomResponseHeaders - { - get - { - return _CustomResponseHeaders; - } - } - #endregion #region Public Methods @@ -401,389 +351,6 @@ namespace Rssdp handlers(this, new DeviceEventArgs(device)); } - /// <summary> - /// Writes this device to the specified <see cref="System.Xml.XmlWriter"/> as a device node and it's content. - /// </summary> - /// <param name="writer">The <see cref="System.Xml.XmlWriter"/> to output to.</param> - /// <param name="device">The <see cref="SsdpDevice"/> to write out.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="writer"/> or <paramref name="device"/> argument is null.</exception> - protected virtual void WriteDeviceDescriptionXml(XmlWriter writer, SsdpDevice device) - { - if (writer == null) throw new ArgumentNullException("writer"); - if (device == null) throw new ArgumentNullException("device"); - - writer.WriteStartElement("device"); - - if (!String.IsNullOrEmpty(device.FullDeviceType)) - WriteNodeIfNotEmpty(writer, "deviceType", device.FullDeviceType); - - WriteNodeIfNotEmpty(writer, "friendlyName", device.FriendlyName); - WriteNodeIfNotEmpty(writer, "manufacturer", device.Manufacturer); - WriteNodeIfNotEmpty(writer, "manufacturerURL", device.ManufacturerUrl); - WriteNodeIfNotEmpty(writer, "modelDescription", device.ModelDescription); - WriteNodeIfNotEmpty(writer, "modelName", device.ModelName); - WriteNodeIfNotEmpty(writer, "modelNumber", device.ModelNumber); - WriteNodeIfNotEmpty(writer, "modelURL", device.ModelUrl); - WriteNodeIfNotEmpty(writer, "presentationURL", device.PresentationUrl); - WriteNodeIfNotEmpty(writer, "serialNumber", device.SerialNumber); - WriteNodeIfNotEmpty(writer, "UDN", device.Udn); - WriteNodeIfNotEmpty(writer, "UPC", device.Upc); - - WriteCustomProperties(writer, device); - WriteIcons(writer, device); - WriteChildDevices(writer, device); - - writer.WriteEndElement(); - } - - /// <summary> - /// Converts a string to a <see cref="Uri"/>, or returns null if the string provided is null. - /// </summary> - /// <param name="value">The string value to convert.</param> - /// <returns>A <see cref="Uri"/>.</returns> - protected static Uri StringToUri(string value) - { - if (!String.IsNullOrEmpty(value)) - return new Uri(value, UriKind.RelativeOrAbsolute); - - return null; - } - - #endregion - - #region Private Methods - - #region Serialisation Methods - - private static void WriteCustomProperties(XmlWriter writer, SsdpDevice device) - { - foreach (var prop in device.CustomProperties) - { - writer.WriteElementString(prop.Namespace, prop.Name, SsdpConstants.SsdpDeviceDescriptionXmlNamespace, prop.Value); - } - } - - private static void WriteIcons(XmlWriter writer, SsdpDevice device) - { - if (device.Icons.Count > 0) - { - writer.WriteStartElement("iconList"); - - foreach (var icon in device.Icons) - { - writer.WriteStartElement("icon"); - - writer.WriteElementString("mimetype", icon.MimeType); - writer.WriteElementString("width", icon.Width.ToString()); - writer.WriteElementString("height", icon.Height.ToString()); - writer.WriteElementString("depth", icon.ColorDepth.ToString()); - writer.WriteElementString("url", icon.Url.ToString()); - - writer.WriteEndElement(); - } - - writer.WriteEndElement(); - } - } - - private void WriteChildDevices(XmlWriter writer, SsdpDevice parentDevice) - { - if (parentDevice.Devices.Count > 0) - { - writer.WriteStartElement("deviceList"); - - foreach (var device in parentDevice.Devices) - { - WriteDeviceDescriptionXml(writer, device); - } - - writer.WriteEndElement(); - } - } - - private static void WriteNodeIfNotEmpty(XmlWriter writer, string nodeName, string value) - { - if (!String.IsNullOrEmpty(value)) - writer.WriteElementString(nodeName, value); - } - - private static void WriteNodeIfNotEmpty(XmlWriter writer, string nodeName, Uri value) - { - if (value != null) - writer.WriteElementString(nodeName, value.ToString()); - } - - #endregion - - #region Deserialisation Methods - - private void LoadDeviceProperties(XmlReader reader, SsdpDevice device) - { - ReadUntilDeviceNode(reader); - - while (!reader.EOF) - { - if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "device") - { - reader.Read(); - break; - } - - if (!SetPropertyFromReader(reader, device)) - reader.Read(); - } - } - - private static void ReadUntilDeviceNode(XmlReader reader) - { - while (!reader.EOF && (reader.LocalName != "device" || reader.NodeType != XmlNodeType.Element)) - { - reader.Read(); - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Yes, there is a large switch statement, not it's not really complex and doesn't really need to be rewritten at this point.")] - private bool SetPropertyFromReader(XmlReader reader, SsdpDevice device) - { - switch (reader.LocalName) - { - case "friendlyName": - device.FriendlyName = reader.ReadElementContentAsString(); - break; - - case "manufacturer": - device.Manufacturer = reader.ReadElementContentAsString(); - break; - - case "manufacturerURL": - device.ManufacturerUrl = StringToUri(reader.ReadElementContentAsString()); - break; - - case "modelDescription": - device.ModelDescription = reader.ReadElementContentAsString(); - break; - - case "modelName": - device.ModelName = reader.ReadElementContentAsString(); - break; - - case "modelNumber": - device.ModelNumber = reader.ReadElementContentAsString(); - break; - - case "modelURL": - device.ModelUrl = StringToUri(reader.ReadElementContentAsString()); - break; - - case "presentationURL": - device.PresentationUrl = StringToUri(reader.ReadElementContentAsString()); - break; - - case "serialNumber": - device.SerialNumber = reader.ReadElementContentAsString(); - break; - - case "UDN": - device.Udn = reader.ReadElementContentAsString(); - SetUuidFromUdn(device); - break; - - case "UPC": - device.Upc = reader.ReadElementContentAsString(); - break; - - case "deviceType": - SetDeviceTypePropertiesFromFullDeviceType(device, reader.ReadElementContentAsString()); - break; - - case "iconList": - reader.Read(); - LoadIcons(reader, device); - break; - - case "deviceList": - reader.Read(); - LoadChildDevices(reader, device); - break; - - case "serviceList": - reader.Skip(); - break; - - default: - if (reader.NodeType == XmlNodeType.Element && reader.Name != "device" && reader.Name != "icon") - { - AddCustomProperty(reader, device); - break; - } - else - return false; - } - return true; - } - - private static void SetDeviceTypePropertiesFromFullDeviceType(SsdpDevice device, string value) - { - if (String.IsNullOrEmpty(value) || !value.Contains(":")) - device.DeviceType = value; - else - { - var parts = value.Split(':'); - if (parts.Length == 5) - { - int deviceVersion = 1; - if (Int32.TryParse(parts[4], out deviceVersion)) - { - device.DeviceTypeNamespace = parts[1]; - device.DeviceType = parts[3]; - device.DeviceVersion = deviceVersion; - } - else - device.DeviceType = value; - } - else - device.DeviceType = value; - } - } - - private static void SetUuidFromUdn(SsdpDevice device) - { - if (device.Udn != null && device.Udn.StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) - device.Uuid = device.Udn.Substring(5).Trim(); - else - device.Uuid = device.Udn; - } - - private static void LoadIcons(XmlReader reader, SsdpDevice device) - { - while (!reader.EOF) - { - while (!reader.EOF && reader.NodeType != XmlNodeType.Element) - { - reader.Read(); - } - - if (reader.LocalName != "icon") break; - - while (reader.Name == "icon") - { - var icon = new SsdpDeviceIcon(); - LoadIconProperties(reader, icon); - device.Icons.Add(icon); - - reader.Read(); - } - } - } - - private static void LoadIconProperties(XmlReader reader, SsdpDeviceIcon icon) - { - while (!reader.EOF) - { - if (reader.NodeType != XmlNodeType.Element) - { - if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "icon") break; - - reader.Read(); - continue; - } - - switch (reader.LocalName) - { - case "depth": - icon.ColorDepth = reader.ReadElementContentAsInt(); - break; - - case "height": - icon.Height = reader.ReadElementContentAsInt(); - break; - - case "width": - icon.Width = reader.ReadElementContentAsInt(); - break; - - case "mimetype": - icon.MimeType = reader.ReadElementContentAsString(); - break; - - case "url": - icon.Url = StringToUri(reader.ReadElementContentAsString()); - break; - - } - - reader.Read(); - } - } - - private void LoadChildDevices(XmlReader reader, SsdpDevice device) - { - while (!reader.EOF && reader.NodeType != XmlNodeType.Element) - { - reader.Read(); - } - - while (!reader.EOF) - { - while (!reader.EOF && reader.NodeType != XmlNodeType.Element) - { - reader.Read(); - } - - if (reader.LocalName == "device") - { - var childDevice = new SsdpEmbeddedDevice(); - LoadDeviceProperties(reader, childDevice); - device.AddDevice(childDevice); - } - else - break; - } - } - - private static void AddCustomProperty(XmlReader reader, SsdpDevice device) - { - // If the property is an empty element, there is no value to read - // Advance the reader and return - if (reader.IsEmptyElement) - { - reader.Read(); - return; - } - - var newProp = new SsdpDeviceProperty() { Namespace = reader.Prefix, Name = reader.LocalName }; - int depth = reader.Depth; - reader.Read(); - while (reader.NodeType == XmlNodeType.Whitespace || reader.NodeType == XmlNodeType.Comment) - { - reader.Read(); - } - - if (reader.NodeType != XmlNodeType.CDATA && reader.NodeType != XmlNodeType.Text) - { - while (!reader.EOF && (reader.NodeType != XmlNodeType.EndElement || reader.Name != newProp.Name || reader.Prefix != newProp.Namespace || reader.Depth != depth)) - { - reader.Read(); - } - if (!reader.EOF) - reader.Read(); - return; - } - - newProp.Value = reader.Value; - - // We don't support complex nested types or repeat/multi-value properties - if (!device.CustomProperties.Contains(newProp.FullName)) - device.CustomProperties.Add(newProp); - } - - #endregion - - //private bool ChildDeviceExists(SsdpDevice device) - //{ - // return (from d in _Devices where device.Uuid == d.Uuid select d).Any(); - //} - #endregion } diff --git a/RSSDP/SsdpDeviceExtensions.cs b/RSSDP/SsdpDeviceExtensions.cs deleted file mode 100644 index 6e1b45646..000000000 --- a/RSSDP/SsdpDeviceExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Rssdp -{ - /// <summary> - /// Extensions for <see cref="SsdpDevice"/> and derived types. - /// </summary> - public static class SsdpDeviceExtensions - { - - /// <summary> - /// Returns the root device associated with a device instance derived from <see cref="SsdpDevice"/>. - /// </summary> - /// <param name="device">The device instance to find the <see cref="SsdpRootDevice"/> for.</param> - /// <remarks> - /// <para>The <paramref name="device"/> must be or inherit from <see cref="SsdpRootDevice"/> or <see cref="SsdpEmbeddedDevice"/>, otherwise an <see cref="System.InvalidCastException"/> will occur.</para> - /// <para>May return null if the <paramref name="device"/> instance is an embedded device not yet associated with a <see cref="SsdpRootDevice"/> instance yet.</para> - /// <para>If <paramref name="device"/> is an instance of <see cref="SsdpRootDevice"/> (or derives from it), returns the same instance cast to <see cref="SsdpRootDevice"/>.</para> - /// </remarks> - /// <returns>The <see cref="SsdpRootDevice"/> instance associated with the device instance specified, or null otherwise.</returns> - /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="device"/> is null.</exception> - /// <exception cref="System.InvalidCastException">Thrown if <paramref name="device"/> is not an instance of or dervied from either <see cref="SsdpRootDevice"/> or <see cref="SsdpEmbeddedDevice"/>.</exception> - public static SsdpRootDevice ToRootDevice(this SsdpDevice device) - { - if (device == null) throw new System.ArgumentNullException("device"); - - var rootDevice = device as SsdpRootDevice; - if (rootDevice == null) - rootDevice = ((SsdpEmbeddedDevice)device).RootDevice; - - return rootDevice; - } - } -} diff --git a/RSSDP/SsdpDeviceIcon.cs b/RSSDP/SsdpDeviceIcon.cs deleted file mode 100644 index 3ed707c80..000000000 --- a/RSSDP/SsdpDeviceIcon.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace Rssdp -{ - /// <summary> - /// Represents an icon published by an <see cref="SsdpDevice"/>. - /// </summary> - public sealed class SsdpDeviceIcon - { - /// <summary> - /// The mime type for the image data returned by the <see cref="Url"/> property. - /// </summary> - /// <remarks> - /// <para>Required. Icon's MIME type (cf. RFC 2045, 2046, and 2387). Single MIME image type. At least one icon should be of type “image/png” (Portable Network Graphics, see IETF RFC 2083).</para> - /// </remarks> - /// <seealso cref="Url"/> - public string MimeType { get; set; } - - /// <summary> - /// The URL that can be called with an HTTP GET command to retrieve the image data. - /// </summary> - /// <remarks> - /// <para>Required. May be relative to base URL. Specified by UPnP vendor. Single URL.</para> - /// </remarks> - /// <seealso cref="MimeType"/> - public Uri Url { get; set; } - - /// <summary> - /// The width of the image in pixels. - /// </summary> - /// <remarks><para>Required, must be greater than zero.</para></remarks> - public int Width { get; set; } - - /// <summary> - /// The height of the image in pixels. - /// </summary> - /// <remarks><para>Required, must be greater than zero.</para></remarks> - public int Height { get; set; } - - /// <summary> - /// The colour depth of the image. - /// </summary> - /// <remarks><para>Required, must be greater than zero.</para></remarks> - public int ColorDepth { get; set; } - - } -} diff --git a/RSSDP/SsdpDeviceLocator.cs b/RSSDP/SsdpDeviceLocator.cs index 8fe4d4a14..6a61a52f3 100644 --- a/RSSDP/SsdpDeviceLocator.cs +++ b/RSSDP/SsdpDeviceLocator.cs @@ -1,35 +1,624 @@ -using System; +using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Net.Http; using System.Text; +using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Model.Net; using MediaBrowser.Model.Threading; -using Rssdp.Infrastructure; -namespace Rssdp +namespace Rssdp.Infrastructure { - // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS - // Be careful to check any changes compile and work for all platform projects it is shared in. - - /// <summary> - /// Allows you to search the network for a particular device, device types, or UPnP service types. Also listenings for broadcast notifications of device availability and raises events to indicate changes in status. - /// </summary> - public sealed class SsdpDeviceLocator : SsdpDeviceLocatorBase - { - - /// <summary> - /// Default constructor. Constructs a new instance using the default <see cref="ISsdpCommunicationsServer"/> and <see cref="ISocketFactory"/> implementations for this platform. - /// </summary> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification="Can't expose along exception paths here (exceptions should be very rare anyway, and probably fatal too) and we shouldn't dipose the items we pass to base in any other case.")] - public SsdpDeviceLocator(ISsdpCommunicationsServer communicationsServer, ITimerFactory timerFacatory) : base(communicationsServer, timerFacatory) - { - // This is not the problem you are looking for; - // Yes, this is poor man's dependency injection which some call an anti-pattern. - // However, it makes the library really simple to get started with or to use if the calling code isn't using IoC/DI. - // The fact we have injected dependencies is really an internal architectural implementation detail to allow for the - // cross platform and testing concerns of this library. It shouldn't be something calling code worries about and is - // not a deliberate extension point, except where adding new platform support in which case... - // There is a constructor that takes a manually injected dependency anyway, so proper DI using - // a container or whatever can be done anyway. - } - } + /// <summary> + /// Allows you to search the network for a particular device, device types, or UPnP service types. Also listenings for broadcast notifications of device availability and raises events to indicate changes in status. + /// </summary> + public class SsdpDeviceLocator : DisposableManagedObjectBase + { + + #region Fields & Constants + + private List<DiscoveredSsdpDevice> _Devices; + private ISsdpCommunicationsServer _CommunicationsServer; + + private ITimer _BroadcastTimer; + private ITimerFactory _timerFactory; + private object _timerLock = new object(); + + private readonly TimeSpan DefaultSearchWaitTime = TimeSpan.FromSeconds(4); + private readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); + + #endregion + + #region Constructors + + /// <summary> + /// Default constructor. + /// </summary> + public SsdpDeviceLocator(ISsdpCommunicationsServer communicationsServer, ITimerFactory timerFactory) + { + if (communicationsServer == null) throw new ArgumentNullException("communicationsServer"); + + _CommunicationsServer = communicationsServer; + _timerFactory = timerFactory; + _CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived; + + _Devices = new List<DiscoveredSsdpDevice>(); + } + + #endregion + + #region Events + + /// <summary> + /// Raised for when + /// <list type="bullet"> + /// <item>An 'alive' notification is received that a device, regardless of whether or not that device is not already in the cache or has previously raised this event.</item> + /// <item>For each item found during a device <see cref="SearchAsync()"/> (cached or not), allowing clients to respond to found devices before the entire search is complete.</item> + /// <item>Only if the notification type matches the <see cref="NotificationFilter"/> property. By default the filter is null, meaning all notifications raise events (regardless of ant </item> + /// </list> + /// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para> + /// </summary> + /// <seealso cref="NotificationFilter"/> + /// <seealso cref="DeviceUnavailable"/> + /// <seealso cref="StartListeningForNotifications"/> + /// <seealso cref="StopListeningForNotifications"/> + public event EventHandler<DeviceAvailableEventArgs> DeviceAvailable; + + /// <summary> + /// Raised when a notification is received that indicates a device has shutdown or otherwise become unavailable. + /// </summary> + /// <remarks> + /// <para>Devices *should* broadcast these types of notifications, but not all devices do and sometimes (in the event of power loss for example) it might not be possible for a device to do so. You should also implement error handling when trying to contact a device, even if RSSDP is reporting that device as available.</para> + /// <para>This event is only raised if the notification type matches the <see cref="NotificationFilter"/> property. A null or empty string for the <see cref="NotificationFilter"/> will be treated as no filter and raise the event for all notifications.</para> + /// <para>The <see cref="DeviceUnavailableEventArgs.DiscoveredDevice"/> property may contain either a fully complete <see cref="DiscoveredSsdpDevice"/> instance, or one containing just a USN and NotificationType property. Full information is available if the device was previously discovered and cached, but only partial information if a byebye notification was received for a previously unseen or expired device.</para> + /// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para> + /// </remarks> + /// <seealso cref="NotificationFilter"/> + /// <seealso cref="DeviceAvailable"/> + /// <seealso cref="StartListeningForNotifications"/> + /// <seealso cref="StopListeningForNotifications"/> + public event EventHandler<DeviceUnavailableEventArgs> DeviceUnavailable; + + #endregion + + #region Public Methods + + #region Search Overloads + + public void RestartBroadcastTimer(TimeSpan dueTime, TimeSpan period) + { + lock (_timerLock) + { + if (_BroadcastTimer == null) + { + _BroadcastTimer = _timerFactory.Create(OnBroadcastTimerCallback, null, dueTime, period); + } + else + { + _BroadcastTimer.Change(dueTime, period); + } + } + } + + public void DisposeBroadcastTimer() + { + lock (_timerLock) + { + if (_BroadcastTimer != null) + { + _BroadcastTimer.Dispose(); + _BroadcastTimer = null; + } + } + } + + private async void OnBroadcastTimerCallback(object state) + { + StartListeningForNotifications(); + RemoveExpiredDevicesFromCache(); + + try + { + await SearchAsync(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + + } + } + + /// <summary> + /// Performs a search for all devices using the default search timeout. + /// </summary> + private Task SearchAsync(CancellationToken cancellationToken) + { + return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, DefaultSearchWaitTime, cancellationToken); + } + + /// <summary> + /// Performs a search for the specified search target (criteria) and default search timeout. + /// </summary> + /// <param name="searchTarget">The criteria for the search. Value can be; + /// <list type="table"> + /// <item><term>Root devices</term><description>upnp:rootdevice</description></item> + /// <item><term>Specific device by UUID</term><description>uuid:<device uuid></description></item> + /// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item> + /// </list> + /// </param> + private Task SearchAsync(string searchTarget) + { + return SearchAsync(searchTarget, DefaultSearchWaitTime, CancellationToken.None); + } + + /// <summary> + /// Performs a search for all devices using the specified search timeout. + /// </summary> + /// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 seconds is recommended by the UPnP 1.1 specification, this method requires the value be greater 1 second if it is not zero. Specify TimeSpan.Zero to return only devices already in the cache.</param> + private Task SearchAsync(TimeSpan searchWaitTime) + { + return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, searchWaitTime, CancellationToken.None); + } + + private Task SearchAsync(string searchTarget, TimeSpan searchWaitTime, CancellationToken cancellationToken) + { + if (searchTarget == null) throw new ArgumentNullException("searchTarget"); + if (searchTarget.Length == 0) throw new ArgumentException("searchTarget cannot be an empty string.", "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); + } + + #endregion + + /// <summary> + /// Starts listening for broadcast notifications of service availability. + /// </summary> + /// <remarks> + /// <para>When called the system will listen for 'alive' and 'byebye' notifications. This can speed up searching, as well as provide dynamic notification of new devices appearing on the network, and previously discovered devices disappearing.</para> + /// </remarks> + /// <seealso cref="StopListeningForNotifications"/> + /// <seealso cref="DeviceAvailable"/> + /// <seealso cref="DeviceUnavailable"/> + /// <exception cref="System.ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> ty is true.</exception> + public void StartListeningForNotifications() + { + ThrowIfDisposed(); + + _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; + _CommunicationsServer.RequestReceived += CommsServer_RequestReceived; + _CommunicationsServer.BeginListeningForBroadcasts(); + } + + /// <summary> + /// Stops listening for broadcast notifications of service availability. + /// </summary> + /// <remarks> + /// <para>Does nothing if this instance is not already listening for notifications.</para> + /// </remarks> + /// <seealso cref="StartListeningForNotifications"/> + /// <seealso cref="DeviceAvailable"/> + /// <seealso cref="DeviceUnavailable"/> + /// <exception cref="System.ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true.</exception> + public void StopListeningForNotifications() + { + ThrowIfDisposed(); + + _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; + } + + /// <summary> + /// Raises the <see cref="DeviceAvailable"/> event. + /// </summary> + /// <seealso cref="DeviceAvailable"/> + protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IpAddressInfo localIpAddress) + { + if (this.IsDisposed) return; + + var handlers = this.DeviceAvailable; + if (handlers != null) + handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) + { + LocalIpAddress = localIpAddress + }); + } + + /// <summary> + /// Raises the <see cref="DeviceUnavailable"/> event. + /// </summary> + /// <param name="device">A <see cref="DiscoveredSsdpDevice"/> representing the device that is no longer available.</param> + /// <param name="expired">True if the device expired from the cache without being renewed, otherwise false to indicate the device explicitly notified us it was being shutdown.</param> + /// <seealso cref="DeviceUnavailable"/> + protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) + { + if (this.IsDisposed) return; + + var handlers = this.DeviceUnavailable; + if (handlers != null) + handlers(this, new DeviceUnavailableEventArgs(device, expired)); + } + + #endregion + + #region Public Properties + + /// <summary> + /// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the <see cref="ISsdpDeviceLocator.DeviceAvailable"/> or <see cref="ISsdpDeviceLocator.DeviceUnavailable"/> events. + /// </summary> + /// <remarks> + /// <para>Device alive/byebye notifications whose NT header does not match this filter value will still be captured and cached internally, but will not raise events about device availability. Usually used with either a device type of uuid NT header value.</para> + /// <para>If the value is null or empty string then, all notifications are reported.</para> + /// <para>Example filters follow;</para> + /// <example>upnp:rootdevice</example> + /// <example>urn:schemas-upnp-org:device:WANDevice:1</example> + /// <example>uuid:9F15356CC-95FA-572E-0E99-85B456BD3012</example> + /// </remarks> + /// <seealso cref="ISsdpDeviceLocator.DeviceAvailable"/> + /// <seealso cref="ISsdpDeviceLocator.DeviceUnavailable"/> + /// <seealso cref="ISsdpDeviceLocator.StartListeningForNotifications"/> + /// <seealso cref="ISsdpDeviceLocator.StopListeningForNotifications"/> + public string NotificationFilter + { + get; + set; + } + + #endregion + + #region Overrides + + /// <summary> + /// Disposes this object and all internal resources. Stops listening for all network messages. + /// </summary> + /// <param name="disposing">True if managed resources should be disposed, or false is only unmanaged resources should be cleaned up.</param> + protected override void Dispose(bool disposing) + { + if (disposing) + { + DisposeBroadcastTimer(); + + var commsServer = _CommunicationsServer; + _CommunicationsServer = null; + if (commsServer != null) + { + commsServer.ResponseReceived -= this.CommsServer_ResponseReceived; + commsServer.RequestReceived -= this.CommsServer_RequestReceived; + } + } + } + + #endregion + + #region Private Methods + + #region Discovery/Device Add + + private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IpAddressInfo localIpAddress) + { + bool isNewDevice = false; + lock (_Devices) + { + var existingDevice = FindExistingDeviceNotification(_Devices, device.NotificationType, device.Usn); + if (existingDevice == null) + { + _Devices.Add(device); + isNewDevice = true; + } + else + { + _Devices.Remove(existingDevice); + _Devices.Add(device); + } + } + + DeviceFound(device, isNewDevice, localIpAddress); + } + + private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IpAddressInfo localIpAddress) + { + if (!NotificationTypeMatchesFilter(device)) return; + + OnDeviceAvailable(device, isNewDevice, localIpAddress); + } + + private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device) + { + return String.IsNullOrEmpty(this.NotificationFilter) + || this.NotificationFilter == SsdpConstants.SsdpDiscoverAllSTHeader + || device.NotificationType == this.NotificationFilter; + } + + #endregion + + #region Network Message Processing + + private Task BroadcastDiscoverMessage(string serviceType, TimeSpan mxValue, CancellationToken cancellationToken) + { + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + values["HOST"] = "239.255.255.250:1900"; + values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/1.0.4.2"; + //values["X-EMBY-SERVERID"] = _appHost.SystemId; + + values["MAN"] = "\"ssdp:discover\""; + + // Search target + values["ST"] = "ssdp:all"; + + // Seconds to delay response + values["MX"] = "3"; + + var header = "M-SEARCH * HTTP/1.1"; + + var message = BuildMessage(header, values); + + return _CommunicationsServer.SendMulticastMessage(message, cancellationToken); + } + + private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress) + { + if (!message.IsSuccessStatusCode) return; + + var location = GetFirstHeaderUriValue("Location", message); + if (location != 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, localIpAddress); + } + } + + private void ProcessNotificationMessage(HttpRequestMessage message, IpAddressInfo localIpAddress) + { + 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, localIpAddress); + else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) + ProcessByeByeNotification(message); + } + + private void ProcessAliveNotification(HttpRequestMessage message, IpAddressInfo localIpAddress) + { + var location = GetFirstHeaderUriValue("Location", message); + if (location != 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, localIpAddress); + } + } + + 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); + } + } + } + + #region Header/Message Processing Utilities + + private string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) + { + string retVal = null; + IEnumerable<string> values; + if (message.Headers.Contains(headerName)) + { + message.Headers.TryGetValues(headerName, out values); + if (values != null) + retVal = values.FirstOrDefault(); + } + + return retVal; + } + + private string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) + { + string retVal = null; + IEnumerable<string> values; + if (message.Headers.Contains(headerName)) + { + message.Headers.TryGetValues(headerName, out values); + if (values != null) + retVal = values.FirstOrDefault(); + } + + return retVal; + } + + private Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) + { + string value = null; + IEnumerable<string> values; + if (request.Headers.Contains(headerName)) + { + request.Headers.TryGetValues(headerName, out values); + if (values != null) + value = values.FirstOrDefault(); + } + + Uri retVal; + Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out retVal); + return retVal; + } + + private Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) + { + string value = null; + IEnumerable<string> values; + if (response.Headers.Contains(headerName)) + { + response.Headers.TryGetValues(headerName, out values); + if (values != null) + value = values.FirstOrDefault(); + } + + Uri retVal; + Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out retVal); + return retVal; + } + + private TimeSpan CacheAgeFromHeader(System.Net.Http.Headers.CacheControlHeaderValue headerValue) + { + if (headerValue == null) return TimeSpan.Zero; + + return (TimeSpan)(headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero); + } + + #endregion + + #endregion + + #region Expiry and Device Removal + + private void RemoveExpiredDevicesFromCache() + { + if (this.IsDisposed) return; + + DiscoveredSsdpDevice[] expiredDevices = null; + lock (_Devices) + { + expiredDevices = (from device in _Devices where device.IsExpired() select device).ToArray(); + + foreach (var device in expiredDevices) + { + if (this.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 (this.IsDisposed) return; + + DeviceDied(expiredUsn, true); + } + } + + private bool DeviceDied(string deviceUsn, bool expired) + { + List<DiscoveredSsdpDevice> existingDevices = null; + lock (_Devices) + { + existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn); + foreach (var existingDevice in existingDevices) + { + if (this.IsDisposed) return true; + + _Devices.Remove(existingDevice); + } + } + + if (existingDevices != null && existingDevices.Count > 0) + { + foreach (var removedDevice in existingDevices) + { + if (NotificationTypeMatchesFilter(removedDevice)) + OnDeviceUnavailable(removedDevice, expired); + } + + return true; + } + + return false; + } + + #endregion + + private TimeSpan SearchTimeToMXValue(TimeSpan searchWaitTime) + { + if (searchWaitTime.TotalSeconds < 2 || searchWaitTime == TimeSpan.Zero) + return OneSecond; + else + return searchWaitTime.Subtract(OneSecond); + } + + private DiscoveredSsdpDevice FindExistingDeviceNotification(IEnumerable<DiscoveredSsdpDevice> devices, string notificationType, string usn) + { + foreach (var d in devices) + { + if (d.NotificationType == notificationType && d.Usn == usn) + { + return d; + } + } + return null; + } + + private List<DiscoveredSsdpDevice> FindExistingDeviceNotifications(IList<DiscoveredSsdpDevice> devices, string usn) + { + var list = new List<DiscoveredSsdpDevice>(); + + foreach (var d in devices) + { + if (d.Usn == usn) + { + list.Add(d); + } + } + + return list; + } + + #endregion + + #region Event Handlers + + 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.LocalIpAddress); + } + + #endregion + + } }
\ No newline at end of file diff --git a/RSSDP/SsdpDeviceLocatorBase.cs b/RSSDP/SsdpDeviceLocatorBase.cs deleted file mode 100644 index d1eaef88a..000000000 --- a/RSSDP/SsdpDeviceLocatorBase.cs +++ /dev/null @@ -1,628 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Threading; -using RSSDP; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Allows you to search the network for a particular device, device types, or UPnP service types. Also listenings for broadcast notifications of device availability and raises events to indicate changes in status. - /// </summary> - public abstract class SsdpDeviceLocatorBase : DisposableManagedObjectBase - { - - #region Fields & Constants - - private List<DiscoveredSsdpDevice> _Devices; - private ISsdpCommunicationsServer _CommunicationsServer; - - private ITimer _BroadcastTimer; - private ITimerFactory _timerFactory; - private object _timerLock = new object(); - - private static readonly TimeSpan DefaultSearchWaitTime = TimeSpan.FromSeconds(4); - private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); - - #endregion - - #region Constructors - - /// <summary> - /// Default constructor. - /// </summary> - protected SsdpDeviceLocatorBase(ISsdpCommunicationsServer communicationsServer, ITimerFactory timerFactory) - { - if (communicationsServer == null) throw new ArgumentNullException("communicationsServer"); - - _CommunicationsServer = communicationsServer; - _timerFactory = timerFactory; - _CommunicationsServer.ResponseReceived += CommsServer_ResponseReceived; - - _Devices = new List<DiscoveredSsdpDevice>(); - } - - #endregion - - #region Events - - /// <summary> - /// Raised for when - /// <list type="bullet"> - /// <item>An 'alive' notification is received that a device, regardless of whether or not that device is not already in the cache or has previously raised this event.</item> - /// <item>For each item found during a device <see cref="SearchAsync()"/> (cached or not), allowing clients to respond to found devices before the entire search is complete.</item> - /// <item>Only if the notification type matches the <see cref="NotificationFilter"/> property. By default the filter is null, meaning all notifications raise events (regardless of ant </item> - /// </list> - /// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para> - /// </summary> - /// <seealso cref="NotificationFilter"/> - /// <seealso cref="DeviceUnavailable"/> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="StopListeningForNotifications"/> - public event EventHandler<DeviceAvailableEventArgs> DeviceAvailable; - - /// <summary> - /// Raised when a notification is received that indicates a device has shutdown or otherwise become unavailable. - /// </summary> - /// <remarks> - /// <para>Devices *should* broadcast these types of notifications, but not all devices do and sometimes (in the event of power loss for example) it might not be possible for a device to do so. You should also implement error handling when trying to contact a device, even if RSSDP is reporting that device as available.</para> - /// <para>This event is only raised if the notification type matches the <see cref="NotificationFilter"/> property. A null or empty string for the <see cref="NotificationFilter"/> will be treated as no filter and raise the event for all notifications.</para> - /// <para>The <see cref="DeviceUnavailableEventArgs.DiscoveredDevice"/> property may contain either a fully complete <see cref="DiscoveredSsdpDevice"/> instance, or one containing just a USN and NotificationType property. Full information is available if the device was previously discovered and cached, but only partial information if a byebye notification was received for a previously unseen or expired device.</para> - /// <para>This event may be raised from a background thread, if interacting with UI or other objects with specific thread affinity invoking to the relevant thread is required.</para> - /// </remarks> - /// <seealso cref="NotificationFilter"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="StopListeningForNotifications"/> - public event EventHandler<DeviceUnavailableEventArgs> DeviceUnavailable; - - #endregion - - #region Public Methods - - #region Search Overloads - - public void RestartBroadcastTimer(TimeSpan dueTime, TimeSpan period) - { - lock (_timerLock) - { - if (_BroadcastTimer == null) - { - _BroadcastTimer = _timerFactory.Create(OnBroadcastTimerCallback, null, dueTime, period); - } - else - { - _BroadcastTimer.Change(dueTime, period); - } - } - } - - public void DisposeBroadcastTimer() - { - lock (_timerLock) - { - if (_BroadcastTimer != null) - { - _BroadcastTimer.Dispose(); - _BroadcastTimer = null; - } - } - } - - private async void OnBroadcastTimerCallback(object state) - { - StartListeningForNotifications(); - RemoveExpiredDevicesFromCache(); - - try - { - await SearchAsync(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - - } - } - - /// <summary> - /// Performs a search for all devices using the default search timeout. - /// </summary> - private Task SearchAsync(CancellationToken cancellationToken) - { - return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, DefaultSearchWaitTime, cancellationToken); - } - - /// <summary> - /// Performs a search for the specified search target (criteria) and default search timeout. - /// </summary> - /// <param name="searchTarget">The criteria for the search. Value can be; - /// <list type="table"> - /// <item><term>Root devices</term><description>upnp:rootdevice</description></item> - /// <item><term>Specific device by UUID</term><description>uuid:<device uuid></description></item> - /// <item><term>Device type</term><description>Fully qualified device type starting with urn: i.e urn:schemas-upnp-org:Basic:1</description></item> - /// </list> - /// </param> - private Task SearchAsync(string searchTarget) - { - return SearchAsync(searchTarget, DefaultSearchWaitTime, CancellationToken.None); - } - - /// <summary> - /// Performs a search for all devices using the specified search timeout. - /// </summary> - /// <param name="searchWaitTime">The amount of time to wait for network responses to the search request. Longer values will likely return more devices, but increase search time. A value between 1 and 5 seconds is recommended by the UPnP 1.1 specification, this method requires the value be greater 1 second if it is not zero. Specify TimeSpan.Zero to return only devices already in the cache.</param> - private Task SearchAsync(TimeSpan searchWaitTime) - { - return SearchAsync(SsdpConstants.SsdpDiscoverAllSTHeader, searchWaitTime, CancellationToken.None); - } - - private Task SearchAsync(string searchTarget, TimeSpan searchWaitTime, CancellationToken cancellationToken) - { - if (searchTarget == null) throw new ArgumentNullException("searchTarget"); - if (searchTarget.Length == 0) throw new ArgumentException("searchTarget cannot be an empty string.", "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); - } - - #endregion - - /// <summary> - /// Starts listening for broadcast notifications of service availability. - /// </summary> - /// <remarks> - /// <para>When called the system will listen for 'alive' and 'byebye' notifications. This can speed up searching, as well as provide dynamic notification of new devices appearing on the network, and previously discovered devices disappearing.</para> - /// </remarks> - /// <seealso cref="StopListeningForNotifications"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="DeviceUnavailable"/> - /// <exception cref="System.ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> ty is true.</exception> - public void StartListeningForNotifications() - { - ThrowIfDisposed(); - - _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; - _CommunicationsServer.RequestReceived += CommsServer_RequestReceived; - _CommunicationsServer.BeginListeningForBroadcasts(); - } - - /// <summary> - /// Stops listening for broadcast notifications of service availability. - /// </summary> - /// <remarks> - /// <para>Does nothing if this instance is not already listening for notifications.</para> - /// </remarks> - /// <seealso cref="StartListeningForNotifications"/> - /// <seealso cref="DeviceAvailable"/> - /// <seealso cref="DeviceUnavailable"/> - /// <exception cref="System.ObjectDisposedException">Throw if the <see cref="DisposableManagedObjectBase.IsDisposed"/> property is true.</exception> - public void StopListeningForNotifications() - { - ThrowIfDisposed(); - - _CommunicationsServer.RequestReceived -= CommsServer_RequestReceived; - } - - /// <summary> - /// Raises the <see cref="DeviceAvailable"/> event. - /// </summary> - /// <seealso cref="DeviceAvailable"/> - protected virtual void OnDeviceAvailable(DiscoveredSsdpDevice device, bool isNewDevice, IpAddressInfo localIpAddress) - { - if (this.IsDisposed) return; - - var handlers = this.DeviceAvailable; - if (handlers != null) - handlers(this, new DeviceAvailableEventArgs(device, isNewDevice) - { - LocalIpAddress = localIpAddress - }); - } - - /// <summary> - /// Raises the <see cref="DeviceUnavailable"/> event. - /// </summary> - /// <param name="device">A <see cref="DiscoveredSsdpDevice"/> representing the device that is no longer available.</param> - /// <param name="expired">True if the device expired from the cache without being renewed, otherwise false to indicate the device explicitly notified us it was being shutdown.</param> - /// <seealso cref="DeviceUnavailable"/> - protected virtual void OnDeviceUnavailable(DiscoveredSsdpDevice device, bool expired) - { - if (this.IsDisposed) return; - - var handlers = this.DeviceUnavailable; - if (handlers != null) - handlers(this, new DeviceUnavailableEventArgs(device, expired)); - } - - #endregion - - #region Public Properties - - /// <summary> - /// Sets or returns a string containing the filter for notifications. Notifications not matching the filter will not raise the <see cref="ISsdpDeviceLocator.DeviceAvailable"/> or <see cref="ISsdpDeviceLocator.DeviceUnavailable"/> events. - /// </summary> - /// <remarks> - /// <para>Device alive/byebye notifications whose NT header does not match this filter value will still be captured and cached internally, but will not raise events about device availability. Usually used with either a device type of uuid NT header value.</para> - /// <para>If the value is null or empty string then, all notifications are reported.</para> - /// <para>Example filters follow;</para> - /// <example>upnp:rootdevice</example> - /// <example>urn:schemas-upnp-org:device:WANDevice:1</example> - /// <example>uuid:9F15356CC-95FA-572E-0E99-85B456BD3012</example> - /// </remarks> - /// <seealso cref="ISsdpDeviceLocator.DeviceAvailable"/> - /// <seealso cref="ISsdpDeviceLocator.DeviceUnavailable"/> - /// <seealso cref="ISsdpDeviceLocator.StartListeningForNotifications"/> - /// <seealso cref="ISsdpDeviceLocator.StopListeningForNotifications"/> - public string NotificationFilter - { - get; - set; - } - - #endregion - - #region Overrides - - /// <summary> - /// Disposes this object and all internal resources. Stops listening for all network messages. - /// </summary> - /// <param name="disposing">True if managed resources should be disposed, or false is only unmanaged resources should be cleaned up.</param> - protected override void Dispose(bool disposing) - { - - if (disposing) - { - DisposeBroadcastTimer(); - - var commsServer = _CommunicationsServer; - _CommunicationsServer = null; - if (commsServer != null) - { - commsServer.ResponseReceived -= this.CommsServer_ResponseReceived; - commsServer.RequestReceived -= this.CommsServer_RequestReceived; - if (!commsServer.IsShared) - commsServer.Dispose(); - } - } - } - - #endregion - - #region Private Methods - - #region Discovery/Device Add - - private void AddOrUpdateDiscoveredDevice(DiscoveredSsdpDevice device, IpAddressInfo localIpAddress) - { - bool isNewDevice = false; - lock (_Devices) - { - var existingDevice = FindExistingDeviceNotification(_Devices, device.NotificationType, device.Usn); - if (existingDevice == null) - { - _Devices.Add(device); - isNewDevice = true; - } - else - { - _Devices.Remove(existingDevice); - _Devices.Add(device); - } - } - - DeviceFound(device, isNewDevice, localIpAddress); - } - - private void DeviceFound(DiscoveredSsdpDevice device, bool isNewDevice, IpAddressInfo localIpAddress) - { - if (!NotificationTypeMatchesFilter(device)) return; - - OnDeviceAvailable(device, isNewDevice, localIpAddress); - } - - private bool NotificationTypeMatchesFilter(DiscoveredSsdpDevice device) - { - return String.IsNullOrEmpty(this.NotificationFilter) - || this.NotificationFilter == SsdpConstants.SsdpDiscoverAllSTHeader - || device.NotificationType == this.NotificationFilter; - } - - #endregion - - #region Network Message Processing - - private Task BroadcastDiscoverMessage(string serviceType, TimeSpan mxValue, CancellationToken cancellationToken) - { - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - values["HOST"] = "239.255.255.250:1900"; - values["USER-AGENT"] = "UPnP/1.0 DLNADOC/1.50 Platinum/1.0.4.2"; - //values["X-EMBY-SERVERID"] = _appHost.SystemId; - - values["MAN"] = "\"ssdp:discover\""; - - // Search target - values["ST"] = "ssdp:all"; - - // Seconds to delay response - values["MX"] = "3"; - - var header = "M-SEARCH * HTTP/1.1"; - - var message = SsdpHelper.BuildMessage(header, values); - - return _CommunicationsServer.SendMulticastMessage(message, cancellationToken); - } - - private void ProcessSearchResponseMessage(HttpResponseMessage message, IpAddressInfo localIpAddress) - { - if (!message.IsSuccessStatusCode) return; - - var location = GetFirstHeaderUriValue("Location", message); - if (location != 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, localIpAddress); - } - } - - private void ProcessNotificationMessage(HttpRequestMessage message, IpAddressInfo localIpAddress) - { - 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, localIpAddress); - else if (String.Compare(notificationType, SsdpConstants.SsdpByeByeNotification, StringComparison.OrdinalIgnoreCase) == 0) - ProcessByeByeNotification(message); - } - - private void ProcessAliveNotification(HttpRequestMessage message, IpAddressInfo localIpAddress) - { - var location = GetFirstHeaderUriValue("Location", message); - if (location != 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, localIpAddress); - } - } - - 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); - } - } - } - - #region Header/Message Processing Utilities - - private static string GetFirstHeaderStringValue(string headerName, HttpResponseMessage message) - { - string retVal = null; - IEnumerable<string> values; - if (message.Headers.Contains(headerName)) - { - message.Headers.TryGetValues(headerName, out values); - if (values != null) - retVal = values.FirstOrDefault(); - } - - return retVal; - } - - private static string GetFirstHeaderStringValue(string headerName, HttpRequestMessage message) - { - string retVal = null; - IEnumerable<string> values; - if (message.Headers.Contains(headerName)) - { - message.Headers.TryGetValues(headerName, out values); - if (values != null) - retVal = values.FirstOrDefault(); - } - - return retVal; - } - - private static Uri GetFirstHeaderUriValue(string headerName, HttpRequestMessage request) - { - string value = null; - IEnumerable<string> values; - if (request.Headers.Contains(headerName)) - { - request.Headers.TryGetValues(headerName, out values); - if (values != null) - value = values.FirstOrDefault(); - } - - Uri retVal; - Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out retVal); - return retVal; - } - - private static Uri GetFirstHeaderUriValue(string headerName, HttpResponseMessage response) - { - string value = null; - IEnumerable<string> values; - if (response.Headers.Contains(headerName)) - { - response.Headers.TryGetValues(headerName, out values); - if (values != null) - value = values.FirstOrDefault(); - } - - Uri retVal; - Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out retVal); - return retVal; - } - - private static TimeSpan CacheAgeFromHeader(System.Net.Http.Headers.CacheControlHeaderValue headerValue) - { - if (headerValue == null) return TimeSpan.Zero; - - return (TimeSpan)(headerValue.MaxAge ?? headerValue.SharedMaxAge ?? TimeSpan.Zero); - } - - #endregion - - #endregion - - #region Expiry and Device Removal - - private void RemoveExpiredDevicesFromCache() - { - if (this.IsDisposed) return; - - DiscoveredSsdpDevice[] expiredDevices = null; - lock (_Devices) - { - expiredDevices = (from device in _Devices where device.IsExpired() select device).ToArray(); - - foreach (var device in expiredDevices) - { - if (this.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 (this.IsDisposed) return; - - DeviceDied(expiredUsn, true); - } - } - - private bool DeviceDied(string deviceUsn, bool expired) - { - List<DiscoveredSsdpDevice> existingDevices = null; - lock (_Devices) - { - existingDevices = FindExistingDeviceNotifications(_Devices, deviceUsn); - foreach (var existingDevice in existingDevices) - { - if (this.IsDisposed) return true; - - _Devices.Remove(existingDevice); - } - } - - if (existingDevices != null && existingDevices.Count > 0) - { - foreach (var removedDevice in existingDevices) - { - if (NotificationTypeMatchesFilter(removedDevice)) - OnDeviceUnavailable(removedDevice, expired); - } - - return true; - } - - return false; - } - - #endregion - - private static TimeSpan SearchTimeToMXValue(TimeSpan searchWaitTime) - { - if (searchWaitTime.TotalSeconds < 2 || searchWaitTime == TimeSpan.Zero) - return OneSecond; - else - return searchWaitTime.Subtract(OneSecond); - } - - private static DiscoveredSsdpDevice FindExistingDeviceNotification(IEnumerable<DiscoveredSsdpDevice> devices, string notificationType, string usn) - { - foreach (var d in devices) - { - if (d.NotificationType == notificationType && d.Usn == usn) - { - return d; - } - } - return null; - } - - private static List<DiscoveredSsdpDevice> FindExistingDeviceNotifications(IList<DiscoveredSsdpDevice> devices, string usn) - { - var list = new List<DiscoveredSsdpDevice>(); - - foreach (var d in devices) - { - if (d.Usn == usn) - { - list.Add(d); - } - } - - return list; - } - - #endregion - - #region Event Handlers - - 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.LocalIpAddress); - } - - #endregion - - } -}
\ No newline at end of file diff --git a/RSSDP/SsdpDeviceProperties.cs b/RSSDP/SsdpDeviceProperties.cs deleted file mode 100644 index ae5309da5..000000000 --- a/RSSDP/SsdpDeviceProperties.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Rssdp -{ - /// <summary> - /// Represents a collection of <see cref="SsdpDeviceProperty"/> instances keyed by the <see cref="SsdpDeviceProperty.FullName"/> property value. - /// </summary> - /// <remarks> - /// <para>Items added to this collection are keyed by their <see cref="SsdpDeviceProperty.FullName"/> property value, at the time they were added. If the name changes after they were added to the collection, the key is not updated unless the item is manually removed and re-added to the collection.</para> - /// </remarks> - public class SsdpDevicePropertiesCollection : IEnumerable<SsdpDeviceProperty> - { - - #region Fields - - private IDictionary<string, SsdpDeviceProperty> _Properties; - - #endregion - - #region Constructors - - /// <summary> - /// Default constructor. - /// </summary> - public SsdpDevicePropertiesCollection() - { - _Properties = new Dictionary<string, SsdpDeviceProperty>(); - } - - /// <summary> - /// Full constructor. - /// </summary> - /// <param name="capacity">Specifies the initial capacity of the collection.</param> - public SsdpDevicePropertiesCollection(int capacity) - { - _Properties = new Dictionary<string, SsdpDeviceProperty>(capacity); - } - - #endregion - - #region Public Methpds - - /// <summary> - /// Adds a <see cref="SsdpDeviceProperty"/> instance to the collection. - /// </summary> - /// <param name="customDeviceProperty">The property instance to add to the collection.</param> - /// <remarks> - /// <para></para> - /// </remarks> - /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="customDeviceProperty"/> is null.</exception> - /// <exception cref="System.ArgumentException">Thrown if the <see cref="SsdpDeviceProperty.FullName"/> property of the <paramref name="customDeviceProperty"/> argument is null or empty string, or if the collection already contains an item with the same key.</exception> - public void Add(SsdpDeviceProperty customDeviceProperty) - { - if (customDeviceProperty == null) throw new ArgumentNullException("customDeviceProperty"); - if (String.IsNullOrEmpty(customDeviceProperty.FullName)) throw new ArgumentException("customDeviceProperty.FullName cannot be null or empty."); - - lock (_Properties) - { - _Properties.Add(customDeviceProperty.FullName, customDeviceProperty); - } - } - - #region Remove Overloads - - /// <summary> - /// Removes the specified property instance from the collection. - /// </summary> - /// <param name="customDeviceProperty">The <see cref="SsdpDeviceProperty"/> instance to remove from the collection.</param> - /// <remarks> - /// <para>Only remove the specified property if that instance was in the collection, if another property with the same full name exists in the collection it is not removed.</para> - /// </remarks> - /// <returns>True if an item was removed from the collection, otherwise false (because it did not exist or was not the same instance).</returns> - /// <seealso cref="Remove(string)"/> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="customDeviceProperty"/> is null.</exception> - /// <exception cref="System.ArgumentException">Thrown if the <see cref="SsdpDeviceProperty.FullName"/> property of the <paramref name="customDeviceProperty"/> argument is null or empty string, or if the collection already contains an item with the same key.</exception> - public bool Remove(SsdpDeviceProperty customDeviceProperty) - { - if (customDeviceProperty == null) throw new ArgumentNullException("customDeviceProperty"); - if (String.IsNullOrEmpty(customDeviceProperty.FullName)) throw new ArgumentException("customDeviceProperty.FullName cannot be null or empty."); - - lock (_Properties) - { - if (_Properties.ContainsKey(customDeviceProperty.FullName) && _Properties[customDeviceProperty.FullName] == customDeviceProperty) - return _Properties.Remove(customDeviceProperty.FullName); - } - - return false; - } - - /// <summary> - /// Removes the property with the specified key (<see cref="SsdpDeviceProperty.FullName"/> from the collection. - /// </summary> - /// <param name="customDevicePropertyFullName">The full name of the <see cref="SsdpDeviceProperty"/> instance to remove from the collection.</param> - /// <returns>True if an item was removed from the collection, otherwise false (because no item exists in the collection with that key).</returns> - /// <exception cref="System.ArgumentException">Thrown if the <paramref name="customDevicePropertyFullName"/> argument is null or empty string.</exception> - public bool Remove(string customDevicePropertyFullName) - { - if (String.IsNullOrEmpty(customDevicePropertyFullName)) throw new ArgumentException("customDevicePropertyFullName cannot be null or empty."); - - lock (_Properties) - { - return _Properties.Remove(customDevicePropertyFullName); - } - } - - #endregion - - /// <summary> - /// Returns a boolean indicating whether or not the specified <see cref="SsdpDeviceProperty"/> instance is in the collection. - /// </summary> - /// <param name="customDeviceProperty">An <see cref="SsdpDeviceProperty"/> instance to check the collection for.</param> - /// <returns>True if the specified instance exists in the collection, otherwise false.</returns> - public bool Contains(SsdpDeviceProperty customDeviceProperty) - { - if (customDeviceProperty == null) throw new ArgumentNullException("customDeviceProperty"); - if (String.IsNullOrEmpty(customDeviceProperty.FullName)) throw new ArgumentException("customDeviceProperty.FullName cannot be null or empty."); - - lock (_Properties) - { - if (_Properties.ContainsKey(customDeviceProperty.FullName)) - return _Properties[customDeviceProperty.FullName] == customDeviceProperty; - } - - return false; - } - - /// <summary> - /// Returns a boolean indicating whether or not a <see cref="SsdpDeviceProperty"/> instance with the specified full name value exists in the collection. - /// </summary> - /// <param name="customDevicePropertyFullName">A string containing the full name of the <see cref="SsdpDeviceProperty"/> instance to check for.</param> - /// <returns>True if an item with the specified full name exists in the collection, otherwise false.</returns> - public bool Contains(string customDevicePropertyFullName) - { - if (String.IsNullOrEmpty(customDevicePropertyFullName)) throw new ArgumentException("customDevicePropertyFullName cannot be null or empty."); - - lock (_Properties) - { - return _Properties.ContainsKey(customDevicePropertyFullName); - } - } - - #endregion - - #region Public Properties - - /// <summary> - /// Returns the number of items in the collection. - /// </summary> - public int Count - { - get { return _Properties.Count; } - } - - /// <summary> - /// Returns the <see cref="SsdpDeviceProperty"/> instance from the collection that has the specified <see cref="SsdpDeviceProperty.FullName"/> value. - /// </summary> - /// <param name="fullName">The full name of the property to return.</param> - /// <returns>A <see cref="SsdpDeviceProperty"/> instance from the collection.</returns> - /// <exception cref="System.Collections.Generic.KeyNotFoundException">Thrown if no item exists in the collection with the specified <paramref name="fullName"/> value.</exception> - public SsdpDeviceProperty this[string fullName] - { - get - { - return _Properties[fullName]; - } - } - - #endregion - - #region IEnumerable<SsdpDeviceProperty> Members - - /// <summary> - /// Returns an enumerator of <see cref="SsdpDeviceProperty"/> instances in this collection. - /// </summary> - /// <returns>An enumerator of <see cref="SsdpDeviceProperty"/> instances in this collection.</returns> - public IEnumerator<SsdpDeviceProperty> GetEnumerator() - { - lock (_Properties) - { - return _Properties.Values.GetEnumerator(); - } - } - - #endregion - - #region IEnumerable Members - - /// <summary> - /// Returns an enumerator of <see cref="SsdpDeviceProperty"/> instances in this collection. - /// </summary> - /// <returns>An enumerator of <see cref="SsdpDeviceProperty"/> instances in this collection.</returns> - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - lock (_Properties) - { - return _Properties.Values.GetEnumerator(); - } - } - - #endregion - - } -}
\ No newline at end of file diff --git a/RSSDP/SsdpDeviceProperty.cs b/RSSDP/SsdpDeviceProperty.cs deleted file mode 100644 index 3abcfb9aa..000000000 --- a/RSSDP/SsdpDeviceProperty.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace Rssdp -{ - /// <summary> - /// Represents a custom property of an <see cref="SsdpDevice"/>. - /// </summary> - public sealed class SsdpDeviceProperty - { - - /// <summary> - /// Sets or returns the namespace this property exists in. - /// </summary> - public string Namespace { get; set; } - - /// <summary> - /// Sets or returns the name of this property. - /// </summary> - public string Name { get; set; } - - /// <summary> - /// Returns the full name of this property (namespace and name). - /// </summary> - public string FullName { get { return String.IsNullOrEmpty(this.Namespace) ? this.Name : this.Namespace + ":" + this.Name; } } - - /// <summary> - /// Sets or returns the value of this property. - /// </summary> - public string Value { get; set; } - - } -} diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 2aa143775..8d57deb5e 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -1,37 +1,561 @@ -using System; +using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.Http; using System.Text; +using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Model.Net; using MediaBrowser.Model.Threading; -using Rssdp.Infrastructure; +using Rssdp; -namespace Rssdp +namespace Rssdp.Infrastructure { - /// <summary> - /// Allows publishing devices both as notification and responses to search requests. - /// </summary> - /// <remarks> - /// This is the 'server' part of the system. You add your devices to an instance of this class so clients can find them. - /// </remarks> - public class SsdpDevicePublisher : SsdpDevicePublisherBase - { - - #region Constructors - - /// <summary> - /// Default constructor. - /// </summary> - /// <remarks> - /// <para>Uses the default <see cref="ISsdpCommunicationsServer"/> implementation and network settings for Windows and the SSDP specification.</para> - /// </remarks> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "No way to do this here, and we don't want to dispose it except in the (rare) case of an exception anyway.")] - public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, ITimerFactory timerFactory, string osName, string osVersion) - : base(communicationsServer, timerFactory, osName, osVersion) - { - - } - - #endregion + /// <summary> + /// Provides the platform independent logic for publishing SSDP devices (notifications and search responses). + /// </summary> + public class SsdpDevicePublisher : DisposableManagedObjectBase, ISsdpDevicePublisher + { + private ISsdpCommunicationsServer _CommsServer; + private string _OSName; + private string _OSVersion; + + private bool _SupportPnpRootDevice; + + private IList<SsdpRootDevice> _Devices; + private IReadOnlyList<SsdpRootDevice> _ReadOnlyDevices; + + private ITimer _RebroadcastAliveNotificationsTimer; + private ITimerFactory _timerFactory; + + private IDictionary<string, SearchRequest> _RecentSearchRequests; + + private Random _Random; + + private const string ServerVersion = "1.0"; + + /// <summary> + /// Default constructor. + /// </summary> + public SsdpDevicePublisher(ISsdpCommunicationsServer communicationsServer, ITimerFactory timerFactory, string osName, string osVersion) + { + if (communicationsServer == null) throw new ArgumentNullException("communicationsServer"); + if (osName == null) throw new ArgumentNullException("osName"); + if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", "osName"); + if (osVersion == null) throw new ArgumentNullException("osVersion"); + if (osVersion.Length == 0) throw new ArgumentException("osVersion cannot be an empty string.", "osName"); + + _SupportPnpRootDevice = true; + _timerFactory = timerFactory; + _Devices = new List<SsdpRootDevice>(); + _ReadOnlyDevices = new ReadOnlyCollection<SsdpRootDevice>(_Devices); + _RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase); + _Random = new Random(); + + _CommsServer = communicationsServer; + _CommsServer.RequestReceived += CommsServer_RequestReceived; + _OSName = osName; + _OSVersion = osVersion; + + _CommsServer.BeginListeningForBroadcasts(); + } + + public void StartBroadcastingAliveMessages(TimeSpan interval) + { + _RebroadcastAliveNotificationsTimer = _timerFactory.Create(SendAllAliveNotifications, null, TimeSpan.FromSeconds(5), interval); + } + + /// <summary> + /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients. + /// </summary> + /// <remarks> + /// <para>Adding a device causes "alive" notification messages to be sent immediately, or very soon after. Ensure your device/description service is running before adding the device object here.</para> + /// <para>Devices added here with a non-zero cache life time will also have notifications broadcast periodically.</para> + /// <para>This method ignores duplicate device adds (if the same device instance is added multiple times, the second and subsequent add calls do nothing).</para> + /// </remarks> + /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param> + /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> + /// <exception cref="System.InvalidOperationException">Thrown if the <paramref name="device"/> contains property values that are not acceptable to the UPnP 1.0 specification.</exception> + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "t", Justification = "Capture task to local variable supresses compiler warning, but task is not really needed.")] + public void AddDevice(SsdpRootDevice device) + { + if (device == null) throw new ArgumentNullException("device"); + + ThrowIfDisposed(); + + TimeSpan minCacheTime = TimeSpan.Zero; + bool wasAdded = false; + lock (_Devices) + { + if (!_Devices.Contains(device)) + { + _Devices.Add(device); + wasAdded = true; + minCacheTime = GetMinimumNonZeroCacheLifetime(); + } + } + + if (wasAdded) + { + WriteTrace("Device Added", device); + + SendAliveNotifications(device, true, CancellationToken.None); + } + } + + /// <summary> + /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable. + /// </summary> + /// <remarks> + /// <para>Removing a device causes "byebye" notification messages to be sent immediately, advising clients of the device/service becoming unavailable. We recommend removing the device from the published list before shutting down the actual device/service, if possible.</para> + /// <para>This method does nothing if the device was not found in the collection.</para> + /// </remarks> + /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param> + /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> + public async Task RemoveDevice(SsdpRootDevice device) + { + if (device == null) throw new ArgumentNullException("device"); + + bool wasRemoved = false; + TimeSpan minCacheTime = TimeSpan.Zero; + lock (_Devices) + { + if (_Devices.Contains(device)) + { + _Devices.Remove(device); + wasRemoved = true; + minCacheTime = GetMinimumNonZeroCacheLifetime(); + } + } + + if (wasRemoved) + { + WriteTrace("Device Removed", device); + + await SendByeByeNotifications(device, true, CancellationToken.None).ConfigureAwait(false); + } + } + + /// <summary> + /// Returns a read only list of devices being published by this instance. + /// </summary> + public IEnumerable<SsdpRootDevice> Devices + { + get + { + return _ReadOnlyDevices; + } + } + + /// <summary> + /// If true (default) treats root devices as both upnp:rootdevice and pnp:rootdevice types. + /// </summary> + /// <remarks> + /// <para>Enabling this option will cause devices to show up in Microsoft Windows Explorer's network screens (if discovery is enabled etc.). Windows Explorer appears to search only for pnp:rootdeivce and not upnp:rootdevice.</para> + /// <para>If false, the system will only use upnp:rootdevice for notifiation broadcasts and and search responses, which is correct according to the UPnP/SSDP spec.</para> + /// </remarks> + public bool SupportPnpRootDevice + { + get { return _SupportPnpRootDevice; } + set + { + _SupportPnpRootDevice = value; + } + } + + /// <summary> + /// Stops listening for requests, stops sending periodic broadcasts, disposes all internal resources. + /// </summary> + /// <param name="disposing"></param> + protected override void Dispose(bool disposing) + { + if (disposing) + { + DisposeRebroadcastTimer(); + + var commsServer = _CommsServer; + if (commsServer != null) + { + commsServer.RequestReceived -= this.CommsServer_RequestReceived; + } + + var tasks = Devices.ToList().Select(RemoveDevice).ToArray(); + Task.WaitAll(tasks); + + _CommsServer = null; + if (commsServer != null) + { + if (!commsServer.IsShared) + commsServer.Dispose(); + } + + _RecentSearchRequests = null; + } + } + + private void ProcessSearchRequest(string mx, string searchTarget, IpEndPointInfo remoteEndPoint, IpAddressInfo receivedOnlocalIpAddress, CancellationToken cancellationToken) + { + if (String.IsNullOrEmpty(searchTarget)) + { + WriteTrace(String.Format("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. + int maxWaitInterval = 0; + 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 (!Int32.TryParse(mx, out 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))).ContinueWith((parentTask) => + { + //Copying devices to local array here to avoid threading issues/enumerator exceptions. + IEnumerable<SsdpDevice> devices = null; + lock (_Devices) + { + if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) + devices = GetAllDevicesAsFlatEnumerable().ToArray(); + else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) + devices = _Devices.ToArray(); + else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) + devices = (from device in GetAllDevicesAsFlatEnumerable() where String.Compare(device.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0 select device).ToArray(); + else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) + devices = (from device in GetAllDevicesAsFlatEnumerable() where String.Compare(device.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 select device).ToArray(); + } + + if (devices != null) + { + var deviceList = devices.ToList(); + //WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); + + foreach (var device in deviceList) + { + SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); + } + } + else + { + //WriteTrace(String.Format("Sending 0 search responses.")); + } + }); + } + + private IEnumerable<SsdpDevice> GetAllDevicesAsFlatEnumerable() + { + return _Devices.Union(_Devices.SelectManyRecursive<SsdpDevice>((d) => d.Devices)); + } + + private void SendDeviceSearchResponses(SsdpDevice device, IpEndPointInfo endPoint, IpAddressInfo receivedOnlocalIpAddress, CancellationToken cancellationToken) + { + bool isRootDevice = (device as SsdpRootDevice) != null; + if (isRootDevice) + { + SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIpAddress, cancellationToken); + if (this.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("{0}::{1}", udn, fullDeviceType); + } + + private async void SendSearchResponse(string searchTarget, SsdpDevice device, string uniqueServiceName, IpEndPointInfo endPoint, IpAddressInfo receivedOnlocalIpAddress, CancellationToken cancellationToken) + { + var rootDevice = device.ToRootDevice(); + + //var additionalheaders = FormatCustomHeadersForResponse(device); + + const string header = "HTTP/1.1 200 OK"; + + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + values["EXT"] = ""; + values["DATE"] = DateTime.UtcNow.ToString("r"); + values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; + values["ST"] = searchTarget; + values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); + values["USN"] = uniqueServiceName; + values["LOCATION"] = rootDevice.Location.ToString(); + + var message = BuildMessage(header, values); + + try + { + await _CommsServer.SendMessage(System.Text.Encoding.UTF8.GetBytes(message), endPoint, receivedOnlocalIpAddress, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + + } + + //WriteTrace(String.Format("Sent search response to " + endPoint.ToString()), device); + } + + private bool IsDuplicateSearchRequest(string searchTarget, IpEndPointInfo 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 (this.SupportPnpRootDevice) + SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); + } + + SendAliveNotification(device, device.Udn, device.Udn, cancellationToken); + SendAliveNotification(device, device.FullDeviceType, GetUsn(device.Udn, device.FullDeviceType), cancellationToken); + + foreach (var childDevice in device.Devices) + { + SendAliveNotifications(childDevice, false, cancellationToken); + } + } + + private void SendAliveNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) + { + var rootDevice = device.ToRootDevice(); + + const string header = "NOTIFY * HTTP/1.1"; + + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + // If needed later for non-server devices, these headers will need to be dynamic + values["HOST"] = "239.255.255.250:1900"; + values["DATE"] = DateTime.UtcNow.ToString("r"); + values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; + values["LOCATION"] = rootDevice.Location.ToString(); + values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); + values["NTS"] = "ssdp:alive"; + values["NT"] = notificationType; + values["USN"] = uniqueServiceName; + + var message = BuildMessage(header, values); + + _CommsServer.SendMulticastMessage(message, cancellationToken); + + //WriteTrace(String.Format("Sent alive notification"), device); + } + + private Task SendByeByeNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken) + { + var tasks = new List<Task>(); + if (isRoot) + { + tasks.Add(SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken)); + if (this.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("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); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "byebye", Justification = "Correct value for this type of notification in SSDP.")] + private Task SendByeByeNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) + { + const string header = "NOTIFY * HTTP/1.1"; + + var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + // If needed later for non-server devices, these headers will need to be dynamic + values["HOST"] = "239.255.255.250:1900"; + values["DATE"] = DateTime.UtcNow.ToString("r"); + values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); + values["NTS"] = "ssdp:byebye"; + values["NT"] = notificationType; + values["USN"] = uniqueServiceName; + + var message = BuildMessage(header, values); + + var sendCount = IsDisposed ? 1 : 3; + WriteTrace(String.Format("Sent byebye notification"), device); + return _CommsServer.SendMulticastMessage(message, sendCount, cancellationToken); + } + + private void DisposeRebroadcastTimer() + { + var timer = _RebroadcastAliveNotificationsTimer; + _RebroadcastAliveNotificationsTimer = null; + if (timer != 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(); + else + return TimeSpan.Zero; + } + + private string GetFirstHeaderValue(System.Net.Http.Headers.HttpRequestHeaders httpRequestHeaders, string headerName) + { + string retVal = null; + IEnumerable<String> values = null; + if (httpRequestHeaders.TryGetValues(headerName, out values) && values != null) + retVal = values.FirstOrDefault(); + + return retVal; + } + + public Action<string> LogFunction { get; set; } + + private void WriteTrace(string text) + { + if (LogFunction != null) + { + LogFunction(text); + } + //System.Diagnostics.Debug.WriteLine(text, "SSDP Publisher"); + } + + private void WriteTrace(string text, SsdpDevice device) + { + var rootDevice = device as SsdpRootDevice; + if (rootDevice != 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 IpEndPointInfo EndPoint { get; set; } + public DateTime Received { get; set; } + public string SearchTarget { get; set; } + + public string Key + { + get { return this.SearchTarget + ":" + this.EndPoint.ToString(); } + } + + public bool IsOld() + { + return DateTime.UtcNow.Subtract(this.Received).TotalMilliseconds > 500; + } + } } }
\ No newline at end of file diff --git a/RSSDP/SsdpDevicePublisherBase.cs b/RSSDP/SsdpDevicePublisherBase.cs deleted file mode 100644 index eda769da6..000000000 --- a/RSSDP/SsdpDevicePublisherBase.cs +++ /dev/null @@ -1,709 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Threading; -using RSSDP; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Provides the platform independent logic for publishing SSDP devices (notifications and search responses). - /// </summary> - public abstract class SsdpDevicePublisherBase : DisposableManagedObjectBase, ISsdpDevicePublisher - { - - #region Fields & Constants - - private ISsdpCommunicationsServer _CommsServer; - private string _OSName; - private string _OSVersion; - - private bool _SupportPnpRootDevice; - - private IList<SsdpRootDevice> _Devices; - private IReadOnlyList<SsdpRootDevice> _ReadOnlyDevices; - - private ITimer _RebroadcastAliveNotificationsTimer; - private ITimerFactory _timerFactory; - //private TimeSpan _RebroadcastAliveNotificationsTimeSpan; - private DateTime _LastNotificationTime; - - private IDictionary<string, SearchRequest> _RecentSearchRequests; - private IUpnpDeviceValidator _DeviceValidator; - - private Random _Random; - //private TimeSpan _MinCacheTime; - - private const string ServerVersion = "1.0"; - - #endregion - - #region Message Format Constants - - #endregion - - #region Constructors - - /// <summary> - /// Default constructor. - /// </summary> - protected SsdpDevicePublisherBase(ISsdpCommunicationsServer communicationsServer, ITimerFactory timerFactory, string osName, string osVersion) - { - if (communicationsServer == null) throw new ArgumentNullException("communicationsServer"); - if (osName == null) throw new ArgumentNullException("osName"); - if (osName.Length == 0) throw new ArgumentException("osName cannot be an empty string.", "osName"); - if (osVersion == null) throw new ArgumentNullException("osVersion"); - if (osVersion.Length == 0) throw new ArgumentException("osVersion cannot be an empty string.", "osName"); - - _SupportPnpRootDevice = true; - _timerFactory = timerFactory; - _Devices = new List<SsdpRootDevice>(); - _ReadOnlyDevices = new ReadOnlyCollection<SsdpRootDevice>(_Devices); - _RecentSearchRequests = new Dictionary<string, SearchRequest>(StringComparer.OrdinalIgnoreCase); - _Random = new Random(); - _DeviceValidator = new Upnp10DeviceValidator(); //Should probably inject this later, but for now we only support 1.0. - - _CommsServer = communicationsServer; - _CommsServer.RequestReceived += CommsServer_RequestReceived; - _OSName = osName; - _OSVersion = osVersion; - - _CommsServer.BeginListeningForBroadcasts(); - } - - #endregion - - #region Public Methods - - /// <summary> - /// Adds a device (and it's children) to the list of devices being published by this server, making them discoverable to SSDP clients. - /// </summary> - /// <remarks> - /// <para>Adding a device causes "alive" notification messages to be sent immediately, or very soon after. Ensure your device/description service is running before adding the device object here.</para> - /// <para>Devices added here with a non-zero cache life time will also have notifications broadcast periodically.</para> - /// <para>This method ignores duplicate device adds (if the same device instance is added multiple times, the second and subsequent add calls do nothing).</para> - /// </remarks> - /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - /// <exception cref="System.InvalidOperationException">Thrown if the <paramref name="device"/> contains property values that are not acceptable to the UPnP 1.0 specification.</exception> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "t", Justification = "Capture task to local variable supresses compiler warning, but task is not really needed.")] - public void AddDevice(SsdpRootDevice device) - { - if (device == null) throw new ArgumentNullException("device"); - - ThrowIfDisposed(); - - _DeviceValidator.ThrowIfDeviceInvalid(device); - - TimeSpan minCacheTime = TimeSpan.Zero; - bool wasAdded = false; - lock (_Devices) - { - if (!_Devices.Contains(device)) - { - _Devices.Add(device); - wasAdded = true; - minCacheTime = GetMinimumNonZeroCacheLifetime(); - } - } - - if (wasAdded) - { - //_MinCacheTime = minCacheTime; - - ConnectToDeviceEvents(device); - - WriteTrace("Device Added", device); - - SetRebroadcastAliveNotificationsTimer(minCacheTime); - - SendAliveNotifications(device, true, CancellationToken.None); - } - } - - /// <summary> - /// Removes a device (and it's children) from the list of devices being published by this server, making them undiscoverable. - /// </summary> - /// <remarks> - /// <para>Removing a device causes "byebye" notification messages to be sent immediately, advising clients of the device/service becoming unavailable. We recommend removing the device from the published list before shutting down the actual device/service, if possible.</para> - /// <para>This method does nothing if the device was not found in the collection.</para> - /// </remarks> - /// <param name="device">The <see cref="SsdpDevice"/> instance to add.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - public async Task RemoveDevice(SsdpRootDevice device) - { - if (device == null) throw new ArgumentNullException("device"); - - ThrowIfDisposed(); - - bool wasRemoved = false; - TimeSpan minCacheTime = TimeSpan.Zero; - lock (_Devices) - { - if (_Devices.Contains(device)) - { - _Devices.Remove(device); - wasRemoved = true; - minCacheTime = GetMinimumNonZeroCacheLifetime(); - } - } - - if (wasRemoved) - { - //_MinCacheTime = minCacheTime; - - DisconnectFromDeviceEvents(device); - - WriteTrace("Device Removed", device); - - await SendByeByeNotifications(device, true, CancellationToken.None).ConfigureAwait(false); - - SetRebroadcastAliveNotificationsTimer(minCacheTime); - } - } - - #endregion - - #region Public Properties - - /// <summary> - /// Returns a read only list of devices being published by this instance. - /// </summary> - public IEnumerable<SsdpRootDevice> Devices - { - get - { - return _ReadOnlyDevices; - } - } - - /// <summary> - /// If true (default) treats root devices as both upnp:rootdevice and pnp:rootdevice types. - /// </summary> - /// <remarks> - /// <para>Enabling this option will cause devices to show up in Microsoft Windows Explorer's network screens (if discovery is enabled etc.). Windows Explorer appears to search only for pnp:rootdeivce and not upnp:rootdevice.</para> - /// <para>If false, the system will only use upnp:rootdevice for notifiation broadcasts and and search responses, which is correct according to the UPnP/SSDP spec.</para> - /// </remarks> - public bool SupportPnpRootDevice - { - get { return _SupportPnpRootDevice; } - set - { - _SupportPnpRootDevice = value; - } - } - - #endregion - - #region Overrides - - /// <summary> - /// Stops listening for requests, stops sending periodic broadcasts, disposes all internal resources. - /// </summary> - /// <param name="disposing"></param> - protected override void Dispose(bool disposing) - { - if (disposing) - { - var commsServer = _CommsServer; - _CommsServer = null; - - if (commsServer != null) - { - commsServer.RequestReceived -= this.CommsServer_RequestReceived; - if (!commsServer.IsShared) - commsServer.Dispose(); - } - - DisposeRebroadcastTimer(); - - foreach (var device in this.Devices) - { - DisconnectFromDeviceEvents(device); - } - - _RecentSearchRequests = null; - } - } - - #endregion - - #region Private Methods - - #region Search Related Methods - - private void ProcessSearchRequest(string mx, string searchTarget, IpEndPointInfo remoteEndPoint, IpAddressInfo receivedOnlocalIpAddress, CancellationToken cancellationToken) - { - if (String.IsNullOrEmpty(searchTarget)) - { - WriteTrace(String.Format("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. - int maxWaitInterval = 0; - 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 (!Int32.TryParse(mx, out 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))).ContinueWith((parentTask) => - { - //Copying devices to local array here to avoid threading issues/enumerator exceptions. - IEnumerable<SsdpDevice> devices = null; - lock (_Devices) - { - if (String.Compare(SsdpConstants.SsdpDiscoverAllSTHeader, searchTarget, StringComparison.OrdinalIgnoreCase) == 0) - devices = GetAllDevicesAsFlatEnumerable().ToArray(); - else if (String.Compare(SsdpConstants.UpnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 || (this.SupportPnpRootDevice && String.Compare(SsdpConstants.PnpDeviceTypeRootDevice, searchTarget, StringComparison.OrdinalIgnoreCase) == 0)) - devices = _Devices.ToArray(); - else if (searchTarget.Trim().StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) - devices = (from device in GetAllDevicesAsFlatEnumerable() where String.Compare(device.Uuid, searchTarget.Substring(5), StringComparison.OrdinalIgnoreCase) == 0 select device).ToArray(); - else if (searchTarget.StartsWith("urn:", StringComparison.OrdinalIgnoreCase)) - devices = (from device in GetAllDevicesAsFlatEnumerable() where String.Compare(device.FullDeviceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 select device).ToArray(); - } - - if (devices != null) - { - var deviceList = devices.ToList(); - //WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); - - foreach (var device in deviceList) - { - SendDeviceSearchResponses(device, remoteEndPoint, receivedOnlocalIpAddress, cancellationToken); - } - } - else - { - //WriteTrace(String.Format("Sending 0 search responses.")); - } - }); - } - - private IEnumerable<SsdpDevice> GetAllDevicesAsFlatEnumerable() - { - return _Devices.Union(_Devices.SelectManyRecursive<SsdpDevice>((d) => d.Devices)); - } - - private void SendDeviceSearchResponses(SsdpDevice device, IpEndPointInfo endPoint, IpAddressInfo receivedOnlocalIpAddress, CancellationToken cancellationToken) - { - bool isRootDevice = (device as SsdpRootDevice) != null; - if (isRootDevice) - { - SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint, receivedOnlocalIpAddress, cancellationToken); - if (this.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 static string GetUsn(string udn, string fullDeviceType) - { - return String.Format("{0}::{1}", udn, fullDeviceType); - } - - private async void SendSearchResponse(string searchTarget, SsdpDevice device, string uniqueServiceName, IpEndPointInfo endPoint, IpAddressInfo receivedOnlocalIpAddress, CancellationToken cancellationToken) - { - var rootDevice = device.ToRootDevice(); - - //var additionalheaders = FormatCustomHeadersForResponse(device); - - const string header = "HTTP/1.1 200 OK"; - - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - values["EXT"] = ""; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["CACHE-CONTROL"] = "max-age = 600"; - values["ST"] = searchTarget; - values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); - values["USN"] = uniqueServiceName; - values["LOCATION"] = rootDevice.Location.ToString(); - - var message = SsdpHelper.BuildMessage(header, values); - - try - { - await _CommsServer.SendMessage(System.Text.Encoding.UTF8.GetBytes(message), endPoint, receivedOnlocalIpAddress, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - - } - - //WriteTrace(String.Format("Sent search response to " + endPoint.ToString()), device); - } - - private bool IsDuplicateSearchRequest(string searchTarget, IpEndPointInfo 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); - } - } - } - - #endregion - - #region Notification Related Methods - - #region Alive - - private void SendAllAliveNotifications(object state) - { - try - { - if (IsDisposed) return; - - //DisposeRebroadcastTimer(); - - //WriteTrace("Begin Sending Alive Notifications For All Devices"); - - _LastNotificationTime = DateTime.Now; - - IEnumerable<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(); - } - //finally - //{ - // // This is causing all notifications to stop - // //if (!this.IsDisposed) - // //SetRebroadcastAliveNotificationsTimer(_MinCacheTime); - //} - } - - private void SendAliveNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken) - { - if (isRoot) - { - SendAliveNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken); - if (this.SupportPnpRootDevice) - SendAliveNotification(device, SsdpConstants.PnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), cancellationToken); - } - - SendAliveNotification(device, device.Udn, device.Udn, cancellationToken); - SendAliveNotification(device, device.FullDeviceType, GetUsn(device.Udn, device.FullDeviceType), cancellationToken); - - foreach (var childDevice in device.Devices) - { - SendAliveNotifications(childDevice, false, cancellationToken); - } - } - - private void SendAliveNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) - { - var rootDevice = device.ToRootDevice(); - - const string header = "NOTIFY * HTTP/1.1"; - - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["CACHE-CONTROL"] = "max-age = " + rootDevice.CacheLifetime.TotalSeconds; - values["LOCATION"] = rootDevice.Location.ToString(); - values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); - values["NTS"] = "ssdp:alive"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; - - var message = SsdpHelper.BuildMessage(header, values); - - _CommsServer.SendMulticastMessage(message, cancellationToken); - - //WriteTrace(String.Format("Sent alive notification"), device); - } - - #endregion - - #region ByeBye - - private async Task SendByeByeNotifications(SsdpDevice device, bool isRoot, CancellationToken cancellationToken) - { - if (isRoot) - { - await SendByeByeNotification(device, SsdpConstants.UpnpDeviceTypeRootDevice, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), cancellationToken).ConfigureAwait(false); - if (this.SupportPnpRootDevice) - await SendByeByeNotification(device, "pnp:rootdevice", GetUsn(device.Udn, "pnp:rootdevice"), cancellationToken).ConfigureAwait(false); ; - } - - await SendByeByeNotification(device, device.Udn, device.Udn, cancellationToken).ConfigureAwait(false); ; - await SendByeByeNotification(device, String.Format("urn:{0}", device.FullDeviceType), GetUsn(device.Udn, device.FullDeviceType), cancellationToken).ConfigureAwait(false); ; - - foreach (var childDevice in device.Devices) - { - await SendByeByeNotifications(childDevice, false, cancellationToken).ConfigureAwait(false); ; - } - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "byebye", Justification = "Correct value for this type of notification in SSDP.")] - private Task SendByeByeNotification(SsdpDevice device, string notificationType, string uniqueServiceName, CancellationToken cancellationToken) - { - const string header = "NOTIFY * HTTP/1.1"; - - var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - // If needed later for non-server devices, these headers will need to be dynamic - values["HOST"] = "239.255.255.250:1900"; - values["DATE"] = DateTime.UtcNow.ToString("r"); - values["SERVER"] = string.Format("{0}/{1} UPnP/1.0 RSSDP/{2}", _OSName, _OSVersion, ServerVersion); - values["NTS"] = "ssdp:byebye"; - values["NT"] = notificationType; - values["USN"] = uniqueServiceName; - - var message = SsdpHelper.BuildMessage(header, values); - - return _CommsServer.SendMulticastMessage(message, cancellationToken); - - //WriteTrace(String.Format("Sent byebye notification"), device); - } - - #endregion - - #region Rebroadcast Timer - - private void DisposeRebroadcastTimer() - { - var timer = _RebroadcastAliveNotificationsTimer; - _RebroadcastAliveNotificationsTimer = null; - if (timer != null) - timer.Dispose(); - } - - private void SetRebroadcastAliveNotificationsTimer(TimeSpan minCacheTime) - { - //if (minCacheTime == _RebroadcastAliveNotificationsTimeSpan) return; - - DisposeRebroadcastTimer(); - - if (minCacheTime == TimeSpan.Zero) return; - - // According to UPnP/SSDP spec, we should randomise the interval at - // which we broadcast notifications, to help with network congestion. - // Specs also advise to choose a random interval up to *half* the cache time. - // Here we do that, but using the minimum non-zero cache time of any device we are publishing. - var rebroadCastInterval = new TimeSpan(minCacheTime.Ticks); - - // If we were already setup to rebroadcast someime in the future, - // don't just blindly reset the next broadcast time to the new interval - // as repeatedly changing the interval might end up causing us to over - // delay in sending the next one. - var nextBroadcastInterval = rebroadCastInterval; - if (_LastNotificationTime != DateTime.MinValue) - { - nextBroadcastInterval = rebroadCastInterval.Subtract(DateTime.Now.Subtract(_LastNotificationTime)); - if (nextBroadcastInterval.Ticks < 0) - nextBroadcastInterval = TimeSpan.Zero; - else if (nextBroadcastInterval > rebroadCastInterval) - nextBroadcastInterval = rebroadCastInterval; - } - - //_RebroadcastAliveNotificationsTimeSpan = rebroadCastInterval; - _RebroadcastAliveNotificationsTimer = _timerFactory.Create(SendAllAliveNotifications, null, nextBroadcastInterval, rebroadCastInterval); - - WriteTrace(String.Format("Rebroadcast Interval = {0}, Next Broadcast At = {1}", rebroadCastInterval.ToString(), nextBroadcastInterval.ToString())); - } - - private TimeSpan GetMinimumNonZeroCacheLifetime() - { - var nonzeroCacheLifetimesQuery = (from device - in _Devices - where device.CacheLifetime != TimeSpan.Zero - select device.CacheLifetime).ToList(); - - if (nonzeroCacheLifetimesQuery.Any()) - return nonzeroCacheLifetimesQuery.Min(); - else - return TimeSpan.Zero; - } - - #endregion - - #endregion - - private static string GetFirstHeaderValue(System.Net.Http.Headers.HttpRequestHeaders httpRequestHeaders, string headerName) - { - string retVal = null; - IEnumerable<String> values = null; - if (httpRequestHeaders.TryGetValues(headerName, out values) && values != null) - retVal = values.FirstOrDefault(); - - return retVal; - } - - public static Action<string> LogFunction { get; set; } - - private static void WriteTrace(string text) - { - if (LogFunction != null) - { - LogFunction(text); - } - //System.Diagnostics.Debug.WriteLine(text, "SSDP Publisher"); - } - - private static void WriteTrace(string text, SsdpDevice device) - { - var rootDevice = device as SsdpRootDevice; - if (rootDevice != null) - WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid + " - " + rootDevice.Location); - else - WriteTrace(text + " " + device.DeviceType + " - " + device.Uuid); - } - - private void ConnectToDeviceEvents(SsdpDevice device) - { - device.DeviceAdded += device_DeviceAdded; - device.DeviceRemoved += device_DeviceRemoved; - - foreach (var childDevice in device.Devices) - { - ConnectToDeviceEvents(childDevice); - } - } - - private void DisconnectFromDeviceEvents(SsdpDevice device) - { - device.DeviceAdded -= device_DeviceAdded; - device.DeviceRemoved -= device_DeviceRemoved; - - foreach (var childDevice in device.Devices) - { - DisconnectFromDeviceEvents(childDevice); - } - } - - #endregion - - #region Event Handlers - - private void device_DeviceAdded(object sender, DeviceEventArgs e) - { - SendAliveNotifications(e.Device, false, CancellationToken.None); - ConnectToDeviceEvents(e.Device); - } - - private void device_DeviceRemoved(object sender, DeviceEventArgs e) - { - var task = SendByeByeNotifications(e.Device, false, CancellationToken.None); - Task.WaitAll(task); - DisconnectFromDeviceEvents(e.Device); - } - - 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); - } - } - - #endregion - - #region Private Classes - - private class SearchRequest - { - public IpEndPointInfo EndPoint { get; set; } - public DateTime Received { get; set; } - public string SearchTarget { get; set; } - - public string Key - { - get { return this.SearchTarget + ":" + this.EndPoint.ToString(); } - } - - public bool IsOld() - { - return DateTime.UtcNow.Subtract(this.Received).TotalMilliseconds > 500; - } - } - - #endregion - - } -}
\ No newline at end of file diff --git a/RSSDP/SsdpEmbeddedDevice.cs b/RSSDP/SsdpEmbeddedDevice.cs index 28948f950..dca1ff5e3 100644 --- a/RSSDP/SsdpEmbeddedDevice.cs +++ b/RSSDP/SsdpEmbeddedDevice.cs @@ -25,17 +25,6 @@ namespace Rssdp { } - /// <summary> - /// Deserialisation constructor. - /// </summary> - /// <param name="deviceDescriptionXml">A UPnP device description XML document.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="deviceDescriptionXml"/> argument is null.</exception> - /// <exception cref="System.ArgumentException">Thrown if the <paramref name="deviceDescriptionXml"/> argument is empty.</exception> - public SsdpEmbeddedDevice(string deviceDescriptionXml) - : base(deviceDescriptionXml) - { - } - #endregion #region Public Properties diff --git a/RSSDP/SsdpHelper.cs b/RSSDP/SsdpHelper.cs deleted file mode 100644 index 2eacf3c11..000000000 --- a/RSSDP/SsdpHelper.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using MediaBrowser.Model.Net; -using MediaBrowser.Model.Text; - -namespace RSSDP -{ - public class SsdpHelper - { - private readonly ITextEncoding _encoding; - - public SsdpHelper(ITextEncoding encoding) - { - _encoding = encoding; - } - - public SsdpMessageInfo ParseSsdpResponse(byte[] data) - { - using (var ms = new MemoryStream(data)) - { - using (var reader = new StreamReader(ms, _encoding.GetASCIIEncoding())) - { - var proto = (reader.ReadLine() ?? string.Empty).Trim(); - var method = proto.Split(new[] { ' ' }, 2)[0]; - var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) - { - line = line.Trim(); - if (string.IsNullOrEmpty(line)) - { - break; - } - var parts = line.Split(new[] { ':' }, 2); - - if (parts.Length >= 2) - { - headers[parts[0]] = parts[1].Trim(); - } - } - - return new SsdpMessageInfo - { - Method = method, - Headers = headers, - Message = data - }; - } - } - } - - public static string BuildMessage(string header, Dictionary<string, string> values) - { - var builder = new StringBuilder(); - - const string argFormat = "{0}: {1}\r\n"; - - builder.AppendFormat("{0}\r\n", header); - - foreach (var pair in values) - { - builder.AppendFormat(argFormat, pair.Key, pair.Value); - } - - builder.Append("\r\n"); - - return builder.ToString(); - } - } - - public class SsdpMessageInfo - { - public string Method { get; set; } - - public IpEndPointInfo EndPoint { get; set; } - - public Dictionary<string, string> Headers { get; set; } - - public IpEndPointInfo LocalEndPoint { get; set; } - public byte[] Message { get; set; } - - public SsdpMessageInfo() - { - Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/RSSDP/SsdpRootDevice.cs b/RSSDP/SsdpRootDevice.cs index 5b7d0f454..6d0fcafbb 100644 --- a/RSSDP/SsdpRootDevice.cs +++ b/RSSDP/SsdpRootDevice.cs @@ -31,25 +31,6 @@ namespace Rssdp { } - /// <summary> - /// Deserialisation constructor. - /// </summary> - /// <param name="location">The url from which the device description document was retrieved.</param> - /// <param name="cacheLifetime">A <see cref="System.TimeSpan"/> representing the time maximum period of time the device description can be cached for.</param> - /// <param name="deviceDescriptionXml">The device description XML as a string.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="deviceDescriptionXml"/> or <paramref name="location"/> arguments are null.</exception> - /// <exception cref="System.ArgumentException">Thrown if the <paramref name="deviceDescriptionXml"/> argument is empty.</exception> - public SsdpRootDevice(Uri location, TimeSpan cacheLifetime, string deviceDescriptionXml) - : base(deviceDescriptionXml) - { - if (location == null) throw new ArgumentNullException("location"); - - this.CacheLifetime = cacheLifetime; - this.Location = location; - - LoadFromDescriptionDocument(deviceDescriptionXml); - } - #endregion #region Public Properties @@ -94,82 +75,5 @@ namespace Rssdp #endregion - #region Public Methods - - /// <summary> - /// Saves the property values of this device object to an a string in the full UPnP device description XML format, including child devices and outer root node and XML document declaration. - /// </summary> - /// <returns>A string containing XML in the UPnP device description format</returns> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispsoing memory stream twice is 'safe' and easier to read than correct code for ensuring it is only closed once.")] - public virtual string ToDescriptionDocument() - { - if (String.IsNullOrEmpty(this.Uuid)) throw new InvalidOperationException("Must provide a UUID value."); - - //This would have been so much nicer with Xml.Linq, but that's - //not available until .NET 4.03 at the earliest, and I want to - //target 4.0 :( - using (System.IO.MemoryStream ms = new System.IO.MemoryStream()) - { - System.Xml.XmlWriter writer = System.Xml.XmlWriter.Create(ms, new XmlWriterSettings() { Encoding = System.Text.UTF8Encoding.UTF8, Indent = true, NamespaceHandling = NamespaceHandling.OmitDuplicates }); - writer.WriteStartDocument(); - writer.WriteStartElement("root", SsdpConstants.SsdpDeviceDescriptionXmlNamespace); - - writer.WriteStartElement("specVersion"); - writer.WriteElementString("major", "1"); - writer.WriteElementString("minor", "0"); - writer.WriteEndElement(); - - if (this.UrlBase != null && this.UrlBase != this.Location) - writer.WriteElementString("URLBase", this.UrlBase.ToString()); - - WriteDeviceDescriptionXml(writer, this); - - writer.WriteEndElement(); - writer.Flush(); - - ms.Seek(0, System.IO.SeekOrigin.Begin); - using (var reader = new System.IO.StreamReader(ms)) - { - return reader.ReadToEnd(); - } - } - } - - #endregion - - #region Private Methods - - #region Deserialisation Methods - - private void LoadFromDescriptionDocument(string deviceDescriptionXml) - { - using (var ms = new System.IO.MemoryStream(System.Text.UTF8Encoding.UTF8.GetBytes(deviceDescriptionXml))) - { - var reader = XmlReader.Create(ms); - while (!reader.EOF) - { - reader.Read(); - if (reader.NodeType != XmlNodeType.Element || reader.LocalName != "root") continue; - - while (!reader.EOF) - { - reader.Read(); - - if (reader.NodeType != XmlNodeType.Element) continue; - - if (reader.LocalName == "URLBase") - { - this.UrlBase = StringToUri(reader.ReadElementContentAsString()); - break; - } - } - } - } - } - - #endregion - - #endregion - } }
\ No newline at end of file diff --git a/RSSDP/UPnP10DeviceValidator.cs b/RSSDP/UPnP10DeviceValidator.cs deleted file mode 100644 index 2a8a9ccd2..000000000 --- a/RSSDP/UPnP10DeviceValidator.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Rssdp.Infrastructure -{ - /// <summary> - /// Validates a <see cref="SsdpDevice"/> object's properties meet the UPnP 1.0 specification. - /// </summary> - /// <remarks> - /// <para>This is a best effort validation for known rules, it doesn't guarantee 100% compatibility with the specification. Reading the specification yourself is the best way to ensure compatibility.</para> - /// </remarks> - public class Upnp10DeviceValidator : IUpnpDeviceValidator - { - - #region Public Methods - - /// <summary> - /// Returns an enumerable set of strings, each one being a description of an invalid property on the specified root device. - /// </summary> - /// <remarks> - /// <para>If no errors are found, an empty (but non-null) enumerable is returned.</para> - /// </remarks> - /// <param name="device">The <see cref="SsdpRootDevice"/> to validate.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - /// <returns>A non-null enumerable set of strings, empty if there are no validation errors, otherwise each string represents a discrete problem.</returns> - public List<string> GetValidationErrors(SsdpRootDevice device) - { - if (device == null) throw new ArgumentNullException("device"); - - var retVal = GetValidationErrors((SsdpDevice)device); - - if (device.Location == null) - retVal.Add("Location cannot be null."); - else if (!device.Location.IsAbsoluteUri) - retVal.Add("Location must be an absolute URL."); - - return retVal; - } - - /// <summary> - /// Returns an enumerable set of strings, each one being a description of an invalid property on the specified device. - /// </summary> - /// <remarks> - /// <para>If no errors are found, an empty (but non-null) enumerable is returned.</para> - /// </remarks> - /// <param name="device">The <see cref="SsdpDevice"/> to validate.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - /// <returns>A non-null enumerable set of strings, empty if there are no validation errors, otherwise each string represents a discrete problem.</returns> - public List<string> GetValidationErrors(SsdpDevice device) - { - if (device == null) throw new ArgumentNullException("device"); - - var retVal = new List<string>(); - - if (String.IsNullOrEmpty(device.Uuid)) - retVal.Add("Uuid is not set."); - - if (!String.IsNullOrEmpty(device.Upc)) - ValidateUpc(device, retVal); - - if (String.IsNullOrEmpty(device.Udn)) - retVal.Add("UDN is not set."); - else - ValidateUdn(device, retVal); - - if (String.IsNullOrEmpty(device.DeviceType)) - retVal.Add("DeviceType is not set."); - - if (String.IsNullOrEmpty(device.DeviceTypeNamespace)) - retVal.Add("DeviceTypeNamespace is not set."); - else - { - if (IsOverLength(device.DeviceTypeNamespace, 64)) - retVal.Add("DeviceTypeNamespace cannot be longer than 64 characters."); - - //if (device.DeviceTypeNamespace.Contains(".")) - // retVal.Add("Period (.) characters in the DeviceTypeNamespace property must be replaced with hyphens (-)."); - } - - if (device.DeviceVersion <= 0) - retVal.Add("DeviceVersion must be 1 or greater."); - - if (IsOverLength(device.ModelName, 32)) - retVal.Add("ModelName cannot be longer than 32 characters."); - - if (IsOverLength(device.ModelNumber, 32)) - retVal.Add("ModelNumber cannot be longer than 32 characters."); - - if (IsOverLength(device.FriendlyName, 64)) - retVal.Add("FriendlyName cannot be longer than 64 characters."); - - if (IsOverLength(device.Manufacturer, 64)) - retVal.Add("Manufacturer cannot be longer than 64 characters."); - - if (IsOverLength(device.SerialNumber, 64)) - retVal.Add("SerialNumber cannot be longer than 64 characters."); - - if (IsOverLength(device.ModelDescription, 128)) - retVal.Add("ModelDescription cannot be longer than 128 characters."); - - if (String.IsNullOrEmpty(device.FriendlyName)) - retVal.Add("FriendlyName is required."); - - if (String.IsNullOrEmpty(device.Manufacturer)) - retVal.Add("Manufacturer is required."); - - if (String.IsNullOrEmpty(device.ModelName)) - retVal.Add("ModelName is required."); - - if (device.Icons.Count > 0) - ValidateIcons(device, retVal); - - ValidateChildDevices(device, retVal); - - return retVal; - } - - /// <summary> - /// Validates the specified device and throws an <see cref="System.InvalidOperationException"/> if there are any validation errors. - /// </summary> - /// <param name="device">The <see cref="SsdpDevice"/> to validate.</param> - /// <exception cref="System.ArgumentNullException">Thrown if the <paramref name="device"/> argument is null.</exception> - /// <exception cref="System.InvalidOperationException">Thrown if the device object does not pass validation.</exception> - public void ThrowIfDeviceInvalid(SsdpDevice device) - { - var errors = this.GetValidationErrors(device); - if (errors != null && errors.Count > 0) throw new InvalidOperationException("Invalid device settings : " + String.Join(Environment.NewLine, errors)); - } - - #endregion - - #region Private Methods - - private static void ValidateUpc(SsdpDevice device, List<string> retVal) - { - if (device.Upc.Length != 12) - retVal.Add("Upc, if provided, should be 12 digits."); - - foreach (char c in device.Upc) - { - if (!Char.IsDigit(c)) - { - retVal.Add("Upc, if provided, should contain only digits (numeric characters)."); - break; - } - } - } - - private static void ValidateUdn(SsdpDevice device, List<string> retVal) - { - if (!device.Udn.StartsWith("uuid:", StringComparison.OrdinalIgnoreCase)) - retVal.Add("UDN must begin with uuid:. Correct format is uuid:<uuid>"); - else if (device.Udn.Substring(5).Trim() != device.Uuid) - retVal.Add("UDN incorrect. Correct format is uuid:<uuid>"); - } - - private static void ValidateIcons(SsdpDevice device, List<string> retVal) - { - if (device.Icons.Any((di) => di.Url == null)) - retVal.Add("Device icon is missing URL."); - - if (device.Icons.Any((di) => String.IsNullOrEmpty(di.MimeType))) - retVal.Add("Device icon is missing mime type."); - - if (device.Icons.Any((di) => di.Width <= 0 || di.Height <= 0)) - retVal.Add("Device icon has zero (or negative) height, width or both."); - - if (device.Icons.Any((di) => di.ColorDepth <= 0)) - retVal.Add("Device icon has zero (or negative) colordepth."); - } - - private void ValidateChildDevices(SsdpDevice device, List<string> retVal) - { - foreach (var childDevice in device.Devices) - { - foreach (var validationError in this.GetValidationErrors(childDevice)) - { - retVal.Add("Embedded Device : " + childDevice.Uuid + ": " + validationError); - } - } - } - - private static bool IsOverLength(string value, int maxLength) - { - return !String.IsNullOrEmpty(value) && value.Length > maxLength; - } - - #endregion - - } -} |
