| Index: remoting/webapp/xmpp_connection.js
|
| diff --git a/remoting/webapp/xmpp_connection.js b/remoting/webapp/xmpp_connection.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..5be329554b1982a6b09122ab944d17f7af3fbea3
|
| --- /dev/null
|
| +++ b/remoting/webapp/xmpp_connection.js
|
| @@ -0,0 +1,363 @@
|
| +// 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.<ArrayBuffer>} @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 <stream>. 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_);
|
| + }
|
| +};
|
|
|