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.
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
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.
16 * See RFC3920 for description of XMPP and authentication handshake.
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
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.
34 remoting.XmppLoginHandler = function(server,
37 needHandshakeBeforeTls,
40 onHandshakeDoneCallback,
43 this.server_ = server;
45 this.username_ = username;
47 this.authToken_ = authToken;
49 this.needHandshakeBeforeTls_ = needHandshakeBeforeTls;
51 this.sendMessageCallback_ = sendMessageCallback;
53 this.startTlsCallback_ = startTlsCallback;
55 this.onHandshakeDoneCallback_ = onHandshakeDoneCallback;
57 this.onErrorCallback_ = onErrorCallback;
60 this.state_ = remoting.XmppLoginHandler.State.INIT;
64 /** @type {remoting.XmppStreamParser} @private */
65 this.streamParser_ = null;
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.
72 * Following messages are sent/received in each state:
74 * client -> server: Stream header
75 * client -> server: <starttls>
77 * client <- server: Stream header with list of supported features which
78 * should include starttls.
79 * WAIT_STARTTLS_RESPONSE
80 * client <- server: <proceed>
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
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
97 * client <- server: <bind> result with JID.
98 * WAIT_SESSION_IQ_RESULT
99 * client <- server: result for <iq><session/></iq>
104 remoting.XmppLoginHandler.State = {
106 WAIT_STREAM_HEADER: 1,
107 WAIT_STARTTLS_RESPONSE: 2,
109 WAIT_STREAM_HEADER_AFTER_TLS: 4,
111 WAIT_STREAM_HEADER_AFTER_AUTH: 6,
113 WAIT_SESSION_IQ_RESULT: 8,
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"/>');
123 // If <starttls> handshake is not required then start TLS right away.
124 this.state_ = remoting.XmppLoginHandler.State.STARTING_TLS;
125 this.startTlsCallback_();
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
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;
148 this.onError_(remoting.Error.UNEXPECTED, "Server doesn't support TLS.");
152 case remoting.XmppLoginHandler.State.WAIT_STARTTLS_RESPONSE:
153 if (stanza.localName == "proceed") {
154 this.state_ = remoting.XmppLoginHandler.State.STARTING_TLS;
155 this.startTlsCallback_();
157 this.onError_(remoting.Error.UNEXPECTED,
158 "Failed to start TLS: " +
159 (new XMLSerializer().serializeToString(stanza)));
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.");
174 this.state_ = remoting.XmppLoginHandler.State.WAIT_AUTH_RESULT;
178 case remoting.XmppLoginHandler.State.WAIT_AUTH_RESULT:
179 if (stanza.localName == 'success') {
181 remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_AUTH;
183 '<iq type="set" id="0">' +
184 '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">' +
185 '<resource>chromoting</resource>'+
188 '<iq type="set" id="1">' +
189 '<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>' +
192 this.onError_(remoting.Error.AUTHENTICATION_FAILED,
193 'Failed to authenticate: ' +
194 (new XMLSerializer().serializeToString(stanza)));
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;
202 this.onError_(remoting.Error.UNEXPECTED,
203 "Server doesn't support bind after authentication.");
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)));
216 this.jid_ = jidElement.textContent;
217 this.state_ = remoting.XmppLoginHandler.State.WAIT_SESSION_IQ_RESULT;
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)));
228 this.state_ = remoting.XmppLoginHandler.State.DONE;
229 this.onHandshakeDoneCallback_(this.jid_, this.streamParser_);
233 base.debug.assert(false);
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_);
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">' +
256 * @param {string} text
259 remoting.XmppLoginHandler.prototype.onParserError_ = function(text) {
260 this.onError_(remoting.Error.UNEXPECTED, text);
264 * @param {string} firstMessage Message to send after stream header.
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">' +
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
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;