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