public static HttpStatusCode HandleEventNotification(Stream stream, Encoding streamEncoding, CpService service, UPnPVersion upnpVersion) { try { // Parse XML document using (StreamReader streamReader = new StreamReader(stream, streamEncoding)) using (XmlReader reader = XmlReader.Create(streamReader, UPnPConfiguration.DEFAULT_XML_READER_SETTINGS)) { reader.MoveToContent(); reader.ReadStartElement("propertyset", UPnPConsts.NS_UPNP_EVENT); while (reader.LocalName == "property" && reader.NamespaceURI == UPnPConsts.NS_UPNP_EVENT) { reader.ReadStartElement("property", UPnPConsts.NS_UPNP_EVENT); HandleVariableChangeNotification(reader, service, upnpVersion); reader.ReadEndElement(); // property } reader.Close(); } return(HttpStatusCode.OK); } catch (Exception e) { UPnPConfiguration.LOGGER.Warn("GENAClientController: Error handling event notification", e); return(HttpStatusCode.BadRequest); } }
/// <summary> /// Given the <paramref name="userAgentStr"/> from an HTTP USER-AGENT header, this method extracts the UPnP version /// from the string. /// </summary> /// <param name="userAgentStr">USER-AGENT header entry of the form "OS/version UPnP/1.1 product/version".</param> /// <param name="minorVersion">Returns the minor version number in the specified <paramref name="userAgentStr"/>.</param> /// <returns><c>true</c>, if the user agent string could successfully be parsed and denotes a UPnP major version of 1.</returns> /// <exception cref="MediaPortal.Utilities.Exceptions.InvalidDataException">If the specified header value is malformed.</exception> public static bool ParseUserAgentUPnP1MinorVersion(string userAgentStr, out int minorVersion) { if (string.IsNullOrEmpty(userAgentStr)) { minorVersion = 0; return(false); } UPnPVersion ver; if (!UPnPVersion.TryParseFromUserAgent(userAgentStr, out ver)) { if (UPnPConfiguration.LAX_USER_AGENT_PARSING) { // If a client sent a malformed USER-AGENT, we'll assume UPnP Version 1.0 minorVersion = 0; return(true); } throw new UnsupportedRequestException(string.Format("Unsupported USER-AGENT header entry '{0}'", userAgentStr)); } minorVersion = 0; if (ver.VerMax != 1) { return(false); } minorVersion = ver.VerMin; return(true); }
/// <summary> /// Creates a new <see cref="RootEntry"/> instance. /// </summary> /// <param name="rwLock">Synchronization object to use for multi-threaded access.</param> /// <param name="upnpVersion">UPnP version the remote device is using.</param> /// <param name="osVersion">OS and version our partner is using.</param> /// <param name="productVersion">Product and version our partner is using.</param> /// <param name="expirationTime">Time when the advertisement will expire.</param> public RootEntry(ReaderWriterLockSlim rwLock, UPnPVersion upnpVersion, string osVersion, string productVersion, DateTime expirationTime) { _rwLock = rwLock; _upnpVersion = upnpVersion; _osVersion = osVersion; _productVersion = productVersion; _expirationTime = expirationTime; }
/// <summary> /// Creates a new <see cref="RootEntry"/> instance. /// </summary> /// <param name="syncObj">Synchronization object to use for multithread access.</param> /// <param name="upnpVersion">UPnP version the remote device is using.</param> /// <param name="osVersion">OS and version our partner is using.</param> /// <param name="productVersion">Product and version our partner is using.</param> /// <param name="expirationTime">Time when the advertisement will expire.</param> public RootEntry(object syncObj, UPnPVersion upnpVersion, string osVersion, string productVersion, DateTime expirationTime) { _syncObj = syncObj; _upnpVersion = upnpVersion; _osVersion = osVersion; _productVersion = productVersion; _expirationTime = expirationTime; }
public GENAClientController(CPData cpData, DeviceConnection connection, EndpointConfiguration endpoint, UPnPVersion upnpVersion) { _cpData = cpData; _connection = connection; _endpoint = endpoint; _upnpVersion = upnpVersion; _eventNotificationPath = "/" + Guid.NewGuid(); IPAddress address = endpoint.EndPointIPAddress; _eventNotificationEndpoint = new IPEndPoint(address, address.AddressFamily == AddressFamily.InterNetwork ? cpData.HttpPortV4 : cpData.HttpPortV6); _subscriptionRenewalTimer = new Timer(OnSubscriptionRenewalTimerElapsed); }
public GENAClientController(CPData cpData, DeviceConnection connection, EndpointConfiguration endpoint, UPnPVersion upnpVersion) { _cpData = cpData; _connection = connection; _endpoint = endpoint; _upnpVersion = upnpVersion; _eventNotificationPath = cpData.ServicePrefix + "/" + Guid.NewGuid(); IPAddress address = endpoint.EndPointIPAddress; var port = UPnPServer.DEFAULT_UPNP_AND_SERVICE_PORT_NUMBER; _eventNotificationEndpoint = new IPEndPoint(address, port); _subscriptionRenewalTimer = new Timer(OnSubscriptionRenewalTimerElapsed); }
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); } } }
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); } }
/// <summary> /// Takes the XML document provided by the given <paramref name="body"/> stream, parses it in the given /// <paramref name="contentEncoding"/> and provides the action result to the appropriate receiver. /// </summary> /// <param name="body">Body stream of the SOAP XML action result message.</param> /// <param name="contentEncoding">Encoding of the body stream.</param> /// <param name="action">Action which was called before.</param> /// <param name="clientState">State object which was given in the action call and which will be returned to the client.</param> /// <param name="upnpVersion">UPnP version of the UPnP server.</param> public static void HandleResult(Stream body, Encoding contentEncoding, CpAction action, object clientState, UPnPVersion upnpVersion) { bool sourceSupportsUPnP11 = upnpVersion.VerMin >= 1; IList <object> outParameterValues; try { if (!body.CanRead) { UPnPConfiguration.LOGGER.Error("SOAPHandler: Empty action result document"); action.ActionErrorResultPresent(new UPnPError(501, "Invalid server result"), clientState); return; } using (TextReader textReader = new StreamReader(body, contentEncoding)) outParameterValues = ParseResult(textReader, action, sourceSupportsUPnP11); } catch (Exception e) { UPnPConfiguration.LOGGER.Error("SOAPHandler: Error parsing action result document", e); action.ActionErrorResultPresent(new UPnPError(501, "Invalid server result"), clientState); return; } try { // Invoke action result action.ActionResultPresent(outParameterValues, clientState); } catch (Exception e) { UPnPConfiguration.LOGGER.Error("UPnP subsystem: Error invoking action '{0}'", e, action.FullQualifiedName); } }
/// <summary> /// Encodes a call of the specified <paramref name="action"/> with the given <paramref name="inParamValues"/> and /// returns the resulting SOAP XML string. /// </summary> /// <param name="action">Action to be called.</param> /// <param name="inParamValues">List of parameter values which must match the action's signature. /// Can be <c>null</c> if the parameter list is empty.</param> /// <param name="upnpVersion">UPnP version to use for the encoding.</param> /// <returns>XML string which contains the SOAP document.</returns> public static string EncodeCall(CpAction action, IList <object> inParamValues, UPnPVersion upnpVersion) { bool targetSupportsUPnP11 = upnpVersion.VerMin >= 1; StringBuilder result = new StringBuilder(5000); using (StringWriterWithEncoding stringWriter = new StringWriterWithEncoding(result, UPnPConsts.UTF8_NO_BOM)) using (XmlWriter writer = XmlWriter.Create(stringWriter, UPnPConfiguration.DEFAULT_XML_WRITER_SETTINGS)) { SoapHelper.WriteSoapEnvelopeStart(writer, true); writer.WriteStartElement("u", action.Name, action.ParentService.ServiceTypeVersion_URN); // Check input parameters IList <CpArgument> formalArguments = action.InArguments; if (inParamValues == null) { inParamValues = EMPTY_OBJECT_LIST; } if (inParamValues.Count != formalArguments.Count) { throw new ArgumentException("Invalid argument count"); } for (int i = 0; i < formalArguments.Count; i++) { CpArgument argument = formalArguments[i]; object value = inParamValues[i]; writer.WriteStartElement(argument.Name); argument.SoapSerializeArgument(value, !targetSupportsUPnP11, writer); writer.WriteEndElement(); // argument.Name } SoapHelper.WriteSoapEnvelopeEndAndClose(writer); } return(result.ToString()); }
protected static void HandleVariableChangeNotification(XmlReader reader, CpService service, UPnPVersion upnpVersion) { string variableName = reader.LocalName; CpStateVariable stateVariable; if (!service.StateVariables.TryGetValue(variableName, out stateVariable)) { // We don't know that variable - this is an error case but we won't raise an exception here return; } object value = stateVariable.DataType.SoapDeserializeValue(reader, upnpVersion.VerMin == 0); service.InvokeStateVariableChanged(stateVariable, value); }