Upstream version 11.40.271.0
[platform/framework/web/crosswalk.git] / src / remoting / webapp / crd / js / xmpp_connection.js
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 };