Commit ea321001 authored by sergeyu's avatar sergeyu Committed by Commit bot

XMPP implementation in JavaScript.

This adds XMPP implementation that will be used for signaling in the
webapp instead of WCS. It depends on TLS support added in the TCP API
in Chrome 38.

BUG=274652

Review URL: https://codereview.chromium.org/514343002

Cr-Commit-Position: refs/heads/master@{#293068}
parent 24388953
......@@ -108,6 +108,12 @@
'webapp/host_settings.js',
'webapp/host_table_entry.js',
],
# Remoting XMPP JavaScript files.
'remoting_webapp_js_xmpp_files': [
'webapp/xmpp_connection.js',
'webapp/xmpp_login_handler.js',
'webapp/xmpp_stream_parser.js',
],
# Remoting WCS container JavaScript files.
'remoting_webapp_js_wcs_container_files': [
'webapp/wcs_sandbox_container.js',
......@@ -149,11 +155,14 @@
'webapp/js_proto/chrome_proto.js',
'webapp/unittests/chrome_mocks.js',
'webapp/unittests/base_unittest.js',
'webapp/unittests/l10n_unittest.js',
'webapp/unittests/menu_button_unittest.js',
'webapp/unittests/it2me_helpee_channel_unittest.js',
'webapp/unittests/it2me_helper_channel_unittest.js',
'webapp/unittests/it2me_service_unittest.js'
'webapp/unittests/it2me_service_unittest.js',
'webapp/unittests/l10n_unittest.js',
'webapp/unittests/menu_button_unittest.js',
'webapp/unittests/xmpp_connection_unittest.js',
'webapp/unittests/xmpp_login_handler_unittest.js',
'webapp/unittests/xmpp_stream_parser_unittest.js',
],
'remoting_webapp_unittest_additional_files': [
'webapp/menu_button.css',
......@@ -177,6 +186,7 @@
'<@(remoting_webapp_js_ui_host_control_files)',
'<@(remoting_webapp_js_ui_host_display_files)',
'<@(remoting_webapp_js_wcs_container_files)',
'<@(remoting_webapp_js_xmpp_files)',
# Uncomment this line to include browser test files in the web app
# to expedite debugging or local development.
# '<@(remoting_webapp_js_browser_test_files)'
......
......@@ -385,3 +385,28 @@ base.EventSource.prototype = {
});
}
};
/**
* Converts UTF-8 string to ArrayBuffer.
*
* @param {string} string
* @return {ArrayBuffer}
*/
base.encodeUtf8 = function(string) {
var utf8String = unescape(encodeURIComponent(string));
var result = new Uint8Array(utf8String.length);
for (var i = 0; i < utf8String.length; i++)
result[i] = utf8String.charCodeAt(i);
return result.buffer;
}
/**
* Decodes UTF-8 string from ArrayBuffer.
*
* @param {ArrayBuffer} buffer
* @return {string}
*/
base.decodeUtf8 = function(buffer) {
return decodeURIComponent(
escape(String.fromCharCode.apply(null, new Uint8Array(buffer))));
}
......@@ -524,3 +524,156 @@ chrome.cast.initialize =
*/
chrome.cast.requestSession =
function(successCallback, errorCallback) {};
/** @type {Object} */
chrome.sockets = {};
/** @type {Object} */
chrome.sockets.tcp = {};
/** @constructor */
chrome.sockets.tcp.CreateInfo = function() {
/** @type {number} */
this.socketId = 0;
}
/**
* @param {Object} properties
* @param {function(chrome.sockets.tcp.CreateInfo):void} callback
*/
chrome.sockets.tcp.create = function(properties, callback) {};
/** @constructor */
chrome.sockets.tcp.ConnectInfo = function() {
/** @type {number} */
this.result = 0;
}
/**
* @param {number} socketId
* @param {string} peerAddress
* @param {number} peerPort
* @param {function(chrome.sockets.tcp.ConnectInfo):void} callback
*/
chrome.sockets.tcp.connect =
function(socketId, peerAddress, peerPort, callback) {};
/** @constructor */
chrome.sockets.tcp.SendInfo = function() {
/** @type {number} */
this.resultCode = 0;
/** @type {number} */
this.bytesSent = 0;
}
/**
* @param {number} socketId
* @param {ArrayBuffer} data
* @param {function(chrome.sockets.tcp.SendInfo):void} callback
*/
chrome.sockets.tcp.send = function(socketId, data, callback) {};
/**
* @param {number} socketId
*/
chrome.sockets.tcp.close = function(socketId) {};
/**
* @param {number} socketId
* @param {Object} options
* @param {function(number):void} callback
*/
chrome.sockets.tcp.secure = function(socketId, options, callback) {};
/** @constructor */
chrome.sockets.tcp.ReceiveInfo = function() {
/** @type {number} */
this.socketId = 0;
/** @type {ArrayBuffer} */
this.data = null;
}
/** @type {chrome.Event} */
chrome.sockets.tcp.onReceive = null;
/** @constructor */
chrome.sockets.tcp.ReceiveErrorInfo = function() {
/** @type {number} */
this.socketId = 0;
/** @type {number} */
this.resultCode = 0;
}
/** @type {chrome.Event} */
chrome.sockets.tcp.onReceiveError = null;
/** @type {Object} */
chrome.socket = {};
/** @constructor */
chrome.socket.CreateInfo = function() {
/** @type {number} */
this.socketId = 0;
}
/**
* @param {string} socketType
* @param {Object} options
* @param {function(chrome.socket.CreateInfo):void} callback
*/
chrome.socket.create = function(socketType, options, callback) {};
/**
* @param {number} socketId
* @param {string} hostname
* @param {number} port
* @param {function(number):void} callback
*/
chrome.socket.connect =
function(socketId, hostname, port, callback) {};
/** @constructor */
chrome.socket.WriteInfo = function() {
/** @type {number} */
this.bytesWritten = 0;
}
/**
* @param {number} socketId
* @param {ArrayBuffer} data
* @param {function(chrome.socket.WriteInfo):void} callback
*/
chrome.socket.write = function(socketId, data, callback) {};
/** @constructor */
chrome.socket.ReadInfo = function() {
/** @type {number} */
this.resultCode = 0;
/** @type {ArrayBuffer} */
this.data = null;
}
/**
* @param {number} socketId
* @param {function(chrome.socket.ReadInfo):void} callback
*/
chrome.socket.read = function(socketId, callback) {};
/**
* @param {number} socketId
*/
chrome.socket.destroy = function(socketId) {};
/**
* @param {number} socketId
* @param {Object} options
* @param {function(number):void} callback
*/
chrome.socket.secure = function(socketId, options, callback) {};
......@@ -27,6 +27,8 @@ Document.prototype.webkitIsFullScreen;
/** @type {boolean} */
Document.prototype.webkitHidden;
/** @type {Element} */
Document.prototype.firstElementChild;
/** @type {number} */
Element.ALLOW_KEYBOARD_INPUT;
......@@ -38,6 +40,12 @@ Element.prototype.webkitRequestFullScreen = function(flags) {};
/** @type {boolean} */
Element.prototype.hidden;
/** @type {string} */
Element.prototype.localName;
/** @type {string} */
Element.prototype.textContent;
/** @constructor
@extends {HTMLElement} */
......@@ -242,3 +250,10 @@ Event.prototype.initMouseEvent = function(
screenX, screenY, clientX, clientY,
ctrlKey, altKey, shiftKey, metaKey,
button, relatedTarget) {};
/**
* @param {number} begin
* @param {number=} end
* @return {ArrayBuffer}
*/
ArrayBuffer.prototype.slice = function(begin, end) {};
......@@ -232,4 +232,45 @@ test('removeEventListener() should work even if the listener ' +
sinon.assert.calledOnce(sink.listener);
});
test('encodeUtf8() can encode UTF8 strings', function() {
function toJsArray(arrayBuffer) {
var result = [];
var array = new Uint8Array(arrayBuffer);
for (var i = 0; i < array.length; ++i) {
result.push(array[i]);
}
return result;
}
// ASCII.
QUnit.deepEqual(toJsArray(base.encodeUtf8("ABC")), [0x41, 0x42, 0x43]);
// Some arbitrary characters from the basic Unicode plane.
QUnit.deepEqual(
toJsArray(base.encodeUtf8("挂Ѓф")),
[/* 挂 */ 0xE6, 0x8C, 0x82, /* Ѓ */ 0xD0, 0x83, /* ф */ 0xD1, 0x84]);
// Unicode surrogate pair for U+1F603.
QUnit.deepEqual(toJsArray(base.encodeUtf8("😃")),
[0xF0, 0x9F, 0x98, 0x83]);
});
test('decodeUtf8() can decode UTF8 strings', function() {
// ASCII.
QUnit.equal(base.decodeUtf8(new Uint8Array([0x41, 0x42, 0x43]).buffer),
"ABC");
// Some arbitrary characters from the basic Unicode plane.
QUnit.equal(
base.decodeUtf8(
new Uint8Array([/* 挂 */ 0xE6, 0x8C, 0x82,
/* Ѓ */ 0xD0, 0x83,
/* ф */ 0xD1, 0x84]).buffer),
"挂Ѓф");
// Unicode surrogate pair for U+1F603.
QUnit.equal(base.decodeUtf8(new Uint8Array([0xF0, 0x9F, 0x98, 0x83]).buffer),
"😃");
});
})();
// 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.
(function() {
'use strict';
var testUsername = 'testUsername@gmail.com';
var testToken = 'testToken';
var socketId = 3;
var onStateChange = null;
var onStanzaStr = null;
var connection = null;
module('XmppConnection', {
setup: function() {
onStateChange = sinon.spy();
onStanzaStr = sinon.spy();
function onStanza(stanza) {
onStanzaStr(new XMLSerializer().serializeToString(stanza));
}
sinon.stub(chrome.socket, 'create');
sinon.stub(chrome.socket, 'connect');
sinon.stub(chrome.socket, 'write');
sinon.stub(chrome.socket, 'read');
sinon.stub(chrome.socket, 'destroy');
sinon.stub(chrome.socket, 'secure');
connection = new remoting.XmppConnection(onStateChange, onStanza);
},
teardown: function() {
chrome.socket.create.restore();
chrome.socket.connect.restore();
chrome.socket.write.restore();
chrome.socket.read.restore();
chrome.socket.destroy.restore();
chrome.socket.secure.restore();
}
});
test('should go to FAILED state when failed to connect', function() {
connection.connect(
'xmpp.example.com:123', 'testUsername@gmail.com', 'testToken');
sinon.assert.calledWith(onStateChange,
remoting.XmppConnection.State.CONNECTING);
sinon.assert.calledWith(chrome.socket.create, "tcp", {});
chrome.socket.create.getCall(0).args[2]({socketId: socketId});
sinon.assert.calledWith(
chrome.socket.connect, socketId, "xmpp.example.com", 123);
chrome.socket.connect.getCall(0).args[3](-1);
QUnit.equal(connection.getError(), remoting.Error.NETWORK_FAILURE);
});
test('should use XmppLoginHandler to complete handshake and read data',
function() {
connection.connect(
'xmpp.example.com:123', 'testUsername@gmail.com', 'testToken');
sinon.assert.calledWith(onStateChange,
remoting.XmppConnection.State.CONNECTING);
sinon.assert.calledWith(chrome.socket.create, "tcp", {});
chrome.socket.create.getCall(0).args[2]({socketId: socketId});
sinon.assert.calledWith(
chrome.socket.connect, socketId, "xmpp.example.com", 123);
chrome.socket.connect.getCall(0).args[3](0);
sinon.assert.calledWith(onStateChange,
remoting.XmppConnection.State.HANDSHAKE);
var parser = new remoting.XmppStreamParser();
var parserMock = sinon.mock(parser);
var setCallbacksCalled = parserMock.expects('setCallbacks').once();
connection.loginHandler_.onHandshakeDoneCallback_('test@example.com/123123',
parser);
sinon.assert.calledWith(onStateChange,
remoting.XmppConnection.State.CONNECTED);
setCallbacksCalled.verify();
// Simulate read() callback with |data|. It should be passed to the parser.
var data = base.encodeUtf8('<iq id="1">hello</iq>');
sinon.assert.calledWith(chrome.socket.read, socketId);
var appendDataCalled = parserMock.expects('appendData').once().withArgs(data);
chrome.socket.read.getCall(0).args[1]({resultCode: 0, data: data});
appendDataCalled.verify();
});
})();
// 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.
(function() {
'use strict';
var testUsername = 'testUsername@gmail.com';
var testToken = 'testToken';
var sendMessage = null;
var startTls = null;
var onHandshakeDone = null;
var onStanzaStr = null;
var onError = null;
var loginHandler = null;
module('XmppLoginHandler', {
setup: function() {
sendMessage = sinon.spy();
startTls = sinon.spy();
onHandshakeDone = sinon.spy();
onStanzaStr = sinon.spy();
onError = sinon.spy();
function onStanza(stanza) {
onStanzaStr(new XMLSerializer().serializeToString(stanza));
}
loginHandler = new remoting.XmppLoginHandler(
'google.com', testUsername, testToken, sendMessage, startTls,
onHandshakeDone, onError);
}
});
// Executes handshake base.
function handshakeBase() {
loginHandler.start();
sinon.assert.calledWith(
sendMessage,
'<stream:stream to="google.com" version="1.0" xmlns="jabber:client" ' +
'xmlns:stream="http://etherx.jabber.org/streams">');
sendMessage.reset();
loginHandler.onDataReceived(base.encodeUtf8(
'<stream:stream from="google.com" id="78A87C70559EF28A" version="1.0" ' +
'xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">' +
'<stream:features><starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls">' +
'<required/></starttls><mechanisms ' +
'xmlns="urn:ietf:params:xml:ns:xmpp-sasl">' +
'<mechanism>X-OAUTH2</mechanism><mechanism>X-GOOGLE-TOKEN</mechanism>' +
'</mechanisms></stream:features>'));
sinon.assert.calledWith(
sendMessage, '<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>');
sendMessage.reset();
loginHandler.onDataReceived(
base.encodeUtf8('<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>'));
sinon.assert.calledWith(startTls);
startTls.reset();
loginHandler.onTlsStarted();
sinon.assert.calledWith(
sendMessage,
'<stream:stream to="google.com" version="1.0" xmlns="jabber:client" ' +
'xmlns:stream="http://etherx.jabber.org/streams">');
sendMessage.reset();
loginHandler.onDataReceived(base.encodeUtf8(
'<stream:stream from="google.com" id="DCDDE5171CB2154A" version="1.0" ' +
'xmlns:stream="http://etherx.jabber.org/streams" ' +
'xmlns="jabber:client"><stream:features>' +
'<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">' +
'<mechanism>X-OAUTH2</mechanism><mechanism>X-GOOGLE-TOKEN</mechanism>' +
'<mechanism>PLAIN</mechanism></mechanisms></stream:features>'));
var cookie = window.btoa("\0" + testUsername + "\0" + testToken);
sinon.assert.calledWith(
sendMessage,
'<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" 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>');
sendMessage.reset();
}
test('should authenticate', function() {
handshakeBase();
loginHandler.onDataReceived(
base.encodeUtf8('<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>'));
sinon.assert.calledWith(
sendMessage,
'<stream:stream to="google.com" version="1.0" xmlns="jabber:client" ' +
'xmlns:stream="http://etherx.jabber.org/streams">');
sendMessage.reset();
loginHandler.onDataReceived(base.encodeUtf8(
'<stream:stream from="google.com" id="104FA10576E2AA80" version="1.0" ' +
'xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client">' +
'<stream:features><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/>' +
'<session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>' +
'</stream:features>'));
sinon.assert.calledWith(
sendMessage,
'<iq type="set" id="0"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">' +
'<resource>chromoting</resource></bind></iq>');
sendMessage.reset();
loginHandler.onDataReceived(
base.encodeUtf8('<iq id="0" type="result">' +
'<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>' +
testUsername + '/chromoting52B4920E</jid></bind></iq>'));
sinon.assert.calledWith(
sendMessage,
'<iq type="set" id="1"><session ' +
'xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>');
sendMessage.reset();
loginHandler.onDataReceived(base.encodeUtf8('<iq type="result" id="1"/>'));
sinon.assert.calledWith(onHandshakeDone);
});
test('should return AUTHENTICATION_FAILED error when failed to authenticate',
function() {
handshakeBase();
loginHandler.onDataReceived(
base.encodeUtf8('<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl">' +
'<not-authorized/></failure>'));
sinon.assert.calledWith(onError, remoting.Error.AUTHENTICATION_FAILED);
});
test('should return UNEXPECTED error when failed to parse stream',
function() {
loginHandler.start();
loginHandler.onDataReceived(
base.encodeUtf8('BAD DATA'));
sinon.assert.calledWith(onError, remoting.Error.UNEXPECTED);
});
})();
// 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.
(function() {
'use strict';
var onStanzaStr = null;
var onError = null;
var parser = null;
module('XmppStreamParser', {
setup: function() {
onStanzaStr = sinon.spy();
onError = sinon.spy();
function onStanza(stanza) {
onStanzaStr(new XMLSerializer().serializeToString(stanza));
}
parser = new remoting.XmppStreamParser();
parser.setCallbacks(onStanza, onError);
}
});
test('should parse XMPP stream', function() {
parser.appendData(base.encodeUtf8('<stream><iq>text</iq>'));
sinon.assert.calledWith(onStanzaStr, '<iq>text</iq>');
});
test('should handle multiple incoming stanzas', function() {
parser.appendData(base.encodeUtf8('<stream><iq>text</iq><iq>more text</iq>'));
sinon.assert.calledWith(onStanzaStr, '<iq>text</iq>');
sinon.assert.calledWith(onStanzaStr, '<iq>more text</iq>');
});
test('should ignore whitespace between stanzas', function() {
parser.appendData(base.encodeUtf8('<stream> <iq>text</iq>'));
sinon.assert.calledWith(onStanzaStr, '<iq>text</iq>');
});
test('should assemble messages from small chunks', function() {
parser.appendData(base.encodeUtf8('<stream><i'));
parser.appendData(base.encodeUtf8('q>'));
// Split one UTF-8 sequence into two chunks
var data = base.encodeUtf8('😃');
parser.appendData(data.slice(0, 2));
parser.appendData(data.slice(2));
parser.appendData(base.encodeUtf8('</iq>'));
sinon.assert.calledWith(onStanzaStr, '<iq>😃</iq>');
});
test('should stop parsing on errors', function() {
parser.appendData(base.encodeUtf8('<stream>error<iq>text</iq>'));
sinon.assert.calledWith(onError);
sinon.assert.notCalled(onStanzaStr);
});
test('should fail on invalid stream header', function() {
parser.appendData(base.encodeUtf8('<stream p=\'>'));
sinon.assert.calledWith(onError);
});
test('should fail on loose text', function() {
parser.appendData(base.encodeUtf8('stream'));
sinon.assert.calledWith(onError);
});
test('should fail on loose text with incomplete UTF-8 sequences', function() {
var buffer = base.encodeUtf8('<stream>ф')
// Crop last byte.
buffer = buffer.slice(0, buffer.byteLength - 1);
parser.appendData(buffer);
sinon.assert.calledWith(onError);
});
test('should fail on incomplete UTF-8 sequences', function() {
var buffer = base.encodeUtf8('<stream><iq>ф')
// Crop last byte.
buffer = buffer.slice(0, buffer.byteLength - 1);
parser.appendData(buffer);
parser.appendData(base.encodeUtf8('</iq>'));
sinon.assert.calledWith(onError);
});
})();
This diff is collapsed.
// 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 calls |onHandshakeDoneCallback| after
* authentication is finished successfully or |onErrorCallback| on error.
*
* See RFC3920 for description of XMPP and authentication handshake.
*
* @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 to call to send
* a message.
* @param {function():void} startTlsCallback Callback to call to start TLS on
* the underlying socket.
* @param {function(string, remoting.XmppStreamParser):void}
* onHandshakeDoneCallback Callback to call after authentication is
* completed successfully
* @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,
onErrorCallback) {
/** @private */
this.server_ = server;
/** @private */
this.username_ = username;
/** @private */
this.authToken_ = authToken;
/** @private */
this.sendMessageCallback_ = sendMessageCallback;
/** @private */
this.startTlsCallback_ = startTlsCallback;
/** @private */
this.onHandshakeDoneCallback_ = onHandshakeDoneCallback;
/** @private */
this.onErrorCallback_ = onErrorCallback;
/** @private */
this.state_ = remoting.XmppLoginHandler.State.INIT;
/** @private */
this.jid_ = '';
/** @type {remoting.XmppStreamParser} @private */
this.streamParser_ = null;
}
/**
* States the handshake goes through. States are iterated from INIT to DONE
* sequentially, except for ERROR state which may be accepted at any point.
*
* Following messages are sent/received in each state:
* INIT
* client -> server: Stream header
* START_SENT
* client <- server: Stream header with list of supported features which
* should include starttls.
* client -> server: <starttls>
* STARTTLS_SENT
* client <- server: <proceed>
* STARTING_TLS
* TLS handshake
* client -> server: Stream header
* START_SENT_AFTER_TLS
* client <- server: Stream header with list of supported authentication
* methods which is expected to include X-OAUTH2
* client -> server: <auth> message with the OAuth2 token.
* AUTH_SENT
* client <- server: <success> or <failure>
* client -> server: Stream header
* AUTH_ACCEPTED
* client <- server: Stream header with list of features that should
* include <bind>.
* client -> server: <bind>
* BIND_SENT
* client <- server: <bind> result with JID.
* client -> server: <iq><session/></iq> to start the session
* SESSION_IQ_SENT
* client <- server: iq result
* DONE
*
* @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,
DONE: 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) {
base.debug.assert(this.state_ != remoting.XmppLoginHandler.State.INIT &&
this.state_ != remoting.XmppLoginHandler.State.DONE &&
this.state_ != remoting.XmppLoginHandler.State.ERROR);
this.streamParser_.appendData(data);
}
/**
* @param {Element} stanza
* @private
*/
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" ' +
'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.");
}
break;
case remoting.XmppLoginHandler.State.BIND_SENT:
var jidElement = stanza.querySelector('iq>bind>jid');
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.DONE;
this.onHandshakeDoneCallback_(this.jid_, this.streamParser_);
break;
default:
base.debug.assert(false);
break;
}
}
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_();
};
/**
* @param {string} text
* @private
*/
remoting.XmppLoginHandler.prototype.onParserError_ = function(text) {
this.onError_(remoting.Error.UNEXPECTED, text);
}
/**
* @private
*/
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.streamParser_.setCallbacks(this.onStanza_.bind(this),
this.onParserError_.bind(this));
}
/**
* @param {remoting.Error} error
* @param {string} text
* @private
*/
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);
}
}
// 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 || {};
/**
* XmppStreamParser is used to parse XMPP stream. Data is fed to the parser
* using appendData() method and it calls |onStanzaCallback| and
* |onErrorCallback| specified using setCallbacks().
*
* @constructor
*/
remoting.XmppStreamParser = function() {
/** @type {function(Element):void} @private */
this.onStanzaCallback_ = function(stanza) {};
/** @type {function(string):void} @private */
this.onErrorCallback_ = function(error) {};
/**
* Buffer containing the data that has been received but haven't been parsed.
* @private
*/
this.data_ = new ArrayBuffer(0);
/**
* Current depth in the XML stream.
* @private
*/
this.depth_ = 0;
/**
* Set to true after error.
* @private
*/
this.error_ = false;
/**
* The <stream> opening tag received at the beginning of the stream.
* @private
*/
this.startTag_ = '';
/**
* Closing tag matching |startTag_|.
* @private
*/
this.startTagEnd_ = '';
/**
* String containing current incomplete stanza.
* @private
*/
this.currentStanza_ = '';
}
/**
* Sets callbacks to be called on incoming stanzas and on error.
*
* @param {function(Element):void} onStanzaCallback
* @param {function(string):void} onErrorCallback
*/
remoting.XmppStreamParser.prototype.setCallbacks =
function(onStanzaCallback, onErrorCallback) {
this.onStanzaCallback_ = onStanzaCallback;
this.onErrorCallback_ = onErrorCallback;
}
/** @param {ArrayBuffer} data */
remoting.XmppStreamParser.prototype.appendData = function(data) {
base.debug.assert(!this.error_);
if (this.data_.byteLength > 0) {
// Concatenate two buffers.
var newData = new Uint8Array(this.data_.byteLength + data.byteLength);
newData.set(new Uint8Array(this.data_), 0);
newData.set(new Uint8Array(data), this.data_.byteLength);
this.data_ = newData.buffer;
} else {
this.data_ = data;
}
// Check if the newly appended data completes XML tag or a piece of text by
// looking for '<' and '>' char codes. This has to be done before converting
// data to string because the input may not contain complete UTF-8 sequence.
var tagStartCode = '<'.charCodeAt(0);
var tagEndCode = '>'.charCodeAt(0);
var spaceCode = ' '.charCodeAt(0);
var tryAgain = true;
while (this.data_.byteLength > 0 && tryAgain && !this.error_) {
tryAgain = false;
// If we are not currently in a middle of a stanza then skip spaces (server
// may send spaces periodically as heartbeats) and make sure that the first
// character starts XML tag.
if (this.depth_ <= 1) {
var view = new DataView(this.data_);
var firstChar = view.getUint8(0);
if (firstChar == spaceCode) {
tryAgain = true;
this.data_ = this.data_.slice(1);
continue;
} else if (firstChar != tagStartCode) {
var dataAsText = '';
try {
dataAsText = base.decodeUtf8(this.data_);
} catch (exception) {
dataAsText = 'charCode = ' + firstChar;
}
this.processError_('Received unexpected text data: ' + dataAsText);
return;
}
}
// Iterate over characters in the buffer to find complete tags.
var view = new DataView(this.data_);
for (var i = 0; i < view.byteLength; ++i) {
var currentChar = view.getUint8(i);
if (currentChar == tagStartCode) {
if (i > 0) {
var text = this.extractStringFromBuffer_(i);
if (text == null)
return;
this.processText_(text);
tryAgain = true;
break;
}
} else if (currentChar == tagEndCode) {
var tag = this.extractStringFromBuffer_(i + 1);
if (tag == null)
return;
if (tag.charAt(0) != '<') {
this.processError_('Received \'>\' without \'<\': ' + tag);
return;
}
this.processTag_(tag);
tryAgain = true;
break;
}
}
}
}
/**
* @param {string} text
* @private
*/
remoting.XmppStreamParser.prototype.processText_ = function(text) {
// Tokenization code in appendData() shouldn't allow text tokens in between
// stanzas.
base.debug.assert(this.depth_ > 1);
this.currentStanza_ += text;
}
/**
* @param {string} tag
* @private
*/
remoting.XmppStreamParser.prototype.processTag_ = function(tag) {
base.debug.assert(tag.charAt(0) == '<');
base.debug.assert(tag.charAt(tag.length - 1) == '>');
this.currentStanza_ += tag;
var openTag = tag.charAt(1) != '/';
if (openTag) {
++this.depth_;
if (this.depth_ == 1) {
this.startTag_ = this.currentStanza_;
this.currentStanza_ = '';
// Create end tag matching the start.
var tagName =
this.startTag_.substr(1, this.startTag_.length - 2).split(' ', 1)[0];
this.startTagEnd_ = '</' + tagName + '>';
// Try parsing start together with the end
var parsed = this.parseTag_(this.startTag_ + this.startTagEnd_);
if (!parsed) {
this.processError_('Failed to parse start tag: ' + this.startTag_);
return;
}
}
}
var closingTag =
(tag.charAt(1) == '/') || (tag.charAt(tag.length - 2) == '/');
if (closingTag) {
// The first start tag is not expected to be closed.
if (this.depth_ <= 1) {
this.processError_('Unexpected closing tag: ' + tag)
return;
}
--this.depth_;
if (this.depth_ == 1) {
this.processCompleteStanza_();
this.currentStanza_ = '';
}
}
}
/**
* @private
*/
remoting.XmppStreamParser.prototype.processCompleteStanza_ = function() {
var stanza = this.startTag_ + this.currentStanza_ + this.startTagEnd_;
var parsed = this.parseTag_(stanza);
if (!parsed) {
this.processError_('Failed to parse stanza: ' + this.currentStanza_);
return;
}
this.onStanzaCallback_(parsed.firstElementChild);
}
/**
* @param {string} text
* @private
*/
remoting.XmppStreamParser.prototype.processError_ = function(text) {
this.onErrorCallback_(text);
this.error_ = true;
}
/**
* Helper to extract and decode |bytes| bytes from |data_|. Returns NULL in case
* the buffer contains invalidUTF-8.
*
* @param {number} bytes Specifies how many bytes should be extracted.
* @returns {string?}
* @private
*/
remoting.XmppStreamParser.prototype.extractStringFromBuffer_ = function(bytes) {
var result = '';
try {
result = base.decodeUtf8(this.data_.slice(0, bytes));
} catch (exception) {
this.processError_('Received invalid UTF-8 data.');
result = null;
}
this.data_ = this.data_.slice(bytes);
return result;
}
/**
* @param {string} text
* @return {Element}
* @private
*/
remoting.XmppStreamParser.prototype.parseTag_ = function(text) {
/** @type {Document} */
var result = new DOMParser().parseFromString(text, 'text/xml');
if (result.querySelector('parsererror') != null)
return null;
return result.firstElementChild;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment