Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / xmpp_login_handler.js
blob74be464997962c2e60a4423501e0d8c88d4c80b3
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   /** @type {remoting.XmppStreamParser} @private */
65   this.streamParser_ = null;
68 /**
69  * States the handshake goes through. States are iterated from INIT to DONE
70  * sequentially, except for ERROR state which may be accepted at any point.
71  *
72  * Following messages are sent/received in each state:
73  *    INIT
74  *      client -> server: Stream header
75  *      client -> server: <starttls>
76  *    WAIT_STREAM_HEADER
77  *      client <- server: Stream header with list of supported features which
78  *          should include starttls.
79  *    WAIT_STARTTLS_RESPONSE
80  *      client <- server: <proceed>
81  *    STARTING_TLS
82  *      TLS handshake
83  *      client -> server: Stream header
84  *      client -> server: <auth> message with the OAuth2 token.
85  *    WAIT_STREAM_HEADER_AFTER_TLS
86  *      client <- server: Stream header with list of supported authentication
87  *          methods which is expected to include X-OAUTH2
88  *    WAIT_AUTH_RESULT
89  *      client <- server: <success> or <failure>
90  *      client -> server: Stream header
91  *      client -> server: <bind>
92  *      client -> server: <iq><session/></iq> to start the session
93  *    WAIT_STREAM_HEADER_AFTER_AUTH
94  *      client <- server: Stream header with list of features that should
95  *         include <bind>.
96  *    WAIT_BIND_RESULT
97  *      client <- server: <bind> result with JID.
98  *    WAIT_SESSION_IQ_RESULT
99  *      client <- server: result for <iq><session/></iq>
100  *    DONE
102  * @enum {number}
103  */
104 remoting.XmppLoginHandler.State = {
105   INIT: 0,
106   WAIT_STREAM_HEADER: 1,
107   WAIT_STARTTLS_RESPONSE: 2,
108   STARTING_TLS: 3,
109   WAIT_STREAM_HEADER_AFTER_TLS: 4,
110   WAIT_AUTH_RESULT: 5,
111   WAIT_STREAM_HEADER_AFTER_AUTH: 6,
112   WAIT_BIND_RESULT: 7,
113   WAIT_SESSION_IQ_RESULT: 8,
114   DONE: 9,
115   ERROR: 10
118 remoting.XmppLoginHandler.prototype.start = function() {
119   if (this.needHandshakeBeforeTls_) {
120     this.state_ = remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER;
121     this.startStream_('<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>');
122   } else {
123     // If <starttls> handshake is not required then start TLS right away.
124     this.state_ = remoting.XmppLoginHandler.State.STARTING_TLS;
125     this.startTlsCallback_();
126   }
129 /** @param {ArrayBuffer} data */
130 remoting.XmppLoginHandler.prototype.onDataReceived = function(data) {
131   base.debug.assert(this.state_ != remoting.XmppLoginHandler.State.INIT &&
132                     this.state_ != remoting.XmppLoginHandler.State.DONE &&
133                     this.state_ != remoting.XmppLoginHandler.State.ERROR);
135   this.streamParser_.appendData(data);
139  * @param {Element} stanza
140  * @private
141  */
142 remoting.XmppLoginHandler.prototype.onStanza_ = function(stanza) {
143   switch (this.state_) {
144     case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER:
145       if (stanza.querySelector('features>starttls')) {
146         this.state_ = remoting.XmppLoginHandler.State.WAIT_STARTTLS_RESPONSE;
147       } else {
148         this.onError_(remoting.Error.UNEXPECTED, "Server doesn't support TLS.");
149       }
150       break;
152     case remoting.XmppLoginHandler.State.WAIT_STARTTLS_RESPONSE:
153       if (stanza.localName == "proceed") {
154         this.state_ = remoting.XmppLoginHandler.State.STARTING_TLS;
155         this.startTlsCallback_();
156       } else {
157         this.onError_(remoting.Error.UNEXPECTED,
158                       "Failed to start TLS: " +
159                           (new XMLSerializer().serializeToString(stanza)));
160       }
161       break;
163     case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_TLS:
164       var mechanisms = Array.prototype.map.call(
165           stanza.querySelectorAll('features>mechanisms>mechanism'),
166           /** @param {Element} m */
167           function(m) { return m.textContent; });
168       if (mechanisms.indexOf("X-OAUTH2")) {
169         this.onError_(remoting.Error.UNEXPECTED,
170                       "OAuth2 is not supported by the server.");
171         return;
172       }
174       this.state_ = remoting.XmppLoginHandler.State.WAIT_AUTH_RESULT;
176       break;
178     case remoting.XmppLoginHandler.State.WAIT_AUTH_RESULT:
179       if (stanza.localName == 'success') {
180         this.state_ =
181             remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_AUTH;
182         this.startStream_(
183             '<iq type="set" id="0">' +
184               '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">' +
185                 '<resource>chromoting</resource>'+
186               '</bind>' +
187             '</iq>' +
188             '<iq type="set" id="1">' +
189               '<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>' +
190             '</iq>');
191       } else {
192         this.onError_(remoting.Error.AUTHENTICATION_FAILED,
193                       'Failed to authenticate: ' +
194                           (new XMLSerializer().serializeToString(stanza)));
195       }
196       break;
198     case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_AUTH:
199       if (stanza.querySelector('features>bind')) {
200         this.state_ = remoting.XmppLoginHandler.State.WAIT_BIND_RESULT;
201       } else {
202         this.onError_(remoting.Error.UNEXPECTED,
203                       "Server doesn't support bind after authentication.");
204       }
205       break;
207     case remoting.XmppLoginHandler.State.WAIT_BIND_RESULT:
208       var jidElement = stanza.querySelector('iq>bind>jid');
209       if (stanza.getAttribute('id') != '0' ||
210           stanza.getAttribute('type') != 'result' || !jidElement) {
211         this.onError_(remoting.Error.UNEXPECTED,
212                       'Received unexpected response to bind: ' +
213                           (new XMLSerializer().serializeToString(stanza)));
214         return;
215       }
216       this.jid_ = jidElement.textContent;
217       this.state_ = remoting.XmppLoginHandler.State.WAIT_SESSION_IQ_RESULT;
218       break;
220     case remoting.XmppLoginHandler.State.WAIT_SESSION_IQ_RESULT:
221       if (stanza.getAttribute('id') != '1' ||
222           stanza.getAttribute('type') != 'result') {
223         this.onError_(remoting.Error.UNEXPECTED,
224                       'Failed to start session: ' +
225                           (new XMLSerializer().serializeToString(stanza)));
226         return;
227       }
228       this.state_ = remoting.XmppLoginHandler.State.DONE;
229       this.onHandshakeDoneCallback_(this.jid_, this.streamParser_);
230       break;
232     default:
233       base.debug.assert(false);
234       break;
235   }
238 remoting.XmppLoginHandler.prototype.onTlsStarted = function() {
239   base.debug.assert(this.state_ ==
240                     remoting.XmppLoginHandler.State.STARTING_TLS);
241   this.state_ = remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_TLS;
242   var cookie = window.btoa("\0" + this.username_ + "\0" + this.authToken_);
244   this.startStream_(
245       '<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" ' +
246              'mechanism="X-OAUTH2" auth:service="oauth2" ' +
247              'auth:allow-generated-jid="true" ' +
248              'auth:client-uses-full-bind-result="true" ' +
249              'auth:allow-non-google-login="true" ' +
250              'xmlns:auth="http://www.google.com/talk/protocol/auth">' +
251         cookie +
252       '</auth>');
256  * @param {string} text
257  * @private
258  */
259 remoting.XmppLoginHandler.prototype.onParserError_ = function(text) {
260   this.onError_(remoting.Error.UNEXPECTED, text);
264  * @param {string} firstMessage Message to send after stream header.
265  * @private
266  */
267 remoting.XmppLoginHandler.prototype.startStream_ = function(firstMessage) {
268   this.sendMessageCallback_('<stream:stream to="' + this.server_ +
269                             '" version="1.0" xmlns="jabber:client" ' +
270                             'xmlns:stream="http://etherx.jabber.org/streams">' +
271                             firstMessage);
272   this.streamParser_ = new remoting.XmppStreamParser();
273   this.streamParser_.setCallbacks(this.onStanza_.bind(this),
274                                   this.onParserError_.bind(this));
278  * @param {remoting.Error} error
279  * @param {string} text
280  * @private
281  */
282 remoting.XmppLoginHandler.prototype.onError_ = function(error, text) {
283   if (this.state_ != remoting.XmppLoginHandler.State.ERROR) {
284     this.onErrorCallback_(error, text);
285     this.state_ = remoting.XmppLoginHandler.State.ERROR;
286   } else {
287     console.error(text);
288   }