diff --git a/src/Main/PublishNugetPackage.cmd b/src/Main/PublishNugetPackage.cmd index 1d75bb7..399c788 100644 --- a/src/Main/PublishNugetPackage.cmd +++ b/src/Main/PublishNugetPackage.cmd @@ -1,5 +1,5 @@ @echo off echo Press any key to publish pause -".nuget\NuGet.exe" push Rssdp.3.5.4.nupkg -Source https://www.nuget.org/api/v2/package +".nuget\NuGet.exe" push Rssdp.3.5.6.nupkg -Source https://www.nuget.org/api/v2/package pause \ No newline at end of file diff --git a/src/Main/Rssdp.Shared/AssemblyInfoCommon.cs b/src/Main/Rssdp.Shared/AssemblyInfoCommon.cs index 49287a4..666daad 100644 --- a/src/Main/Rssdp.Shared/AssemblyInfoCommon.cs +++ b/src/Main/Rssdp.Shared/AssemblyInfoCommon.cs @@ -5,5 +5,5 @@ [assembly: AssemblyCopyright("Released under the MIT license; http://choosealicense.com/licenses/mit/; https://github.com/Yortw/RSSDP")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyVersion("3.5.5.0")] -[assembly: AssemblyFileVersion("3.5.5.0")] \ No newline at end of file +[assembly: AssemblyVersion("3.5.6.0")] +[assembly: AssemblyFileVersion("3.5.6.0")] \ No newline at end of file diff --git a/src/Main/Rssdp.Shared/SsdpDevicePublisherBase.cs b/src/Main/Rssdp.Shared/SsdpDevicePublisherBase.cs index 7c2361d..bd8597b 100644 --- a/src/Main/Rssdp.Shared/SsdpDevicePublisherBase.cs +++ b/src/Main/Rssdp.Shared/SsdpDevicePublisherBase.cs @@ -402,24 +402,7 @@ private void ProcessSearchRequest(string mx, string searchTarget, UdpEndPoint en devices = GetDevicesMatchingSearchTarget(searchTarget, devices); if (devices != null) - { - _Log.LogInfo(String.Format("Sending search responses for {0} devices", devices.Count())); - - if (searchTarget.Contains(":service:")) - { - foreach (var device in devices) - { - SendServiceSearchResponses(device, searchTarget, endPoint); - } - } - else - { - foreach (var device in devices) - { - SendDeviceSearchResponses(device, endPoint); - } - } - } + SendSearchResponses(searchTarget, endPoint, devices); else _Log.LogWarning("Sending search responses for 0 devices (no matching targets)."); }); @@ -446,17 +429,18 @@ select device { if (searchTarget.Contains(":service:")) { - devices = (from - device in GetAllDevicesAsFlatEnumerable() - where - ( - from s in - device.Services - where String.Compare(s.FullServiceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 - select s - ).Any() - select device - ).ToArray(); + devices = + ( + from device in GetAllDevicesAsFlatEnumerable() + where + ( + from s in + device.Services + where String.Compare(s.FullServiceType, searchTarget, StringComparison.OrdinalIgnoreCase) == 0 + select s + ).Any() + select device + ).ToArray(); } else { @@ -497,19 +481,67 @@ private IEnumerable GetAllDevicesAsFlatEnumerable() return _Devices.Union(_Devices.SelectManyRecursive((d) => d.Devices)); } - private void SendDeviceSearchResponses(SsdpDevice device, UdpEndPoint endPoint) + private void SendSearchResponses(string searchTarget, UdpEndPoint endPoint, IEnumerable devices) + { + _Log.LogInfo(String.Format("Sending search (target = {1}) responses for {0} devices", devices.Count(), searchTarget)); + + if (searchTarget.Contains(":service:")) + { + foreach (var device in devices) + { + SendServiceSearchResponses(device, searchTarget, endPoint); + } + } + else + { + foreach (var device in devices) + { + SendDeviceSearchResponses(device, searchTarget, endPoint); + } + } + } + + private void SendDeviceSearchResponses(SsdpDevice device, string searchTarget, UdpEndPoint endPoint) { + //http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0-20080424.pdf - page 21 + //For ssdp:all - Respond 3+2d+k times for a root device with d embedded devices and s embedded services but only k distinct service types + //Root devices - Respond once (special handling when in related/Win Explorer support mode) + //Udn (uuid) - Response once + //Device type - response once + //Service type - respond once per service type + bool isRootDevice = (device as SsdpRootDevice) != null; - if (isRootDevice) + bool sendAll = searchTarget == SsdpConstants.SsdpDiscoverAllSTHeader; + bool sendRootDevices = searchTarget == SsdpConstants.UpnpDeviceTypeRootDevice || searchTarget == SsdpConstants.PnpDeviceTypeRootDevice; + + if (isRootDevice && (sendAll || sendRootDevices)) { SendSearchResponse(SsdpConstants.UpnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.UpnpDeviceTypeRootDevice), endPoint); if (IsWindowsExplorerSupportEnabled) SendSearchResponse(SsdpConstants.PnpDeviceTypeRootDevice, device, GetUsn(device.Udn, SsdpConstants.PnpDeviceTypeRootDevice), endPoint); } - SendSearchResponse(device.Udn, device, device.Udn, endPoint); + if (sendAll || searchTarget.StartsWith("uuid:")) + SendSearchResponse(device.Udn, device, device.Udn, endPoint); + + if (sendAll || searchTarget.Contains(":device:")) + SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint); - SendSearchResponse(device.FullDeviceType, device, GetUsn(device.Udn, device.FullDeviceType), endPoint); + if (searchTarget == SsdpConstants.SsdpDiscoverAllSTHeader) + { + //Send 1 search response for each unique service type for all devices found + var serviceTypes = + ( + from s + in device.Services + select s.FullServiceType + ).Distinct().ToArray(); + + foreach (var st in serviceTypes) + { + SendServiceSearchResponses(device, st, endPoint); + } + } } private void SendServiceSearchResponses(SsdpDevice device, string searchTarget, UdpEndPoint endPoint) diff --git a/src/Main/Rssdp.nuspec b/src/Main/Rssdp.nuspec index 737071c..55edb89 100644 --- a/src/Main/Rssdp.nuspec +++ b/src/Main/Rssdp.nuspec @@ -2,7 +2,7 @@ Rssdp - 3.5.5 + 3.5.6-beta Rssdp Troy Willmot Yortw @@ -12,7 +12,7 @@ false Really Simple Service Discovery Protocol - a 100% .Net implementation of the SSDP protocol for publishing custom/basic devices, and discovering all device types on a network. A 100% .Net implementation of the SSDP protocol for basic and custom device types. - Use Multicast socket options for binding to local IP provided to socket factory (regressed in v3.0.0.0). + Fixes for search responses to match UPnP 1.1 specification. Copyright 2017 en-AU portable xamarin ios android windowsphone winrt uwp mobile ssdp discovery device service protocol upnp netfx40 .net4 diff --git a/src/Main/Test.SsdpPortable/DevicePublisherTests.cs b/src/Main/Test.SsdpPortable/DevicePublisherTests.cs index c63ecbe..0aad861 100644 --- a/src/Main/Test.SsdpPortable/DevicePublisherTests.cs +++ b/src/Main/Test.SsdpPortable/DevicePublisherTests.cs @@ -13,9 +13,6 @@ namespace Test.RssdpPortable public class DevicePublisherTests { - // "urn:rssdp-test-namespace:service:test-service-type:1" - - #region Argument Checking #region Constructors @@ -1197,9 +1194,6 @@ public void Publisher_SearchResponse_RespondsToSearchWithoutMXHeaderInRelaxedMod var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1); Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1); Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); } @@ -1227,9 +1221,6 @@ public void Publisher_SearchResponse_RespondsToSearchWithoutMXHeaderInDefaultMod var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1, "Incorrect number of upnp root device responses"); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() >= 1, "Incorrect number of pnp root device responses"); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1, "No response for UDN"); Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1, "No response for full device type"); Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); } @@ -1256,11 +1247,8 @@ public void Publisher_SearchResponse_RespondsToUpnpRootDeviceSearch() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() == 0); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1); - Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); + Assert.AreEqual(1, GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count()); + Assert.AreEqual(0, GetResponsesNotMatchingTarget(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count()); } } @@ -1289,10 +1277,8 @@ public void Publisher_SearchResponse_AddCustomHeaders() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1); + Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() == 1); Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() == 0); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1); Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues(testHeader.Name).First().StartsWith(testHeader.Value)).Count()); } @@ -1317,11 +1303,9 @@ public void Publisher_SearchResponse_RespondsToPnpRootDeviceSearch() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1); - Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); + Assert.AreEqual(2, searchResponses.Count()); + Assert.AreEqual(1, GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count()); + Assert.AreEqual(1, GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count()); } } @@ -1343,11 +1327,8 @@ public void Publisher_SearchResponse_RespondsToUdnSearch() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1); - Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); + Assert.AreEqual(1, searchResponses.Count()); + Assert.AreEqual(1, GetResponses(searchResponses, rootDevice.Udn).Count()); } } @@ -1368,11 +1349,8 @@ public void Publisher_SearchResponse_RespondsToDeviceTypeSearch() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1); - Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); + Assert.AreEqual(1, searchResponses.Count()); + Assert.AreEqual(1, GetResponses(searchResponses, rootDevice.FullDeviceType).Count()); } } @@ -1438,11 +1416,8 @@ public void Publisher_SearchResponse_ResponseWithMissngManHeaderInDefaultMode() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1); - Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); + Assert.AreEqual(1, GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count()); + Assert.AreEqual(1, GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count()); } } @@ -1467,11 +1442,8 @@ public void Publisher_SearchResponse_ResponseWithMissngManHeaderInRelaxedMode() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.Udn).Count() >= 1); - Assert.IsTrue(GetResponses(searchResponses, rootDevice.FullDeviceType).Count() >= 1); - Assert.AreEqual(0, searchResponses.Where((r) => !r.Headers.GetValues("USN").First().StartsWith(rootDevice.Udn)).Count()); + Assert.AreEqual(1, GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count()); + Assert.AreEqual(1, GetResponses(searchResponses, SsdpConstants.PnpDeviceTypeRootDevice).Count()); } } @@ -1545,7 +1517,7 @@ public void Publisher_SearchResponse_RespondsToNonDuplicateSearchRequest() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(0, searchResponses.Where((r) => !r.IsSuccessStatusCode).Count()); - Assert.IsTrue(searchResponses.Count() == 8); + Assert.IsTrue(searchResponses.Count() == 4); Assert.IsTrue(GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice).Count() == 2); } } @@ -1649,6 +1621,18 @@ public void Publisher_SearchResponse_SendsGrandchildResponses() public void Publisher_SearchResponse_RespondsToAllSearch() { var rootDevice = CreateValidRootDevice(); + var service = new SsdpService() + { + ControlUrl = new Uri("/test/control", UriKind.Relative), + ScpdUrl = new Uri("/test", UriKind.Relative), + EventSubUrl = new Uri("/test/events", UriKind.Relative), + ServiceType = "testservicetype", + ServiceTypeNamespace = "my-namespace", + ServiceVersion = 1, + Uuid = System.Guid.NewGuid().ToString() + }; + rootDevice.AddService(service); + var parentDevice = CreateValidEmbeddedDevice(rootDevice); rootDevice.AddDevice(parentDevice); var childDevice = CreateValidEmbeddedDevice(rootDevice); @@ -1672,10 +1656,18 @@ public void Publisher_SearchResponse_RespondsToAllSearch() var rootUuidResponses = GetResponses(searchResponses, childDevice.Udn); var parentUuidResponses = GetResponses(searchResponses, childDevice.Udn); var childUuidResponses = GetResponses(searchResponses, childDevice.Udn); + var deviceTypeResponses = GetResponses(searchResponses, childDevice.FullDeviceType).Union(GetResponses(searchResponses, rootDevice.DeviceType)); + var rootDeviceResponses = GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice); + var pnpRootDeviceResponses = GetResponses(searchResponses, SsdpConstants.UpnpDeviceTypeRootDevice); + var serviceResponses = GetResponses(searchResponses, service.FullServiceType); Assert.AreEqual(1, rootUuidResponses.Count()); Assert.AreEqual(1, parentUuidResponses.Count()); Assert.AreEqual(1, childUuidResponses.Count()); + Assert.AreEqual(2, deviceTypeResponses.Count()); + Assert.AreEqual(1, rootDeviceResponses.Count()); + Assert.AreEqual(1, pnpRootDeviceResponses.Count()); + Assert.AreEqual(1, serviceResponses.Count()); } } @@ -1764,8 +1756,10 @@ public void Publisher_SearchResponse_RespondsToServiceTypeDeviceSearch() var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(1, GetResponses(searchResponses, service.FullServiceType).Count()); + Assert.AreEqual(1, searchResponses.Count()); } } + [TestMethod] public void Publisher_SearchResponse_RespondsToServiceTypeDeviceSearch_WithEmbeddedDeviceService() { @@ -1806,6 +1800,60 @@ public void Publisher_SearchResponse_RespondsToServiceTypeDeviceSearch_WithEmbed var searchResponses = GetSentMessages(server.SentMessages); Assert.AreEqual(1, GetResponses(searchResponses, service.FullServiceType).Count()); + Assert.AreEqual(1, searchResponses.Count()); + } + } + + [TestMethod] + public void Publisher_SearchResponse_RespondsToServiceTypeDeviceSearchOncePerServiceType() + { + var rootDevice = CreateValidRootDevice(); + + var server = new MockCommsServer(); + using (var publisher = new TestDevicePublisher(server)) + { + publisher.StandardsMode = SsdpStandardsMode.Strict; +#pragma warning disable CS0618 // Type or member is obsolete + publisher.SupportPnpRootDevice = false; +#pragma warning restore CS0618 // Type or member is obsolete + + var service = new SsdpService() + { + ControlUrl = new Uri("/test/control", UriKind.Relative), + ScpdUrl = new Uri("/test", UriKind.Relative), + EventSubUrl = new Uri("/test/events", UriKind.Relative), + ServiceType = "testservicetype", + ServiceTypeNamespace = "my-namespace", + ServiceVersion = 1, + Uuid = System.Guid.NewGuid().ToString() + }; + + var service2 = new SsdpService() + { + ControlUrl = new Uri("/test2/control", UriKind.Relative), + ScpdUrl = new Uri("/test2", UriKind.Relative), + EventSubUrl = new Uri("/test2/events", UriKind.Relative), + ServiceType = "testservicetype", + ServiceTypeNamespace = "my-namespace", + ServiceVersion = 1, + Uuid = System.Guid.NewGuid().ToString() + }; + + rootDevice.AddService(service); + rootDevice.AddService(service2); + publisher.AddDevice(rootDevice); + server.WaitForMockBroadcast(1000); + server.SentBroadcasts.Clear(); + server.SentMessages.Clear(); + + ReceivedUdpData searchRequest = GetSearchRequestMessage(service.FullServiceType); + + server.MockReceiveMessage(searchRequest); + server.WaitForMockMessage(1500); + + var searchResponses = GetSentMessages(server.SentMessages); + Assert.AreEqual(1, GetResponses(searchResponses, service.FullServiceType).Count()); + Assert.AreEqual(1, searchResponses.Count()); } } @@ -1930,7 +1978,7 @@ public void Publisher_SearchResponse_RandomisesMxHeaderGreaterThan120() //System.Threading.Thread.Sleep(500); var searchResponses = GetSentMessages(server.SentMessages); - Assert.AreEqual(3, searchResponses.Count()); + Assert.AreEqual(1, searchResponses.Count()); } } @@ -2115,6 +2163,11 @@ private ReceivedUdpData GetSearchRequestMessageWithoutManHeader(string searchTar return (from r in searchResponses where r.Headers.GetValues("ST").First() == searchTarget select r); } + private IEnumerable GetResponsesNotMatchingTarget(IEnumerable searchResponses, string searchTarget) + { + return (from r in searchResponses where r.Headers.GetValues("ST").First() == searchTarget select r); + } + #endregion }