// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** @suppress {duplicate} */ var remoting = remoting || {}; /** * A connection to an XMPP server. * * TODO(sergeyu): Chrome provides two APIs for TCP sockets: chrome.socket and * chrome.sockets.tcp . chrome.socket is deprecated but it's still used here * because TLS support in chrome.sockets.tcp is currently broken, see * crbug.com/403076 . * * @param {function(remoting.XmppConnection.State):void} onStateChangedCallback * Callback to call on state change. * @param {function(Element):void} onIncomingStanzaCallback Callback to call to * handle incoming messages. * @constructor * @implements {base.Disposable} */ remoting.XmppConnection = function(onStateChangedCallback, onIncomingStanzaCallback) { /** @private */ this.server_ = ''; /** @private */ this.port_ = 0; /** @private */ this.onStateChangedCallback_ = onStateChangedCallback; /** @private */ this.onIncomingStanzaCallback_ = onIncomingStanzaCallback; /** @private */ this.socketId_ = -1; /** @private */ this.state_ = remoting.XmppConnection.State.NOT_CONNECTED; /** @private */ this.readPending_ = false; /** @private */ this.sendPending_ = false; /** @private */ this.startTlsPending_ = false; /** @type {Array.} @private */ this.sendQueue_ = []; /** @type {remoting.XmppLoginHandler} @private*/ this.loginHandler_ = null; /** @type {remoting.XmppStreamParser} @private*/ this.streamParser_ = null; /** @private */ this.jid_ = ''; /** @private */ this.error_ = remoting.Error.NONE; }; /** * @enum {number} XmppConnection states. Possible state transitions: * NOT_CONNECTED -> CONNECTING (connect() called). * CONNECTING -> HANDSHAKE (connected successfully). * HANDSHAKE -> CONNECTED (authenticated successfully). * CONNECTING -> FAILED (connection failed). * HANDSHAKE -> FAILED (authentication failed). * * -> CLOSED (dispose() called). */ remoting.XmppConnection.State = { NOT_CONNECTED: 0, CONNECTING: 1, HANDSHAKE: 2, CONNECTED: 3, FAILED: 4, CLOSED: 5 }; /** * @param {string} server * @param {string} username * @param {string} authToken */ remoting.XmppConnection.prototype.connect = function(server, username, authToken) { base.debug.assert(this.state_ == remoting.XmppConnection.State.NOT_CONNECTED); this.error_ = remoting.Error.NONE; var hostnameAndPort = server.split(':', 2); this.server_ = hostnameAndPort[0]; this.port_ = (hostnameAndPort.length == 2) ? parseInt(hostnameAndPort[1], 10) : 5222; // The server name is passed as to attribute in the . When connecting // to talk.google.com it affects the certificate the server will use for TLS: // talk.google.com uses gmail certificate when specified server is gmail.com // or googlemail.com and google.com cert otherwise. In the same time it // doesn't accept talk.google.com as target server. Here we use google.com // server name when authenticating to talk.google.com. This ensures that the // server will use google.com cert which will be accepted by the TLS // implementation in Chrome (TLS API doesn't allow specifying domain other // than the one that was passed to connect()). var xmppServer = this.server_; if (xmppServer == 'talk.google.com') xmppServer = 'google.com'; /** @type {remoting.XmppLoginHandler} */ this.loginHandler_ = new remoting.XmppLoginHandler(xmppServer, username, authToken, this.sendInternal_.bind(this), this.startTls_.bind(this), this.onHandshakeDone_.bind(this), this.onError_.bind(this)); chrome.socket.create("tcp", {}, this.onSocketCreated_.bind(this)); this.setState_(remoting.XmppConnection.State.CONNECTING); }; /** @param {string} message */ remoting.XmppConnection.prototype.sendMessage = function(message) { base.debug.assert(this.state_ == remoting.XmppConnection.State.CONNECTED); this.sendInternal_(message); }; /** @return {remoting.XmppConnection.State} Current state */ remoting.XmppConnection.prototype.getState = function() { return this.state_; }; /** @return {remoting.Error} Error when in FAILED state. */ remoting.XmppConnection.prototype.getError = function() { return this.error_; }; /** @return {string} Current JID when in CONNECTED state. */ remoting.XmppConnection.prototype.getJid = function() { return this.jid_; }; remoting.XmppConnection.prototype.dispose = function() { this.closeSocket_(); this.setState_(remoting.XmppConnection.State.CLOSED); }; /** * @param {chrome.socket.CreateInfo} createInfo * @private */ remoting.XmppConnection.prototype.onSocketCreated_ = function(createInfo) { // Check if connection was destroyed. if (this.state_ != remoting.XmppConnection.State.CONNECTING) { chrome.socket.destroy(createInfo.socketId); return; } this.socketId_ = createInfo.socketId; chrome.socket.connect(this.socketId_, this.server_, this.port_, this.onSocketConnected_.bind(this)); }; /** * @param {number} result * @private */ remoting.XmppConnection.prototype.onSocketConnected_ = function(result) { // Check if connection was destroyed. if (this.state_ != remoting.XmppConnection.State.CONNECTING) { return; } if (result != 0) { this.onError_(remoting.Error.NETWORK_FAILURE, 'Failed to connect to ' + this.server_ + ': ' + result); return; } this.setState_(remoting.XmppConnection.State.HANDSHAKE); this.tryRead_(); this.loginHandler_.start(); }; /** * @private */ remoting.XmppConnection.prototype.tryRead_ = function() { base.debug.assert(!this.readPending_); base.debug.assert(this.state_ == remoting.XmppConnection.State.HANDSHAKE || this.state_ == remoting.XmppConnection.State.CONNECTED); base.debug.assert(!this.startTlsPending_); this.readPending_ = true; chrome.socket.read(this.socketId_, this.onRead_.bind(this)); }; /** * @param {chrome.socket.ReadInfo} readInfo * @private */ remoting.XmppConnection.prototype.onRead_ = function(readInfo) { base.debug.assert(this.readPending_); this.readPending_ = false; // Check if the socket was closed while reading. if (this.state_ != remoting.XmppConnection.State.HANDSHAKE && this.state_ != remoting.XmppConnection.State.CONNECTED) { return; } if (readInfo.resultCode < 0) { this.onError_(remoting.Error.NETWORK_FAILURE, 'Failed to receive from XMPP socket: ' + readInfo.resultCode); return; } if (this.state_ == remoting.XmppConnection.State.HANDSHAKE) { this.loginHandler_.onDataReceived(readInfo.data); } else if (this.state_ == remoting.XmppConnection.State.CONNECTED) { this.streamParser_.appendData(readInfo.data); } if (!this.startTlsPending_) { this.tryRead_(); } }; /** * @param {string} text * @private */ remoting.XmppConnection.prototype.sendInternal_ = function(text) { this.sendQueue_.push(base.encodeUtf8(text)); this.doSend_(); }; /** * @private */ remoting.XmppConnection.prototype.doSend_ = function() { if (this.sendPending_ || this.sendQueue_.length == 0) { return; } var data = this.sendQueue_[0] this.sendPending_ = true; chrome.socket.write(this.socketId_, data, this.onWrite_.bind(this)); }; /** * @param {chrome.socket.WriteInfo} writeInfo * @private */ remoting.XmppConnection.prototype.onWrite_ = function(writeInfo) { base.debug.assert(this.sendPending_); this.sendPending_ = false; // Ignore write() result if the socket was closed. if (this.state_ != remoting.XmppConnection.State.HANDSHAKE && this.state_ != remoting.XmppConnection.State.CONNECTED) { return; } if (writeInfo.bytesWritten < 0) { this.onError_(remoting.Error.NETWORK_FAILURE, 'TCP write failed with error ' + writeInfo.bytesWritten); return; } base.debug.assert(this.sendQueue_.length > 0); var data = this.sendQueue_[0] base.debug.assert(writeInfo.bytesWritten <= data.byteLength); if (writeInfo.bytesWritten == data.byteLength) { this.sendQueue_.shift(); } else { this.sendQueue_[0] = data.slice(data.byteLength - writeInfo.bytesWritten); } this.doSend_(); }; /** * @private */ remoting.XmppConnection.prototype.startTls_ = function() { base.debug.assert(!this.readPending_); base.debug.assert(!this.startTlsPending_); this.startTlsPending_ = true; chrome.socket.secure( this.socketId_, {}, this.onTlsStarted_.bind(this)); } /** * @param {number} resultCode * @private */ remoting.XmppConnection.prototype.onTlsStarted_ = function(resultCode) { base.debug.assert(this.startTlsPending_); this.startTlsPending_ = false; if (resultCode < 0) { this.onError_(remoting.Error.NETWORK_FAILURE, 'Failed to start TLS: ' + resultCode); return; } this.tryRead_(); this.loginHandler_.onTlsStarted(); }; /** * @param {string} jid * @param {remoting.XmppStreamParser} streamParser * @private */ remoting.XmppConnection.prototype.onHandshakeDone_ = function(jid, streamParser) { this.jid_ = jid; this.streamParser_ = streamParser; this.streamParser_.setCallbacks(this.onIncomingStanzaCallback_, this.onParserError_.bind(this)); this.setState_(remoting.XmppConnection.State.CONNECTED); }; /** * @param {string} text * @private */ remoting.XmppConnection.prototype.onParserError_ = function(text) { this.onError_(remoting.Error.UNEXPECTED, text); } /** * @param {remoting.Error} error * @param {string} text * @private */ remoting.XmppConnection.prototype.onError_ = function(error, text) { console.error(text); this.error_ = error; this.closeSocket_(); this.setState_(remoting.XmppConnection.State.FAILED); }; /** * @private */ remoting.XmppConnection.prototype.closeSocket_ = function() { if (this.socketId_ != -1) { chrome.socket.destroy(this.socketId_); this.socketId_ = -1; } }; /** * @param {remoting.XmppConnection.State} newState * @private */ remoting.XmppConnection.prototype.setState_ = function(newState) { if (this.state_ != newState) { this.state_ = newState; this.onStateChangedCallback_(this.state_); } };