Home | History | Annotate | Download | only in webapp
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview
      7  * Module to format IQ messages so they can be displayed in the debug log.
      8  */
      9 
     10 'use strict';
     11 
     12 /** @suppress {duplicate} */
     13 var remoting = remoting || {};
     14 
     15 /**
     16  * @constructor
     17  */
     18 remoting.FormatIq = function() {
     19   this.clientJid = '';
     20   this.hostJid = '';
     21 };
     22 
     23 /**
     24  * Verify that the only attributes on the given |node| are those specified
     25  * in the |attrs| string.
     26  *
     27  * @param {Node} node The node to verify.
     28  * @param {string} validAttrs Comma-separated list of valid attributes.
     29  *
     30  * @return {boolean} True if the node contains only valid attributes.
     31  */
     32 remoting.FormatIq.prototype.verifyAttributes = function(node, validAttrs) {
     33   var attrs = ',' + validAttrs + ',';
     34   var len = node.attributes.length;
     35   for (var i = 0; i < len; i++) {
     36     /** @type {Node} */
     37     var attrNode = node.attributes[i];
     38     var attr = attrNode.nodeName;
     39     if (attrs.indexOf(',' + attr + ',') == -1) {
     40       return false;
     41     }
     42   }
     43   return true;
     44 };
     45 
     46 /**
     47  * Record the client and host JIDs so that we can check them against the
     48  * params in the IQ packets.
     49  *
     50  * @param {string} clientJid The client JID string.
     51  * @param {string} hostJid The host JID string.
     52  */
     53 remoting.FormatIq.prototype.setJids = function(clientJid, hostJid) {
     54   this.clientJid = clientJid;
     55   this.hostJid = hostJid;
     56 };
     57 
     58 /**
     59  * Calculate the 'pretty' version of data from the |server| node.
     60  *
     61  * @param {Node} server Xml node with server info.
     62  *
     63  * @return {?string} Formatted server string. Null if error.
     64  */
     65 remoting.FormatIq.prototype.calcServerString = function(server) {
     66   if (!this.verifyAttributes(server, 'host,udp,tcp,tcpssl')) {
     67     return null;
     68   }
     69   var host = server.getAttribute('host');
     70   var udp = server.getAttribute('udp');
     71   var tcp = server.getAttribute('tcp');
     72   var tcpssl = server.getAttribute('tcpssl');
     73 
     74   var str = "'" + host + "'";
     75   if (udp)
     76     str += ' udp:' + udp;
     77   if (tcp)
     78     str += ' tcp:' + tcp;
     79   if (tcpssl)
     80     str += ' tcpssl:' + tcpssl;
     81 
     82   str += '; ';
     83   return str;
     84 };
     85 
     86 /**
     87  * Calc the 'pretty' version of channel data.
     88  *
     89  * @param {Node} channel Xml node with channel info.
     90  *
     91  * @return {?string} Formatted channel string. Null if error.
     92  */
     93 remoting.FormatIq.prototype.calcChannelString = function(channel) {
     94   var name = channel.nodeName;
     95   if (!this.verifyAttributes(channel, 'transport,version,codec')) {
     96     return null;
     97   }
     98   var transport = channel.getAttribute('transport');
     99   var version = channel.getAttribute('version');
    100 
    101   var str = name + ' ' + transport + ' v' + version;
    102   if (name == 'video') {
    103     str += ' codec=' + channel.getAttribute('codec');
    104   }
    105   str += '; ';
    106   return str;
    107 };
    108 
    109 /**
    110  * Pretty print the jingleinfo from the given Xml node.
    111  *
    112  * @param {Node} query Xml query node with jingleinfo in the child nodes.
    113  *
    114  * @return {?string} Pretty version of jingleinfo. Null if error.
    115  */
    116 remoting.FormatIq.prototype.prettyJingleinfo = function(query) {
    117   var nodes = query.childNodes;
    118   var stun_servers = '';
    119   var result = '';
    120   for (var i = 0; i < nodes.length; i++) {
    121     /** @type {Node} */
    122     var node = nodes[i];
    123     var name = node.nodeName;
    124     if (name == 'stun') {
    125       var sserver = '';
    126       var stun_nodes = node.childNodes;
    127       for(var s = 0; s < stun_nodes.length; s++) {
    128         /** @type {Node} */
    129         var stun_node = stun_nodes[s];
    130         var sname = stun_node.nodeName;
    131         if (sname == 'server') {
    132           var stun_str = this.calcServerString(stun_node);
    133           if (!stun_str) {
    134             return null;
    135           }
    136           sserver += stun_str;
    137         }
    138       }
    139       result += '\n  stun ' + sserver;
    140     } else if (name == 'relay') {
    141       var token = '';
    142       var rserver = '';
    143       var relay_nodes = node.childNodes;
    144       for(var r = 0; r < relay_nodes.length; r++) {
    145         /** @type {Node} */
    146         var relay_node = relay_nodes[r];
    147         var rname = relay_node.nodeName;
    148         if (rname == 'token') {
    149           token = token + relay_node.textContent;
    150         }
    151         if (rname == 'server') {
    152           var relay_str = this.calcServerString(relay_node);
    153           if (!relay_str) {
    154             return null;
    155           }
    156           rserver += relay_str;
    157         }
    158       }
    159       result += '\n  relay ' + rserver + ' token: ' + token;
    160     } else {
    161       return null;
    162     }
    163   }
    164 
    165   return result;
    166 };
    167 
    168 /**
    169  * Pretty print the session-initiate or session-accept info from the given
    170  * Xml node.
    171  *
    172  * @param {Node} jingle Xml node with jingle session-initiate or session-accept
    173  *                      info contained in child nodes.
    174  *
    175  * @return {?string} Pretty version of jingle stanza. Null if error.
    176  */
    177 remoting.FormatIq.prototype.prettySessionInitiateAccept = function(jingle) {
    178   if (jingle.childNodes.length != 1) {
    179     return null;
    180   }
    181   var content = jingle.firstChild;
    182   if (content.nodeName != 'content') {
    183     return null;
    184   }
    185   var content_children = content.childNodes;
    186   var result = '';
    187   for (var c = 0; c < content_children.length; c++) {
    188     /** @type {Node} */
    189     var content_child = content_children[c];
    190     var cname = content_child.nodeName;
    191     if (cname == 'description') {
    192       var channels = '';
    193       var resolution = '';
    194       var auth = '';
    195       var desc_children = content_child.childNodes;
    196       for (var d = 0; d < desc_children.length; d++) {
    197         /** @type {Node} */
    198         var desc = desc_children[d];
    199         var dname = desc.nodeName;
    200         if (dname == 'control' || dname == 'event' || dname == 'video') {
    201           var channel_str = this.calcChannelString(desc);
    202           if (!channel_str) {
    203             return null;
    204           }
    205           channels += channel_str;
    206         } else if (dname == 'initial-resolution') {
    207           resolution = desc.getAttribute('width') + 'x' +
    208               desc.getAttribute('height');
    209         } else if (dname == 'authentication') {
    210           var auth_children = desc.childNodes;
    211           for (var a = 0; a < auth_children.length; a++) {
    212             /** @type {Node} */
    213             var auth_info = auth_children[a];
    214             if (auth_info.nodeName == 'auth-token') {
    215               auth = auth + ' (auth-token) ' + auth_info.textContent;
    216             } else if (auth_info.nodeName == 'certificate') {
    217               auth = auth + ' (certificate) ' + auth_info.textContent;
    218             } else if (auth_info.nodeName == 'master-key') {
    219               auth = auth + ' (master-key) ' + auth_info.textContent;
    220             } else {
    221               return null;
    222             }
    223           }
    224         } else {
    225           return null;
    226         }
    227       }
    228       result += '\n  channels: ' + channels;
    229       result += '\n  auth: ' + auth;
    230       result += '\n  initial resolution: ' + resolution;
    231     } else if (cname == 'transport') {
    232       // The 'transport' node is currently empty.
    233       var transport_children = content_child.childNodes;
    234       if (transport_children.length != 0) {
    235         return null;
    236       }
    237     } else {
    238       return null;
    239     }
    240   }
    241   return result;
    242 };
    243 
    244 /**
    245  * Pretty print the session-terminate info from the given Xml node.
    246  *
    247  * @param {Node} jingle Xml node with jingle session-terminate info contained in
    248  *                      child nodes.
    249  *
    250  * @return {?string} Pretty version of jingle session-terminate stanza. Null if
    251  *                  error.
    252  */
    253 remoting.FormatIq.prototype.prettySessionTerminate = function(jingle) {
    254   if (jingle.childNodes.length != 1) {
    255     return null;
    256   }
    257   var reason = jingle.firstChild;
    258   if (reason.nodeName != 'reason' || reason.childNodes.length != 1) {
    259     return null;
    260   }
    261   var info = reason.firstChild;
    262   if (info.nodeName == 'success' || info.nodeName == 'general-error') {
    263     return '\n  reason=' + info.nodeName;
    264   }
    265   return null;
    266 };
    267 
    268 /**
    269  * Pretty print the transport-info info from the given Xml node.
    270  *
    271  * @param {Node} jingle Xml node with jingle transport info contained in child
    272  *                      nodes.
    273  *
    274  * @return {?string} Pretty version of jingle transport-info stanza. Null if
    275  *                  error.
    276  */
    277 remoting.FormatIq.prototype.prettyTransportInfo = function(jingle) {
    278   if (jingle.childNodes.length != 1) {
    279     return null;
    280   }
    281   var content = jingle.firstChild;
    282   if (content.nodeName != 'content') {
    283     return null;
    284   }
    285   var transport = content.firstChild;
    286   if (transport.nodeName != 'transport') {
    287     return null;
    288   }
    289   var transport_children = transport.childNodes;
    290   var result = '';
    291   for (var t = 0; t < transport_children.length; t++) {
    292     /** @type {Node} */
    293     var candidate = transport_children[t];
    294     if (candidate.nodeName != 'candidate') {
    295       return null;
    296     }
    297     if (!this.verifyAttributes(candidate, 'name,address,port,preference,' +
    298                                'username,protocol,generation,password,type,' +
    299                                'network')) {
    300       return null;
    301     }
    302     var name = candidate.getAttribute('name');
    303     var address = candidate.getAttribute('address');
    304     var port = candidate.getAttribute('port');
    305     var pref = candidate.getAttribute('preference');
    306     var username = candidate.getAttribute('username');
    307     var protocol = candidate.getAttribute('protocol');
    308     var generation = candidate.getAttribute('generation');
    309     var password = candidate.getAttribute('password');
    310     var type = candidate.getAttribute('type');
    311     var network = candidate.getAttribute('network');
    312 
    313     var info = name + ': ' + address + ':' + port + ' ' + protocol +
    314         ' name:' + username + ' pwd:' + password +
    315         ' pref:' + pref +
    316         ' ' + type;
    317     if (network) {
    318       info = info + " network:'" + network + "'";
    319     }
    320     result += '\n  ' + info;
    321   }
    322   return result;
    323 };
    324 
    325 /**
    326  * Pretty print the jingle action contained in the given Xml node.
    327  *
    328  * @param {Node} jingle Xml node with jingle action contained in child nodes.
    329  * @param {string} action String containing the jingle action.
    330  *
    331  * @return {?string} Pretty version of jingle action stanze. Null if error.
    332  */
    333 remoting.FormatIq.prototype.prettyJingleAction = function(jingle, action) {
    334   if (action == 'session-initiate' || action == 'session-accept') {
    335     return this.prettySessionInitiateAccept(jingle);
    336   }
    337   if (action == 'session-terminate') {
    338     return this.prettySessionTerminate(jingle);
    339   }
    340   if (action == 'transport-info') {
    341     return this.prettyTransportInfo(jingle);
    342   }
    343   return null;
    344 };
    345 
    346 /**
    347  * Pretty print the jingle error information contained in the given Xml node.
    348  *
    349  * @param {Node} error Xml node containing error information in child nodes.
    350  *
    351  * @return {?string} Pretty version of error stanze. Null if error.
    352  */
    353 remoting.FormatIq.prototype.prettyError = function(error) {
    354   if (!this.verifyAttributes(error, 'xmlns:err,code,type,err:hostname,' +
    355                              'err:bnsname,err:stacktrace')) {
    356     return null;
    357   }
    358   var code = error.getAttribute('code');
    359   var type = error.getAttribute('type');
    360   var hostname = error.getAttribute('err:hostname');
    361   var bnsname = error.getAttribute('err:bnsname');
    362   var stacktrace = error.getAttribute('err:stacktrace');
    363 
    364   var result = '\n  error ' + code + ' ' + type + " hostname:'" +
    365              hostname + "' bnsname:'" + bnsname + "'";
    366   var children = error.childNodes;
    367   for (var i = 0; i < children.length; i++) {
    368     /** @type {Node} */
    369     var child = children[i];
    370     result += '\n  ' + child.nodeName;
    371   }
    372   if (stacktrace) {
    373     var stack = stacktrace.split(' | ');
    374     result += '\n  stacktrace:';
    375     // We use 'length-1' because the stack trace ends with " | " which results
    376     // in an empty string at the end after the split.
    377     for (var s = 0; s < stack.length - 1; s++) {
    378       result += '\n    ' + stack[s];
    379     }
    380   }
    381   return result;
    382 };
    383 
    384 /**
    385  * Print out the heading line for an iq node.
    386  *
    387  * @param {string} action String describing action (send/receive).
    388  * @param {string} id Packet id.
    389  * @param {string} desc Description of iq action for this node.
    390  * @param {string|null} sid Session id.
    391  *
    392  * @return {string} Pretty version of stanza heading info.
    393  */
    394 remoting.FormatIq.prototype.prettyIqHeading = function(action, id, desc,
    395                                                        sid) {
    396   var message = 'iq ' + action + ' id=' + id;
    397   if (desc) {
    398     message = message + ' ' + desc;
    399   }
    400   if (sid) {
    401     message = message + ' sid=' + sid;
    402   }
    403   return message;
    404 };
    405 
    406 /**
    407  * Print out an iq 'result'-type node.
    408  *
    409  * @param {string} action String describing action (send/receive).
    410  * @param {NodeList} iq_list Node list containing the 'result' xml.
    411  *
    412  * @return {?string} Pretty version of Iq result stanza. Null if error.
    413  */
    414 remoting.FormatIq.prototype.prettyIqResult = function(action, iq_list) {
    415   /** @type {Node} */
    416   var iq = iq_list[0];
    417   var id = iq.getAttribute('id');
    418   var iq_children = iq.childNodes;
    419 
    420   if (iq_children.length == 0) {
    421     return this.prettyIqHeading(action, id, 'result (empty)', null);
    422   } else if (iq_children.length == 1) {
    423     /** @type {Node} */
    424     var child = iq_children[0];
    425     if (child.nodeName == 'query') {
    426       if (!this.verifyAttributes(child, 'xmlns')) {
    427         return null;
    428       }
    429       var xmlns = child.getAttribute('xmlns');
    430       if (xmlns == 'google:jingleinfo') {
    431         var result = this.prettyIqHeading(action, id, 'result ' + xmlns, null);
    432         result += this.prettyJingleinfo(child);
    433         return result;
    434       }
    435       return '';
    436     } else if (child.nodeName == 'rem:log-result') {
    437       if (!this.verifyAttributes(child, 'xmlns:rem')) {
    438         return null;
    439       }
    440       return this.prettyIqHeading(action, id, 'result (log-result)', null);
    441     }
    442   }
    443   return null;
    444 };
    445 
    446 /**
    447  * Print out an Iq 'get'-type node.
    448  *
    449  * @param {string} action String describing action (send/receive).
    450  * @param {NodeList} iq_list Node containing the 'get' xml.
    451  *
    452  * @return {?string} Pretty version of Iq get stanza. Null if error.
    453  */
    454 remoting.FormatIq.prototype.prettyIqGet = function(action, iq_list) {
    455   /** @type {Node} */
    456   var iq = iq_list[0];
    457   var id = iq.getAttribute('id');
    458   var iq_children = iq.childNodes;
    459 
    460   if (iq_children.length != 1) {
    461     return null;
    462   }
    463 
    464   /** @type {Node} */
    465   var query = iq_children[0];
    466   if (query.nodeName != 'query') {
    467     return null;
    468   }
    469   if (!this.verifyAttributes(query, 'xmlns')) {
    470     return null;
    471   }
    472   var xmlns = query.getAttribute('xmlns');
    473   return this.prettyIqHeading(action, id, 'get ' + xmlns, null);
    474 };
    475 
    476 /**
    477  * Print out an iq 'set'-type node.
    478  *
    479  * @param {string} action String describing action (send/receive).
    480  * @param {NodeList} iq_list Node containing the 'set' xml.
    481  *
    482  * @return {?string} Pretty version of Iq set stanza. Null if error.
    483  */
    484 remoting.FormatIq.prototype.prettyIqSet = function(action, iq_list) {
    485   /** @type {Node} */
    486   var iq = iq_list[0];
    487   var id = iq.getAttribute('id');
    488   var iq_children = iq.childNodes;
    489 
    490   var children = iq_children.length;
    491   if (children == 1) {
    492     /** @type {Node} */
    493     var child = iq_children[0];
    494     if (child.nodeName == 'gr:log') {
    495       var grlog = child;
    496       if (!this.verifyAttributes(grlog, 'xmlns:gr')) {
    497         return null;
    498       }
    499 
    500       if (grlog.childNodes.length != 1) {
    501         return null;
    502       }
    503       var grentry = grlog.firstChild;
    504       if (grentry.nodeName != 'gr:entry') {
    505         return null;
    506       }
    507       if (!this.verifyAttributes(grentry, 'role,event-name,session-state,' +
    508                                  'os-name,cpu,browser-version,' +
    509                                  'webapp-version')) {
    510         return null;
    511       }
    512       var role = grentry.getAttribute('role');
    513       var event_name = grentry.getAttribute('event-name');
    514       var session_state = grentry.getAttribute('session-state');
    515       var os_name = grentry.getAttribute('os-name');
    516       var cpu = grentry.getAttribute('cpu');
    517       var browser_version = grentry.getAttribute('browser-version');
    518       var webapp_version = grentry.getAttribute('webapp-version');
    519 
    520       var result = this.prettyIqHeading(action, id, role + ' ' + event_name +
    521                                         ' ' + session_state, null);
    522       result += '\n  ' + os_name + ' ' + cpu + " browser:" + browser_version +
    523                      " webapp:" + webapp_version;
    524       return result;
    525     }
    526     if (child.nodeName == 'jingle') {
    527       var jingle = child;
    528       if (!this.verifyAttributes(jingle, 'xmlns,action,sid,initiator')) {
    529         return null;
    530       }
    531 
    532       var jingle_action = jingle.getAttribute('action');
    533       var sid = jingle.getAttribute('sid');
    534 
    535       var result = this.prettyIqHeading(action, id, 'set ' + jingle_action,
    536                                         sid);
    537       var action_str = this.prettyJingleAction(jingle, jingle_action);
    538       if (!action_str) {
    539         return null;
    540       }
    541       return result + action_str;
    542     }
    543   }
    544   return null;
    545 };
    546 
    547 /**
    548  * Print out an iq 'error'-type node.
    549  *
    550  * @param {string} action String describing action (send/receive).
    551  * @param {NodeList} iq_list Node containing the 'error' xml.
    552  *
    553  * @return {?string} Pretty version of iq error stanza. Null if error parsing
    554  *                  this stanza.
    555  */
    556 remoting.FormatIq.prototype.prettyIqError = function(action, iq_list) {
    557   /** @type {Node} */
    558   var iq = iq_list[0];
    559   var id = iq.getAttribute('id');
    560   var iq_children = iq.childNodes;
    561 
    562   var children = iq_children.length;
    563   if (children != 2) {
    564     return null;
    565   }
    566 
    567   /** @type {Node} */
    568   var jingle = iq_children[0];
    569   if (jingle.nodeName != 'jingle') {
    570     return null;
    571   }
    572   if (!this.verifyAttributes(jingle, 'xmlns,action,sid,initiator')) {
    573     return null;
    574   }
    575   var jingle_action = jingle.getAttribute('action');
    576   var sid = jingle.getAttribute('sid');
    577   var result = this.prettyIqHeading(action, id, 'error from ' + jingle_action,
    578                                     sid);
    579   var action_str = this.prettyJingleAction(jingle, jingle_action);
    580   if (!action_str) {
    581     return null;
    582   }
    583   result += action_str;
    584 
    585   /** @type {Node} */
    586   var error = iq_children[1];
    587   if (error.nodeName != 'cli:error') {
    588     return null;
    589   }
    590 
    591   var error_str = this.prettyError(error);
    592   if (!error_str) {
    593     return null;
    594   }
    595   result += error_str;
    596   return result;
    597 };
    598 
    599 /**
    600  * Try to log a pretty-print the given IQ stanza (XML).
    601  * Return true if the stanza was successfully printed.
    602  *
    603  * @param {boolean} send True if we're sending this stanza; false for receiving.
    604  * @param {string} message The XML stanza to add to the log.
    605  *
    606  * @return {?string} Pretty version of the Iq stanza. Null if error.
    607  */
    608 remoting.FormatIq.prototype.prettyIq = function(send, message) {
    609   var parser = new DOMParser();
    610   var xml = parser.parseFromString(message, 'text/xml');
    611 
    612   var iq_list = xml.getElementsByTagName('iq');
    613 
    614   if (iq_list && iq_list.length > 0) {
    615     /** @type {Node} */
    616     var iq = iq_list[0];
    617     if (!this.verifyAttributes(iq, 'xmlns,xmlns:cli,id,to,from,type'))
    618       return null;
    619 
    620     // Verify that the to/from fields match the expected sender/receiver.
    621     var to = iq.getAttribute('to');
    622     var from = iq.getAttribute('from');
    623     var action = '';
    624     var bot = remoting.settings.DIRECTORY_BOT_JID;
    625     if (send) {
    626       if (to && to != this.hostJid && to != bot) {
    627         console.warn('FormatIq: bad to: ' + to);
    628         return null;
    629       }
    630       if (from && from != this.clientJid) {
    631         console.warn('FormatIq: bad from: ' + from);
    632         return null;
    633       }
    634 
    635       action = "send";
    636       if (to == bot) {
    637         action = action + " (to bot)";
    638       }
    639     } else {
    640       if (to && to != this.clientJid) {
    641         console.warn('FormatIq: bad to: ' + to);
    642         return null;
    643       }
    644       if (from && from != this.hostJid && from != bot) {
    645         console.warn('FormatIq: bad from: ' + from);
    646         return null;
    647       }
    648 
    649       action = "receive";
    650       if (from == bot) {
    651         action = action + " (from bot)";
    652       }
    653     }
    654 
    655     var type = iq.getAttribute('type');
    656     if (type == 'result') {
    657       return this.prettyIqResult(action, iq_list);
    658     } else if (type == 'get') {
    659       return this.prettyIqGet(action, iq_list);
    660     } else if (type == 'set') {
    661       return this.prettyIqSet(action, iq_list);
    662     } else  if (type == 'error') {
    663       return this.prettyIqError(action, iq_list);
    664     }
    665   }
    666 
    667   return null;
    668 };
    669 
    670 /**
    671  * Return a pretty-formatted string for the IQ stanza being sent.
    672  * If the stanza cannot be made pretty, then a string with a raw dump of the
    673  * stanza will be returned.
    674  *
    675  * @param {string} message The XML stanza to make pretty.
    676  *
    677  * @return {string} Pretty version of XML stanza being sent. A raw dump of the
    678  *                  stanza is returned if there was a parsing error.
    679  */
    680 remoting.FormatIq.prototype.prettifySendIq = function(message) {
    681   var result = this.prettyIq(true, message);
    682   if (!result) {
    683     // Fall back to showing the raw stanza.
    684     return 'Sending Iq: ' + message;
    685   }
    686   return result;
    687 };
    688 
    689 /**
    690  * Return a pretty-formatted string for the IQ stanza that was received.
    691  * If the stanza cannot be made pretty, then a string with a raw dump of the
    692  * stanza will be returned.
    693  *
    694  * @param {string} message The XML stanza to make pretty.
    695  *
    696  * @return {string} Pretty version of XML stanza that was received. A raw dump
    697  *                  of the stanza is returned if there was a parsing error.
    698  */
    699 remoting.FormatIq.prototype.prettifyReceiveIq = function(message) {
    700   var result = this.prettyIq(false, message);
    701   if (!result) {
    702     // Fall back to showing the raw stanza.
    703     return 'Receiving Iq: ' + message;
    704   }
    705   return result;
    706 };
    707 
    708 /** @type {remoting.FormatIq} */
    709 remoting.formatIq = null;
    710