| OLD | NEW |
| (Empty) |
| 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 {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 | |
| 24 * a message. | |
| 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. | |
| 32 * @constructor | |
| 33 */ | |
| 34 remoting.XmppLoginHandler = function(server, | |
| 35 username, | |
| 36 authToken, | |
| 37 needHandshakeBeforeTls, | |
| 38 sendMessageCallback, | |
| 39 startTlsCallback, | |
| 40 onHandshakeDoneCallback, | |
| 41 onErrorCallback) { | |
| 42 /** @private */ | |
| 43 this.server_ = server; | |
| 44 /** @private */ | |
| 45 this.username_ = username; | |
| 46 /** @private */ | |
| 47 this.authToken_ = authToken; | |
| 48 /** @private */ | |
| 49 this.needHandshakeBeforeTls_ = needHandshakeBeforeTls; | |
| 50 /** @private */ | |
| 51 this.sendMessageCallback_ = sendMessageCallback; | |
| 52 /** @private */ | |
| 53 this.startTlsCallback_ = startTlsCallback; | |
| 54 /** @private */ | |
| 55 this.onHandshakeDoneCallback_ = onHandshakeDoneCallback; | |
| 56 /** @private */ | |
| 57 this.onErrorCallback_ = onErrorCallback; | |
| 58 | |
| 59 /** @private */ | |
| 60 this.state_ = remoting.XmppLoginHandler.State.INIT; | |
| 61 /** @private */ | |
| 62 this.jid_ = ''; | |
| 63 | |
| 64 /** @private {remoting.XmppStreamParser} */ | |
| 65 this.streamParser_ = null; | |
| 66 } | |
| 67 | |
| 68 /** @return {function(string, remoting.XmppStreamParser):void} */ | |
| 69 remoting.XmppLoginHandler.prototype.getHandshakeDoneCallbackForTesting = | |
| 70 function() { | |
| 71 return this.onHandshakeDoneCallback_; | |
| 72 }; | |
| 73 | |
| 74 /** | |
| 75 * States the handshake goes through. States are iterated from INIT to DONE | |
| 76 * sequentially, except for ERROR state which may be accepted at any point. | |
| 77 * | |
| 78 * Following messages are sent/received in each state: | |
| 79 * INIT | |
| 80 * client -> server: Stream header | |
| 81 * client -> server: <starttls> | |
| 82 * WAIT_STREAM_HEADER | |
| 83 * client <- server: Stream header with list of supported features which | |
| 84 * should include starttls. | |
| 85 * WAIT_STARTTLS_RESPONSE | |
| 86 * client <- server: <proceed> | |
| 87 * STARTING_TLS | |
| 88 * TLS handshake | |
| 89 * client -> server: Stream header | |
| 90 * client -> server: <auth> message with the OAuth2 token. | |
| 91 * WAIT_STREAM_HEADER_AFTER_TLS | |
| 92 * client <- server: Stream header with list of supported authentication | |
| 93 * methods which is expected to include X-OAUTH2 | |
| 94 * WAIT_AUTH_RESULT | |
| 95 * client <- server: <success> or <failure> | |
| 96 * client -> server: Stream header | |
| 97 * client -> server: <bind> | |
| 98 * client -> server: <iq><session/></iq> to start the session | |
| 99 * WAIT_STREAM_HEADER_AFTER_AUTH | |
| 100 * client <- server: Stream header with list of features that should | |
| 101 * include <bind>. | |
| 102 * WAIT_BIND_RESULT | |
| 103 * client <- server: <bind> result with JID. | |
| 104 * WAIT_SESSION_IQ_RESULT | |
| 105 * client <- server: result for <iq><session/></iq> | |
| 106 * DONE | |
| 107 * | |
| 108 * @enum {number} | |
| 109 */ | |
| 110 remoting.XmppLoginHandler.State = { | |
| 111 INIT: 0, | |
| 112 WAIT_STREAM_HEADER: 1, | |
| 113 WAIT_STARTTLS_RESPONSE: 2, | |
| 114 STARTING_TLS: 3, | |
| 115 WAIT_STREAM_HEADER_AFTER_TLS: 4, | |
| 116 WAIT_AUTH_RESULT: 5, | |
| 117 WAIT_STREAM_HEADER_AFTER_AUTH: 6, | |
| 118 WAIT_BIND_RESULT: 7, | |
| 119 WAIT_SESSION_IQ_RESULT: 8, | |
| 120 DONE: 9, | |
| 121 ERROR: 10 | |
| 122 }; | |
| 123 | |
| 124 remoting.XmppLoginHandler.prototype.start = function() { | |
| 125 if (this.needHandshakeBeforeTls_) { | |
| 126 this.state_ = remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER; | |
| 127 this.startStream_('<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>'); | |
| 128 } else { | |
| 129 // If <starttls> handshake is not required then start TLS right away. | |
| 130 this.state_ = remoting.XmppLoginHandler.State.STARTING_TLS; | |
| 131 this.startTlsCallback_(); | |
| 132 } | |
| 133 } | |
| 134 | |
| 135 /** @param {ArrayBuffer} data */ | |
| 136 remoting.XmppLoginHandler.prototype.onDataReceived = function(data) { | |
| 137 base.debug.assert(this.state_ != remoting.XmppLoginHandler.State.INIT && | |
| 138 this.state_ != remoting.XmppLoginHandler.State.DONE && | |
| 139 this.state_ != remoting.XmppLoginHandler.State.ERROR); | |
| 140 | |
| 141 this.streamParser_.appendData(data); | |
| 142 } | |
| 143 | |
| 144 /** | |
| 145 * @param {Element} stanza | |
| 146 * @private | |
| 147 */ | |
| 148 remoting.XmppLoginHandler.prototype.onStanza_ = function(stanza) { | |
| 149 switch (this.state_) { | |
| 150 case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER: | |
| 151 if (stanza.querySelector('features>starttls')) { | |
| 152 this.state_ = remoting.XmppLoginHandler.State.WAIT_STARTTLS_RESPONSE; | |
| 153 } else { | |
| 154 this.onError_( | |
| 155 remoting.Error.unexpected(), | |
| 156 "Server doesn't support TLS."); | |
| 157 } | |
| 158 break; | |
| 159 | |
| 160 case remoting.XmppLoginHandler.State.WAIT_STARTTLS_RESPONSE: | |
| 161 if (stanza.localName == "proceed") { | |
| 162 this.state_ = remoting.XmppLoginHandler.State.STARTING_TLS; | |
| 163 this.startTlsCallback_(); | |
| 164 } else { | |
| 165 this.onError_(remoting.Error.unexpected(), | |
| 166 "Failed to start TLS: " + | |
| 167 (new XMLSerializer().serializeToString(stanza))); | |
| 168 } | |
| 169 break; | |
| 170 | |
| 171 case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_TLS: | |
| 172 var mechanisms = Array.prototype.map.call( | |
| 173 stanza.querySelectorAll('features>mechanisms>mechanism'), | |
| 174 /** @param {Element} m */ | |
| 175 function(m) { return m.textContent; }); | |
| 176 if (mechanisms.indexOf("X-OAUTH2")) { | |
| 177 this.onError_(remoting.Error.unexpected(), | |
| 178 "OAuth2 is not supported by the server."); | |
| 179 return; | |
| 180 } | |
| 181 | |
| 182 this.state_ = remoting.XmppLoginHandler.State.WAIT_AUTH_RESULT; | |
| 183 | |
| 184 break; | |
| 185 | |
| 186 case remoting.XmppLoginHandler.State.WAIT_AUTH_RESULT: | |
| 187 if (stanza.localName == 'success') { | |
| 188 this.state_ = | |
| 189 remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_AUTH; | |
| 190 this.startStream_( | |
| 191 '<iq type="set" id="0">' + | |
| 192 '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">' + | |
| 193 '<resource>chromoting</resource>'+ | |
| 194 '</bind>' + | |
| 195 '</iq>' + | |
| 196 '<iq type="set" id="1">' + | |
| 197 '<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>' + | |
| 198 '</iq>'); | |
| 199 } else { | |
| 200 this.onError_( | |
| 201 new remoting.Error(remoting.Error.Tag.AUTHENTICATION_FAILED), | |
| 202 'Failed to authenticate: ' + | |
| 203 (new XMLSerializer().serializeToString(stanza))); | |
| 204 } | |
| 205 break; | |
| 206 | |
| 207 case remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_AUTH: | |
| 208 if (stanza.querySelector('features>bind')) { | |
| 209 this.state_ = remoting.XmppLoginHandler.State.WAIT_BIND_RESULT; | |
| 210 } else { | |
| 211 this.onError_(remoting.Error.unexpected(), | |
| 212 "Server doesn't support bind after authentication."); | |
| 213 } | |
| 214 break; | |
| 215 | |
| 216 case remoting.XmppLoginHandler.State.WAIT_BIND_RESULT: | |
| 217 var jidElement = stanza.querySelector('iq>bind>jid'); | |
| 218 if (stanza.getAttribute('id') != '0' || | |
| 219 stanza.getAttribute('type') != 'result' || !jidElement) { | |
| 220 this.onError_(remoting.Error.unexpected(), | |
| 221 'Received unexpected response to bind: ' + | |
| 222 (new XMLSerializer().serializeToString(stanza))); | |
| 223 return; | |
| 224 } | |
| 225 this.jid_ = jidElement.textContent; | |
| 226 this.state_ = remoting.XmppLoginHandler.State.WAIT_SESSION_IQ_RESULT; | |
| 227 break; | |
| 228 | |
| 229 case remoting.XmppLoginHandler.State.WAIT_SESSION_IQ_RESULT: | |
| 230 if (stanza.getAttribute('id') != '1' || | |
| 231 stanza.getAttribute('type') != 'result') { | |
| 232 this.onError_(remoting.Error.unexpected(), | |
| 233 'Failed to start session: ' + | |
| 234 (new XMLSerializer().serializeToString(stanza))); | |
| 235 return; | |
| 236 } | |
| 237 this.state_ = remoting.XmppLoginHandler.State.DONE; | |
| 238 this.onHandshakeDoneCallback_(this.jid_, this.streamParser_); | |
| 239 break; | |
| 240 | |
| 241 default: | |
| 242 base.debug.assert(false); | |
| 243 break; | |
| 244 } | |
| 245 } | |
| 246 | |
| 247 remoting.XmppLoginHandler.prototype.onTlsStarted = function() { | |
| 248 base.debug.assert(this.state_ == | |
| 249 remoting.XmppLoginHandler.State.STARTING_TLS); | |
| 250 this.state_ = remoting.XmppLoginHandler.State.WAIT_STREAM_HEADER_AFTER_TLS; | |
| 251 var cookie = window.btoa("\0" + this.username_ + "\0" + this.authToken_); | |
| 252 | |
| 253 this.startStream_( | |
| 254 '<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" ' + | |
| 255 'mechanism="X-OAUTH2" auth:service="oauth2" ' + | |
| 256 'auth:allow-generated-jid="true" ' + | |
| 257 'auth:client-uses-full-bind-result="true" ' + | |
| 258 'auth:allow-non-google-login="true" ' + | |
| 259 'xmlns:auth="http://www.google.com/talk/protocol/auth">' + | |
| 260 cookie + | |
| 261 '</auth>'); | |
| 262 }; | |
| 263 | |
| 264 /** | |
| 265 * @param {string} text | |
| 266 * @private | |
| 267 */ | |
| 268 remoting.XmppLoginHandler.prototype.onParserError_ = function(text) { | |
| 269 this.onError_(remoting.Error.unexpected(), text); | |
| 270 } | |
| 271 | |
| 272 /** | |
| 273 * @param {string} firstMessage Message to send after stream header. | |
| 274 * @private | |
| 275 */ | |
| 276 remoting.XmppLoginHandler.prototype.startStream_ = function(firstMessage) { | |
| 277 this.sendMessageCallback_('<stream:stream to="' + this.server_ + | |
| 278 '" version="1.0" xmlns="jabber:client" ' + | |
| 279 'xmlns:stream="http://etherx.jabber.org/streams">' + | |
| 280 firstMessage); | |
| 281 this.streamParser_ = new remoting.XmppStreamParser(); | |
| 282 this.streamParser_.setCallbacks(this.onStanza_.bind(this), | |
| 283 this.onParserError_.bind(this)); | |
| 284 } | |
| 285 | |
| 286 /** | |
| 287 * @param {!remoting.Error} error | |
| 288 * @param {string} text | |
| 289 * @private | |
| 290 */ | |
| 291 remoting.XmppLoginHandler.prototype.onError_ = function(error, text) { | |
| 292 if (this.state_ != remoting.XmppLoginHandler.State.ERROR) { | |
| 293 this.onErrorCallback_(error, text); | |
| 294 this.state_ = remoting.XmppLoginHandler.State.ERROR; | |
| 295 } else { | |
| 296 console.error(text); | |
| 297 } | |
| 298 } | |
| OLD | NEW |