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