// Copyright (c) 2012 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.
/**
* @fileoverview
* Module to format IQ messages so they can be displayed in the debug log.
*/
'use strict';
/** @suppress {duplicate} */
var remoting = remoting || {};
/**
* @constructor
*/
remoting.FormatIq = function() {
this.clientJid = '';
this.hostJid = '';
};
/**
* Verify that the only attributes on the given |node| are those specified
* in the |attrs| string.
*
* @param {Node} node The node to verify.
* @param {string} validAttrs Comma-separated list of valid attributes.
*
* @return {boolean} True if the node contains only valid attributes.
*/
remoting.FormatIq.prototype.verifyAttributes = function(node, validAttrs) {
var attrs = ',' + validAttrs + ',';
var len = node.attributes.length;
for (var i = 0; i < len; i++) {
/** @type {Node} */
var attrNode = node.attributes[i];
var attr = attrNode.nodeName;
if (attrs.indexOf(',' + attr + ',') == -1) {
return false;
}
}
return true;
};
/**
* Record the client and host JIDs so that we can check them against the
* params in the IQ packets.
*
* @param {string} clientJid The client JID string.
* @param {string} hostJid The host JID string.
*/
remoting.FormatIq.prototype.setJids = function(clientJid, hostJid) {
this.clientJid = clientJid;
this.hostJid = hostJid;
};
/**
* Calculate the 'pretty' version of data from the |server| node.
*
* @param {Node} server Xml node with server info.
*
* @return {?string} Formatted server string. Null if error.
*/
remoting.FormatIq.prototype.calcServerString = function(server) {
if (!this.verifyAttributes(server, 'host,udp,tcp,tcpssl')) {
return null;
}
var host = server.getAttribute('host');
var udp = server.getAttribute('udp');
var tcp = server.getAttribute('tcp');
var tcpssl = server.getAttribute('tcpssl');
var str = "'" + host + "'";
if (udp)
str += ' udp:' + udp;
if (tcp)
str += ' tcp:' + tcp;
if (tcpssl)
str += ' tcpssl:' + tcpssl;
str += '; ';
return str;
};
/**
* Calc the 'pretty' version of channel data.
*
* @param {Node} channel Xml node with channel info.
*
* @return {?string} Formatted channel string. Null if error.
*/
remoting.FormatIq.prototype.calcChannelString = function(channel) {
var name = channel.nodeName;
if (!this.verifyAttributes(channel, 'transport,version,codec')) {
return null;
}
var transport = channel.getAttribute('transport');
var version = channel.getAttribute('version');
var str = name + ' ' + transport + ' v' + version;
if (name == 'video') {
str += ' codec=' + channel.getAttribute('codec');
}
str += '; ';
return str;
};
/**
* Pretty print the jingleinfo from the given Xml node.
*
* @param {Node} query Xml query node with jingleinfo in the child nodes.
*
* @return {?string} Pretty version of jingleinfo. Null if error.
*/
remoting.FormatIq.prototype.prettyJingleinfo = function(query) {
var nodes = query.childNodes;
var stun_servers = '';
var result = '';
for (var i = 0; i < nodes.length; i++) {
/** @type {Node} */
var node = nodes[i];
var name = node.nodeName;
if (name == 'stun') {
var sserver = '';
var stun_nodes = node.childNodes;
for(var s = 0; s < stun_nodes.length; s++) {
/** @type {Node} */
var stun_node = stun_nodes[s];
var sname = stun_node.nodeName;
if (sname == 'server') {
var stun_str = this.calcServerString(stun_node);
if (!stun_str) {
return null;
}
sserver += stun_str;
}
}
result += '\n stun ' + sserver;
} else if (name == 'relay') {
var token = '';
var rserver = '';
var relay_nodes = node.childNodes;
for(var r = 0; r < relay_nodes.length; r++) {
/** @type {Node} */
var relay_node = relay_nodes[r];
var rname = relay_node.nodeName;
if (rname == 'token') {
token = token + relay_node.textContent;
}
if (rname == 'server') {
var relay_str = this.calcServerString(relay_node);
if (!relay_str) {
return null;
}
rserver += relay_str;
}
}
result += '\n relay ' + rserver + ' token: ' + token;
} else {
return null;
}
}
return result;
};
/**
* Pretty print the session-initiate or session-accept info from the given
* Xml node.
*
* @param {Node} jingle Xml node with jingle session-initiate or session-accept
* info contained in child nodes.
*
* @return {?string} Pretty version of jingle stanza. Null if error.
*/
remoting.FormatIq.prototype.prettySessionInitiateAccept = function(jingle) {
if (jingle.childNodes.length != 1) {
return null;
}
var content = jingle.firstChild;
if (content.nodeName != 'content') {
return null;
}
var content_children = content.childNodes;
var result = '';
for (var c = 0; c < content_children.length; c++) {
/** @type {Node} */
var content_child = content_children[c];
var cname = content_child.nodeName;
if (cname == 'description') {
var channels = '';
var resolution = '';
var auth = '';
var desc_children = content_child.childNodes;
for (var d = 0; d < desc_children.length; d++) {
/** @type {Node} */
var desc = desc_children[d];
var dname = desc.nodeName;
if (dname == 'control' || dname == 'event' || dname == 'video') {
var channel_str = this.calcChannelString(desc);
if (!channel_str) {
return null;
}
channels += channel_str;
} else if (dname == 'initial-resolution') {
resolution = desc.getAttribute('width') + 'x' +
desc.getAttribute('height');
} else if (dname == 'authentication') {
var auth_children = desc.childNodes;
for (var a = 0; a < auth_children.length; a++) {
/** @type {Node} */
var auth_info = auth_children[a];
if (auth_info.nodeName == 'auth-token') {
auth = auth + ' (auth-token) ' + auth_info.textContent;
} else if (auth_info.nodeName == 'certificate') {
auth = auth + ' (certificate) ' + auth_info.textContent;
} else if (auth_info.nodeName == 'master-key') {
auth = auth + ' (master-key) ' + auth_info.textContent;
} else {
return null;
}
}
} else {
return null;
}
}
result += '\n channels: ' + channels;
result += '\n auth: ' + auth;
result += '\n initial resolution: ' + resolution;
} else if (cname == 'transport') {
// The 'transport' node is currently empty.
var transport_children = content_child.childNodes;
if (transport_children.length != 0) {
return null;
}
} else {
return null;
}
}
return result;
};
/**
* Pretty print the session-terminate info from the given Xml node.
*
* @param {Node} jingle Xml node with jingle session-terminate info contained in
* child nodes.
*
* @return {?string} Pretty version of jingle session-terminate stanza. Null if
* error.
*/
remoting.FormatIq.prototype.prettySessionTerminate = function(jingle) {
if (jingle.childNodes.length != 1) {
return null;
}
var reason = jingle.firstChild;
if (reason.nodeName != 'reason' || reason.childNodes.length != 1) {
return null;
}
var info = reason.firstChild;
if (info.nodeName == 'success' || info.nodeName == 'general-error') {
return '\n reason=' + info.nodeName;
}
return null;
};
/**
* Pretty print the transport-info info from the given Xml node.
*
* @param {Node} jingle Xml node with jingle transport info contained in child
* nodes.
*
* @return {?string} Pretty version of jingle transport-info stanza. Null if
* error.
*/
remoting.FormatIq.prototype.prettyTransportInfo = function(jingle) {
if (jingle.childNodes.length != 1) {
return null;
}
var content = jingle.firstChild;
if (content.nodeName != 'content') {
return null;
}
var transport = content.firstChild;
if (transport.nodeName != 'transport') {
return null;
}
var transport_children = transport.childNodes;
var result = '';
for (var t = 0; t < transport_children.length; t++) {
/** @type {Node} */
var candidate = transport_children[t];
if (candidate.nodeName != 'candidate') {
return null;
}
if (!this.verifyAttributes(candidate, 'name,address,port,preference,' +
'username,protocol,generation,password,type,' +
'network')) {
return null;
}
var name = candidate.getAttribute('name');
var address = candidate.getAttribute('address');
var port = candidate.getAttribute('port');
var pref = candidate.getAttribute('preference');
var username = candidate.getAttribute('username');
var protocol = candidate.getAttribute('protocol');
var generation = candidate.getAttribute('generation');
var password = candidate.getAttribute('password');
var type = candidate.getAttribute('type');
var network = candidate.getAttribute('network');
var info = name + ': ' + address + ':' + port + ' ' + protocol +
' name:' + username + ' pwd:' + password +
' pref:' + pref +
' ' + type;
if (network) {
info = info + " network:'" + network + "'";
}
result += '\n ' + info;
}
return result;
};
/**
* Pretty print the jingle action contained in the given Xml node.
*
* @param {Node} jingle Xml node with jingle action contained in child nodes.
* @param {string} action String containing the jingle action.
*
* @return {?string} Pretty version of jingle action stanze. Null if error.
*/
remoting.FormatIq.prototype.prettyJingleAction = function(jingle, action) {
if (action == 'session-initiate' || action == 'session-accept') {
return this.prettySessionInitiateAccept(jingle);
}
if (action == 'session-terminate') {
return this.prettySessionTerminate(jingle);
}
if (action == 'transport-info') {
return this.prettyTransportInfo(jingle);
}
return null;
};
/**
* Pretty print the jingle error information contained in the given Xml node.
*
* @param {Node} error Xml node containing error information in child nodes.
*
* @return {?string} Pretty version of error stanze. Null if error.
*/
remoting.FormatIq.prototype.prettyError = function(error) {
if (!this.verifyAttributes(error, 'xmlns:err,code,type,err:hostname,' +
'err:bnsname,err:stacktrace')) {
return null;
}
var code = error.getAttribute('code');
var type = error.getAttribute('type');
var hostname = error.getAttribute('err:hostname');
var bnsname = error.getAttribute('err:bnsname');
var stacktrace = error.getAttribute('err:stacktrace');
var result = '\n error ' + code + ' ' + type + " hostname:'" +
hostname + "' bnsname:'" + bnsname + "'";
var children = error.childNodes;
for (var i = 0; i < children.length; i++) {
/** @type {Node} */
var child = children[i];
result += '\n ' + child.nodeName;
}
if (stacktrace) {
var stack = stacktrace.split(' | ');
result += '\n stacktrace:';
// We use 'length-1' because the stack trace ends with " | " which results
// in an empty string at the end after the split.
for (var s = 0; s < stack.length - 1; s++) {
result += '\n ' + stack[s];
}
}
return result;
};
/**
* Print out the heading line for an iq node.
*
* @param {string} action String describing action (send/receive).
* @param {string} id Packet id.
* @param {string} desc Description of iq action for this node.
* @param {string|null} sid Session id.
*
* @return {string} Pretty version of stanza heading info.
*/
remoting.FormatIq.prototype.prettyIqHeading = function(action, id, desc,
sid) {
var message = 'iq ' + action + ' id=' + id;
if (desc) {
message = message + ' ' + desc;
}
if (sid) {
message = message + ' sid=' + sid;
}
return message;
};
/**
* Print out an iq 'result'-type node.
*
* @param {string} action String describing action (send/receive).
* @param {NodeList} iq_list Node list containing the 'result' xml.
*
* @return {?string} Pretty version of Iq result stanza. Null if error.
*/
remoting.FormatIq.prototype.prettyIqResult = function(action, iq_list) {
/** @type {Node} */
var iq = iq_list[0];
var id = iq.getAttribute('id');
var iq_children = iq.childNodes;
if (iq_children.length == 0) {
return this.prettyIqHeading(action, id, 'result (empty)', null);
} else if (iq_children.length == 1) {
/** @type {Node} */
var child = iq_children[0];
if (child.nodeName == 'query') {
if (!this.verifyAttributes(child, 'xmlns')) {
return null;
}
var xmlns = child.getAttribute('xmlns');
if (xmlns == 'google:jingleinfo') {
var result = this.prettyIqHeading(action, id, 'result ' + xmlns, null);
result += this.prettyJingleinfo(child);
return result;
}
return '';
} else if (child.nodeName == 'rem:log-result') {
if (!this.verifyAttributes(child, 'xmlns:rem')) {
return null;
}
return this.prettyIqHeading(action, id, 'result (log-result)', null);
}
}
return null;
};
/**
* Print out an Iq 'get'-type node.
*
* @param {string} action String describing action (send/receive).
* @param {NodeList} iq_list Node containing the 'get' xml.
*
* @return {?string} Pretty version of Iq get stanza. Null if error.
*/
remoting.FormatIq.prototype.prettyIqGet = function(action, iq_list) {
/** @type {Node} */
var iq = iq_list[0];
var id = iq.getAttribute('id');
var iq_children = iq.childNodes;
if (iq_children.length != 1) {
return null;
}
/** @type {Node} */
var query = iq_children[0];
if (query.nodeName != 'query') {
return null;
}
if (!this.verifyAttributes(query, 'xmlns')) {
return null;
}
var xmlns = query.getAttribute('xmlns');
return this.prettyIqHeading(action, id, 'get ' + xmlns, null);
};
/**
* Print out an iq 'set'-type node.
*
* @param {string} action String describing action (send/receive).
* @param {NodeList} iq_list Node containing the 'set' xml.
*
* @return {?string} Pretty version of Iq set stanza. Null if error.
*/
remoting.FormatIq.prototype.prettyIqSet = function(action, iq_list) {
/** @type {Node} */
var iq = iq_list[0];
var id = iq.getAttribute('id');
var iq_children = iq.childNodes;
var children = iq_children.length;
if (children == 1) {
/** @type {Node} */
var child = iq_children[0];
if (child.nodeName == 'gr:log') {
var grlog = child;
if (!this.verifyAttributes(grlog, 'xmlns:gr')) {
return null;
}
if (grlog.childNodes.length != 1) {
return null;
}
var grentry = grlog.firstChild;
if (grentry.nodeName != 'gr:entry') {
return null;
}
if (!this.verifyAttributes(grentry, 'role,event-name,session-state,' +
'os-name,cpu,browser-version,' +
'webapp-version')) {
return null;
}
var role = grentry.getAttribute('role');
var event_name = grentry.getAttribute('event-name');
var session_state = grentry.getAttribute('session-state');
var os_name = grentry.getAttribute('os-name');
var cpu = grentry.getAttribute('cpu');
var browser_version = grentry.getAttribute('browser-version');
var webapp_version = grentry.getAttribute('webapp-version');
var result = this.prettyIqHeading(action, id, role + ' ' + event_name +
' ' + session_state, null);
result += '\n ' + os_name + ' ' + cpu + " browser:" + browser_version +
" webapp:" + webapp_version;
return result;
}
if (child.nodeName == 'jingle') {
var jingle = child;
if (!this.verifyAttributes(jingle, 'xmlns,action,sid,initiator')) {
return null;
}
var jingle_action = jingle.getAttribute('action');
var sid = jingle.getAttribute('sid');
var result = this.prettyIqHeading(action, id, 'set ' + jingle_action,
sid);
var action_str = this.prettyJingleAction(jingle, jingle_action);
if (!action_str) {
return null;
}
return result + action_str;
}
}
return null;
};
/**
* Print out an iq 'error'-type node.
*
* @param {string} action String describing action (send/receive).
* @param {NodeList} iq_list Node containing the 'error' xml.
*
* @return {?string} Pretty version of iq error stanza. Null if error parsing
* this stanza.
*/
remoting.FormatIq.prototype.prettyIqError = function(action, iq_list) {
/** @type {Node} */
var iq = iq_list[0];
var id = iq.getAttribute('id');
var iq_children = iq.childNodes;
var children = iq_children.length;
if (children != 2) {
return null;
}
/** @type {Node} */
var jingle = iq_children[0];
if (jingle.nodeName != 'jingle') {
return null;
}
if (!this.verifyAttributes(jingle, 'xmlns,action,sid,initiator')) {
return null;
}
var jingle_action = jingle.getAttribute('action');
var sid = jingle.getAttribute('sid');
var result = this.prettyIqHeading(action, id, 'error from ' + jingle_action,
sid);
var action_str = this.prettyJingleAction(jingle, jingle_action);
if (!action_str) {
return null;
}
result += action_str;
/** @type {Node} */
var error = iq_children[1];
if (error.nodeName != 'cli:error') {
return null;
}
var error_str = this.prettyError(error);
if (!error_str) {
return null;
}
result += error_str;
return result;
};
/**
* Try to log a pretty-print the given IQ stanza (XML).
* Return true if the stanza was successfully printed.
*
* @param {boolean} send True if we're sending this stanza; false for receiving.
* @param {string} message The XML stanza to add to the log.
*
* @return {?string} Pretty version of the Iq stanza. Null if error.
*/
remoting.FormatIq.prototype.prettyIq = function(send, message) {
var parser = new DOMParser();
var xml = parser.parseFromString(message, 'text/xml');
var iq_list = xml.getElementsByTagName('iq');
if (iq_list && iq_list.length > 0) {
/** @type {Node} */
var iq = iq_list[0];
if (!this.verifyAttributes(iq, 'xmlns,xmlns:cli,id,to,from,type'))
return null;
// Verify that the to/from fields match the expected sender/receiver.
var to = iq.getAttribute('to');
var from = iq.getAttribute('from');
var action = '';
var bot = remoting.settings.DIRECTORY_BOT_JID;
if (send) {
if (to && to != this.hostJid && to != bot) {
console.warn('FormatIq: bad to: ' + to);
return null;
}
if (from && from != this.clientJid) {
console.warn('FormatIq: bad from: ' + from);
return null;
}
action = "send";
if (to == bot) {
action = action + " (to bot)";
}
} else {
if (to && to != this.clientJid) {
console.warn('FormatIq: bad to: ' + to);
return null;
}
if (from && from != this.hostJid && from != bot) {
console.warn('FormatIq: bad from: ' + from);
return null;
}
action = "receive";
if (from == bot) {
action = action + " (from bot)";
}
}
var type = iq.getAttribute('type');
if (type == 'result') {
return this.prettyIqResult(action, iq_list);
} else if (type == 'get') {
return this.prettyIqGet(action, iq_list);
} else if (type == 'set') {
return this.prettyIqSet(action, iq_list);
} else if (type == 'error') {
return this.prettyIqError(action, iq_list);
}
}
return null;
};
/**
* Return a pretty-formatted string for the IQ stanza being sent.
* If the stanza cannot be made pretty, then a string with a raw dump of the
* stanza will be returned.
*
* @param {string} message The XML stanza to make pretty.
*
* @return {string} Pretty version of XML stanza being sent. A raw dump of the
* stanza is returned if there was a parsing error.
*/
remoting.FormatIq.prototype.prettifySendIq = function(message) {
var result = this.prettyIq(true, message);
if (!result) {
// Fall back to showing the raw stanza.
return 'Sending Iq: ' + message;
}
return result;
};
/**
* Return a pretty-formatted string for the IQ stanza that was received.
* If the stanza cannot be made pretty, then a string with a raw dump of the
* stanza will be returned.
*
* @param {string} message The XML stanza to make pretty.
*
* @return {string} Pretty version of XML stanza that was received. A raw dump
* of the stanza is returned if there was a parsing error.
*/
remoting.FormatIq.prototype.prettifyReceiveIq = function(message) {
var result = this.prettyIq(false, message);
if (!result) {
// Fall back to showing the raw stanza.
return 'Receiving Iq: ' + message;
}
return result;
};
/** @type {remoting.FormatIq} */
remoting.formatIq = null;