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