protected RootEntry GetOrCreateRootEntry(string deviceUUID, string descriptionLocation, UPnPVersion upnpVersion, string osVersion, string productVersion, DateTime expirationTime, EndpointConfiguration endpoint, HTTPVersion httpVersion, int searchPort, out bool wasAdded) { // Because the order of the UDP advertisement packets isn't guaranteed (and even not really specified by the UPnP specification), // in the general case it is not possible to find the correct root entry for each advertisement message. // - We cannot search by root device UUID because the upnp:rootdevice message might not be the first message, so before that message, we don't know the root device ID and // thus we cannot use the root device id as unique key to find the root entry // - We cannot use the device description because for multi-homed devices, more than one device description can belong to the same root device // // Assume the message arrive in an order so that device A over network interface N1 is announced first. Second, device B over network interface N2 is announced. // In that case, we cannot judge if those two devices belong to the same root device or not. // // To face that situation, we first add all advertised devices to _pendingDeviceEntries. When a upnp:rootdevice message is received, // we either simply move the root entry from _pendingDeviceEntries into _cpData.DeviceEntries or we merge the pending entry with an already existing // entry in _cpData.DeviceEntries. At that time the merge is possible because we then have the root device id for both root entries. lock (_cpData.SyncObj) { RootEntry result = GetRootEntryByContainedDeviceUUID(deviceUUID) ?? GetRootEntryByDescriptionLocation(descriptionLocation); if (result != null) { result.ExpirationTime = expirationTime; wasAdded = false; } else { result = new RootEntry(_cpData.SyncObj, upnpVersion, osVersion, productVersion, expirationTime); _pendingDeviceEntries.Add(result); wasAdded = true; } return(result); } }
protected RootEntry GetRootEntryByDescriptionLocation(string descriptionLocation) { RootEntry rootEntry = _cpData.DeviceEntries.Values.FirstOrDefault(entry => entry.AllLinks.ContainsKey(descriptionLocation)) ?? _pendingDeviceEntries.FirstOrDefault(entry => entry.AllLinks.ContainsKey(descriptionLocation)); return(rootEntry); }
protected void RemoveRootEntry(RootEntry rootEntry) { lock (_cpData.SyncObj) { _cpData.DeviceEntries.Remove(rootEntry.RootDeviceUUID); _pendingDeviceEntries.Remove(rootEntry); } }
/// <summary> /// Returns the root entry which contains any device of the given <paramref name="deviceUUID"/>. /// </summary> /// <param name="deviceUUID">UUID of any device to search the enclosing root entry for.</param> /// <returns>Root entry instance or <c>null</c>, if no device with the given UUID was found.</returns> protected RootEntry GetRootEntryByContainedDeviceUUID(string deviceUUID) { lock (_cpData.SyncObj) { RootEntry result = _cpData.DeviceEntries.Values.FirstOrDefault(rootEntry => rootEntry.Devices.ContainsKey(deviceUUID)); if (result != null) { return(result); } return(_pendingDeviceEntries.FirstOrDefault(rootEntry => rootEntry.Devices.ContainsKey(deviceUUID))); } }
protected void InvokeDeviceConfigurationChanged(RootEntry rootEntry) { try { DeviceConfigurationChangedDlgt dlgt = DeviceConfigurationChanged; if (dlgt != null) { dlgt(rootEntry); } } catch (Exception e) { UPnPConfiguration.LOGGER.Warn("SSDPClientController: Error invoking DeviceConfigurationChanged delegate", e); } }
protected void InvokeServiceAdded(RootEntry rootEntry, DeviceEntry deviceEntry, string serviceTypeVersion_URN) { try { ServiceAddedDlgt dlgt = ServiceAdded; if (dlgt != null) { dlgt(rootEntry, deviceEntry, serviceTypeVersion_URN); } } catch (Exception e) { UPnPConfiguration.LOGGER.Warn("SSDPClientController: Error invoking ServiceAdded delegate", e); } }
protected RootEntry MergeOrMoveRootEntry(RootEntry pendingRootEntry, string rootDeviceUUID) { lock (_cpData.SyncObj) { _pendingDeviceEntries.Remove(pendingRootEntry); RootEntry targetEntry; if (_cpData.DeviceEntries.TryGetValue(rootDeviceUUID, out targetEntry)) { targetEntry.MergeRootEntry(pendingRootEntry); return(targetEntry); } targetEntry = pendingRootEntry; // From here on, the entry is not pending any more, so we use the variable targetEntry for clearness targetEntry.RootDeviceUUID = rootDeviceUUID; _cpData.DeviceEntries[rootDeviceUUID] = targetEntry; return(targetEntry); } }
/// <summary> /// Merges the data of the <paramref name="other"/> root entry into this entry. /// </summary> /// <param name="other">Other root entry to merge. The <paramref name="other"/> entry must not contain any of this entry's link data nor /// any of this entry's devices.</param> internal void MergeRootEntry(RootEntry other) { _expirationTime = _expirationTime > other.ExpirationTime ? _expirationTime : other.ExpirationTime; foreach (LinkData linkData in other.AllLinks.Values) { AddOrUpdateLink(linkData.Endpoint, linkData.DescriptionLocation, linkData.HTTPVersion, linkData.SearchPort); } foreach (DeviceEntry deviceEntry in other.Devices.Values) { _devices.Add(deviceEntry.UUID, deviceEntry); } _bootID = Math.Max(_bootID, other.BootID); foreach (KeyValuePair <IPEndPoint, uint> kvp in other._configIDs) { _configIDs[kvp.Key] = kvp.Value; } foreach (KeyValuePair <string, object> clientProperty in other.ClientProperties) { if (!_clientProperties.ContainsKey(clientProperty.Key)) { _clientProperties[clientProperty.Key] = clientProperty.Value; } } }
protected void HandleUpdatePacket(SimpleHTTPRequest header, EndpointConfiguration config) { if (header.Param != "*") { // Invalid message return; } HTTPVersion httpVersion; if (!HTTPVersion.TryParse(header.HttpVersion, out httpVersion)) { // Invalid message return; } // Host, NT, NTS, USN are not interesting //string host = header["HOST"]; //string nt = header["NT"]; //string nts = header["NTS"]; string usn = header["USN"]; //string location = header["LOCATION"]; string bi = header["BOOTID.UPNP.ORG"]; uint bootID; if (!uint.TryParse(bi, out bootID)) { // Invalid message return; } string nbi = header["NEXTBOOTID.UPNP.ORG"]; uint nextBootID; if (!uint.TryParse(nbi, out nextBootID)) { // Invalid message return; } if (!usn.StartsWith("uuid:")) { // Invalid usn return; } int separatorIndex = usn.IndexOf("::"); if (separatorIndex < 6) // separatorIndex == -1 or separatorIndex not after "uuid:" prefix with at least one char UUID // We only use messages containing a "::" substring and discard the "uuid:device-UUID" message { return; } string deviceUUID = usn.Substring(5, separatorIndex - 5); RootEntry rootEntry = GetRootEntryByContainedDeviceUUID(deviceUUID); if (rootEntry == null) { return; } if (rootEntry.BootID > bootID) { // Invalid message return; } bool fireDeviceRebooted = false; lock (_cpData.SyncObj) { if (rootEntry.BootID < bootID) { // Device reboot fireDeviceRebooted = true; } rootEntry.BootID = nextBootID; } if (fireDeviceRebooted) { InvokeDeviceRebooted(rootEntry, false); } }
protected void HandleNotifyPacket(EndpointConfiguration config, IPEndPoint remoteEndPoint, HTTPVersion httpVersion, string date, string cacheControl, string location, string server, string nts, string usn, string bi, string ci, string sp) { uint bootID = 0; if (bi != null && !uint.TryParse(bi, out bootID)) { // Invalid message return; } uint configID = 0; if (ci != null && !uint.TryParse(ci, out configID)) { // Invalid message return; } if (!usn.StartsWith("uuid:")) { // Invalid usn return; } string deviceUUID; string messageType; if (!ParserHelper.TryParseUSN(usn, out deviceUUID, out messageType)) { // We only use messages of the type "uuid:device-UUID::..." and discard the "uuid:device-UUID" message return; } if (nts == "ssdp:alive") { if (server == null) { // Invalid message return; } int maxAge; if (!TryParseMaxAge(cacheControl, out maxAge)) { // Invalid message return; } DateTime d; if (!DateTime.TryParse(date, out d)) { d = DateTime.Now; } DateTime expirationTime = d.AddSeconds(maxAge); // The specification says the SERVER header should contain three entries, separated by space, like // "SERVER: OS/version UPnP/1.1 product/version". // Unfortunately, some clients send entries separated by ", ", like "Linux/2.x.x, UPnP/1.0, pvConnect UPnP SDK/1.0". // We try to handle all situations correctly here, that's the reason for this ugly code. // What we've seen until now: // SERVER: Linux/2.x.x, UPnP/1.0, pvConnect UPnP SDK/1.0 => tokens separated by ',' // SERVER: Windows 2003, UPnP/1.0 DLNADOC/1.50, Serviio/0.5.2 => tokens separated by ',' and additional info in UPnP version token // SERVER: 3Com-ADSL-11g/1.0 UPnP/1.0 => only two tokens string[] versionInfos = server.Contains(", ") ? server.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries) : server.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); string upnpVersionInfo = versionInfos.FirstOrDefault(v => v.StartsWith(UPnPVersion.VERSION_PREFIX)); if (upnpVersionInfo == null) { // Invalid message return; } // upnpVersionInfo = 'UPnP/1.0', 'UPnP/1.1', 'UPnP/1.0 DLNADOC/1.50', ..., the UPnP version is always the first token string[] upnpVersionInfoTokens = upnpVersionInfo.Split(' '); string upnpVersionInfoToken = upnpVersionInfoTokens[0]; UPnPVersion upnpVersion; if (!UPnPVersion.TryParse(upnpVersionInfoToken, out upnpVersion)) { // Invalid message return; } if (upnpVersion.VerMax != 1) { // Incompatible UPnP version return; } int searchPort = 1900; if (upnpVersion.VerMin >= 1) { if (bi == null || ci == null) { // Invalid message return; } if (sp != null && (!int.TryParse(sp, out searchPort) || searchPort < 49152 || searchPort > 65535)) { // Invalid message return; } } RootEntry rootEntry; DeviceEntry deviceEntry = null; string serviceType = null; bool fireDeviceRebooted = false; bool fireConfigurationChanged = false; bool fireRootDeviceAdded = false; bool fireDeviceAdded = false; bool fireServiceAdded = false; lock (_cpData.SyncObj) { bool rootEntryAdded; // Use fail-safe code, see comment above about the different SERVER headers string osVersion = versionInfos.Length < 1 ? string.Empty : versionInfos[0]; string productVersion = versionInfos.Length < 3 ? string.Empty : versionInfos[2]; rootEntry = GetOrCreateRootEntry(deviceUUID, location, upnpVersion, osVersion, productVersion, expirationTime, config, httpVersion, searchPort, out rootEntryAdded); if (bi != null && rootEntry.BootID > bootID) { // Invalid message return; } uint currentConfigId = rootEntry.GetConfigID(remoteEndPoint); if (currentConfigId != 0 && currentConfigId != configID) { fireConfigurationChanged = true; } rootEntry.SetConfigID(remoteEndPoint, configID); if (!rootEntryAdded && bi != null && rootEntry.BootID < bootID) { // Device reboot // A device, which has rebooted, has lost all its links, so we must forget about the old link registrations and wait for new registrations in alive messages rootEntry.ClearLinks(); fireDeviceRebooted = true; } // Don't add the link before a reboot was detected and thus, rootEntry.ClearLinks() was called rootEntry.AddOrUpdateLink(config, location, httpVersion, searchPort); rootEntry.BootID = bootID; if (messageType == "upnp:rootdevice") { rootEntry.GetOrCreateDeviceEntry(deviceUUID); object value; if (!rootEntry.ClientProperties.TryGetValue("RootDeviceSetUp", out value)) { rootEntry = MergeOrMoveRootEntry(rootEntry, deviceUUID); fireRootDeviceAdded = true; rootEntry.ClientProperties["RootDeviceSetUp"] = true; } } else if (messageType.StartsWith("urn:")) { if (messageType.IndexOf(":device:") > -1) { string deviceType; int deviceTypeVersion; if (!ParserHelper.TryParseTypeVersion_URN(messageType, out deviceType, out deviceTypeVersion)) { // Invalid message return; } deviceEntry = rootEntry.GetOrCreateDeviceEntry(deviceUUID); fireDeviceAdded = string.IsNullOrEmpty(deviceEntry.DeviceType); deviceEntry.DeviceType = deviceType; deviceEntry.DeviceTypeVersion = deviceTypeVersion; } else if (messageType.IndexOf(":service:") > -1) { deviceEntry = rootEntry.GetOrCreateDeviceEntry(deviceUUID); serviceType = messageType; if (deviceEntry.Services.Contains(serviceType)) { return; } deviceEntry.Services.Add(serviceType); fireServiceAdded = true; } } else { // Invalid message return; } } // Raise events after returning the lock if (fireDeviceRebooted) { InvokeDeviceRebooted(rootEntry, fireConfigurationChanged); } else if (fireConfigurationChanged) { InvokeDeviceConfigurationChanged(rootEntry); } if (fireRootDeviceAdded) { InvokeRootDeviceAdded(rootEntry); } if (fireDeviceAdded) { InvokeDeviceAdded(rootEntry, deviceEntry); } if (fireServiceAdded) { InvokeServiceAdded(rootEntry, deviceEntry, serviceType); } } else if (nts == "ssdp:byebye") { RootEntry rootEntry = GetRootEntryByContainedDeviceUUID(deviceUUID); if (rootEntry != null) { if (bi != null && rootEntry.BootID > bootID) { // Invalid message return; } RemoveRootEntry(rootEntry); InvokeRootDeviceRemoved(rootEntry); } } }