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.
7 /** @suppress {duplicate} */
8 var remoting
= remoting
|| {};
11 * A connection to an XMPP server.
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
18 * @param {function(remoting.SignalStrategy.State):void} onStateChangedCallback
19 * Callback to call on state change.
21 * @implements {remoting.SignalStrategy}
23 remoting
.XmppConnection = function(onStateChangedCallback
) {
29 this.onStateChangedCallback_
= onStateChangedCallback
;
30 /** @type {?function(Element):void} @private */
31 this.onIncomingStanzaCallback_
= null;
35 this.state_
= remoting
.SignalStrategy
.State
.NOT_CONNECTED
;
37 this.readPending_
= false;
39 this.sendPending_
= false;
41 this.startTlsPending_
= false;
42 /** @type {Array.<ArrayBuffer>} @private */
44 /** @type {remoting.XmppLoginHandler} @private*/
45 this.loginHandler_
= null;
46 /** @type {remoting.XmppStreamParser} @private*/
47 this.streamParser_
= null;
51 this.error_
= remoting
.Error
.NONE
;
55 * @param {?function(Element):void} onIncomingStanzaCallback Callback to call on
58 remoting
.XmppConnection
.prototype.setIncomingStanzaCallback
=
59 function(onIncomingStanzaCallback
) {
60 this.onIncomingStanzaCallback_
= onIncomingStanzaCallback
;
64 * @param {string} server
65 * @param {string} username
66 * @param {string} authToken
68 remoting
.XmppConnection
.prototype.connect
=
69 function(server
, username
, authToken
) {
70 base
.debug
.assert(this.state_
== remoting
.SignalStrategy
.State
.NOT_CONNECTED
);
72 this.error_
= remoting
.Error
.NONE
;
73 var hostnameAndPort
= server
.split(':', 2);
74 this.server_
= hostnameAndPort
[0];
76 (hostnameAndPort
.length
== 2) ? parseInt(hostnameAndPort
[1], 10) : 5222;
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';
91 /** @type {remoting.XmppLoginHandler} */
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
);
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
);
108 /** @return {remoting.SignalStrategy.State} Current state */
109 remoting
.XmppConnection
.prototype.getState = function() {
113 /** @return {remoting.Error} Error when in FAILED state. */
114 remoting
.XmppConnection
.prototype.getError = function() {
118 /** @return {string} Current JID when in CONNECTED state. */
119 remoting
.XmppConnection
.prototype.getJid = function() {
123 remoting
.XmppConnection
.prototype.dispose = function() {
125 this.setState_(remoting
.SignalStrategy
.State
.CLOSED
);
129 * @param {chrome.socket.CreateInfo} createInfo
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
);
139 this.socketId_
= createInfo
.socketId
;
141 chrome
.socket
.connect(this.socketId_
,
144 this.onSocketConnected_
.bind(this));
148 * @param {number} result
151 remoting
.XmppConnection
.prototype.onSocketConnected_ = function(result
) {
152 // Check if connection was destroyed.
153 if (this.state_
!= remoting
.SignalStrategy
.State
.CONNECTING
) {
158 this.onError_(remoting
.Error
.NETWORK_FAILURE
,
159 'Failed to connect to ' + this.server_
+ ': ' + result
);
163 this.setState_(remoting
.SignalStrategy
.State
.HANDSHAKE
);
166 this.loginHandler_
.start();
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_
);
178 this.readPending_
= true;
179 chrome
.socket
.read(this.socketId_
, this.onRead_
.bind(this));
183 * @param {chrome.socket.ReadInfo} readInfo
186 remoting
.XmppConnection
.prototype.onRead_ = function(readInfo
) {
187 base
.debug
.assert(this.readPending_
);
188 this.readPending_
= false;
190 // Check if the socket was closed while reading.
191 if (this.state_
!= remoting
.SignalStrategy
.State
.HANDSHAKE
&&
192 this.state_
!= remoting
.SignalStrategy
.State
.CONNECTED
) {
197 if (readInfo
.resultCode
< 0) {
198 this.onError_(remoting
.Error
.NETWORK_FAILURE
,
199 'Failed to receive from XMPP socket: ' + readInfo
.resultCode
);
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
);
209 if (!this.startTlsPending_
) {
215 * @param {string} text
218 remoting
.XmppConnection
.prototype.sendInternal_ = function(text
) {
219 this.sendQueue_
.push(base
.encodeUtf8(text
));
226 remoting
.XmppConnection
.prototype.doSend_ = function() {
227 if (this.sendPending_
|| this.sendQueue_
.length
== 0) {
231 var data
= this.sendQueue_
[0]
232 this.sendPending_
= true;
233 chrome
.socket
.write(this.socketId_
, data
, this.onWrite_
.bind(this));
237 * @param {chrome.socket.WriteInfo} writeInfo
240 remoting
.XmppConnection
.prototype.onWrite_ = function(writeInfo
) {
241 base
.debug
.assert(this.sendPending_
);
242 this.sendPending_
= false;
244 // Ignore write() result if the socket was closed.
245 if (this.state_
!= remoting
.SignalStrategy
.State
.HANDSHAKE
&&
246 this.state_
!= remoting
.SignalStrategy
.State
.CONNECTED
) {
250 if (writeInfo
.bytesWritten
< 0) {
251 this.onError_(remoting
.Error
.NETWORK_FAILURE
,
252 'TCP write failed with error ' + writeInfo
.bytesWritten
);
256 base
.debug
.assert(this.sendQueue_
.length
> 0);
258 var data
= this.sendQueue_
[0]
259 base
.debug
.assert(writeInfo
.bytesWritten
<= data
.byteLength
);
260 if (writeInfo
.bytesWritten
== data
.byteLength
) {
261 this.sendQueue_
.shift();
263 this.sendQueue_
[0] = data
.slice(data
.byteLength
- writeInfo
.bytesWritten
);
272 remoting
.XmppConnection
.prototype.startTls_ = function() {
273 base
.debug
.assert(!this.readPending_
);
274 base
.debug
.assert(!this.startTlsPending_
);
276 this.startTlsPending_
= true;
277 chrome
.socket
.secure(
278 this.socketId_
, {}, this.onTlsStarted_
.bind(this));
282 * @param {number} resultCode
285 remoting
.XmppConnection
.prototype.onTlsStarted_ = function(resultCode
) {
286 base
.debug
.assert(this.startTlsPending_
);
287 this.startTlsPending_
= false;
289 if (resultCode
< 0) {
290 this.onError_(remoting
.Error
.NETWORK_FAILURE
,
291 'Failed to start TLS: ' + resultCode
);
296 this.loginHandler_
.onTlsStarted();
300 * @param {string} jid
301 * @param {remoting.XmppStreamParser} streamParser
304 remoting
.XmppConnection
.prototype.onHandshakeDone_
=
305 function(jid
, streamParser
) {
307 this.streamParser_
= streamParser
;
308 this.streamParser_
.setCallbacks(this.onIncomingStanza_
.bind(this),
309 this.onParserError_
.bind(this));
310 this.setState_(remoting
.SignalStrategy
.State
.CONNECTED
);
314 * @param {Element} stanza
317 remoting
.XmppConnection
.prototype.onIncomingStanza_ = function(stanza
) {
318 if (this.onIncomingStanzaCallback_
) {
319 this.onIncomingStanzaCallback_(stanza
);
324 * @param {string} text
327 remoting
.XmppConnection
.prototype.onParserError_ = function(text
) {
328 this.onError_(remoting
.Error
.UNEXPECTED
, text
);
332 * @param {remoting.Error} error
333 * @param {string} text
336 remoting
.XmppConnection
.prototype.onError_ = function(error
, text
) {
340 this.setState_(remoting
.SignalStrategy
.State
.FAILED
);
346 remoting
.XmppConnection
.prototype.closeSocket_ = function() {
347 if (this.socketId_
!= -1) {
348 chrome
.socket
.destroy(this.socketId_
);
354 * @param {remoting.SignalStrategy.State} newState
357 remoting
.XmppConnection
.prototype.setState_ = function(newState
) {
358 if (this.state_
!= newState
) {
359 this.state_
= newState
;
360 this.onStateChangedCallback_(this.state_
);