Home | History | Annotate | Download | only in webapp
      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 {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  */
     32 remoting.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  */
     99 remoting.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 
    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"/>');
    116 }
    117 
    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);
    123 
    124   this.streamParser_.appendData(data);
    125 }
    126 
    127 /**
    128  * @param {Element} stanza
    129  * @private
    130  */
    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;
    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 
    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_);
    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  */
    248 remoting.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  */
    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">' +
    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  */
    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;
    275   } else {
    276     console.error(text);
    277   }
    278 }
    279