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
|| {};
20 * XmppLoginHandler handles authentication handshake for XmppConnection. It
21 * receives incoming data using onDataReceived(), calls |sendMessageCallback|
22 * to send outgoing messages and calls |onHandshakeDoneCallback| after
23 * authentication is finished successfully or |onErrorCallback| on error.
25 * See RFC3920 for description of XMPP and authentication handshake.
27 * @param {string} server Domain name of the server we are connecting to.
28 * @param {string} username Username.
29 * @param {string} authToken OAuth2 token.
30 * @param {remoting.TlsMode} tlsMode
31 * @param {function(string):void} sendMessageCallback Callback to call to send
33 * @param {function():void} startTlsCallback Callback to call to start TLS on
34 * the underlying socket.
35 * @param {function(string, remoting.XmppStreamParser):void}
36 * onHandshakeDoneCallback Callback to call after authentication is
37 * completed successfully
38 * @param {function(!remoting.Error, string):void} onErrorCallback Callback to
39 * call on error. Can be called at any point during lifetime of connection.
42 remoting
.XmppLoginHandler = function(server
,
48 onHandshakeDoneCallback
,
51 this.server_
= server
;
53 this.username_
= username
;
55 this.authToken_
= authToken
;
57 this.tlsMode_
= tlsMode
;
59 this.sendMessageCallback_
= sendMessageCallback
;
61 this.startTlsCallback_
= startTlsCallback
;
63 this.onHandshakeDoneCallback_
= onHandshakeDoneCallback
;
65 this.onErrorCallback_
= onErrorCallback
;
68 this.state_
= remoting
.XmppLoginHandler
.State
.INIT
;
72 /** @private {remoting.XmppStreamParser} */
73 this.streamParser_
= null;
76 /** @return {function(string, remoting.XmppStreamParser):void} */
77 remoting
.XmppLoginHandler
.prototype.getHandshakeDoneCallbackForTesting
=
79 return this.onHandshakeDoneCallback_
;
83 * States the handshake goes through. States are iterated from INIT to DONE
84 * sequentially, except for ERROR state which may be accepted at any point.
86 * Following messages are sent/received in each state:
88 * client -> server: Stream header
89 * client -> server: <starttls>
91 * client <- server: Stream header with list of supported features which
92 * should include starttls.
93 * WAIT_STARTTLS_RESPONSE
94 * client <- server: <proceed>
97 * client -> server: Stream header
98 * client -> server: <auth> message with the OAuth2 token.
99 * WAIT_STREAM_HEADER_AFTER_TLS
100 * client <- server: Stream header with list of supported authentication
101 * methods which is expected to include X-OAUTH2
103 * client <- server: <success> or <failure>
104 * client -> server: Stream header
105 * client -> server: <bind>
106 * client -> server: <iq><session/></iq> to start the session
107 * WAIT_STREAM_HEADER_AFTER_AUTH
108 * client <- server: Stream header with list of features that should
111 * client <- server: <bind> result with JID.
112 * WAIT_SESSION_IQ_RESULT
113 * client <- server: result for <iq><session/></iq>
118 remoting
.XmppLoginHandler
.State
= {
120 WAIT_STREAM_HEADER
: 1,
121 WAIT_STARTTLS_RESPONSE
: 2,
123 WAIT_STREAM_HEADER_AFTER_TLS
: 4,
125 WAIT_STREAM_HEADER_AFTER_AUTH
: 6,
127 WAIT_SESSION_IQ_RESULT
: 8,
132 remoting
.XmppLoginHandler
.prototype.start = function() {
133 switch (this.tlsMode_
) {
134 case remoting
.TlsMode
.NO_TLS
:
136 remoting
.XmppLoginHandler
.State
.WAIT_STREAM_HEADER_AFTER_TLS
;
137 this.startAuthStream_();
138 console
.assert(remoting
.settings
.XMPP_SERVER_USE_TLS
=== false,
139 'NO_TLS should only be used in Dev builds.');
141 case remoting
.TlsMode
.WITH_HANDSHAKE
:
142 this.state_
= remoting
.XmppLoginHandler
.State
.WAIT_STREAM_HEADER
;
143 this.startStream_('<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>');
145 case remoting
.TlsMode
.WITHOUT_HANDSHAKE
:
146 this.state_
= remoting
.XmppLoginHandler
.State
.STARTING_TLS
;
147 this.startTlsCallback_();
150 console
.assert(false, 'Unrecognized Tls mode :' + this.tlsMode_
);
154 /** @param {ArrayBuffer} data */
155 remoting
.XmppLoginHandler
.prototype.onDataReceived = function(data
) {
156 console
.assert(this.state_
!= remoting
.XmppLoginHandler
.State
.INIT
&&
157 this.state_
!= remoting
.XmppLoginHandler
.State
.DONE
&&
158 this.state_
!= remoting
.XmppLoginHandler
.State
.ERROR
,
159 'onDataReceived() called in state ' + this.state_
+ '.');
161 this.streamParser_
.appendData(data
);
165 * @param {Element} stanza
168 remoting
.XmppLoginHandler
.prototype.onStanza_ = function(stanza
) {
169 switch (this.state_
) {
170 case remoting
.XmppLoginHandler
.State
.WAIT_STREAM_HEADER
:
171 if (stanza
.querySelector('features>starttls')) {
172 this.state_
= remoting
.XmppLoginHandler
.State
.WAIT_STARTTLS_RESPONSE
;
175 remoting
.Error
.unexpected(),
176 "Server doesn't support TLS.");
180 case remoting
.XmppLoginHandler
.State
.WAIT_STARTTLS_RESPONSE
:
181 if (stanza
.localName
== "proceed") {
182 this.state_
= remoting
.XmppLoginHandler
.State
.STARTING_TLS
;
183 this.startTlsCallback_();
185 this.onError_(remoting
.Error
.unexpected(),
186 "Failed to start TLS: " +
187 (new XMLSerializer().serializeToString(stanza
)));
191 case remoting
.XmppLoginHandler
.State
.WAIT_STREAM_HEADER_AFTER_TLS
:
192 var mechanisms
= Array
.prototype.map
.call(
193 stanza
.querySelectorAll('features>mechanisms>mechanism'),
194 /** @param {Element} m */
195 function(m
) { return m
.textContent
; });
196 if (mechanisms
.indexOf("X-OAUTH2")) {
197 this.onError_(remoting
.Error
.unexpected(),
198 "OAuth2 is not supported by the server.");
202 this.state_
= remoting
.XmppLoginHandler
.State
.WAIT_AUTH_RESULT
;
206 case remoting
.XmppLoginHandler
.State
.WAIT_AUTH_RESULT
:
207 if (stanza
.localName
== 'success') {
209 remoting
.XmppLoginHandler
.State
.WAIT_STREAM_HEADER_AFTER_AUTH
;
211 '<iq type="set" id="0">' +
212 '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">' +
213 '<resource>chromoting</resource>'+
216 '<iq type="set" id="1">' +
217 '<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>' +
221 new remoting
.Error(remoting
.Error
.Tag
.AUTHENTICATION_FAILED
),
222 'Failed to authenticate: ' +
223 (new XMLSerializer().serializeToString(stanza
)));
227 case remoting
.XmppLoginHandler
.State
.WAIT_STREAM_HEADER_AFTER_AUTH
:
228 if (stanza
.querySelector('features>bind')) {
229 this.state_
= remoting
.XmppLoginHandler
.State
.WAIT_BIND_RESULT
;
231 this.onError_(remoting
.Error
.unexpected(),
232 "Server doesn't support bind after authentication.");
236 case remoting
.XmppLoginHandler
.State
.WAIT_BIND_RESULT
:
237 var jidElement
= stanza
.querySelector('iq>bind>jid');
238 if (stanza
.getAttribute('id') != '0' ||
239 stanza
.getAttribute('type') != 'result' || !jidElement
) {
240 this.onError_(remoting
.Error
.unexpected(),
241 'Received unexpected response to bind: ' +
242 (new XMLSerializer().serializeToString(stanza
)));
245 this.jid_
= jidElement
.textContent
;
246 this.state_
= remoting
.XmppLoginHandler
.State
.WAIT_SESSION_IQ_RESULT
;
249 case remoting
.XmppLoginHandler
.State
.WAIT_SESSION_IQ_RESULT
:
250 if (stanza
.getAttribute('id') != '1' ||
251 stanza
.getAttribute('type') != 'result') {
252 this.onError_(remoting
.Error
.unexpected(),
253 'Failed to start session: ' +
254 (new XMLSerializer().serializeToString(stanza
)));
257 this.state_
= remoting
.XmppLoginHandler
.State
.DONE
;
258 this.onHandshakeDoneCallback_(this.jid_
, this.streamParser_
);
262 console
.error('onStanza_() called in state ' + this.state_
+ '.');
267 remoting
.XmppLoginHandler
.prototype.onTlsStarted = function() {
268 console
.assert(this.state_
== remoting
.XmppLoginHandler
.State
.STARTING_TLS
,
269 'onTlsStarted() called in state ' + this.state_
+ '.');
270 this.state_
= remoting
.XmppLoginHandler
.State
.WAIT_STREAM_HEADER_AFTER_TLS
;
271 this.startAuthStream_();
275 remoting
.XmppLoginHandler
.prototype.startAuthStream_ = function() {
276 var cookie
= window
.btoa('\0' + this.username_
+ '\0' + this.authToken_
);
279 '<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" ' +
280 'mechanism="X-OAUTH2" auth:service="oauth2" ' +
281 'auth:allow-generated-jid="true" ' +
282 'auth:client-uses-full-bind-result="true" ' +
283 'auth:allow-non-google-login="true" ' +
284 'xmlns:auth="http://www.google.com/talk/protocol/auth">' +
290 * @param {string} text
293 remoting
.XmppLoginHandler
.prototype.onParserError_ = function(text
) {
294 this.onError_(remoting
.Error
.unexpected(), text
);
298 * @param {string} firstMessage Message to send after stream header.
301 remoting
.XmppLoginHandler
.prototype.startStream_ = function(firstMessage
) {
302 this.sendMessageCallback_('<stream:stream to="' + this.server_
+
303 '" version="1.0" xmlns="jabber:client" ' +
304 'xmlns:stream="http://etherx.jabber.org/streams">' +
306 this.streamParser_
= new remoting
.XmppStreamParser();
307 this.streamParser_
.setCallbacks(this.onStanza_
.bind(this),
308 this.onParserError_
.bind(this));
312 * @param {!remoting.Error} error
313 * @param {string} text
316 remoting
.XmppLoginHandler
.prototype.onError_ = function(error
, text
) {
317 if (this.state_
!= remoting
.XmppLoginHandler
.State
.ERROR
) {
318 this.onErrorCallback_(error
, text
);
319 this.state_
= remoting
.XmppLoginHandler
.State
.ERROR
;