| 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..9a9881f2fcb863ce14993dacbf519a11c67a0b6b
|
| --- /dev/null
|
| +++ b/remoting/webapp/xmpp_stream_parser.js
|
| @@ -0,0 +1,206 @@
|
| +// 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 split data received over XMPP connection into
|
| + * individual stanzas. Data is fed to the parser using appendData() method.
|
| + * onStartCallback is called once for the stream header and after that
|
| + * onStanzaCallback is called for each complete stanza.
|
| + *
|
| + * @param {function(Element):void} onStanzaCallback
|
| + * @param {function(string):void} onErrorCallback
|
| + * @constructor
|
| + */
|
| +remoting.XmppStreamParser = function(onStanzaCallback, onErrorCallback) {
|
| + this.onStanzaCallback_ = onStanzaCallback;
|
| + this.onErrorCallback_ = onErrorCallback;
|
| +
|
| +
|
| + /** @type {Array.<ArrayBuffer>} */
|
| + this.data_ = [];
|
| + this.depth_ = 0;
|
| + this.error_ = false;
|
| + this.startTag_ = '';
|
| + this.startTagEnd_ = '';
|
| + this.currentStanza_ = '';
|
| +}
|
| +
|
| +/** @param {ArrayBuffer} data */
|
| +remoting.XmppStreamParser.prototype.appendData = function(data) {
|
| + base.debug.assert(!this.error_);
|
| +
|
| + this.data_.push(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_.length > 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_[0]);
|
| + var firstChar = view.getUint8(0);
|
| + if (firstChar == spaceCode) {
|
| + tryAgain = true;
|
| + this.extractToken_(1);
|
| + continue;
|
| + } else if (firstChar != tagStartCode) {
|
| + this.processError_('Received unexpected text data: ' +
|
| + base.decodeUtf8(this.data_[0]));
|
| + return;
|
| + }
|
| + }
|
| +
|
| + var view = new DataView(this.data_[this.data_.length - 1]);
|
| + for (var i = 0; i < view.byteLength; ++i) {
|
| + var currentChar = view.getUint8(i);
|
| + if (currentChar == tagStartCode) {
|
| + if (this.data_.length > 1 || i > 0) {
|
| + this.processText_(this.extractToken_(i));
|
| + tryAgain = true;
|
| + break;
|
| + }
|
| + } else if (currentChar == tagEndCode) {
|
| + var tag = this.extractToken_(i + 1);
|
| + if (tag.charAt(0) != '<') {
|
| + this.processError_('Received \'>\' without \'<\': ' + tag);
|
| + return;
|
| + }
|
| + this.processTag_(tag);
|
| + tryAgain = true;
|
| + break;
|
| + }
|
| + }
|
| + }
|
| +}
|
| +
|
| +/** @param {string} text */
|
| +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 */
|
| +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_ = '';
|
| + }
|
| + }
|
| +}
|
| +
|
| +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.childNodes[0]);
|
| +}
|
| +
|
| +/** @param {string} text */
|
| +remoting.XmppStreamParser.prototype.processError_ = function(text) {
|
| + this.onErrorCallback_(text);
|
| + this.error_ = true;
|
| +}
|
| +
|
| +/**
|
| + * Helper that assembles block of data from pieces in this.data_ up to specified
|
| + * position in the last buffer and then removes that data from this.data_ .
|
| + *
|
| + * @param {number} endPos Specifies how many bytes should be taken from the last
|
| + * buffer in this.data_ .
|
| + * @returns {string}
|
| + */
|
| +remoting.XmppStreamParser.prototype.extractToken_ = function(endPos) {
|
| + var size = endPos;
|
| + for (var i = 0; i < this.data_.length - 1; ++i) {
|
| + size += this.data_[i].byteLength;
|
| + }
|
| +
|
| + var buffer = new Uint8Array(size);
|
| + var pos = 0;
|
| + for (var i = 0; i < this.data_.length - 1; ++i) {
|
| + buffer.set(new Uint8Array(this.data_[i]), pos);
|
| + pos += this.data_[i].byteLength;
|
| + }
|
| + if (endPos > 0) {
|
| + buffer.set(new Uint8Array(this.data_[this.data_.length - 1], 0, endPos),
|
| + pos);
|
| + pos += endPos;
|
| + }
|
| + base.debug.assert(pos == size);
|
| +
|
| + // Remove copied data from |data_|.
|
| + var dataLeft = this.data_[this.data_.length - 1].slice(endPos);
|
| + if (dataLeft.byteLength == 0) {
|
| + this.data_ = []
|
| + } else {
|
| + this.data_ = [dataLeft];
|
| + }
|
| +
|
| + return base.decodeUtf8(buffer.buffer);
|
| +}
|
| +
|
| +/**
|
| + * @param {string} text
|
| + * @return {Element}
|
| + */
|
| +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.firstChild;
|
| +}
|
|
|