• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} */
13var remoting = remoting || {};
14
15/**
16 * @constructor
17 */
18remoting.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 */
32remoting.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 */
53remoting.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 */
65remoting.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 */
93remoting.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 */
116remoting.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 */
177remoting.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 */
253remoting.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 */
277remoting.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 */
333remoting.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 */
353remoting.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 */
394remoting.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 */
414remoting.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 */
454remoting.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 */
484remoting.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 */
556remoting.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 */
608remoting.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 */
680remoting.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 */
699remoting.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} */
709remoting.formatIq = null;
710