Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / remoting / webapp / xmpp_login_handler.js
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} */
8 var 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  */
32 remoting.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  */
99 remoting.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
113 remoting.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 */
119 remoting.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  */
131 remoting.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
227 remoting.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  */
248 remoting.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  */
256 remoting.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  */
271 remoting.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 }