Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1814)

Unified Diff: remoting/webapp/xmpp_stream_parser.js

Issue 514343002: XMPP implementation in JavaScript. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « remoting/webapp/xmpp_login_handler.js ('k') | no next file » | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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;
+}
« no previous file with comments | « remoting/webapp/xmpp_login_handler.js ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698