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
19 * @implements {remoting.SignalStrategy}
21 remoting
.XmppConnection = function() {
26 /** @type {?function(remoting.SignalStrategy.State):void} @private */
27 this.onStateChangedCallback_
= null;
28 /** @type {?function(Element):void} @private */
29 this.onIncomingStanzaCallback_
= null;
33 this.state_
= remoting
.SignalStrategy
.State
.NOT_CONNECTED
;
35 this.readPending_
= false;
37 this.sendPending_
= false;
39 this.startTlsPending_
= false;
40 /** @type {Array<ArrayBuffer>} @private */
42 /** @type {remoting.XmppLoginHandler} @private*/
43 this.loginHandler_
= null;
44 /** @type {remoting.XmppStreamParser} @private*/
45 this.streamParser_
= null;
49 this.error_
= remoting
.Error
.NONE
;
53 * @param {function(remoting.SignalStrategy.State):void} onStateChangedCallback
55 remoting
.XmppConnection
.prototype.setStateChangedCallback = function(
56 onStateChangedCallback
) {
57 this.onStateChangedCallback_
= onStateChangedCallback
;
61 * @param {?function(Element):void} onIncomingStanzaCallback Callback to call on
64 remoting
.XmppConnection
.prototype.setIncomingStanzaCallback
=
65 function(onIncomingStanzaCallback
) {
66 this.onIncomingStanzaCallback_
= onIncomingStanzaCallback
;
70 * @param {string} server
71 * @param {string} username
72 * @param {string} authToken
74 remoting
.XmppConnection
.prototype.connect
=
75 function(server
, username
, authToken
) {
76 base
.debug
.assert(this.state_
== remoting
.SignalStrategy
.State
.NOT_CONNECTED
);
77 base
.debug
.assert(this.onStateChangedCallback_
!= null);
79 this.error_
= remoting
.Error
.NONE
;
80 var hostnameAndPort
= server
.split(':', 2);
81 this.server_
= hostnameAndPort
[0];
83 (hostnameAndPort
.length
== 2) ? parseInt(hostnameAndPort
[1], 10) : 5222;
85 // The server name is passed as to attribute in the <stream>. When connecting
86 // to talk.google.com it affects the certificate the server will use for TLS:
87 // talk.google.com uses gmail certificate when specified server is gmail.com
88 // or googlemail.com and google.com cert otherwise. In the same time it
89 // doesn't accept talk.google.com as target server. Here we use google.com
90 // server name when authenticating to talk.google.com. This ensures that the
91 // server will use google.com cert which will be accepted by the TLS
92 // implementation in Chrome (TLS API doesn't allow specifying domain other
93 // than the one that was passed to connect()).
94 var xmppServer
= this.server_
;
95 if (xmppServer
== 'talk.google.com')
96 xmppServer
= 'google.com';
98 // <starttls> handshake before starting TLS is not needed when connecting on
100 var needHandshakeBeforeTls
= this.port_
!= 443;
102 /** @type {remoting.XmppLoginHandler} */
103 this.loginHandler_
= new remoting
.XmppLoginHandler(
104 xmppServer
, username
, authToken
, needHandshakeBeforeTls
,
105 this.sendString_
.bind(this), this.startTls_
.bind(this),
106 this.onHandshakeDone_
.bind(this), this.onError_
.bind(this));
107 chrome
.socket
.create("tcp", {}, this.onSocketCreated_
.bind(this));
108 this.setState_(remoting
.SignalStrategy
.State
.CONNECTING
);
111 /** @param {string} message */
112 remoting
.XmppConnection
.prototype.sendMessage = function(message
) {
113 base
.debug
.assert(this.state_
== remoting
.SignalStrategy
.State
.CONNECTED
);
114 this.sendString_(message
);
118 * @param {remoting.LogToServer} logToServer The LogToServer instance for the
121 remoting
.XmppConnection
.prototype.sendConnectionSetupResults
=
122 function(logToServer
) {
125 /** @return {remoting.SignalStrategy.State} Current state */
126 remoting
.XmppConnection
.prototype.getState = function() {
130 /** @return {remoting.Error} Error when in FAILED state. */
131 remoting
.XmppConnection
.prototype.getError = function() {
135 /** @return {string} Current JID when in CONNECTED state. */
136 remoting
.XmppConnection
.prototype.getJid = function() {
140 /** @return {remoting.SignalStrategy.Type} The signal strategy type. */
141 remoting
.XmppConnection
.prototype.getType = function() {
142 return remoting
.SignalStrategy
.Type
.XMPP
;
145 remoting
.XmppConnection
.prototype.dispose = function() {
147 this.setState_(remoting
.SignalStrategy
.State
.CLOSED
);
151 * @param {chrome.socket.CreateInfo} createInfo
154 remoting
.XmppConnection
.prototype.onSocketCreated_ = function(createInfo
) {
155 // Check if connection was destroyed.
156 if (this.state_
!= remoting
.SignalStrategy
.State
.CONNECTING
) {
157 chrome
.socket
.destroy(createInfo
.socketId
);
161 this.socketId_
= createInfo
.socketId
;
163 chrome
.socket
.connect(this.socketId_
,
166 this.onSocketConnected_
.bind(this));
170 * @param {number} result
173 remoting
.XmppConnection
.prototype.onSocketConnected_ = function(result
) {
174 // Check if connection was destroyed.
175 if (this.state_
!= remoting
.SignalStrategy
.State
.CONNECTING
) {
180 this.onError_(remoting
.Error
.NETWORK_FAILURE
,
181 'Failed to connect to ' + this.server_
+ ': ' + result
);
185 this.setState_(remoting
.SignalStrategy
.State
.HANDSHAKE
);
186 this.loginHandler_
.start();
188 if (!this.startTlsPending_
) {
196 remoting
.XmppConnection
.prototype.tryRead_ = function() {
197 base
.debug
.assert(!this.readPending_
);
198 base
.debug
.assert(this.state_
== remoting
.SignalStrategy
.State
.HANDSHAKE
||
199 this.state_
== remoting
.SignalStrategy
.State
.CONNECTED
);
200 base
.debug
.assert(!this.startTlsPending_
);
202 this.readPending_
= true;
203 chrome
.socket
.read(this.socketId_
, this.onRead_
.bind(this));
207 * @param {chrome.socket.ReadInfo} readInfo
210 remoting
.XmppConnection
.prototype.onRead_ = function(readInfo
) {
211 base
.debug
.assert(this.readPending_
);
212 this.readPending_
= false;
214 // Check if the socket was closed while reading.
215 if (this.state_
!= remoting
.SignalStrategy
.State
.HANDSHAKE
&&
216 this.state_
!= remoting
.SignalStrategy
.State
.CONNECTED
) {
220 if (readInfo
.resultCode
< 0) {
221 this.onError_(remoting
.Error
.NETWORK_FAILURE
,
222 'Failed to receive from XMPP socket: ' + readInfo
.resultCode
);
226 if (this.state_
== remoting
.SignalStrategy
.State
.HANDSHAKE
) {
227 this.loginHandler_
.onDataReceived(readInfo
.data
);
228 } else if (this.state_
== remoting
.SignalStrategy
.State
.CONNECTED
) {
229 this.streamParser_
.appendData(readInfo
.data
);
232 if (!this.startTlsPending_
&&
233 this.state_
!= remoting
.SignalStrategy
.State
.CLOSED
) {
239 * @param {string} text
242 remoting
.XmppConnection
.prototype.sendString_ = function(text
) {
243 this.sendBuffer_(base
.encodeUtf8(text
));
247 * @param {ArrayBuffer} data
250 remoting
.XmppConnection
.prototype.sendBuffer_ = function(data
) {
251 this.sendQueue_
.push(data
);
252 this.flushSendQueue_();
258 remoting
.XmppConnection
.prototype.flushSendQueue_ = function() {
259 if (this.sendPending_
|| this.sendQueue_
.length
== 0) {
263 var data
= this.sendQueue_
[0]
264 this.sendPending_
= true;
265 chrome
.socket
.write(this.socketId_
, data
, this.onWrite_
.bind(this));
269 * @param {chrome.socket.WriteInfo} writeInfo
272 remoting
.XmppConnection
.prototype.onWrite_ = function(writeInfo
) {
273 base
.debug
.assert(this.sendPending_
);
274 this.sendPending_
= false;
276 // Ignore write() result if the socket was closed.
277 if (this.state_
!= remoting
.SignalStrategy
.State
.HANDSHAKE
&&
278 this.state_
!= remoting
.SignalStrategy
.State
.CONNECTED
) {
282 if (writeInfo
.bytesWritten
< 0) {
283 this.onError_(remoting
.Error
.NETWORK_FAILURE
,
284 'TCP write failed with error ' + writeInfo
.bytesWritten
);
288 base
.debug
.assert(this.sendQueue_
.length
> 0);
290 var data
= this.sendQueue_
[0]
291 base
.debug
.assert(writeInfo
.bytesWritten
<= data
.byteLength
);
292 if (writeInfo
.bytesWritten
== data
.byteLength
) {
293 this.sendQueue_
.shift();
295 this.sendQueue_
[0] = data
.slice(data
.byteLength
- writeInfo
.bytesWritten
);
298 this.flushSendQueue_();
304 remoting
.XmppConnection
.prototype.startTls_ = function() {
305 base
.debug
.assert(!this.readPending_
);
306 base
.debug
.assert(!this.startTlsPending_
);
308 this.startTlsPending_
= true;
309 chrome
.socket
.secure(
310 this.socketId_
, {}, this.onTlsStarted_
.bind(this));
314 * @param {number} resultCode
317 remoting
.XmppConnection
.prototype.onTlsStarted_ = function(resultCode
) {
318 base
.debug
.assert(this.startTlsPending_
);
319 this.startTlsPending_
= false;
321 if (resultCode
< 0) {
322 this.onError_(remoting
.Error
.NETWORK_FAILURE
,
323 'Failed to start TLS: ' + resultCode
);
328 this.loginHandler_
.onTlsStarted();
332 * @param {string} jid
333 * @param {remoting.XmppStreamParser} streamParser
336 remoting
.XmppConnection
.prototype.onHandshakeDone_
=
337 function(jid
, streamParser
) {
339 this.streamParser_
= streamParser
;
340 this.streamParser_
.setCallbacks(this.onIncomingStanza_
.bind(this),
341 this.onParserError_
.bind(this));
342 this.setState_(remoting
.SignalStrategy
.State
.CONNECTED
);
346 * @param {Element} stanza
349 remoting
.XmppConnection
.prototype.onIncomingStanza_ = function(stanza
) {
350 if (this.onIncomingStanzaCallback_
) {
351 this.onIncomingStanzaCallback_(stanza
);
356 * @param {string} text
359 remoting
.XmppConnection
.prototype.onParserError_ = function(text
) {
360 this.onError_(remoting
.Error
.UNEXPECTED
, text
);
364 * @param {remoting.Error} error
365 * @param {string} text
368 remoting
.XmppConnection
.prototype.onError_ = function(error
, text
) {
372 this.setState_(remoting
.SignalStrategy
.State
.FAILED
);
378 remoting
.XmppConnection
.prototype.closeSocket_ = function() {
379 if (this.socketId_
!= -1) {
380 chrome
.socket
.destroy(this.socketId_
);
386 * @param {remoting.SignalStrategy.State} newState
389 remoting
.XmppConnection
.prototype.setState_ = function(newState
) {
390 if (this.state_
!= newState
) {
391 this.state_
= newState
;
392 this.onStateChangedCallback_(this.state_
);