Chromium Code Reviews| Index: remoting/webapp/xmpp_login_handler.js |
| diff --git a/remoting/webapp/xmpp_login_handler.js b/remoting/webapp/xmpp_login_handler.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..1916923285d2fb859375b4aa7c310ff4e2f6f44e |
| --- /dev/null |
| +++ b/remoting/webapp/xmpp_login_handler.js |
| @@ -0,0 +1,233 @@ |
| +// Copyright 2014 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +'use strict'; |
| + |
| +/** @suppress {duplicate} */ |
| +var remoting = remoting || {}; |
| + |
| +/** |
| + * XmppLoginHandler handles authentication handshake for XmppConnection. It |
| + * receives incoming data using onDataReceived() calls |sendMessageCallback| |
| + * to send outgoing messages and |
|
Jamie
2014/08/29 02:14:09
Incomplete comment.
I think an overview of the pr
Sergey Ulanov
2014/08/29 23:40:30
Added reference to RFC and State description lists
|
| + * |
| + * @param {string} server Domain name of the server we are connecting to. |
| + * @param {string} username Username. |
| + * @param {string} authToken OAuth2 token. |
| + * @param {function(string):void} sendMessageCallback Callback call to send |
|
Jamie
2014/08/29 02:14:09
s/call/to call/
Sergey Ulanov
2014/08/29 23:40:30
Done.
|
| + * a message. |
| + * @param {function():void} startTlsCallback Callback to call to start TLS on |
| + * the underlying socket. |
|
Jamie
2014/08/29 02:14:09
Why does this need to be a callback? There's only
Sergey Ulanov
2014/08/29 23:40:30
This class doesn't know anything about the socket.
|
| + * @param {function(string):void} onHandshakeDoneCallback Callback to call |
| + * after authentication is completed successfully |
| + * @param {function(Element):void} onStanzaCallback Callback to call for each |
| + * incoming stanza (when authenticated). |
| + * @param {function(remoting.Error, string):void} onErrorCallback Callback to |
| + * call on error. Can be called at any point during lifetime of connection. |
| + * @constructor |
| + */ |
| +remoting.XmppLoginHandler = function(server, |
| + username, |
| + authToken, |
| + sendMessageCallback, |
| + startTlsCallback, |
| + onHandshakeDoneCallback, |
| + onStanzaCallback, |
| + onErrorCallback) { |
| + this.server_ = server; |
|
Jamie
2014/08/29 02:14:09
@private
Sergey Ulanov
2014/08/29 23:40:30
Done.
|
| + this.username_ = username; |
| + this.authToken_ = authToken; |
| + this.sendMessageCallback_ = sendMessageCallback; |
| + this.startTlsCallback_ = startTlsCallback; |
| + this.onHandshakeDoneCallback_ = onHandshakeDoneCallback; |
| + this.onStanzaCallback_ = onStanzaCallback; |
| + this.onErrorCallback_ = onErrorCallback; |
| + |
| + this.state_ = remoting.XmppLoginHandler.State.INIT; |
| + this.jid_ = ''; |
| + |
| + /** @type {remoting.XmppStreamParser} */ |
| + this.streamParser_ = null; |
| +} |
| + |
| +/** |
| + * @enum {number} |
| + */ |
| +remoting.XmppLoginHandler.State = { |
| + INIT: 0, |
| + START_SENT: 1, |
| + STARTTLS_SENT: 2, |
| + STARTING_TLS: 3, |
| + START_SENT_AFTER_TLS: 4, |
| + AUTH_SENT: 5, |
| + AUTH_ACCEPTED: 6, |
| + BIND_SENT: 7, |
| + SESSION_IQ_SENT: 8, |
| + SESSION_ACTIVE: 9, |
| + ERROR: 10 |
| +}; |
| + |
| +remoting.XmppLoginHandler.prototype.start = function() { |
| + this.state_ = remoting.XmppLoginHandler.State.START_SENT; |
| + this.startStream_(); |
| +} |
| + |
| +/** @param {ArrayBuffer} data */ |
| +remoting.XmppLoginHandler.prototype.onDataReceived = function(data) { |
| + switch (this.state_) { |
| + case remoting.XmppLoginHandler.State.INIT: |
| + this.onError_(remoting.Error.UNEXPECTED, |
| + 'Data was received before the stream was started.'); |
| + break; |
| + |
| + case remoting.XmppLoginHandler.State.ERROR: |
| + // Ignore data received in the error state. |
| + break; |
| + |
| + default: |
| + this.streamParser_.appendData(data); |
|
kelvinp
2014/08/29 01:37:10
Have you consider moving streamParser to XMPPConne
Sergey Ulanov
2014/08/29 23:40:30
The issue here is that streamParser needs to be re
|
| + break; |
| + } |
| +} |
| + |
| +/** @param {Element} stanza */ |
| +remoting.XmppLoginHandler.prototype.onStanza_ = function(stanza) { |
| + switch (this.state_) { |
| + case remoting.XmppLoginHandler.State.START_SENT: |
| + if (stanza.querySelector('features>starttls')) { |
| + this.sendMessageCallback_( |
| + '<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>'); |
| + this.state_ = remoting.XmppLoginHandler.State.STARTTLS_SENT; |
| + } else { |
| + this.onError_(remoting.Error.UNEXPECTED, "Server doesn't support TLS."); |
| + } |
| + break; |
| + |
| + case remoting.XmppLoginHandler.State.STARTTLS_SENT: |
| + if (stanza.localName == "proceed") { |
| + this.state_ = remoting.XmppLoginHandler.State.STARTING_TLS; |
| + this.startTlsCallback_(); |
| + } else { |
| + this.onError_(remoting.Error.UNEXPECTED, |
| + "Failed to start TLS: " + |
| + (new XMLSerializer().serializeToString(stanza))); |
| + } |
| + break; |
| + |
| + case remoting.XmppLoginHandler.State.START_SENT_AFTER_TLS: |
| + var mechanisms = Array.prototype.map.call( |
| + stanza.querySelectorAll('features>mechanisms>mechanism'), |
| + /** @param {Element} m */ |
| + function(m) { return m.textContent; }); |
| + if (mechanisms.indexOf("X-OAUTH2")) { |
| + this.onError_(remoting.Error.UNEXPECTED, |
| + "OAuth2 is not supported by the server."); |
| + return; |
| + } |
| + |
| + var cookie = window.btoa("\0" + this.username_ + "\0" + this.authToken_); |
| + |
| + this.state_ = remoting.XmppLoginHandler.State.AUTH_SENT; |
| + this.sendMessageCallback_( |
| + '<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" ' + |
|
Jamie
2014/08/29 02:14:09
When constructing XML hierarchies from string lite
Sergey Ulanov
2014/08/29 23:40:30
Done.
|
| + 'mechanism="X-OAUTH2" auth:service="oauth2" ' + |
| + 'auth:allow-generated-jid="true" ' + |
| + 'auth:client-uses-full-bind-result="true" ' + |
| + 'auth:allow-non-google-login="true" ' + |
| + 'xmlns:auth="http://www.google.com/talk/protocol/auth">' + |
| + cookie + '</auth>'); |
| + break; |
| + |
| + case remoting.XmppLoginHandler.State.AUTH_SENT: |
| + if (stanza.localName == 'success') { |
| + this.state_ = remoting.XmppLoginHandler.State.AUTH_ACCEPTED; |
| + this.startStream_(); |
| + } else { |
| + this.onError_(remoting.Error.AUTHENTICATION_FAILED, |
| + 'Failed to authenticate: ' + |
| + (new XMLSerializer().serializeToString(stanza))); |
| + } |
| + break; |
| + |
| + case remoting.XmppLoginHandler.State.AUTH_ACCEPTED: |
| + if (stanza.querySelector('features>bind')) { |
| + this.sendMessageCallback_( |
| + '<iq type="set" id="0">' + |
| + '<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">' + |
| + '<resource>chromoting</resource></bind></iq>'); |
| + this.state_ = remoting.XmppLoginHandler.State.BIND_SENT; |
| + } else { |
| + this.onError_(remoting.Error.UNEXPECTED, |
| + 'Server doesn\'t support bind after authentication.'); |
|
Jamie
2014/08/29 02:14:09
You can use double-quotes if the string has an apo
Sergey Ulanov
2014/08/29 23:40:30
Done.
|
| + } |
| + break; |
| + |
| + case remoting.XmppLoginHandler.State.BIND_SENT: |
| + var jidElement = stanza.querySelector('iq>bind>jid'); |
| + if (!jidElement) |
|
Jamie
2014/08/29 02:14:09
This is indented badly, and very confusing without
Sergey Ulanov
2014/08/29 23:40:30
Actually this |if| doesn't even need to be here.
|
| + if (stanza.getAttribute('id') != '0' || |
| + stanza.getAttribute('type') != 'result' || !jidElement) { |
| + this.onError_(remoting.Error.UNEXPECTED, |
| + 'Received unexpected response to bind: ' + |
| + (new XMLSerializer().serializeToString(stanza))); |
| + return; |
| + } |
| + this.jid_ = jidElement.textContent; |
| + this.sendMessageCallback_( |
| + '<iq type="set" id="1">' + |
| + '<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>'); |
| + this.state_ = remoting.XmppLoginHandler.State.SESSION_IQ_SENT; |
| + break; |
| + |
| + case remoting.XmppLoginHandler.State.SESSION_IQ_SENT: |
| + if (stanza.getAttribute('id') != '1' || |
| + stanza.getAttribute('type') != 'result') { |
| + this.onError_(remoting.Error.UNEXPECTED, |
| + 'Failed to start session: ' + |
| + (new XMLSerializer().serializeToString(stanza))); |
| + return; |
| + } |
| + this.state_ = remoting.XmppLoginHandler.State.SESSION_ACTIVE; |
| + this.onHandshakeDoneCallback_(this.jid_); |
| + break; |
| + |
| + case remoting.XmppLoginHandler.State.SESSION_ACTIVE: |
| + this.onStanzaCallback_(stanza); |
| + break; |
| + } |
| +} |
| + |
| +/** @param {string} text */ |
| +remoting.XmppLoginHandler.prototype.onParserError_ = function(text) { |
| + this.onError_(remoting.Error.UNEXPECTED, text); |
| +} |
| + |
| +remoting.XmppLoginHandler.prototype.onTlsStarted = function() { |
| + base.debug.assert(this.state_ == |
| + remoting.XmppLoginHandler.State.STARTING_TLS); |
| + this.state_ = remoting.XmppLoginHandler.State.START_SENT_AFTER_TLS; |
| + this.startStream_(); |
| +}; |
| + |
| +remoting.XmppLoginHandler.prototype.startStream_ = function() { |
| + this.sendMessageCallback_('<stream:stream to="' + this.server_ + |
| + '" version="1.0" xmlns="jabber:client" ' + |
| + 'xmlns:stream="http://etherx.jabber.org/streams">'); |
| + this.streamParser_ = |
| + new remoting.XmppStreamParser(this.onStanza_.bind(this), |
| + this.onParserError_.bind(this)); |
| +} |
| + |
| +/** |
| + * @param {remoting.Error} error |
| + * @param {string} text |
| + */ |
| +remoting.XmppLoginHandler.prototype.onError_ = function(error, text) { |
| + if (this.state_ != remoting.XmppLoginHandler.State.ERROR) { |
| + this.onErrorCallback_(error, text); |
| + this.state_ = remoting.XmppLoginHandler.State.ERROR; |
| + } else { |
| + console.error(text); |
| + } |
| +} |