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 {function(string):void} sendMessageCallback Callback to call to send
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.
32 remoting
.XmppLoginHandler = function(server
,
37 onHandshakeDoneCallback
,
40 this.server_
= server
;
42 this.username_
= username
;
44 this.authToken_
= authToken
;
46 this.sendMessageCallback_
= sendMessageCallback
;
48 this.startTlsCallback_
= startTlsCallback
;
50 this.onHandshakeDoneCallback_
= onHandshakeDoneCallback
;
52 this.onErrorCallback_
= onErrorCallback
;
55 this.state_
= remoting
.XmppLoginHandler
.State
.INIT
;
59 /** @type {remoting.XmppStreamParser} @private */
60 this.streamParser_
= null;
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.
67 * Following messages are sent/received in each state:
69 * client -> server: Stream header
70 * client -> server: <starttls>
72 * client <- server: Stream header with list of supported features which
73 * should include starttls.
74 * WAIT_STARTTLS_RESPONSE
75 * client <- server: <proceed>
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
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
92 * client <- server: <bind> result with JID.
93 * WAIT_SESSION_IQ_RESULT
94 * client <- server: result for <iq><session/></iq>
99 remoting
.XmppLoginHandler
.State
= {
101 WAIT_STREAM_HEADER
: 1,
102 WAIT_STARTTLS_RESPONSE
: 2,
104 WAIT_STREAM_HEADER_AFTER_TLS
: 4,
106 WAIT_STREAM_HEADER_AFTER_AUTH
: 6,
108 WAIT_SESSION_IQ_RESULT
: 8,
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"/>');
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
);
124 this.streamParser_
.appendData(data
);
128 * @param {Element} stanza
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
;
137 this.onError_(remoting
.Error
.UNEXPECTED
, "Server doesn't support TLS.");
141 case remoting
.XmppLoginHandler
.State
.WAIT_STARTTLS_RESPONSE
:
142 if (stanza
.localName
== "proceed") {
143 this.state_
= remoting
.XmppLoginHandler
.State
.STARTING_TLS
;
144 this.startTlsCallback_();
146 this.onError_(remoting
.Error
.UNEXPECTED
,
147 "Failed to start TLS: " +
148 (new XMLSerializer().serializeToString(stanza
)));
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.");
163 this.state_
= remoting
.XmppLoginHandler
.State
.WAIT_AUTH_RESULT
;
167 case remoting
.XmppLoginHandler
.State
.WAIT_AUTH_RESULT
:
168 if (stanza
.localName
== 'success') {
170 remoting
.XmppLoginHandler
.State
.WAIT_STREAM_HEADER_AFTER_AUTH
;
172 '<iq type="set" id="0">' +
173 '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">' +
174 '<resource>chromoting</resource>'+
177 '<iq type="set" id="1">' +
178 '<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>' +
181 this.onError_(remoting
.Error
.AUTHENTICATION_FAILED
,
182 'Failed to authenticate: ' +
183 (new XMLSerializer().serializeToString(stanza
)));
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
;
191 this.onError_(remoting
.Error
.UNEXPECTED
,
192 "Server doesn't support bind after authentication.");
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
)));
205 this.jid_
= jidElement
.textContent
;
206 this.state_
= remoting
.XmppLoginHandler
.State
.WAIT_SESSION_IQ_RESULT
;
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
)));
217 this.state_
= remoting
.XmppLoginHandler
.State
.DONE
;
218 this.onHandshakeDoneCallback_(this.jid_
, this.streamParser_
);
222 base
.debug
.assert(false);
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_
);
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">' +
245 * @param {string} text
248 remoting
.XmppLoginHandler
.prototype.onParserError_ = function(text
) {
249 this.onError_(remoting
.Error
.UNEXPECTED
, text
);
253 * @param {string} firstMessage Message to send after stream header.
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">' +
261 this.streamParser_
= new remoting
.XmppStreamParser();
262 this.streamParser_
.setCallbacks(this.onStanza_
.bind(this),
263 this.onParserError_
.bind(this));
267 * @param {remoting.Error} error
268 * @param {string} text
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
;