/// <summary> /// Searches the network for UPnP devices. /// </summary> /// <param name="deviceType">UPnP device type. For example: "urn:schemas-upnp-org:device:InternetGatewayDevice:1".</param> /// <param name="timeout">Search wait timeout in milliseconds.</param> /// <returns>Returns matched UPnP devices.</returns> public UPnP_Device[] Search(string deviceType, int timeout) { if (timeout < 1) { timeout = 1; } /* www.unpnp.org. Search request with M-SEARCH. * When a control point desires to search the network for devices, it MUST send a multicast request with method M-SEARCH in the * following format. Control points that know the address of a specific device MAY also use a similar format to send unicast requests * with method M-SEARCH. * * For multicast M-SEARCH, the message format is defined below. Values in italics are placeholders for actual values. * * M-SEARCH * HTTP/1.1 * HOST: 239.255.255.250:1900 * MAN: "ssdp:discover" * MX: seconds to delay response * ST: search target * USER-AGENT: OS/version UPnP/1.1 product/version * * Note: No body is present in requests with method M-SEARCH, but note that the message MUST have a blank line following the * last header field. * Note: The TTL for the IP packet SHOULD default to 2 and SHOULD be configurable. * * Listed below are details for the request line and header fields appearing in the listing above. Field names are not case sensitive. * All field values are case sensitive except where noted. * * Request line * Must be “M-SEARCH * HTTP/1.1” * * M-SEARCH * Method for search requests. * * Request applies generally and not to a specific resource. MUST be *. * * HTTP/1.1 * HTTP version. * * Header fields * * HOST * REQUIRED. Field value contains the multicast address and port reserved for SSDP by Internet Assigned Numbers Authority * (IANA). MUST be 239.255.255.250:1900. * * MAN * REQUIRED by HTTP Extension Framework. Unlike the NTS and ST field values, the field value of the MAN header field is * enclosed in double quotes; it defines the scope (namespace) of the extension. MUST be "ssdp:discover". * * MX * REQUIRED. Field value contains maximum wait time in seconds. MUST be greater than or equal to 1 and SHOULD be less than * 5 inclusive. Device responses SHOULD be delayed a random duration between 0 and this many seconds to balance load for * the control point when it processes responses. This value MAY be increased if a large number of devices are expected to * respond. The MX field value SHOULD NOT be increased to accommodate network characteristics such as latency or * propagation delay (for more details, see the explanation below). Specified by UPnP vendor. Integer. * * ST * REQUIRED. Field value contains Search Target. MUST be one of the following. (See NT header field in NOTIFY with ssdp:alive * above.) Single URI. * * ssdp:all * Search for all devices and services. * * upnp:rootdevice * Search for root devices only. * * uuid:device-UUID * Search for a particular device. device-UUID specified by UPnP vendor. See section 1.1.4, “UUID format and * RECOMMENDED generation algorithms” for the MANDATORY UUID format. * * urn:schemas-upnp-org:device:deviceType:ver * Search for any device of this type where deviceType and ver are defined by the UPnP Forum working committee. * * urn:schemas-upnp-org:service:serviceType:ver * Search for any service of this type where serviceType and ver are defined by the UPnP Forum working committee. * * urn:domain-name:device:deviceType:ver * Search for any device of this typewhere domain-name (a Vendor Domain Name), deviceType and ver are defined * by the UPnP vendor and ver specifies the highest specifies the highest supported version of the device type. * Period characters in the Vendor Domain Name MUST be replaced with hyphens in accordance with RFC 2141. * * urn:domain-name:service:serviceType:ver * Search for any service of this type. Where domain-name (a Vendor Domain Name), serviceType and ver are * defined by the UPnP vendor and ver specifies the highest specifies the highest supported version of the service * type. Period characters in the Vendor Domain Name MUST be replaced with hyphens in accordance with RFC 2141. * * USER-AGENT * OPTIONAL. Specified by UPnP vendor. String. Field value MUST begin with the following “product tokens” (defined by * HTTP/1.1). The first product token identifes the operating system in the form OS name/OS version, the second token * represents the UPnP version and MUST be UPnP/1.1, and the third token identifes the product using the form * product name/product version. For example, “USER-AGENT: unix/5.1 UPnP/1.1 MyProduct/1.0”. Control points MUST be * prepared to accept a higher minor version number of the UPnP version than the control point itself implements. For * example, control points implementing UDA version 1.0 will be able to interoperate with devices implementing * UDA version 1.1. */ StringBuilder query = new StringBuilder(); query.Append("M-SEARCH * HTTP/1.1\r\n"); query.Append("HOST: 239.255.255.250:1900\r\n"); query.Append("MAN: \"ssdp:discover\"\r\n"); query.Append("MX: 1\r\n"); query.Append("ST: " + deviceType + "\r\n"); query.Append("\r\n"); using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.IpTimeToLive, 2); socket.SendTo(Encoding.UTF8.GetBytes(query.ToString()), new IPEndPoint(IPAddress.Broadcast, 1900)); List <string> deviceLocations = new List <string>(); byte[] buffer = new byte[32000]; DateTime startTime = DateTime.Now; // Receive responses while timeout reached. while (startTime.AddMilliseconds(timeout) > DateTime.Now) { // We have response, read it. if (socket.Poll(1, SelectMode.SelectRead)) { int countReceived = socket.Receive(buffer); string[] responseLines = Encoding.UTF8.GetString(buffer, 0, countReceived).Split('\n'); foreach (string responseLine in responseLines) { string[] name_value = responseLine.Split(new char[] { ':' }, 2); if (String2.Equals(name_value[0], "location", StringComparison2.InvariantCultureIgnoreCase)) { deviceLocations.Add(name_value[1].Trim()); } } } } // Create devices. List <UPnP_Device> devices = new List <UPnP_Device>(); foreach (string location in deviceLocations) { try { devices.Add(new UPnP_Device(location)); } catch { } } return(devices.ToArray()); } }
/// <summary> /// Searches the network for UPnP devices. /// </summary> /// <param name="ip">IP address of UPnP device.</param> /// <param name="deviceType">UPnP device type. For example: "urn:schemas-upnp-org:device:InternetGatewayDevice:1".</param> /// <param name="timeout">Search wait timeout in milliseconds.</param> /// <returns>Returns matched UPnP devices.</returns> /// <exception cref="ArgumentNullException">Is raised when <b>ip</b> is null reference.</exception> public UPnP_Device[] Search(IPAddress ip, string deviceType, int timeout) { if (ip == null) { throw new ArgumentNullException("ip"); } if (timeout < 1) { timeout = 1; } using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.IpTimeToLive, 2); StringBuilder query = new StringBuilder(); query.Append("M-SEARCH * HTTP/1.1\r\n"); query.Append("MAN: \"ssdp:discover\"\r\n"); query.Append("MX: 1\r\n"); query.Append("ST: " + deviceType + "\r\n"); query.Append("\r\n"); socket.SendTo(Encoding.UTF8.GetBytes(query.ToString()), new IPEndPoint(ip, 1900)); List <string> deviceLocations = new List <string>(); byte[] buffer = new byte[32000]; DateTime startTime = DateTime.Now; // Receive responses while timeout reached. while (startTime.AddMilliseconds(timeout) > DateTime.Now) { // We have response, read it. if (socket.Poll(1, SelectMode.SelectRead)) { int countReceived = socket.Receive(buffer); string[] responseLines = Encoding.UTF8.GetString(buffer, 0, countReceived).Split('\n'); foreach (string responseLine in responseLines) { string[] name_value = responseLine.Split(new char[] { ':' }, 2); if (String2.Equals(name_value[0], "location", StringComparison2.InvariantCultureIgnoreCase)) { deviceLocations.Add(name_value[1].Trim()); } } } } // Create devices. List <UPnP_Device> devices = new List <UPnP_Device>(); foreach (string location in deviceLocations) { try { devices.Add(new UPnP_Device(location)); } catch { } } return(devices.ToArray()); } }
/// <summary> /// Parses authetication info from client digest response. /// </summary> /// <param name="digestResponse">Client returned digest response.</param> private void Parse(string digestResponse) { string[] parameters = TextUtils.SplitQuotedString(digestResponse, ','); foreach (string parameter in parameters) { string[] name_value = parameter.Split(new char[] { '=' }, 2); string name = name_value[0].Trim(); if (name_value.Length == 2) { if (String2.Equals(name, "realm", StringComparison2.InvariantCultureIgnoreCase)) { m_Realm = TextUtils.UnQuoteString(name_value[1]); } else if (String2.Equals(name, "nonce", StringComparison2.InvariantCultureIgnoreCase)) { m_Nonce = TextUtils.UnQuoteString(name_value[1]); } // RFC bug ?: RFC 2831. digest-uri = "digest-uri" "=" <"> digest-uri-value <"> // RFC 2617 digest-uri = "uri" "=" digest-uri-value else if (String2.Equals(name, "uri", StringComparison2.InvariantCultureIgnoreCase) || String2.Equals(name, "digest-uri", StringComparison2.InvariantCultureIgnoreCase)) { m_Uri = TextUtils.UnQuoteString(name_value[1]); } else if (String2.Equals(name, "qop", StringComparison2.InvariantCultureIgnoreCase)) { m_Qop = TextUtils.UnQuoteString(name_value[1]); } else if (String2.Equals(name, "nc", StringComparison2.InvariantCultureIgnoreCase)) { m_NonceCount = Convert.ToInt32(TextUtils.UnQuoteString(name_value[1])); } else if (String2.Equals(name, "cnonce", StringComparison2.InvariantCultureIgnoreCase)) { m_Cnonce = TextUtils.UnQuoteString(name_value[1]); } else if (String2.Equals(name, "response", StringComparison2.InvariantCultureIgnoreCase)) { m_Response = TextUtils.UnQuoteString(name_value[1]); } else if (String2.Equals(name, "opaque", StringComparison2.InvariantCultureIgnoreCase)) { m_Opaque = TextUtils.UnQuoteString(name_value[1]); } else if (String2.Equals(name, "username", StringComparison2.InvariantCultureIgnoreCase)) { m_UserName = TextUtils.UnQuoteString(name_value[1]); } else if (String2.Equals(name, "algorithm", StringComparison2.InvariantCultureIgnoreCase)) { m_Algorithm = TextUtils.UnQuoteString(name_value[1]); } else if (String2.Equals(name, "charset", StringComparison2.InvariantCultureIgnoreCase)) { m_Charset = TextUtils.UnQuoteString(name_value[1]); } } } }
/// <summary> /// Calculates response value. /// </summary> /// <param name="userName">User name.</param> /// <param name="password">User password.</param> /// <returns>Returns calculated rsponse value.</returns> public string CalculateResponse(string userName, string password) { /* RFC 2617. * * 3.2.2.1 Request-Digest * * If the "qop" value is "auth" or "auth-int": * * request-digest = <"> < KD ( H(A1),unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) )> <"> * * If the "qop" directive is not present (this construction is for * compatibility with RFC 2069): * * request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> * * 3.2.2.2 A1 * * If the "algorithm" directive's value is "MD5" or is unspecified, then A1 is: * * A1 = unq(username-value) ":" unq(realm-value) ":" passwd * * If the "algorithm" directive's value is "MD5-sess", then A1 is * calculated only once - on the first request by the client following * receipt of a WWW-Authenticate challenge from the server. It uses the * server nonce from that challenge, and the first client nonce value to * construct A1 as follows: * * A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) ":" unq(nonce-value) ":" unq(cnonce-value) * * This creates a 'session key' for the authentication of subsequent * requests and responses which is different for each "authentication * session", thus limiting the amount of material hashed with any one * key. (Note: see further discussion of the authentication session in * section 3.3.) Because the server need only use the hash of the user * credentials in order to create the A1 value, this construction could * be used in conjunction with a third party authentication service so * that the web server would not need the actual password value. The * specification of such a protocol is beyond the scope of this * specification. * * 3.2.2.3 A2 * * If the "qop" directive's value is "auth" or is unspecified, then A2 is: * * A2 = Method ":" digest-uri-value * * If the "qop" value is "auth-int", then A2 is: * * A2 = Method ":" digest-uri-value ":" H(entity-body) * * * H(data) = MD5(data) * KD(secret, data) = H(concat(secret, ":", data)) * unc = unqoute string */ string A1 = ""; if (string.IsNullOrEmpty(this.Algorithm) || String2.Equals(this.Algorithm, "md5", StringComparison2.InvariantCultureIgnoreCase)) { A1 = userName + ":" + this.Realm + ":" + password; } else if (String2.Equals(this.Algorithm, "md5-sess", StringComparison2.InvariantCultureIgnoreCase)) { A1 = H(userName + ":" + this.Realm + ":" + password) + ":" + this.Nonce + ":" + this.CNonce; } else { throw new ArgumentException("Invalid 'algorithm' value '" + this.Algorithm + "'."); } string A2 = ""; if (string.IsNullOrEmpty(this.Qop) || String2.Equals(this.Qop, "auth", StringComparison2.InvariantCultureIgnoreCase)) { A2 = this.RequestMethod + ":" + this.Uri; } else { throw new ArgumentException("Invalid 'qop' value '" + this.Qop + "'."); } if (String2.Equals(this.Qop, "auth", StringComparison2.InvariantCultureIgnoreCase) || String2.Equals(this.Qop, "auth-int", StringComparison2.InvariantCultureIgnoreCase)) { // request-digest = <"> < KD ( H(A1),unq(nonce-value) ":" nc-value ":" unq(cnonce-value) ":" unq(qop-value) ":" H(A2) )> <"> // We don't add quoutes here. return(KD(H(A1), this.Nonce + ":" + this.NonceCount.ToString("x8") + ":" + this.CNonce + ":" + this.Qop + ":" + H(A2))); } else if (string.IsNullOrEmpty(this.Qop)) { // request-digest = <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> // We don't add quoutes here. return(KD(H(A1), this.Nonce + ":" + H(A2))); } else { throw new ArgumentException("Invalid 'qop' value '" + this.Qop + "'."); } }
/// <summary> /// Calculates 'rspauth' value. /// </summary> /// <param name="userName">User name.</param> /// <param name="password">Password.</param> /// <returns>Returns 'rspauth' value.</returns> public string CalculateRspAuth(string userName, string password) { /* RFC 2617 3.2.3. * The optional response digest in the "response-auth" directive * supports mutual authentication -- the server proves that it knows the * user's secret, and with qop=auth-int also provides limited integrity * protection of the response. The "response-digest" value is calculated * as for the "request-digest" in the Authorization header, except that * if "qop=auth" or is not specified in the Authorization header for the * request, A2 is * * A2 = ":" digest-uri-value * * and if "qop=auth-int", then A2 is * * A2 = ":" digest-uri-value ":" H(entity-body) * * where "digest-uri-value" is the value of the "uri" directive on the * Authorization header in the request. The "cnonce-value" and "nc- * value" MUST be the ones for the client request to which this message * is the response. The "response-auth", "cnonce", and "nonce-count" * directives MUST BE present if "qop=auth" or "qop=auth-int" is * specified. */ string a1 = ""; string a2 = ""; // Create A1 if (string.IsNullOrEmpty(this.Algorithm) || String2.Equals(this.Algorithm, "md5", StringComparison2.InvariantCultureIgnoreCase)) { a1 = userName + ":" + this.Realm + ":" + password; } else if (String2.Equals(this.Algorithm, "md5-sess", StringComparison2.InvariantCultureIgnoreCase)) { a1 = Net_Utils.ComputeMd5(userName + ":" + this.Realm + ":" + password, false) + ":" + this.Nonce + ":" + this.CNonce; } else { throw new ArgumentException("Invalid Algorithm value '" + this.Algorithm + "' !"); } // Create A2 if (string.IsNullOrEmpty(this.Qop) || String2.Equals(this.Qop, "auth", StringComparison2.InvariantCultureIgnoreCase)) { a2 = ":" + this.Uri; } else { throw new ArgumentException("Invalid qop value '" + this.Qop + "' !"); } // Calculate response value. // qop present if (!string.IsNullOrEmpty(this.Qop)) { return(Net_Utils.ComputeMd5(Net_Utils.ComputeMd5(a1, true) + ":" + this.Nonce + ":" + this.NonceCount.ToString("x8") + ":" + this.CNonce + ":" + this.Qop + ":" + Net_Utils.ComputeMd5(a2, true), true)); } // qop not present else { return(Net_Utils.ComputeMd5(Net_Utils.ComputeMd5(a1, true) + ":" + this.Nonce + ":" + Net_Utils.ComputeMd5(a2, true), true)); } }
/// <summary> /// Decodes non-ascii text with MIME <b>encoded-word</b> method. Defined in RFC 2047 2. /// </summary> /// <param name="text">Text.</param> /// <returns>Returns decoded text.</returns> /// <exception cref="ArgumentNullException">Is raised when <b>text</b> is null reference.</exception> public static string DecodeTextS(string text) { if (text == null) { throw new ArgumentNullException("word"); } /* RFC 2047 2. * encoded-word = "=?" charset "?" encoding "?" encoded-text "?=" * * encoded-text = 1*<Any printable ASCII character other than "?" or SPACE> * ; (but see "Use of encoded-words in message * ; headers", section 5) * * An 'encoded-word' may not be more than 75 characters long, including * 'charset', 'encoding', 'encoded-text', and delimiters. If it is * desirable to encode more text than will fit in an 'encoded-word' of * 75 characters, multiple 'encoded-word's (separated by CRLF SPACE) may * be used. * * RFC 2231 updates. * encoded-word := "=?" charset ["*" language] "?" encoded-text "?=" */ string retVal = text; retVal = encodedword_regex.Replace(retVal, delegate(Match m) { // We have encoded word, try to decode it. // Also if we have continuing encoded word, we need to skip all whitespaces between words. string encodedWord = m.Value; try { if (String2.Equals(m.Groups["encoding"].Value, "Q", StringComparison2.InvariantCultureIgnoreCase)) { encodedWord = MIME_Utils.QDecode(Encoding.GetEncoding(m.Groups["charset"].Value), m.Groups["value"].Value); } else if (String2.Equals(m.Groups["encoding"].Value, "B", StringComparison2.InvariantCultureIgnoreCase)) { encodedWord = Encoding.GetEncoding(m.Groups["charset"].Value).GetString(Net_Utils.FromBase64(Encoding2.Default.GetBytes(m.Groups["value"].Value))); } // Failed to parse encoded-word, leave it as is. RFC 2047 6.3. // else{ // No continuing encoded-word, append whitespaces to retval. Match mNext = encodedword_regex.Match(retVal, m.Index + m.Length); if (!(mNext.Success && mNext.Index == (m.Index + m.Length))) { encodedWord += m.Groups["whitespaces"].Value; } // We have continuing encoded-word, so skip all whitespaces. //else{ return(encodedWord); } catch { // Failed to parse encoded-word, leave it as is. RFC 2047 6.3. return(encodedWord); } }); return(retVal); }