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