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 * XmppLoginHandler handles authentication handshake for XmppConnection. It
12 * receives incoming data using onDataReceived(), calls |sendMessageCallback|
13 * to send outgoing messages and calls |onHandshakeDoneCallback| after
14 * authentication is finished successfully or |onErrorCallback| on error.
15 *
16 * See RFC3920 for description of XMPP and authentication handshake.
17 *
18 * @param {string} server Domain name of the server we are connecting to.
19 * @param {string} username Username.
20 * @param {string} authToken OAuth2 token.
21 * @param {function(string):void} sendMessageCallback Callback to call to send
22 *     a message.
23 * @param {function():void} startTlsCallback Callback to call to start TLS on
24 *     the underlying socket.
25 * @param {function(string, remoting.XmppStreamParser):void}
26 *     onHandshakeDoneCallback Callback to call after authentication is
27 *     completed successfully
28 * @param {function(remoting.Error, string):void} onErrorCallback Callback to
29 *     call on error. Can be called at any point during lifetime of connection.
30 * @constructor
31 */
32remoting.XmppLoginHandler = function(server,
33                                     username,
34                                     authToken,
35                                     sendMessageCallback,
36                                     startTlsCallback,
37                                     onHandshakeDoneCallback,
38                                     onErrorCallback) {
39  /** @private */
40  this.server_ = server;
41  /** @private */
42  this.username_ = username;
43  /** @private */
44  this.authToken_ = authToken;
45  /** @private */
46  this.sendMessageCallback_ = sendMessageCallback;
47  /** @private */
48  this.startTlsCallback_ = startTlsCallback;
49  /** @private */
50  this.onHandshakeDoneCallback_ = onHandshakeDoneCallback;
51  /** @private */
52  this.onErrorCallback_ = onErrorCallback;
53
54  /** @private */
55  this.state_ = remoting.XmppLoginHandler.State.INIT;
56  /** @private */
57  this.jid_ = '';
58
59  /** @type {remoting.XmppStreamParser} @private */
60  this.streamParser_ = null;
61}
62
63/**
64 * States the handshake goes through. States are iterated from INIT to DONE
65 * sequentially, except for ERROR state which may be accepted at any point.
66 *
67 * Following messages are sent/received in each state:
68 *    INIT
69 *      client -> server: Stream header
70 *      client -> server: <starttls>
71 *    WAIT_STREAM_HEADER
72 *      client <- server: Stream header with list of supported features which
73 *          should include starttls.
74 *    WAIT_STARTTLS_RESPONSE
75 *      client <- server: <proceed>
76 *    STARTING_TLS
77 *      TLS handshake
78 *      client -> server: Stream header
79 *      client -> server: <auth> message with the OAuth2 token.
80 *    WAIT_STREAM_HEADER_AFTER_TLS
81 *      client <- server: Stream header with list of supported authentication
82 *          methods which is expected to include X-OAUTH2
83 *    WAIT_AUTH_RESULT
84 *      client <- server: <success> or <failure>
85 *      client -> server: Stream header
86 *      client -> server: <bind>
87 *      client -> server: <iq><session/></iq> to start the session
88 *    WAIT_STREAM_HEADER_AFTER_AUTH
89 *      client <- server: Stream header with list of features that should
90 *         include <bind>.
91 *    WAIT_BIND_RESULT
92 *      client <- server: <bind> result with JID.
93 *    WAIT_SESSION_IQ_RESULT
94 *      client <- server: result for <iq><session/></iq>
95 *    DONE
96 *
97 * @enum {number}
98 */
99remoting.XmppLoginHandler.State = {
100  INIT: 0,
101  WAIT_STREAM_HEADER: 1,
102  WAIT_STARTTLS_RESPONSE: 2,
103  STARTING_TLS: 3,
104  WAIT_STREAM_HEADER_AFTER_TLS: 4,
105  WAIT_AUTH_RESULT: 5,
106  WAIT_STREAM_HEADER_AFTER_AUTH: 6,
107  WAIT_BIND_RESULT: 7,
108  WAIT_SESSION_IQ_RESULT: 8,
109  DONE: 9,
110  ERROR: 10
111};
112
113remoting.XmppLoginHandler.prototype.start = function() {
114  this.state_ = remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER;
115  this.startStream_('<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>');
116}
117
118/** @param {ArrayBuffer} data */
119remoting.XmppLoginHandler.prototype.onDataReceived = function(data) {
120  base.debug.assert(this.state_ != remoting.XmppLoginHandler.State.INIT &&
121                    this.state_ != remoting.XmppLoginHandler.State.DONE &&
122                    this.state_ != remoting.XmppLoginHandler.State.ERROR);
123
124  this.streamParser_.appendData(data);
125}
126
127/**
128 * @param {Element} stanza
129 * @private
130 */
131remoting.XmppLoginHandler.prototype.onStanza_ = function(stanza) {
132  switch (this.state_) {
133    case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER:
134      if (stanza.querySelector('features>starttls')) {
135        this.state_ = remoting.XmppLoginHandler.State.WAIT_STARTTLS_RESPONSE;
136      } else {
137        this.onError_(remoting.Error.UNEXPECTED, "Server doesn't support TLS.");
138      }
139      break;
140
141    case remoting.XmppLoginHandler.State.WAIT_STARTTLS_RESPONSE:
142      if (stanza.localName == "proceed") {
143        this.state_ = remoting.XmppLoginHandler.State.STARTING_TLS;
144        this.startTlsCallback_();
145      } else {
146        this.onError_(remoting.Error.UNEXPECTED,
147                      "Failed to start TLS: " +
148                          (new XMLSerializer().serializeToString(stanza)));
149      }
150      break;
151
152    case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_TLS:
153      var mechanisms = Array.prototype.map.call(
154          stanza.querySelectorAll('features>mechanisms>mechanism'),
155          /** @param {Element} m */
156          function(m) { return m.textContent; });
157      if (mechanisms.indexOf("X-OAUTH2")) {
158        this.onError_(remoting.Error.UNEXPECTED,
159                      "OAuth2 is not supported by the server.");
160        return;
161      }
162
163      this.state_ = remoting.XmppLoginHandler.State.WAIT_AUTH_RESULT;
164
165      break;
166
167    case remoting.XmppLoginHandler.State.WAIT_AUTH_RESULT:
168      if (stanza.localName == 'success') {
169        this.state_ =
170            remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_AUTH;
171        this.startStream_(
172            '<iq type="set" id="0">' +
173              '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">' +
174                '<resource>chromoting</resource>'+
175              '</bind>' +
176            '</iq>' +
177            '<iq type="set" id="1">' +
178              '<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>' +
179            '</iq>');
180      } else {
181        this.onError_(remoting.Error.AUTHENTICATION_FAILED,
182                      'Failed to authenticate: ' +
183                          (new XMLSerializer().serializeToString(stanza)));
184      }
185      break;
186
187    case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_AUTH:
188      if (stanza.querySelector('features>bind')) {
189        this.state_ = remoting.XmppLoginHandler.State.WAIT_BIND_RESULT;
190      } else {
191        this.onError_(remoting.Error.UNEXPECTED,
192                      "Server doesn't support bind after authentication.");
193      }
194      break;
195
196    case remoting.XmppLoginHandler.State.WAIT_BIND_RESULT:
197      var jidElement = stanza.querySelector('iq>bind>jid');
198      if (stanza.getAttribute('id') != '0' ||
199          stanza.getAttribute('type') != 'result' || !jidElement) {
200        this.onError_(remoting.Error.UNEXPECTED,
201                      'Received unexpected response to bind: ' +
202                          (new XMLSerializer().serializeToString(stanza)));
203        return;
204      }
205      this.jid_ = jidElement.textContent;
206      this.state_ = remoting.XmppLoginHandler.State.WAIT_SESSION_IQ_RESULT;
207      break;
208
209    case remoting.XmppLoginHandler.State.WAIT_SESSION_IQ_RESULT:
210      if (stanza.getAttribute('id') != '1' ||
211          stanza.getAttribute('type') != 'result') {
212        this.onError_(remoting.Error.UNEXPECTED,
213                      'Failed to start session: ' +
214                          (new XMLSerializer().serializeToString(stanza)));
215        return;
216      }
217      this.state_ = remoting.XmppLoginHandler.State.DONE;
218      this.onHandshakeDoneCallback_(this.jid_, this.streamParser_);
219      break;
220
221    default:
222      base.debug.assert(false);
223      break;
224  }
225}
226
227remoting.XmppLoginHandler.prototype.onTlsStarted = function() {
228  base.debug.assert(this.state_ ==
229                    remoting.XmppLoginHandler.State.STARTING_TLS);
230  this.state_ = remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_TLS;
231  var cookie = window.btoa("\0" + this.username_ + "\0" + this.authToken_);
232
233  this.startStream_(
234      '<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" ' +
235             'mechanism="X-OAUTH2" auth:service="oauth2" ' +
236             'auth:allow-generated-jid="true" ' +
237             'auth:client-uses-full-bind-result="true" ' +
238             'auth:allow-non-google-login="true" ' +
239             'xmlns:auth="http://www.google.com/talk/protocol/auth">' +
240        cookie +
241      '</auth>');
242};
243
244/**
245 * @param {string} text
246 * @private
247 */
248remoting.XmppLoginHandler.prototype.onParserError_ = function(text) {
249  this.onError_(remoting.Error.UNEXPECTED, text);
250}
251
252/**
253 * @param {string} firstMessage Message to send after stream header.
254 * @private
255 */
256remoting.XmppLoginHandler.prototype.startStream_ = function(firstMessage) {
257  this.sendMessageCallback_('<stream:stream to="' + this.server_ +
258                            '" version="1.0" xmlns="jabber:client" ' +
259                            'xmlns:stream="http://etherx.jabber.org/streams">' +
260                            firstMessage);
261  this.streamParser_ = new remoting.XmppStreamParser();
262  this.streamParser_.setCallbacks(this.onStanza_.bind(this),
263                                  this.onParserError_.bind(this));
264}
265
266/**
267 * @param {remoting.Error} error
268 * @param {string} text
269 * @private
270 */
271remoting.XmppLoginHandler.prototype.onError_ = function(error, text) {
272  if (this.state_ != remoting.XmppLoginHandler.State.ERROR) {
273    this.onErrorCallback_(error, text);
274    this.state_ = remoting.XmppLoginHandler.State.ERROR;
275  } else {
276    console.error(text);
277  }
278}
279