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} */ 8var 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 */ 32remoting.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 */ 99remoting.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 113remoting.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 */ 119remoting.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 */ 131remoting.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 227remoting.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 */ 248remoting.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 */ 256remoting.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 */ 271remoting.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} 279