| Index: remoting/webapp/xmpp_stream_parser.js
|
| diff --git a/remoting/webapp/xmpp_stream_parser.js b/remoting/webapp/xmpp_stream_parser.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..918caf01051f0bbe8bb9b4618ea92fc3d9703cc4
|
| --- /dev/null
|
| +++ b/remoting/webapp/xmpp_stream_parser.js
|
| @@ -0,0 +1,258 @@
|
| +// 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;
|
| +}
|
|
|