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