Home | History | Annotate | Download | only in webapp
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /** @suppress {duplicate} */
      8 var remoting = remoting || {};
      9 
     10 /**
     11  * A connection to an XMPP server.
     12  *
     13  * TODO(sergeyu): Chrome provides two APIs for TCP sockets: chrome.socket and
     14  * chrome.sockets.tcp . chrome.socket is deprecated but it's still used here
     15  * because TLS support in chrome.sockets.tcp is currently broken, see
     16  * crbug.com/403076 .
     17  *
     18  * @param {function(remoting.SignalStrategy.State):void} onStateChangedCallback
     19  *   Callback to call on state change.
     20  * @constructor
     21  * @implements {remoting.SignalStrategy}
     22  */
     23 remoting.XmppConnection = function(onStateChangedCallback) {
     24   /** @private */
     25   this.server_ = '';
     26   /** @private */
     27   this.port_ = 0;
     28   /** @private */
     29   this.onStateChangedCallback_ = onStateChangedCallback;
     30   /** @type {?function(Element):void} @private */
     31   this.onIncomingStanzaCallback_ = null;
     32   /** @private */
     33   this.socketId_ = -1;
     34   /** @private */
     35   this.state_ = remoting.SignalStrategy.State.NOT_CONNECTED;
     36   /** @private */
     37   this.readPending_ = false;
     38   /** @private */
     39   this.sendPending_ = false;
     40   /** @private */
     41   this.startTlsPending_ = false;
     42   /** @type {Array.<ArrayBuffer>} @private */
     43   this.sendQueue_ = [];
     44   /** @type {remoting.XmppLoginHandler} @private*/
     45   this.loginHandler_ = null;
     46   /** @type {remoting.XmppStreamParser} @private*/
     47   this.streamParser_ = null;
     48   /** @private */
     49   this.jid_ = '';
     50   /** @private */
     51   this.error_ = remoting.Error.NONE;
     52 };
     53 
     54 /**
     55  * @param {?function(Element):void} onIncomingStanzaCallback Callback to call on
     56  *     incoming messages.
     57  */
     58 remoting.XmppConnection.prototype.setIncomingStanzaCallback =
     59     function(onIncomingStanzaCallback) {
     60   this.onIncomingStanzaCallback_ = onIncomingStanzaCallback;
     61 };
     62 
     63 /**
     64  * @param {string} server
     65  * @param {string} username
     66  * @param {string} authToken
     67  */
     68 remoting.XmppConnection.prototype.connect =
     69     function(server, username, authToken) {
     70   base.debug.assert(this.state_ == remoting.SignalStrategy.State.NOT_CONNECTED);
     71 
     72   this.error_ = remoting.Error.NONE;
     73   var hostnameAndPort = server.split(':', 2);
     74   this.server_ = hostnameAndPort[0];
     75   this.port_ =
     76       (hostnameAndPort.length == 2) ? parseInt(hostnameAndPort[1], 10) : 5222;
     77 
     78   // The server name is passed as to attribute in the <stream>. When connecting
     79   // to talk.google.com it affects the certificate the server will use for TLS:
     80   // talk.google.com uses gmail certificate when specified server is gmail.com
     81   // or googlemail.com and google.com cert otherwise. In the same time it
     82   // doesn't accept talk.google.com as target server. Here we use google.com
     83   // server name when authenticating to talk.google.com. This ensures that the
     84   // server will use google.com cert which will be accepted by the TLS
     85   // implementation in Chrome (TLS API doesn't allow specifying domain other
     86   // than the one that was passed to connect()).
     87   var xmppServer = this.server_;
     88   if (xmppServer == 'talk.google.com')
     89     xmppServer = 'google.com';
     90 
     91   /** @type {remoting.XmppLoginHandler} */
     92   this.loginHandler_ =
     93       new remoting.XmppLoginHandler(xmppServer, username, authToken,
     94                                     this.sendInternal_.bind(this),
     95                                     this.startTls_.bind(this),
     96                                     this.onHandshakeDone_.bind(this),
     97                                     this.onError_.bind(this));
     98   chrome.socket.create("tcp", {}, this.onSocketCreated_.bind(this));
     99   this.setState_(remoting.SignalStrategy.State.CONNECTING);
    100 };
    101 
    102 /** @param {string} message */
    103 remoting.XmppConnection.prototype.sendMessage = function(message) {
    104   base.debug.assert(this.state_ == remoting.SignalStrategy.State.CONNECTED);
    105   this.sendInternal_(message);
    106 };
    107 
    108 /** @return {remoting.SignalStrategy.State} Current state */
    109 remoting.XmppConnection.prototype.getState = function() {
    110   return this.state_;
    111 };
    112 
    113 /** @return {remoting.Error} Error when in FAILED state. */
    114 remoting.XmppConnection.prototype.getError = function() {
    115   return this.error_;
    116 };
    117 
    118 /** @return {string} Current JID when in CONNECTED state. */
    119 remoting.XmppConnection.prototype.getJid = function() {
    120   return this.jid_;
    121 };
    122 
    123 remoting.XmppConnection.prototype.dispose = function() {
    124   this.closeSocket_();
    125   this.setState_(remoting.SignalStrategy.State.CLOSED);
    126 };
    127 
    128 /**
    129  * @param {chrome.socket.CreateInfo} createInfo
    130  * @private
    131  */
    132 remoting.XmppConnection.prototype.onSocketCreated_ = function(createInfo) {
    133   // Check if connection was destroyed.
    134   if (this.state_ != remoting.SignalStrategy.State.CONNECTING) {
    135     chrome.socket.destroy(createInfo.socketId);
    136     return;
    137   }
    138 
    139   this.socketId_ = createInfo.socketId;
    140 
    141   chrome.socket.connect(this.socketId_,
    142                         this.server_,
    143                         this.port_,
    144                         this.onSocketConnected_.bind(this));
    145 };
    146 
    147 /**
    148  * @param {number} result
    149  * @private
    150  */
    151 remoting.XmppConnection.prototype.onSocketConnected_ = function(result) {
    152   // Check if connection was destroyed.
    153   if (this.state_ != remoting.SignalStrategy.State.CONNECTING) {
    154     return;
    155   }
    156 
    157   if (result != 0) {
    158     this.onError_(remoting.Error.NETWORK_FAILURE,
    159                   'Failed to connect to ' + this.server_ + ': ' +  result);
    160     return;
    161   }
    162 
    163   this.setState_(remoting.SignalStrategy.State.HANDSHAKE);
    164 
    165   this.tryRead_();
    166   this.loginHandler_.start();
    167 };
    168 
    169 /**
    170  * @private
    171  */
    172 remoting.XmppConnection.prototype.tryRead_ = function() {
    173   base.debug.assert(!this.readPending_);
    174   base.debug.assert(this.state_ == remoting.SignalStrategy.State.HANDSHAKE ||
    175                     this.state_ == remoting.SignalStrategy.State.CONNECTED);
    176   base.debug.assert(!this.startTlsPending_);
    177 
    178   this.readPending_ = true;
    179   chrome.socket.read(this.socketId_, this.onRead_.bind(this));
    180 };
    181 
    182 /**
    183  * @param {chrome.socket.ReadInfo} readInfo
    184  * @private
    185  */
    186 remoting.XmppConnection.prototype.onRead_ = function(readInfo) {
    187   base.debug.assert(this.readPending_);
    188   this.readPending_ = false;
    189 
    190   // Check if the socket was closed while reading.
    191   if (this.state_ != remoting.SignalStrategy.State.HANDSHAKE &&
    192       this.state_ != remoting.SignalStrategy.State.CONNECTED) {
    193     return;
    194   }
    195 
    196 
    197   if (readInfo.resultCode < 0) {
    198     this.onError_(remoting.Error.NETWORK_FAILURE,
    199                   'Failed to receive from XMPP socket: ' + readInfo.resultCode);
    200     return;
    201   }
    202 
    203   if (this.state_ == remoting.SignalStrategy.State.HANDSHAKE) {
    204     this.loginHandler_.onDataReceived(readInfo.data);
    205   } else if (this.state_ == remoting.SignalStrategy.State.CONNECTED) {
    206     this.streamParser_.appendData(readInfo.data);
    207   }
    208 
    209   if (!this.startTlsPending_) {
    210     this.tryRead_();
    211   }
    212 };
    213 
    214 /**
    215  * @param {string} text
    216  * @private
    217  */
    218 remoting.XmppConnection.prototype.sendInternal_ = function(text) {
    219   this.sendQueue_.push(base.encodeUtf8(text));
    220   this.doSend_();
    221 };
    222 
    223 /**
    224  * @private
    225  */
    226 remoting.XmppConnection.prototype.doSend_ = function() {
    227   if (this.sendPending_ || this.sendQueue_.length == 0) {
    228     return;
    229   }
    230 
    231   var data = this.sendQueue_[0]
    232   this.sendPending_ = true;
    233   chrome.socket.write(this.socketId_, data, this.onWrite_.bind(this));
    234 };
    235 
    236 /**
    237  * @param {chrome.socket.WriteInfo} writeInfo
    238  * @private
    239  */
    240 remoting.XmppConnection.prototype.onWrite_ = function(writeInfo) {
    241   base.debug.assert(this.sendPending_);
    242   this.sendPending_ = false;
    243 
    244   // Ignore write() result if the socket was closed.
    245   if (this.state_ != remoting.SignalStrategy.State.HANDSHAKE &&
    246       this.state_ != remoting.SignalStrategy.State.CONNECTED) {
    247     return;
    248   }
    249 
    250   if (writeInfo.bytesWritten < 0) {
    251     this.onError_(remoting.Error.NETWORK_FAILURE,
    252                   'TCP write failed with error ' + writeInfo.bytesWritten);
    253     return;
    254   }
    255 
    256   base.debug.assert(this.sendQueue_.length > 0);
    257 
    258   var data = this.sendQueue_[0]
    259   base.debug.assert(writeInfo.bytesWritten <= data.byteLength);
    260   if (writeInfo.bytesWritten == data.byteLength) {
    261     this.sendQueue_.shift();
    262   } else {
    263     this.sendQueue_[0] = data.slice(data.byteLength - writeInfo.bytesWritten);
    264   }
    265 
    266   this.doSend_();
    267 };
    268 
    269 /**
    270  * @private
    271  */
    272 remoting.XmppConnection.prototype.startTls_ = function() {
    273   base.debug.assert(!this.readPending_);
    274   base.debug.assert(!this.startTlsPending_);
    275 
    276   this.startTlsPending_ = true;
    277   chrome.socket.secure(
    278       this.socketId_, {}, this.onTlsStarted_.bind(this));
    279 }
    280 
    281 /**
    282  * @param {number} resultCode
    283  * @private
    284  */
    285 remoting.XmppConnection.prototype.onTlsStarted_ = function(resultCode) {
    286   base.debug.assert(this.startTlsPending_);
    287   this.startTlsPending_ = false;
    288 
    289   if (resultCode < 0) {
    290     this.onError_(remoting.Error.NETWORK_FAILURE,
    291                   'Failed to start TLS: ' + resultCode);
    292     return;
    293   }
    294 
    295   this.tryRead_();
    296   this.loginHandler_.onTlsStarted();
    297 };
    298 
    299 /**
    300  * @param {string} jid
    301  * @param {remoting.XmppStreamParser} streamParser
    302  * @private
    303  */
    304 remoting.XmppConnection.prototype.onHandshakeDone_ =
    305     function(jid, streamParser) {
    306   this.jid_ = jid;
    307   this.streamParser_ = streamParser;
    308   this.streamParser_.setCallbacks(this.onIncomingStanza_.bind(this),
    309                                   this.onParserError_.bind(this));
    310   this.setState_(remoting.SignalStrategy.State.CONNECTED);
    311 };
    312 
    313 /**
    314  * @param {Element} stanza
    315  * @private
    316  */
    317 remoting.XmppConnection.prototype.onIncomingStanza_ = function(stanza) {
    318   if (this.onIncomingStanzaCallback_) {
    319     this.onIncomingStanzaCallback_(stanza);
    320   }
    321 };
    322 
    323 /**
    324  * @param {string} text
    325  * @private
    326  */
    327 remoting.XmppConnection.prototype.onParserError_ = function(text) {
    328   this.onError_(remoting.Error.UNEXPECTED, text);
    329 }
    330 
    331 /**
    332  * @param {remoting.Error} error
    333  * @param {string} text
    334  * @private
    335  */
    336 remoting.XmppConnection.prototype.onError_ = function(error, text) {
    337   console.error(text);
    338   this.error_ = error;
    339   this.closeSocket_();
    340   this.setState_(remoting.SignalStrategy.State.FAILED);
    341 };
    342 
    343 /**
    344  * @private
    345  */
    346 remoting.XmppConnection.prototype.closeSocket_ = function() {
    347   if (this.socketId_ != -1) {
    348     chrome.socket.destroy(this.socketId_);
    349     this.socketId_ = -1;
    350   }
    351 };
    352 
    353 /**
    354  * @param {remoting.SignalStrategy.State} newState
    355  * @private
    356  */
    357 remoting.XmppConnection.prototype.setState_ = function(newState) {
    358   if (this.state_ != newState) {
    359     this.state_ = newState;
    360     this.onStateChangedCallback_(this.state_);
    361   }
    362 };
    363