/// <summary> /// Initiates the stream. /// </summary> private void OpenStream() { // Generate new client and message ids _clientId = Guid.NewGuid().ToString(); _messageId = 1; // Stream initialization request var xml = Xml.Element("stream:stream", "jabber:client") .Attr("to", "connect.logitech.com") .Attr("version", "1.0") .Attr("xmlns:stream", "http://etherx.jabber.org/streams") .Attr("xml:lang", CultureInfo.CurrentCulture.Name); Send(xml.ToXmlString(xmlDeclaration: true, leaveOpen: true)); // Create a new parser instance. if (_parser != null) { _parser.Dispose(); } _parser = new StreamParser(_stream, true); // The first element of the stream must be <stream:features>. var features = _parser.NextElement("stream:features"); MessageReceived?.Invoke(this, new MessageReceivedEventArgs(features.OuterXml)); // Enable keepalive if (_heartbeat != null) { _heartbeat.Dispose(); } _heartbeat = new System.Timers.Timer(); _heartbeat.Elapsed += HeatbeatAsync; _heartbeat.Interval = 30000; _heartbeat.Start(); Connected = true; }
/// <summary> /// Gets the session token for the supplied Logitech username and password. /// </summary> /// <param name="ip">IP address of the Harmony Hub.</param> /// <param name="username">Username.</param> /// <param name="password">Passwprd.</param> /// <returns>Harmony session token.</returns> private string GetSessionToken(string ip, string username, string password, bool bypassLogitech) { var sessionToken = ""; var authToken = "";; if (!bypassLogitech) { // Authenticate to Logitech authToken = LoginToLogitech(username, password); if (string.IsNullOrEmpty(authToken)) { throw new AuthTokenException("Could not get token from Logitech server."); } } // Swap auth token for a session token using (var authClient = new TcpClient(ip, 5222)) { using (var authStream = authClient.GetStream()) { // Initialize stream var streamXml = Xml.Element("stream:stream", "jabber:client") .Attr("to", "connect.logitech.com") .Attr("version", "1.0") .Attr("xmlns:stream", "http://etherx.jabber.org/streams") .Attr("xml:lang", CultureInfo.CurrentCulture.Name); byte[] streamBuffer = Encoding.UTF8.GetBytes(streamXml.ToXmlString(xmlDeclaration: true, leaveOpen: true)); authStream.Write(streamBuffer, 0, streamBuffer.Length); // Begin reading auth stream using (var authParser = new StreamParser(authStream, true)) { // The first element of the stream must be <stream:features>. var features = authParser.NextElement("stream:features"); MessageReceived?.Invoke(this, new MessageReceivedEventArgs(features.OuterXml)); // Auth as guest // <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AGd1ZXN0QHguY29tAGd1ZXN0</auth> var saslXml = Xml.Element("auth", "urn:ietf:params:xml:ns:xmpp-sasl") .Attr("mechanism", "PLAIN") .Text(Convert.ToBase64String(Encoding.UTF8.GetBytes("\0" + "*****@*****.**" + "\0" + "guest"))); byte[] saslBuffer = Encoding.UTF8.GetBytes(saslXml.ToXmlString()); authStream.Write(saslBuffer, 0, saslBuffer.Length); // Handle response while (true) { var response = authParser.NextElement("challenge", "success", "failure"); MessageReceived?.Invoke(this, new MessageReceivedEventArgs(response.OuterXml)); if (response.Name != "success") { throw new SaslAuthenticationException("SASL authentication failed."); } break; } // Swap the authToken for a sessionToken on the Harmony Hub /* * <iq type="get" id="3174962747" from="guest"> * <oa xmlns="connect.logitech.com" mime="vnd.logitech.connect/vnd.logitech.pair"> * token=y6jZtSuYYOoQ2XXiU9cYovqtT+cCbcyjhWqGbhQsLV/mWi4dJVglFEBGpm08OjCW:name=SOMEID#iOS6.0.1#iPhone * </oa> * </iq> */ var methodOrToken = bypassLogitech ? "method=pair" : "token=" + authToken; var swapXml = Xml.Element("iq") .Attr("type", "get") .Attr("id", _clientId + "_" + _messageId.ToString()) .Child(Xml.Element("oa", "connect.logitech.com") .Attr("mime", HarmonyMimeTypes.Pair) .Text(methodOrToken + ":name=foo#iOS8.3.0#iPhone")); byte[] swapBuffer = Encoding.UTF8.GetBytes(swapXml.ToXmlString()); authStream.Write(swapBuffer, 0, swapBuffer.Length); // Handle response while (true) { XmlElement ret = authParser.NextElement("iq"); MessageReceived?.Invoke(this, new MessageReceivedEventArgs(ret.OuterXml)); if (ret.Name == "iq") { if (ret.FirstChild != null && ret.FirstChild.Name == "oa") { if (ret.FirstChild.InnerXml.Contains("status=succeeded")) { const string identityRegEx = "identity=([A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12}):status"; var regex = new Regex(identityRegEx, RegexOptions.IgnoreCase | RegexOptions.Singleline); var match = regex.Match(ret.FirstChild.InnerXml); if (match.Success) { sessionToken = match.Groups[1].ToString(); break; } else { throw new AuthTokenException("AuthToken swap failed."); } } } } } } // Kill stream byte[] killBuffer = Encoding.UTF8.GetBytes("</stream:stream>"); authStream.Write(killBuffer, 0, killBuffer.Length); } } return(sessionToken); }
/// <summary> /// Listens for incoming XML stanzas and raises the appropriate events. /// </summary> /// <remarks>This runs in the context of a separate thread. In case of an /// exception, the Error event is raised and the thread is shutdown.</remarks> private void ReadXmlStream() { try { while (_parser != null && !_cancellationTokenSource.Token.IsCancellationRequested) { XmlElement elem = _parser.NextElement("iq", "message", "presence"); MessageReceived?.Invoke(this, new MessageReceivedEventArgs(elem.OuterXml)); if (elem.Name == "iq" && elem.HasChildNodes) { var messageId = elem.Attributes["id"].Value; TaskCompletionSource <XmlElement> resultTaskCompletionSource; if (messageId != null && _resultTaskCompletionSources.TryGetValue(messageId, out resultTaskCompletionSource)) { var oaNode = elem.FirstChild; if (oaNode != null && oaNode.Name == "oa") { switch (oaNode.Attributes["errorcode"].Value) { case "200": resultTaskCompletionSource.TrySetResult(elem); _resultTaskCompletionSources.Remove(messageId); break; case "100": // Ignore continuation messages break; default: var errorMessage = oaNode.Attributes["errorstring"].Value; resultTaskCompletionSource.TrySetException(new Exception(errorMessage)); _resultTaskCompletionSources.Remove(messageId); break; } } } } else if (elem.Name == "message" && elem.HasChildNodes) { var eventNode = elem.FirstChild; if (eventNode != null && eventNode.Name == "event" && eventNode.Attributes["type"] != null) { switch (eventNode.Attributes["type"].Value) { case HarmonyEventTypes.StartActivityFinished: var startActivityParts = eventNode.InnerText.Split(':'); if (startActivityParts.Length == 3) { var startActivityIdParts = startActivityParts[0].Split('='); if (startActivityIdParts.Length == 2) { int id; if (int.TryParse(startActivityIdParts[1], out id)) { CurrentActivityUpdated?.Invoke(this, id); } } } break; case HarmonyEventTypes.StateDigest: // TODO: What to do with this? // It seems that these are purely informational, currently comments out to avoid the serialization hit // but left in, for the case that this becomes useful later on //var notify = JsonSerializer<HarmonyNotify>.Deserialize(eventNode.InnerText); break; } } } } } catch (Exception e) { // Stream has dropped, this happens naturally on Dispose if (e is IOException) { Authenticated = false; Connected = false; } // An exception outside of Dispose should be raised to the caller to handle if (!_disposed) { Error?.Invoke(this, new ErrorEventArgs(e)); } } }