1 Xmpp4Js.Lang.namespace( "Xmpp4Js.Transport" ); 2 3 /** 4 * Functionality that needs testing: 5 * write: 6 * sid 7 * no sid on first request 8 * always present after session has started 9 * rid 10 * always present 11 * starts random 12 * sequential 13 * rollover at int limit 14 * key 15 * present / not present if it should be 16 * first, middle, last, first (init with length 3) 17 * 18 * beginSession: 19 * rid, no sid, correct attributes 20 * error if called when open 21 * event is raised 22 * 23 * endSession: 24 * terminate type and correct attributes are present 25 * error if called while not open 26 * event is raised 27 * 28 * send: 29 * error before beginSession or after endSession 30 * multible nodes are combined to make one request 31 * number of open requests > max open requets 32 * 33 * polling: 34 * doesn't send if there is an open request 35 * doesn't send if there are items in the queue 36 * sends empty body if both are empty 37 */ 38 Xmpp4Js.Transport.Base = function(config) { 39 40 /** 41 * The domain of the server you're connecting to. 42 * @private 43 */ 44 this.domain = config.domain; 45 /** 46 * The hostname or IP of the server to route to. defaults to domain. 47 * @private 48 */ 49 this.server = config.server || config.domain; 50 /** 51 * The port to route to. defaults to 5222 52 * @private 53 */ 54 this.port = config.port || 5222; 55 /** 56 * The time to wait for a response from the server, in seconds. defaults to 45 and can be adjusted by server. 57 * This is 45 because Safari has a 60 second timeout and it's problematic. - 7/2008 58 * @private 59 */ 60 this.wait = config.wait || 45; 61 62 /** 63 * Picked up by Observable. 64 * @private 65 */ 66 this.listeners = config.listeners; 67 68 69 /** 70 * This is set to true when the session creation response is 71 * received and it was successful. 72 * 73 * @private 74 */ 75 this.isSessionOpen = false; 76 77 /** 78 * @type Ext.util.TaskRunner 79 * @private 80 */ 81 this.taskRunner = new Xmpp4Js.Lang.TaskRunner(500); 82 83 /** 84 * @private 85 */ 86 this.sendQueueTask = { 87 scope: this, 88 run: this.sendQueue 89 }; 90 91 /** 92 * @private 93 */ 94 this.sendPollTask = { 95 scope: this, 96 run: this.sendPoll 97 }; 98 99 /** 100 * @private 101 */ 102 this.queue = []; 103 104 /** 105 * The session ID sent by the server. 106 * @private 107 */ 108 this.sid = null; 109 110 /** 111 * The request ID set in beginSession, and cleared in endSession 112 * @private 113 */ 114 this.rid = null; 115 116 /** 117 * The keysequence object 118 * @private 119 */ 120 this.keySeq = config.useKeys ? new KeySequence(25): null; 121 122 /** 123 * The max number of requests that can be open at once, sent by the server 124 * @private 125 */ 126 this.maxRequests = null; 127 128 /** 129 * The max number of requests that the server will keep open, sent by the server. 130 * Typically this is maxRequests - 1. 131 * @private 132 */ 133 this.hold = null; 134 135 /** 136 * 137 * @private 138 */ 139 this.polling = 5; 140 141 /** 142 * The number of open XHR requests. Used for polling. 143 * @private 144 */ 145 this.openRequestCount = 0; 146 147 var superConfig = config; 148 149 150 this.addEvents({ 151 /** 152 * @event recv 153 * @param {DomElement} the body element of the node received. 154 * 155 * A packet node has been received. Typically cline code will register 156 * its recv handlers in response to the sessionStarted event and remove 157 * them in response to the sessionEnded and termerror events. 158 */ 159 recv : true, 160 161 162 /** 163 * @event write 164 * @param {DomElement} the body element of the node about to be written. 165 * 166 * A packet node is about to be written. It includes all frame data, but 167 * the event is fired just before the open request count is incremented. 168 */ 169 write : true, 170 171 172 /** 173 * @event error 174 * @param {DomElement} the body element of the node received. 175 * 176 * A non-terminal error has occured. Connection is not necisarily closed. 177 */ 178 error : true, 179 180 /** 181 * @event termerror 182 * @param {String} title 183 * @param {String} message 184 * @param {DomElement} the body element of the node received. 185 * 186 * Raised when the session is been forcibly closed due to an error. 187 * Client code should remove any recv handlers here (should we remove?) 188 */ 189 termerror : true, 190 191 192 streamerror : true, 193 194 /** 195 * @event sessionStarted 196 * 197 * Raised when the session has successfully be started. Clients should 198 * register recv handlers here. 199 */ 200 beginsession : true, 201 202 /** 203 * @event sessionEnded 204 * 205 * Raised when the session has been closed (voluntarily). Client code 206 * should remove any recv handlers here (should we forcibly remove all?). 207 */ 208 endsession: true, 209 210 beforepause: true, 211 212 pause: true, 213 214 resume: true 215 }); 216 217 Xmpp4Js.Transport.Base.superclass.constructor.call( this, superConfig ); 218 219 } 220 221 Xmpp4Js.Transport.Base.logger = Xmpp4Js.createLogger("xmpp4js.transport.base"); 222 223 Xmpp4Js.Transport.Base.prototype = { 224 225 226 /** 227 * Send a session creation request, and if it is successfully responded to 228 * then mark the session open and start the sendQueueTask. 229 */ 230 beginSession: function() { 231 this.isPausing = false; 232 233 this.rid = this.createInitialRid(); 234 235 var packetNode = this.createPacketNode(); 236 packetNode.setAttribute( "wait", this.wait ); 237 packetNode.setAttribute( "to", this.domain ); 238 packetNode.setAttribute( "route", "xmpp:" + this.server + ":" + this.port); 239 packetNode.setAttribute( "ver", "1.6"); 240 packetNode.setAttribute( "xml:lang", "en"); 241 packetNode.setAttribute( "xmlns:xmpp", "urn:xmpp:xbosh"); 242 packetNode.setAttribute( "xmpp:version", "1.0" ); 243 244 245 this.on("recv", this.onBeginSessionResponse, this, {single:true}); 246 247 this.write( packetNode ); 248 }, 249 250 /** 251 * Callback to the beginSession packet (recv event). 252 * 253 * @param {DomElement} packetNode 254 * @private 255 */ 256 onBeginSessionResponse: function(packetNode) { 257 // HACK single doesn't seem to work... 258 //this.un("recv", arguments.callee /* the current function */, this ); 259 260 261 this.sid = packetNode.getAttribute( "sid" ).toString(); 262 this.maxRequests = packetNode.getAttribute( "requests" ).toString(); 263 264 if( packetNode.hasAttribute("hold") ) { 265 this.hold = packetNode.getAttribute("hold").toString(); 266 } else { 267 // sensible default 268 this.hold = packetNode.maxRequests - 1; 269 } 270 271 if( packetNode.hasAttribute("wait") ) { 272 // FIXME ideally xhr's timeout should be updated 273 this.wait = packetNode.getAttribute("wait").toString(); 274 } 275 276 if( packetNode.hasAttribute("polling") ) { 277 this.polling = packetNode.getAttribute("polling").toString(); 278 } 279 280 ;;; Xmpp4Js.Transport.Base.logger.debug( "Get beginSession response. Session ID="+this.sid+", hold="+this.hold+", wait="+this.wait+", polling="+this.polling ); 281 282 this.startup(); 283 284 this.fireEvent( "beginsession" ); 285 }, 286 287 /** 288 * Set isSessionOpen to true and start sendQueue and sendPoll tasks 289 * @private 290 */ 291 startup: function() { 292 ;;; Xmpp4Js.Transport.Base.logger.info( "Starting up transport" ); 293 this.isSessionOpen = true; 294 this.taskRunner.start( this.sendQueueTask ); 295 this.taskRunner.start( this.sendPollTask ); 296 297 }, 298 299 /** 300 * Send a terminate message, mark the sesion as closed, and stop the polling task. 301 */ 302 endSession: function() { 303 ;;; Xmpp4Js.Transport.Base.logger.info( "End Session. Session ID="+this.sid ); 304 var packetNode = this.createPacketNode(); 305 packetNode.setAttribute( "type", "terminate" ); 306 307 // TODO we could be civil and append any remaining packets in the queue here. 308 309 this.shutdown(); 310 311 this.write( packetNode ); 312 313 this.fireEvent( "endsession" ); 314 }, 315 316 /** 317 * Set isSessionOpen to false and stop sendQueue and sendPoll tasks 318 * @private 319 */ 320 shutdown: function() { 321 ;;; Xmpp4Js.Transport.Base.logger.info( "Transport Shutdown (stopping tasks)" ); 322 this.isSessionOpen = false; 323 this.taskRunner.stop( this.sendQueueTask ); 324 this.taskRunner.stop( this.sendPollTask ); 325 }, 326 327 /** 328 * Send a packet as soon as possible. If the session is not currently open, 329 * packets will queue up until it is. 330 * 331 * Should it throw an error if not currently open? 332 * 333 * @param {DomElement} node 334 */ 335 send: function(node) { 336 ;;; Xmpp4Js.Transport.Base.logger.debug( "Sending packet." ); 337 this.queue.push( node ); 338 }, 339 340 prepareWrite: function(packetNode) { 341 this.addFrameData( packetNode ); 342 this.fireEvent( "write", packetNode ); 343 344 this.openRequestCount++; 345 }, 346 347 /** 348 * Immediately write a raw packet node to the wire. Adds frame data including 349 * RID, SID and Key if they are present. 350 * 351 * Also increments the openRequestCount, which is then decremented in the 352 * onWriteResponse method. 353 * 354 * A possible addition could be to add a "no headers" flag. 355 * 356 * @param {DomElement} packetNode 357 */ 358 write: function(packetNode) { 359 ;;; Xmpp4Js.Transport.Base.logger.error( "write: Not Implemented" ); 360 }, 361 362 /** 363 * Handles the response to a write call. 364 * 365 * Decrements the openRequestCount that was incremented in write. 366 * @private 367 */ 368 onWriteResponse: function() { 369 ;;; Xmpp4Js.Transport.Base.logger.error( "onWriteResponse: Not Implemented" ); 370 }, 371 372 /** 373 * Create an empty packet node in the httpbind namespace. 374 * @private 375 * @return {DomElement} a body element with the correct namespace and basic attributes 376 */ 377 createPacketNode: function() { 378 var packetNode = DomBuilder.node( "body", { 379 xmlns: "http://jabber.org/protocol/httpbind" 380 } 381 ); 382 383 return packetNode; 384 }, 385 386 /** 387 * Write a blank node if there is no data waiting and no requests open. 388 * @private 389 */ 390 sendPoll: function() { 391 392 // if we're trying to poll too frequently 393 /*var now = new Date().getTime(); 394 if( this.lastPoll != undefined && this.polling != 0 && (now - this.lastPoll < (this.polling * 1000)) ) { 395 return; 396 } 397 this.lastPoll = now; 398 */ 399 400 if( this.openRequestCount == 0 && this.queue.length == 0 ) { 401 ;;; Xmpp4Js.Transport.Base.logger.debug( "Send Poll." ); 402 var packetNode = this.createPacketNode(); 403 this.write( packetNode ); 404 } 405 }, 406 407 /** 408 * Pull all packets off the queue; first-in, first-out; and send them 409 * within the body of a single packet. Don't send if # open requests 410 * is greater than max requests. 411 * 412 * @private 413 */ 414 sendQueue: function() { 415 // don't send anything if there is no work to do. 416 if( this.queue.length == 0 || this.openRequestCount > this.maxRequests ) { 417 return; 418 } 419 420 ;;; Xmpp4Js.Transport.Base.logger.debug( "sendQueue with "+this.queue.length+" waiting stanzas." ); 421 422 var packetNode = this.createPacketNode(); 423 424 while( this.queue.length > 0 ) { 425 var node = this.queue.shift(); 426 var importedNode = packetNode.ownerDocument.importNode( node, true ); 427 428 packetNode.appendChild( importedNode ); 429 } 430 431 this.write( packetNode ); 432 }, 433 434 /** 435 * Add sid attribute to a packet, if there is one. 436 * @param {Element} packetNode 437 * @private 438 */ 439 addSid: function( packetNode ) { 440 if( this.sid !== null ) { 441 packetNode.setAttribute( "sid", this.sid ); 442 } 443 }, 444 445 /** 446 * Add rid attribute to a packet, if there is one. 447 * @param {Element} packetNode 448 * @private 449 */ 450 addRid: function( packetNode ) { 451 if( this.rid !== null ) { 452 packetNode.setAttribute( "rid", ++this.rid ); 453 } 454 }, 455 456 /** 457 * Add the key attribute to the request, and if needed, 458 * generate a new sequence and add the newkey attribute. 459 * @param {Element} packetNode 460 * @private 461 */ 462 addKey: function( packetNode ) { 463 if( this.keySeq instanceof KeySequence ) { 464 var keySeq = this.keySeq; 465 466 var isFirstKey = keySeq.isFirstKey(); 467 var isLastKey = keySeq.isLastKey(); 468 var key = keySeq.getNextKey(); 469 470 // if it's the first key, use ONLY the newkey attribute. 471 if( isFirstKey ) { 472 packetNode.setAttribute( "newkey", key ); 473 } else { 474 packetNode.setAttribute( "key", key ); 475 } 476 477 // if it's the last key, reset the KeySequence and add a newkey attribute. 478 if( isLastKey ) { 479 ;;; Xmpp4Js.Transport.Base.logger.debug( "Resetting key sequence." ); 480 keySeq.reset(); 481 482 var newKey = keySeq.getNextKey(); 483 packetNode.setAttribute( "newkey", newKey ); 484 } 485 } 486 487 }, 488 489 /** 490 * Add RID, SID and Key to a packet node. Calls each respective function. 491 * @private 492 */ 493 addFrameData: function(packetNode) { 494 this.addRid( packetNode ); 495 this.addSid( packetNode ); 496 this.addKey( packetNode ); 497 }, 498 499 /** 500 * Generate a random number to be used as the initial request ID. 501 * @private 502 */ 503 createInitialRid: function() { 504 return Math.floor( Math.random() * 10000 ); 505 }, 506 507 isPausing: false, 508 509 pause: function(time) { 510 this.isPausing = true; 511 512 ;;; Xmpp4Js.Transport.Base.logger.info( "Pausing session." ); 513 514 this.fireEvent( "beforepause", time ); 515 516 var pauseNode = this.createPacketNode(); 517 pauseNode.setAttribute( "pause", time ); 518 519 /* 520 * the connection manager SHOULD respond immediately to all pending 521 * requests (including the pause request) and temporarily increase 522 * the maximum inactivity period to the requested time. 523 524 this.on("recv", function(packet) { 525 this.fireEvent( "pause", pauseStruct ); 526 }, this, {single:true}); 527 */ 528 this.sendQueue(); 529 530 this.write( pauseNode ); 531 532 this.shutdown(); 533 534 var pauseStruct = this.serializeState(); 535 536 // give others an opportunity to serialize proprties 537 this.fireEvent( "pause", pauseStruct ); 538 539 return pauseStruct; 540 }, 541 542 serializeState: function() { 543 var pauseStruct = { 544 maxpause: 120, // TODO not hard code me 545 maxRequests: this.maxRequests, 546 hold: this.hold, 547 polling: this.polling, 548 server: this.server, 549 port: this.port, 550 domain: this.domain, 551 wait: this.wait, 552 sid: this.sid, 553 rid: this.rid, 554 endpoint: this.endpoint, // TODO subclass implementations should handle this 555 keysSeqKeys : this.keySeq._keys, 556 keySeqIdx: this.keySeq._idx 557 }; 558 559 return pauseStruct; 560 }, 561 562 deserializeState: function(pauseStruct) { 563 // this.maxpause = pauseStruct.maxpause; 564 this.maxpause = pauseStruct.maxpause; 565 this.hold = pauseStruct.hold; 566 this.polling = pauseStruct.polling; 567 this.server = pauseStruct.server; 568 this.port = pauseStruct.port; 569 this.wait = pauseStruct.wait; 570 this.sid = pauseStruct.sid; 571 this.rid = pauseStruct.rid; 572 this.domain = pauseStruct.domain; 573 this.endpoint = pauseStruct.endpoint; 574 this.maxRequests = pauseStruct.maxRequests; 575 576 this.keySeq._keys = pauseStruct.keysSeqKeys; 577 this.keySeq._idx = pauseStruct.keySeqIdx; 578 }, 579 580 resume: function(pauseStruct) { 581 this.isPausing = false; 582 583 ;;; Xmpp4Js.Transport.Base.logger.info( "Resume session. Session ID="+pauseStruct.sid+", Request ID="+pauseStruct.rid ); 584 585 this.deserializeState(pauseStruct); 586 587 this.startup(); 588 589 // give others an opportunity to deserialize properties 590 this.fireEvent( "resume", pauseStruct ); 591 }, 592 593 handleErrors: function(packetNode) { 594 // TODO add log messages here 595 var errorNode = packetNode.getElementsByTagNameNS("http://etherx.jabber.org/streams","error"); 596 errorNode = errorNode.getLength() > 0 ? errorNode.item(0) : null; 597 598 // HACK these errors should be given with terminate / remote-stream-error but in Openfire they are not. 599 if( errorNode == null && (packetNode.getAttribute("type").toString() == "terminate" || 600 packetNode.getAttribute("type").toString() == "terminal")) { // HACK openfire uses terminal? 601 var condition = packetNode.getAttribute( "condition" ).toString(); 602 603 var title = Xmpp4Js.PacketFilter.TerminalErrorPacketFilter.conditions[ condition ].title; 604 var message = Xmpp4Js.PacketFilter.TerminalErrorPacketFilter.conditions[ condition ].message; 605 606 this.fireEvent( "termerror", packetNode, title, message ); 607 throw new Error( "Error in packet" ); 608 } else if( packetNode.getAttribute("type").toString() == "error" ) { 609 // 17.3 Recoverable Binding Conditions 610 611 // TODO this should attempt to resend all packets back 612 // to the one that created the error. This could be 613 // implemented by putting each sent packet into a queue 614 // and removing it upon a successful response. 615 // 616 // Ideally this error event would not even be visible beyond 617 // the the BOSH transport. 618 619 this.fireEvent( "error", packetNode ); 620 throw new Error( "Error in packet" ); 621 } else if(errorNode != null) { 622 // loop through stream nodes to find the condition and 623 // optionally text 624 var childNodes = errorNode.getChildNodes(); 625 for( var i = 0; i < childNodes.getLength(); i++ ) { 626 var node = childNodes.item(i); 627 if( node.getNamespaceURI() == "urn:ietf:params:xml:ns:xmpp-streams" ) { 628 if( node.getLocalName() == "text" ) { 629 var text = node.getText(); 630 } else { 631 var errorCode = node.getLocalName(); 632 } 633 } 634 } 635 636 this.fireEvent( "streamerror", packetNode, errorNode, errorCode, text ); 637 throw new Error( "Error in packet" ); 638 } 639 } 640 } 641 642 Xmpp4Js.Lang.extend( Xmpp4Js.Transport.Base, Xmpp4Js.Event.EventProvider, Xmpp4Js.Transport.Base.prototype ); 643