• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} */
8var 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 */
23remoting.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 */
58remoting.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 */
68remoting.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 */
103remoting.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 */
109remoting.XmppConnection.prototype.getState = function() {
110  return this.state_;
111};
112
113/** @return {remoting.Error} Error when in FAILED state. */
114remoting.XmppConnection.prototype.getError = function() {
115  return this.error_;
116};
117
118/** @return {string} Current JID when in CONNECTED state. */
119remoting.XmppConnection.prototype.getJid = function() {
120  return this.jid_;
121};
122
123remoting.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 */
132remoting.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 */
151remoting.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 */
172remoting.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 */
186remoting.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 */
218remoting.XmppConnection.prototype.sendInternal_ = function(text) {
219  this.sendQueue_.push(base.encodeUtf8(text));
220  this.doSend_();
221};
222
223/**
224 * @private
225 */
226remoting.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 */
240remoting.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 */
272remoting.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 */
285remoting.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 */
304remoting.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 */
317remoting.XmppConnection.prototype.onIncomingStanza_ = function(stanza) {
318  if (this.onIncomingStanzaCallback_) {
319    this.onIncomingStanzaCallback_(stanza);
320  }
321};
322
323/**
324 * @param {string} text
325 * @private
326 */
327remoting.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 */
336remoting.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 */
346remoting.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 */
357remoting.XmppConnection.prototype.setState_ = function(newState) {
358  if (this.state_ != newState) {
359    this.state_ = newState;
360    this.onStateChangedCallback_(this.state_);
361  }
362};
363