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