├── .gitignore ├── .jshintignore ├── .travis.yml ├── .editorconfig ├── test ├── parser.test.js ├── connect.test.js ├── frame.test.js └── client.test.js ├── package.json ├── LICENSE ├── examples └── clientServerConnect.js ├── .jshintrc ├── lib ├── frame.js ├── parser.js ├── server.js └── client.js ├── TODO.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | *.md 2 | package.json 3 | node_modules/* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 -------------------------------------------------------------------------------- /test/parser.test.js: -------------------------------------------------------------------------------- 1 | var util = require('sys'), 2 | Events = require('events').EventEmitter, 3 | nodeunit = require('nodeunit'), 4 | testCase = require('nodeunit').testCase, 5 | StompFrame = require('../lib/frame').StompFrame; 6 | 7 | // Mock net object so we never try to send any real data 8 | var connectionObserver = new Events(); 9 | connectionObserver.writeBuffer = []; 10 | connectionObserver.write = function(data) { 11 | this.writeBuffer.push(data); 12 | }; 13 | 14 | module.exports = testCase({ 15 | 16 | setUp: function(callback) { 17 | callback(); 18 | }, 19 | 20 | tearDown: function(callback) { 21 | connectionObserver.writeBuffer = []; 22 | callback(); 23 | } 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /test/connect.test.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Events = require('events').EventEmitter, 3 | nodeunit = require('nodeunit'), 4 | testCase = require('nodeunit').testCase; 5 | 6 | var StompClient = require('../lib/client').StompClient; 7 | 8 | // surpress logs for the test 9 | util.log = function() {}; 10 | 11 | module.exports = testCase({ 12 | 13 | 'check connect to closed port errors': function(test) { 14 | var stompClient = new StompClient('127.0.0.1', 4); 15 | 16 | stompClient.connect(function() {}); 17 | 18 | stompClient.once('error', function(er) { 19 | test.done(); 20 | }); 21 | }, 22 | 23 | 'check that invalid protocol version errors': function(test) { 24 | try { 25 | new StompClient('127.0.0.1', null, null, null, '0.1'); 26 | } catch(er) { 27 | test.done(); 28 | } 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Ben Marvell", 3 | "contributors": [ 4 | { 5 | "name": "Ben Marvell", 6 | "email": "ben@marvell-consulting.com" 7 | }, 8 | { 9 | "name": "Ryan Grenz", 10 | "email": "info@ryangrenz.com" 11 | } 12 | ], 13 | "name": "stomp-client", 14 | "description": "A STOMP protocol implementation in node.js", 15 | "version": "0.9.0", 16 | "repository": { 17 | "url": "http://github.com/easternbloc/node-stomp-client" 18 | }, 19 | "main": "lib/client.js", 20 | "engines": { 21 | "node": ">= 0.10" 22 | }, 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "nodeunit": "0.9.1", 26 | "jshint": "2.8.0" 27 | }, 28 | "license": "MIT", 29 | "keywords": [ 30 | "stomp", 31 | "messaging" 32 | ], 33 | "scripts": { 34 | "test": "nodeunit test", 35 | "lint": "[ -z \"$LINTFILES\" ] && LINTFILES='**'; ./node_modules/jshint/bin/jshint ${LINTFILES}" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | 22 | Copyright © 2013 Ben Marvell, Ryan Grenz, Russell Haering -------------------------------------------------------------------------------- /examples/clientServerConnect.js: -------------------------------------------------------------------------------- 1 | var SERVER_ADDRESS = '127.0.0.1'; 2 | var SERVER_PORT = 61613; 3 | var QUEUE = '/queue/thing'; 4 | 5 | if (process.argv.length > 2) { 6 | if (process.argv[2]) { 7 | SERVER_PORT = process.argv[2]; 8 | } 9 | } else { 10 | var stompServer = require('../lib/server').createStompServer(SERVER_PORT).listen(); 11 | } 12 | var StompClient = require('../lib/client').StompClient; 13 | 14 | var stompClient = new StompClient(SERVER_ADDRESS, SERVER_PORT, '', '', '1.0'); 15 | 16 | stompClient.connect(function() { 17 | stompClient.subscribe(QUEUE, function(data, headers){ 18 | console.log('GOT A MESSAGE', data, headers); 19 | }); 20 | 21 | setTimeout(function(){ 22 | stompClient.publish(QUEUE, 'oh herrow!'); 23 | }, 1000); 24 | setTimeout(function(){ 25 | stompClient.publish(QUEUE, 'wonely...'); 26 | }, 2000); 27 | setTimeout(function(){ 28 | stompClient.publish(QUEUE, 'so wonely...'); 29 | }, 3000); 30 | setTimeout(function(){ 31 | stompClient.publish(QUEUE, 'so wonely, so wonely and bwue!'); 32 | }, 4000); 33 | setTimeout(function(){ 34 | stompClient.disconnect(function() { 35 | console.log('DISCONNECTED'); 36 | }); 37 | }, 5000); 38 | }); 39 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "define" 4 | ], 5 | 6 | "node" : true, 7 | 8 | "bitwise" : false, 9 | "camelcase" : true, 10 | "curly" : false, 11 | "eqeqeq" : true, 12 | "forin" : false, 13 | "immed" : true, 14 | "indent" : 2, 15 | "latedef" : false, 16 | "newcap" : true, 17 | "noarg" : true, 18 | "noempty" : true, 19 | "nonew" : true, 20 | "plusplus" : false, 21 | "quotmark" : "single", 22 | "regexp" : true, 23 | "undef" : true, 24 | "unused" : true, 25 | "strict" : false, 26 | "trailing" : true, 27 | "maxdepth" : 3, 28 | "maxstatements" : 15, 29 | "maxcomplexity" : 7, 30 | 31 | "asi" : false, 32 | "boss" : true, 33 | "debug" : false, 34 | "eqnull" : false, 35 | "esnext" : false, 36 | "evil" : false, 37 | "expr" : true, 38 | "funcscope" : false, 39 | "globalstrict" : false, 40 | "iterator" : false, 41 | "lastsemic" : false, 42 | "loopfunc" : false, 43 | "multistr" : false, 44 | "onecase" : false, 45 | "proto" : false, 46 | "regexdash" : false, 47 | "scripturl" : false, 48 | "smarttabs" : false, 49 | "shadow" : false, 50 | "sub" : true, 51 | "supernew" : false, 52 | 53 | "nomen" : false, 54 | "onevar" : false, 55 | "passfail" : false, 56 | "white" : true 57 | } -------------------------------------------------------------------------------- /lib/frame.js: -------------------------------------------------------------------------------- 1 | function StompFrame(frame) { 2 | if (frame === undefined) { 3 | frame = {}; 4 | } 5 | this.command = frame.command || ''; 6 | this.headers = frame.headers || {}; 7 | this.body = frame.body || ''; 8 | this.contentLength = -1; 9 | } 10 | 11 | StompFrame.prototype.toString = function() { 12 | return JSON.stringify({ 13 | command: this.command, 14 | headers: this.headers, 15 | body: this.body 16 | }); 17 | }; 18 | 19 | StompFrame.prototype.send = function(stream) { 20 | // Avoid small writes, they get sent in their own tcp packet, which 21 | // is not efficient (and v8 does fast string concat). 22 | var frame = this.command + '\n'; 23 | for (var key in this.headers) { 24 | frame += key + ':' + this.headers[key] + '\n'; 25 | } 26 | if (this.body.length > 0) { 27 | if (!this.headers.hasOwnProperty('suppress-content-length')) { 28 | frame += 'content-length:' + Buffer.byteLength(this.body) + '\n'; 29 | } 30 | } 31 | frame += '\n'; 32 | if (this.body.length > 0) { 33 | frame += this.body; 34 | } 35 | frame += '\0'; 36 | if(frame) 37 | stream.write(frame); 38 | }; 39 | 40 | StompFrame.prototype.setCommand = function(command) { 41 | this.command = command; 42 | }; 43 | 44 | StompFrame.prototype.setHeader = function(key, value) { 45 | this.headers[key] = value; 46 | if (key.toLowerCase() === 'content-length') { 47 | this.contentLength = parseInt(value); 48 | } 49 | }; 50 | 51 | StompFrame.prototype.appendToBody = function(data) { 52 | this.body += data; 53 | }; 54 | 55 | StompFrame.prototype.validate = function(frameConstruct) { 56 | var frameHeaders = Object.keys(this.headers); 57 | 58 | // Check validity of frame headers 59 | for (var header in frameConstruct.headers) { 60 | var headerConstruct = frameConstruct.headers[header]; 61 | 62 | // Check required (if specified) 63 | if (headerConstruct.hasOwnProperty('required') && headerConstruct.required === true) { 64 | if (frameHeaders.indexOf(header) === -1) { 65 | return { 66 | isValid: false, 67 | message: 'Header "' + header + '" is required for '+this.command, 68 | details: 'Frame: ' + this.toString() 69 | }; 70 | } 71 | } 72 | 73 | // Check regex of header value (if specified) 74 | if (headerConstruct.hasOwnProperty('regex') && frameHeaders.indexOf(header) > -1) { 75 | if (!this.headers[header].match(headerConstruct.regex)) { 76 | return { 77 | isValid: false, 78 | message: 'Header "' + header + '" has value "' + this.headers[header] + '" which does not match against the following regex: ' + headerConstruct.regex + ' (Frame: ' + this.toString() + ')' 79 | }; 80 | } 81 | } 82 | } 83 | 84 | return { isValid: true }; 85 | }; 86 | 87 | exports.StompFrame = StompFrame; 88 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Here's a list of things that could be done, in no particular order. 4 | 5 | ## Receipts 6 | 7 | Every send function that sends a frame that can have a receipt header should 8 | also take a receipt callback, and auto-add a receipt header if callback is 9 | passed. 10 | 11 | The DISCONNECT header should probably always request a RECEIPT, so guarantee 12 | flushing of data. 1.2 says you 13 | [should](http://stomp.github.io/stomp-specification-1.2.html#DISCONNECT), and 14 | while its not mentioned as a should in 1.0, receipt IDs are supported. 15 | 16 | ## Version 1.1 and 1.2 17 | 18 | Version 1.1 and 1.2 support should be added, and options should allow the higher 19 | protocol versions to be requested (note that version fallback will occur, the 20 | newer versions fall back to 1.0). 21 | 22 | ## Wildcard topic support 23 | 24 | Wildcards, http://activemq.apache.org/wildcards.html, should be supported, they 25 | don't work now because the destination queue is fully qualified, so node-client 26 | doesn't know which callback to pass the message to, this implies it needs to 27 | implement the wildcards itself to find which subscription callback to pass 28 | the message to. 29 | 30 | ## TLS support 31 | 32 | TLS support should be implemented, SecureStompClient hasn't worked since node 33 | 0.4 dropped .setSecure(), but for when it gets fixed, note: 34 | - it only half-initializes self, it should share the init code with 35 | StompClient 36 | - it doesn't allow protocol version to be specified, it should probably 37 | just take an options object, and pass some of it to the tls methods. 38 | 39 | ## Fix the 'disconnect' event 40 | 41 | The current connection state is not well managed, the 'disconnect' event is 42 | fired even if we haven't called .disconnect(). It should be more like net, 43 | if we request a disconnect, we will get a disconnect event (or an error). 44 | If we didn't request, and get closed, we should emit an error. And either way, 45 | we should emit close when the socket goes away, so there is always a final 46 | event. 47 | 48 | ## Invalid output 49 | 50 | It is possible to cause invalid headers and bodies to be sent using various 51 | combinations of non-ascii strings, and invalid chars such as NUL, :, NL, etc. 52 | For 1.0, some chars simply must not be used in headers, in later versions, they 53 | can be escaped. 54 | 55 | Also, the ActiveMQ STOMP support describes using the presence or absence of the 56 | content-length header to decide whether the content is a string. The frame 57 | emitter always attaches content-length, it probably should not if its known to 58 | be a utf-8 string without embedded NUL. 59 | 60 | ## Options object 61 | 62 | The exported StompClient should accept either (port, address, ...), or an 63 | options object, or a URL. This can be done in a backwards compatible way. Also, 64 | if secure STOMP was supported, it could be requested by setting `options.secure` to 65 | an options object acceptable to 66 | 67 | 68 | ## JSON support 69 | 70 | Allows publishing objects as JSON with a content-type of application/json, and 71 | converting incoming messages to objects based on content-type. 72 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var events = require('events'); 3 | var StompFrame = require('./frame').StompFrame; 4 | 5 | var ParserStates = { 6 | COMMAND: 0, 7 | HEADERS: 1, 8 | BODY: 2, 9 | ERROR: 3 10 | }; 11 | 12 | function StompFrameEmitter(frameValidators) { 13 | events.EventEmitter.call(this); 14 | this.state = ParserStates.COMMAND; 15 | this.frame = new StompFrame(); 16 | this.frames = []; 17 | this.buffer = ''; 18 | this.frameValidators = frameValidators || {}; 19 | this.commands = Object.keys(this.frameValidators); 20 | } 21 | 22 | util.inherits(StompFrameEmitter, events.EventEmitter); 23 | 24 | StompFrameEmitter.prototype.incrementState = function () { 25 | if (this.state === ParserStates.BODY || this.state === ParserStates.ERROR) { 26 | this.state = ParserStates.COMMAND; 27 | } else { 28 | this.state++; 29 | } 30 | }; 31 | 32 | StompFrameEmitter.prototype.handleData = function (data) { 33 | this.buffer += data; 34 | do { 35 | if (this.state === ParserStates.COMMAND) { 36 | this.parseCommand(); 37 | } 38 | if (this.state === ParserStates.HEADERS) { 39 | this.parseHeaders(); 40 | } 41 | if (this.state === ParserStates.BODY) { 42 | this.parseBody(); 43 | } 44 | if (this.state === ParserStates.ERROR) { 45 | this.parseError(); 46 | } 47 | } while (this.state === ParserStates.COMMAND && this.hasLine()); 48 | }; 49 | 50 | StompFrameEmitter.prototype.hasLine = function () { 51 | return (this.buffer.indexOf('\n') > -1); 52 | }; 53 | 54 | StompFrameEmitter.prototype.popLine = function () { 55 | var index = this.buffer.indexOf('\n'); 56 | var line = this.buffer.slice(0, index); 57 | this.buffer = this.buffer.substr(index + 1); 58 | return line; 59 | }; 60 | 61 | StompFrameEmitter.prototype.error = function (err) { 62 | this.emit('parseError', err); 63 | this.state = ParserStates.ERROR; 64 | }; 65 | 66 | StompFrameEmitter.prototype.parseCommand = function () { 67 | while (this.hasLine()) { 68 | var line = this.popLine(); 69 | if (line !== '') { 70 | if (this.commands.indexOf(line) === -1) { 71 | this.error({ 72 | message: 'No such command ', 73 | details: 'Unrecognized Command \'' + line + '\'' 74 | }); 75 | break; 76 | } 77 | this.frame.setCommand(line); 78 | this.incrementState(); 79 | break; 80 | } 81 | } 82 | }; 83 | 84 | StompFrameEmitter.prototype.parseHeaders = function () { 85 | while (this.hasLine()) { 86 | var line = this.popLine(); 87 | if (line === '') { 88 | this.incrementState(); 89 | break; 90 | } else { 91 | var kv = line.split(':'); 92 | if (kv.length < 2) { 93 | this.error({ 94 | message: 'Error parsing header', 95 | details: 'No ":" in line "' + line + '"' 96 | }); 97 | break; 98 | } 99 | this.frame.setHeader(kv[0], kv.slice(1).join(':')); 100 | } 101 | } 102 | }; 103 | 104 | StompFrameEmitter.prototype.parseBody = function () { 105 | var bufferBuffer = new Buffer(this.buffer); 106 | 107 | if (this.frame.contentLength > -1) { 108 | var remainingLength = this.frame.contentLength - this.frame.body.length; 109 | 110 | if(remainingLength < bufferBuffer.length) { 111 | this.frame.appendToBody(bufferBuffer.slice(0, remainingLength).toString()); 112 | this.buffer = bufferBuffer.slice(remainingLength, bufferBuffer.length).toString(); 113 | 114 | if (this.frame.contentLength === Buffer.byteLength(this.frame.body)) { 115 | this.frame.contentLength = -1; 116 | } else { 117 | return; 118 | } 119 | } 120 | } 121 | 122 | var index = this.buffer.indexOf('\0'); 123 | 124 | if (index == -1) { 125 | this.frame.appendToBody(this.buffer); 126 | this.buffer = ''; 127 | } else { 128 | // The end of the frame has been identified, finish creating it 129 | this.frame.appendToBody(this.buffer.slice(0, index)); 130 | 131 | var frameValidation = this.getFrameValidation(this.frame.command); 132 | 133 | if (frameValidation.isValid) { 134 | // Emit the frame and reset 135 | this.emit('frame', this.frame); // Event emit to catch any frame emission 136 | this.emit(this.frame.command, this.frame); // Specific frame emission 137 | } else { 138 | this.emit('parseError', { 139 | message: frameValidation.message, 140 | details: frameValidation.details, 141 | }); 142 | } 143 | 144 | this.frame = new StompFrame(); 145 | this.incrementState(); 146 | this.buffer = this.buffer.substr(index + 1); 147 | } 148 | }; 149 | 150 | StompFrameEmitter.prototype.getFrameValidation = function (command) { 151 | if (!this.frameValidators.hasOwnProperty(command)) { 152 | this.emit('parseError', { message: 'No validator defined for ' + command }); 153 | } else { 154 | return this.frame.validate(this.frameValidators[command]); 155 | } 156 | }; 157 | 158 | StompFrameEmitter.prototype.parseError = function () { 159 | var index = this.buffer.indexOf('\0'); 160 | if (index > -1) { 161 | this.buffer = this.buffer.substr(index + 1); 162 | this.incrementState(); 163 | } else { 164 | this.buffer = ""; 165 | } 166 | }; 167 | 168 | new StompFrameEmitter(); 169 | 170 | exports.StompFrameEmitter = StompFrameEmitter; 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stomp Client 2 | =========== 3 | 4 | [![Build Status](https://img.shields.io/travis/easternbloc/node-stomp-client.svg?style=flat-square)](http://travis-ci.org/easternbloc/node-stomp-client) 5 | [![Monthly Downloads](https://img.shields.io/npm/dm/stomp-client.svg?style=flat-square)](https://www.npmjs.com/package/stomp-client) 6 | [![Version](https://img.shields.io/npm/v/stomp-client.svg?style=flat-square)](https://www.npmjs.com/package/stomp-client) 7 | [![Licence](https://img.shields.io/npm/l/stomp-client.svg?style=flat-square)](https://github.com/easternbloc/node-stomp-client/blob/master/LICENSE) 8 | 9 | A node.js [STOMP](http://stomp.github.com) client. Props goes to [Russell 10 | Haering](https://github.com/russellhaering/node-stomp-broker) for doing the 11 | initial legwork. 12 | 13 | The following enhancements have been added: 14 | 15 | * Unit tests 16 | * Ability to support different protocol versions (1.0 or 1.1) - more work needed 17 | * Inbound frame validation (required / regex'able header values) 18 | * Support for UNSUBSCRIBE frames in client 19 | * Ability to add custom headers to SUBSCRIBE/UNSUBSCRIBE frames 20 | * ACK and NACK support 21 | 22 | ## Installation 23 | 24 | npm install stomp-client 25 | 26 | ## Super basic example 27 | 28 | var Stomp = require('stomp-client'); 29 | var destination = '/queue/someQueueName'; 30 | var client = new Stomp('127.0.0.1', 61613, 'user', 'pass'); 31 | 32 | client.connect(function(sessionId) { 33 | client.subscribe(destination, function(body, headers) { 34 | console.log('This is the body of a message on the subscribed queue:', body); 35 | }); 36 | 37 | client.publish(destination, 'Oh herrow'); 38 | }); 39 | 40 | The client comes in two forms, a standard or secure client. The example below is 41 | using the standard client. To use the secure client simply change 42 | `StompClient` to `SecureStompClient` 43 | 44 | 45 | # API 46 | 47 | ## Queue Names 48 | 49 | The meaning of queue names is not defined by the STOMP spec, but by the Broker. 50 | However, with ActiveMQ, they should begin with `"/queue/"` or with `"/topic/"`, see 51 | [STOMP1.0](http://stomp.github.io/stomp-specification-1.0.html#frame-SEND) for 52 | more detail. 53 | 54 | ## Stomp = require('stomp-client') 55 | 56 | Require returns a constructor for STOMP client instances. 57 | 58 | For backwards compatibility, `require('stomp-client').StompClient` is also 59 | supported. 60 | 61 | ## Stomp(address, [port], [user], [pass], [protocolVersion], [vhost], [reconnectOpts], [tls]) 62 | 63 | - `address`: address to connect to, default is `"127.0.0.1"` 64 | - `port`: port to connect to, default is `61613` 65 | - `user`: user to authenticate as, default is `""` 66 | - `pass`: password to authenticate with, default is `""` 67 | - `protocolVersion`: see below, defaults to `"1.0"` 68 | - `vhost`: see below, defaults to `null` 69 | - `reconnectOpts`: see below, defaults to `{}` 70 | - `tls`: Establish a tls/ssl connection. If an object is passed for this argument it will passed as options to the tls module. 71 | 72 | Protocol version negotiation is not currently supported and version `"1.0"` is 73 | the only supported version. 74 | 75 | ReconnectOpts should contain an integer `retries` specifying the maximum number 76 | of reconnection attempts, and a `delay` which specifies the reconnection delay. 77 | (reconnection timings are calculated using exponential backoff. The first reconnection 78 | happens immediately, the second reconnection happens at `+delay` ms, the third at `+ 2*delay` ms, etc). 79 | 80 | ## Stomp(options) 81 | 82 | - `options`: Properties are named the same as the positional parameters above. The property 'host' is accepted as an alias for 'address'. 83 | 84 | ## stomp.connect([callback, [errorCallback]]) 85 | 86 | Connect to the STOMP server. If the callbacks are provided, they will be 87 | attached on the `'connect'` and `'error'` event, respectively. 88 | 89 | ## virtualhosts 90 | 91 | If using virtualhosts to namespace your queues, you must pass a `version` header of '1.1' otherwise it is ignored. 92 | 93 | ## stomp.disconnect(callback) 94 | 95 | Disconnect from the STOMP server. The callback will be executed when disconnection is complete. 96 | No reconnections should be attempted, nor errors thrown as a result of this call. 97 | 98 | ## stomp.subscribe(queue, [headers,] callback) 99 | 100 | - `queue`: queue to subscribe to 101 | - `headers`: headers to add to the SUBSCRIBE message 102 | - `callback`: will be called with message body as first argument, 103 | and header object as the second argument 104 | 105 | ## stomp.unsubscribe(queue, [headers]) 106 | 107 | - `queue`: queue to unsubscribe from 108 | - `headers`: headers to add to the UNSUBSCRIBE message 109 | 110 | ## stomp.publish(queue, message, [headers]) 111 | 112 | - `queue`: queue to publish to 113 | - `message`: message to publish, a string or buffer 114 | - `headers`: headers to add to the PUBLISH message 115 | 116 | ## stomp.ack(messageId, subscription, [transaction]), 117 | ## stomp.nack(messageId, subscription, [transaction]) 118 | 119 | - `messageId`: the id of the message to ack/nack 120 | - `subscription`: the id of the subscription 121 | - `transaction`: optional transaction name 122 | 123 | ## Property: `stomp.publishable` (boolean) 124 | Returns whether or not the connection is currently writable. During normal operation 125 | this should be true, however if the client is in the process of reconnecting, 126 | this will be false. 127 | 128 | ## Event: `'connect'` 129 | 130 | Emitted on successful connect to the STOMP server. 131 | 132 | ## Event: `'error'` 133 | 134 | Emitted on an error at either the TCP or STOMP protocol layer. An Error object 135 | will be passed. All error objects have a `.message` property, STOMP protocol 136 | errors may also have a `.details` property. 137 | 138 | If the error was caused by a failure to reconnect after exceeding the number of 139 | reconnection attempts, the error object will have a `reconnectionFailed` property. 140 | 141 | ## Event: `'reconnect'` 142 | 143 | Emitted when the client has successfully reconnected. The event arguments are 144 | the new `sessionId` and the reconnection attempt number. 145 | 146 | ## Event: `'reconnecting'` 147 | 148 | Emitted when the client has been disconnected for whatever reason, but is going 149 | to attempt to reconnect. 150 | 151 | ## Event: `'message'` (body, headers) 152 | 153 | Emitted for each message received. This can be used as a simple way to receive 154 | messages for wildcard destination subscriptions that would otherwise not trigger 155 | the subscription callback. 156 | 157 | ## LICENSE 158 | 159 | [MIT](LICENSE) 160 | -------------------------------------------------------------------------------- /test/frame.test.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Events = require('events').EventEmitter, 3 | nodeunit = require('nodeunit'), 4 | testCase = require('nodeunit').testCase, 5 | StompFrame = require('../lib/frame').StompFrame; 6 | 7 | // Mock net object so we never try to send any real data 8 | var connectionObserver = new Events(); 9 | connectionObserver.writeBuffer = []; 10 | connectionObserver.write = function(data) { 11 | this.writeBuffer.push(data); 12 | }; 13 | 14 | module.exports = testCase({ 15 | 16 | setUp: function(callback) { 17 | callback(); 18 | }, 19 | 20 | tearDown: function(callback) { 21 | connectionObserver.writeBuffer = []; 22 | callback(); 23 | }, 24 | 25 | 'test StompFrame utility methods work correctly': function (test) { 26 | var frame = new StompFrame({ 27 | 'command': 'HITME', 28 | 'headers': { 29 | 'header1': 'value1', 30 | 'header2': 'value2' 31 | }, 32 | 'body': 'wewp de doo' 33 | }); 34 | 35 | test.equal(frame.command, 'HITME'); 36 | 37 | // setCommand 38 | frame.setCommand('SOMETHINGELSE'); 39 | test.equal(frame.command, 'SOMETHINGELSE'); 40 | 41 | // setHeader 42 | frame.setHeader('header2', 'newvalue'); 43 | test.equal(frame.headers['header2'], 'newvalue'); 44 | 45 | frame.setHeader('new-header', 'blah'); 46 | test.equal(frame.headers['new-header'], 'blah'); 47 | 48 | // TODO - Content-length assignment? Why is this done? 49 | 50 | // appendToBody 51 | frame.appendToBody('pip pop'); 52 | test.equal(frame.body, 'wewp de doopip pop'); 53 | 54 | test.done(); 55 | }, 56 | 57 | 'test stream writes are correct on arbitrary frame definition': function (test) { 58 | var frame = new StompFrame({ 59 | 'command': 'HITME', 60 | 'headers': { 61 | 'header1': 'value1', 62 | 'header2': 'value2' 63 | }, 64 | 'body': 'wewp de doo' 65 | }); 66 | 67 | // Command before headers, content-length auto-inserted, and terminating with null char (line feed chars for each line too) 68 | var expectedStream = [ 69 | 'HITME\n', 70 | 'header1:value1\n', 71 | 'header2:value2\n', 72 | 'content-length:11\n', 73 | '\n', 74 | 'wewp de doo', 75 | '\u0000' 76 | ]; 77 | 78 | 79 | 80 | frame.send(connectionObserver); 81 | 82 | test.deepEqual(expectedStream.join(''), connectionObserver.writeBuffer.join(''), 'frame stream data is correctly output on the mocked wire'); 83 | test.done(); 84 | }, 85 | 86 | 'check validation of arbitrary frame with arbitrary frame construct': function (test) { 87 | var validation, 88 | frameConstruct = { 89 | 'headers': { 90 | 'blah': { required: true }, 91 | 'regexheader': { required: true, regex: /(wibble|wobble)/ } 92 | } 93 | }; 94 | 95 | var frame = new StompFrame({ 96 | 'command': 'COMMAND', 97 | 'headers': {}, 98 | 'body': '' 99 | }); 100 | 101 | // Invalid header (required) 102 | validation = frame.validate(frameConstruct); 103 | test.equal(validation.isValid, false); 104 | test.equal(validation.message, 'Header "blah" is required for COMMAND'); 105 | test.equal(validation.details, 'Frame: {"command":"COMMAND","headers":{},"body":""}'); 106 | frame.setHeader('blah', 'something or other'); // Set it now so it doesn't complain in later tests 107 | 108 | // Invalid header (regex) 109 | frame.setHeader('regexheader', 'not what it should be'); 110 | validation = frame.validate(frameConstruct); 111 | test.equal(validation.isValid, false); 112 | test.equal(validation.message, 'Header "regexheader" has value "not what it should be" which does not match against the following regex: /(wibble|wobble)/ (Frame: {"command":"COMMAND","headers":{"blah":"something or other","regexheader":"not what it should be"},"body":""})'); 113 | 114 | // Now make the header valid 115 | frame.setHeader('regexheader', 'wibble'); 116 | validation = frame.validate(frameConstruct); 117 | test.equal(validation.isValid, true); 118 | 119 | frame.setHeader('regexheader', 'wobble'); 120 | validation = frame.validate(frameConstruct); 121 | test.equal(validation.isValid, true, 'still valid!'); 122 | 123 | test.done(); 124 | }, 125 | 'test content-length header is present when suppress-content-length is not': function(test) { 126 | var frame = new StompFrame({ 127 | 'command': 'SEND', 128 | 'body' : 'Content length is 20' 129 | }); 130 | frame.send(connectionObserver); 131 | 132 | //Check the headers for the content-length header 133 | var writtenString = connectionObserver.writeBuffer.join(''); 134 | var containsContentLengthHeader = (writtenString.split("\n").indexOf("content-length:20") == -1 ? false : true); 135 | test.equal(containsContentLengthHeader, true, "Content length header should exist since we are not suppressing it"); 136 | 137 | test.done(); 138 | }, 139 | 'test content-length is not present when suppress-content-length is provided': function(test) { 140 | var frame = new StompFrame({ 141 | 'command': 'SEND', 142 | 'headers': { 143 | 'suppress-content-length': true 144 | }, 145 | 'body' : 'Content length is 20' 146 | }); 147 | frame.send(connectionObserver); 148 | 149 | //Check the headers for the content-length header 150 | var writtenString = connectionObserver.writeBuffer.join(''); 151 | var containsContentLengthHeader = (writtenString.split("\n").indexOf("content-length:20") == -1 ? false : true); 152 | test.equal(containsContentLengthHeader, false, "Content length header should not exist since we are suppressing it"); 153 | test.done(); 154 | }, 155 | 'test stream write correctly handles single-byte UTF-8 characters': function(test) { 156 | var frame = new StompFrame({ 157 | 'command': 'SEND', 158 | 'body' : 'Welcome!' 159 | }); 160 | frame.send(connectionObserver); 161 | 162 | var writtenString = connectionObserver.writeBuffer.join(''); 163 | //Assume content-length header is second line 164 | var contentLengthHeaderLine = writtenString.split("\n")[1]; 165 | var contentLengthValue = contentLengthHeaderLine.split(":")[1].trim(); 166 | 167 | test.equal(Buffer.byteLength(frame.body), contentLengthValue, "We should be truthful about how much data we plan to send to the server"); 168 | 169 | test.done(); 170 | }, 171 | 'test stream write correctly handles multi-byte UTF-8 characters': function(test) { 172 | var frame = new StompFrame({ 173 | 'command': 'SEND', 174 | 'body' : 'Ẇḗḽḉớḿẽ☃' 175 | }); 176 | frame.send(connectionObserver); 177 | 178 | var writtenString = connectionObserver.writeBuffer.join(''); 179 | //Assume content-length header is second line 180 | var contentLengthHeaderLine = writtenString.split("\n")[1]; 181 | var contentLengthValue = contentLengthHeaderLine.split(":")[1].trim(); 182 | 183 | test.equal(Buffer.byteLength(frame.body), contentLengthValue, "We should be truthful about how much data we plan to send to the server"); 184 | 185 | test.done(); 186 | } 187 | 188 | }); 189 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var fs = require('fs'); 3 | var util = require('util'); 4 | var crypto = require('crypto'); 5 | var StompFrame = require('./frame').StompFrame; 6 | var StompFrameEmitter = require('./parser').StompFrameEmitter; 7 | 8 | //var privateKey = fs.readFileSync('CA/newkeyopen.pem', 'ascii'); 9 | //var certificate = fs.readFileSync('CA/newcert.pem', 'ascii'); 10 | //var certificateAuthority = fs.readFileSync('CA/demoCA/private/cakey.pem', 'ascii'); 11 | /* 12 | var credentials = crypto.createCredentials({ 13 | key: privateKey, 14 | cert: certificate, 15 | ca: certificateAuthority, 16 | }); 17 | */ 18 | 19 | var StompClientCommands = ['CONNECT', 'SEND', 'SUBSCRIBE', 'UNSUBSCRIBE', 'BEGIN', 'COMMIT', 'ACK', 'ABORT', 'DISCONNECT']; 20 | 21 | function StompSubscription(stream, session, ack) { 22 | this.ack = ack; 23 | this.session = session; 24 | this.stream = stream; 25 | } 26 | 27 | StompSubscription.prototype.send = function(stompFrame) { 28 | stompFrame.send(this.stream); 29 | }; 30 | 31 | function StompQueueManager() { 32 | this.queues = {}; 33 | this.msgId = 0; 34 | this.sessionId = 0; 35 | } 36 | 37 | StompQueueManager.prototype.generateMessageId = function() { 38 | return this.msgId++; 39 | }; 40 | 41 | StompQueueManager.prototype.generateSessionId = function() { 42 | return this.sessionId++; 43 | }; 44 | 45 | StompQueueManager.prototype.subscribe = function(queue, stream, session, ack) { 46 | if (!(queue in this.queues)) { 47 | this.queues[queue] = []; 48 | } 49 | this.queues[queue].push(new StompSubscription(stream, session, ack)); 50 | }; 51 | 52 | StompQueueManager.prototype.publish = function(queue, message) { 53 | if (!(queue in this.queues)) { 54 | throw new StompFrame({ 55 | command: 'ERROR', 56 | headers: { 57 | message: 'Queue does not exist' 58 | }, 59 | body: 'Queue "' + frame.headers.destination + '" does not exist' 60 | }); 61 | } 62 | var message = new StompFrame({ 63 | command: 'MESSAGE', 64 | headers: { 65 | 'destination': queue, 66 | 'message-id': this.generateMessageId(), 67 | }, 68 | body: message, 69 | }); 70 | this.queues[queue].map(function(subscription) { 71 | subscription.send(message); 72 | }); 73 | }; 74 | 75 | StompQueueManager.prototype.unsubscribe = function(queue, session) { 76 | if (!(queue in this.queues)) { 77 | throw new StompFrame({ 78 | command: 'ERROR', 79 | headers: { 80 | message: 'Queue does not exist' 81 | }, 82 | body: 'Queue "' + frame.headers.destination + '" does not exist' 83 | }); 84 | } 85 | // TODO: Profile this 86 | this.queues[queue] = this.queues[queue].filter(function(subscription) { 87 | return (subscription.session != session); 88 | }); 89 | }; 90 | 91 | function StompStreamHandler(stream, queueManager) { 92 | var frameEmitter = new StompFrameEmitter(StompClientCommands); 93 | var authenticated = false; 94 | var sessionId = -1; 95 | var subscriptions = []; 96 | var transactions = {}; 97 | 98 | stream.on('data', function(data) { 99 | frameEmitter.handleData(data); 100 | }); 101 | 102 | stream.on('end', function() { 103 | subscriptions.map(function(queue) { 104 | queueManager.unsubscribe(queue, sessionId); 105 | }); 106 | stream.end(); 107 | }); 108 | 109 | frameEmitter.on('frame', function(frame) { 110 | if (!authenticated && frame.command != 'CONNECT') { 111 | new StompFrame({ 112 | command: 'ERROR', 113 | headers: { 114 | message: 'Not connected' 115 | }, 116 | body: 'You must first issue a CONNECT command' 117 | }).send(stream); 118 | return; 119 | } 120 | if (frame.command != 'CONNECT' && 'receipt' in frame.headers) { 121 | new StompFrame({ 122 | command: 'RECEIPT', 123 | headers: { 124 | 'receipt-id': frame.headers.receipt 125 | } 126 | }).send(stream); 127 | } 128 | try { 129 | switch (frame.command) { 130 | case 'CONNECT': 131 | // TODO: Actual authentication 132 | authenticated = true; 133 | sessionId = queueManager.generateSessionId(); 134 | new StompFrame({ 135 | command: 'CONNECTED', 136 | headers: { 137 | session: sessionId 138 | } 139 | }).send(stream); 140 | break; 141 | 142 | case 'SUBSCRIBE': 143 | queueManager.subscribe(frame.headers.destination, stream, sessionId, frame.headers.ack || "auto"); 144 | subscriptions.push(frame.headers.destination); 145 | break; 146 | 147 | case 'UNSUBSCRIBE': 148 | queueManager.unsubscribe(frame.headers.destination, sessionId); 149 | break; 150 | 151 | case 'SEND': 152 | queueManager.publish(frame.headers.destination, frame.body); 153 | break; 154 | 155 | case 'BEGIN': 156 | if (frame.headers.transaction in transactions) { 157 | throw new StompFrame({ 158 | command: 'ERROR', 159 | headers: { 160 | message: 'Transaction already exists' 161 | }, 162 | body: 'Transaction "' + frame.headers.transaction + '" already exists' 163 | }); 164 | } 165 | transactions[frame.headers.transaction] = []; 166 | break; 167 | 168 | case 'COMMIT': 169 | // TODO: Actually apply the transaction, this is just an abort 170 | delete transactions[frame.headers.transaction]; 171 | break; 172 | 173 | case 'ABORT': 174 | delete transactions[frame.headers.transaction]; 175 | break; 176 | 177 | case 'DISCONECT': 178 | subscriptions.map(function(queue) { 179 | queueManager.unsubscribe(queue, sessionId); 180 | }); 181 | stream.end(); 182 | break; 183 | } 184 | } catch (e) { 185 | e.send(stream); 186 | } 187 | }); 188 | 189 | frameEmitter.on('error', function(err) { 190 | var response = new StompFrame(); 191 | response.setCommand('ERROR'); 192 | response.setHeader('message', err['message']); 193 | if ('details' in err) { 194 | response.appendToBody(err['details']); 195 | } 196 | response.send(stream); 197 | }); 198 | } 199 | 200 | function StompServer(port) { 201 | this.port = port; 202 | var queueManager = new StompQueueManager(); 203 | this.server = net.createServer(function(stream) { 204 | stream.on('connect', function() { 205 | console.log('Received Unsecured Connection'); 206 | new StompStreamHandler(stream, queueManager); 207 | }); 208 | }); 209 | } 210 | 211 | function SecureStompServer(port, credentials) { 212 | StompServer.call(this); 213 | var queueManager = new StompQueueManager(); 214 | this.port = port; 215 | this.server = net.createServer(function(stream) { 216 | stream.on('connect', function() { 217 | console.log('Received Connection, securing'); 218 | stream.setSecure(credentials); 219 | }); 220 | stream.on('secure', function() { 221 | new StompStreamHandler(stream, queueManager); 222 | }); 223 | }); 224 | } 225 | 226 | util.inherits(SecureStompServer, StompServer); 227 | 228 | StompServer.prototype.listen = function() { 229 | this.server.listen(this.port, 'localhost'); 230 | }; 231 | 232 | StompServer.prototype.stop = function(port) { 233 | this.server.close(); 234 | }; 235 | 236 | //new SecureStompServer(8124, credentials).listen(); 237 | exports.createStompServer = function(port) { 238 | return new StompServer(port); 239 | }; 240 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var net = require('net'); 3 | var tls = require('tls'); 4 | var util = require('util'); 5 | var events = require('events'); 6 | var StompFrame = require('./frame').StompFrame; 7 | var StompFrameEmitter = require('./parser').StompFrameEmitter; 8 | 9 | // Copied from modern node util._extend, because it didn't exist 10 | // in node 0.4. 11 | function _extend(origin, add) { 12 | // Don't do anything if add isn't an object 13 | if (!add || typeof add !== 'object') return origin; 14 | 15 | var keys = Object.keys(add); 16 | var i = keys.length; 17 | while (i--) { 18 | origin[keys[i]] = add[keys[i]]; 19 | } 20 | return origin; 21 | }; 22 | 23 | // Inbound frame validators 24 | var StompFrameCommands = { 25 | '1.0': { 26 | 'CONNECTED': { 27 | 'headers': { 'session': { required: true } } 28 | }, 29 | 'MESSAGE' : { 30 | 'headers': { 31 | 'destination': { required: true }, 32 | 'message-id': { required: true } 33 | } 34 | }, 35 | 'ERROR': {}, 36 | 'RECEIPT': {} 37 | }, 38 | '1.1': { 39 | 'CONNECTED': { 40 | 'headers': { 'session': { required: true } } 41 | }, 42 | 'MESSAGE' : { 43 | 'headers': { 44 | 'destination': { required: true }, 45 | 'message-id': { required: true } 46 | } 47 | }, 48 | 'ERROR': {}, 49 | 'RECEIPT': {} 50 | } 51 | }; 52 | 53 | function StompClient(opts) { 54 | 55 | var address, port, user, pass, protocolVersion, vhost, reconnectOpts, tlsOpts; 56 | 57 | if(arguments.length !== 1 || typeof opts === 'string') { 58 | address = opts; 59 | port = arguments[1]; 60 | user = arguments[2]; 61 | pass = arguments[3]; 62 | protocolVersion = arguments[4]; 63 | vhost = arguments[5]; 64 | reconnectOpts = arguments[6]; 65 | tlsOpts = arguments[7]; 66 | if(tlsOpts === true) { 67 | tlsOpts = {}; 68 | } 69 | } 70 | else { 71 | address = opts.address || opts.host; 72 | port = opts.port; 73 | user = opts.user; 74 | pass = opts.pass; 75 | protocolVersion = opts.protocolVersion; 76 | vhost = opts.vhost; 77 | reconnectOpts = opts.reconnectOpts; 78 | tlsOpts = opts.tls; 79 | // If boolean then TLS options are mixed in with other options 80 | if(tlsOpts === true) { 81 | tlsOpts = opts; 82 | } 83 | } 84 | 85 | events.EventEmitter.call(this); 86 | this.user = (user || ''); 87 | this.pass = (pass || ''); 88 | this.address = (address || '127.0.0.1'); 89 | this.port = (port || 61613); 90 | this.version = (protocolVersion || '1.0'); 91 | this.subscriptions = {}; 92 | assert(StompFrameCommands[this.version], 'STOMP version '+this.version+' is not supported'); 93 | this._stompFrameEmitter = new StompFrameEmitter(StompFrameCommands[this.version]); 94 | this.vhost = vhost || null; 95 | this.reconnectOpts = reconnectOpts || {}; 96 | this.tls = tlsOpts; 97 | this._retryNumber = 0; 98 | this._retryDelay = this.reconnectOpts.delay; 99 | return this; 100 | } 101 | 102 | util.inherits(StompClient, events.EventEmitter); 103 | 104 | StompClient.prototype.connect = function (connectedCallback, errorCallback) { 105 | var self = this; 106 | 107 | //reset this field. 108 | delete this._disconnectCallback; 109 | 110 | if (errorCallback) { 111 | self.on('error', errorCallback); 112 | } 113 | 114 | var connectEvent; 115 | 116 | if(this.tls) { 117 | self.stream = tls.connect(self.port, self.address, this.tls); 118 | connectEvent = 'secureConnect'; 119 | } 120 | else { 121 | self.stream = net.createConnection(self.port, self.address); 122 | connectEvent = 'connect'; 123 | } 124 | 125 | self.stream.on(connectEvent, self.onConnect.bind(this)); 126 | 127 | self.stream.on('error', function(err) { 128 | process.nextTick(function() { 129 | //clear all of the stomp frame emitter listeners - we don't need them, we've disconnected. 130 | self._stompFrameEmitter.removeAllListeners(); 131 | }); 132 | if (self._retryNumber < self.reconnectOpts.retries) { 133 | if (self._retryNumber === 0) { 134 | //we're disconnected, but we're going to try and reconnect. 135 | self.emit('reconnecting'); 136 | } 137 | self._reconnectTimer = setTimeout(function() { 138 | self.connect(); 139 | }, self._retryNumber++ * self.reconnectOpts.delay) 140 | } else { 141 | if (self._retryNumber === self.reconnectOpts.retries) { 142 | err.message += ' [reconnect attempts reached]'; 143 | err.reconnectionFailed = true; 144 | } 145 | self.emit('error', err); 146 | } 147 | }); 148 | 149 | if (connectedCallback) { 150 | self.on('connect', connectedCallback); 151 | } 152 | 153 | return this; 154 | }; 155 | 156 | StompClient.prototype.disconnect = function (callback) { 157 | var self = this; 158 | 159 | //just a bit of housekeeping. Remove the no-longer-useful reconnect timer. 160 | if (self._reconnectTimer) { 161 | clearTimeout(self._reconnectTimer); 162 | } 163 | 164 | if (this.stream) { 165 | //provide a default no-op function as the callback is optional 166 | this._disconnectCallback = callback || function() {}; 167 | 168 | var frame = new StompFrame({ 169 | command: 'DISCONNECT' 170 | }).send(this.stream); 171 | 172 | process.nextTick(function() { 173 | self.stream.end(); 174 | }); 175 | } 176 | 177 | return this; 178 | }; 179 | 180 | StompClient.prototype.onConnect = function() { 181 | 182 | var self = this; 183 | 184 | // First set up the frame parser 185 | var frameEmitter = self._stompFrameEmitter; 186 | 187 | self.stream.on('data', function(data) { 188 | frameEmitter.handleData(data); 189 | }); 190 | 191 | self.stream.on('end', function() { 192 | if (self._disconnectCallback) { 193 | self._disconnectCallback(); 194 | } else { 195 | self.stream.emit('error', new Error('Server has gone away')); 196 | } 197 | }); 198 | 199 | frameEmitter.on('MESSAGE', function(frame) { 200 | var subscribed = self.subscriptions[frame.headers.destination]; 201 | // .unsubscribe() deletes the subscribed callbacks from the subscriptions, 202 | // but until that UNSUBSCRIBE message is processed, we might still get 203 | // MESSAGE. Check to make sure we don't call .map() on null. 204 | if (subscribed) { 205 | subscribed.listeners.map(function(callback) { 206 | callback(frame.body, frame.headers); 207 | }); 208 | } 209 | self.emit('message', frame.body, frame.headers); 210 | }); 211 | 212 | frameEmitter.on('CONNECTED', function(frame) { 213 | if (self._retryNumber > 0) { 214 | //handle a reconnection differently to the initial connection. 215 | self.emit('reconnect', frame.headers.session, self._retryNumber); 216 | self._retryNumber = 0; 217 | } else { 218 | self.emit('connect', frame.headers.session); 219 | } 220 | }); 221 | 222 | frameEmitter.on('ERROR', function(frame) { 223 | var er = new Error(frame.headers.message); 224 | // frame.headers used to be passed as er, so put the headers on er object 225 | _extend(er, frame.headers); 226 | self.emit('error', er, frame.body); 227 | }); 228 | 229 | frameEmitter.on('parseError', function(err) { 230 | // XXX(sam) err should be an Error object to more easily track the 231 | // point of error detection, but it isn't, so create one now. 232 | var er = new Error(err.message); 233 | if (err.details) { 234 | er.details = err.details; 235 | } 236 | self.emit('error', er); 237 | self.stream.destroy(); 238 | }); 239 | 240 | // Send the CONNECT frame 241 | var headers = { 242 | 'login': self.user, 243 | 'passcode': self.pass 244 | }; 245 | 246 | if(this.vhost && this.version === '1.1') 247 | headers.host = this.vhost; 248 | 249 | var frame = new StompFrame({ 250 | command: 'CONNECT', 251 | headers: headers 252 | }).send(self.stream); 253 | 254 | //if we've just reconnected, we'll need to re-subscribe 255 | for (var queue in self.subscriptions) { 256 | new StompFrame({ 257 | command: 'SUBSCRIBE', 258 | headers: self.subscriptions[queue].headers 259 | }).send(self.stream); 260 | } 261 | }; 262 | 263 | StompClient.prototype.subscribe = function(queue, _headers, _callback) { 264 | // Allow _headers or callback in any order, for backwards compat: so headers 265 | // is whichever arg is not a function, callback is whatever is left over. 266 | var callback; 267 | if (typeof _headers === 'function') { 268 | callback = _headers; 269 | _headers = null; 270 | } 271 | if (typeof _callback === 'function') { 272 | callback = _callback; 273 | _callback = null; 274 | } 275 | // Error now, preventing errors thrown from inside the 'MESSAGE' event handler 276 | assert(callback, 'callback is mandatory on subscribe'); 277 | 278 | var headers = _extend({}, _headers || _callback); 279 | headers.destination = queue; 280 | if (!(queue in this.subscriptions)) { 281 | this.subscriptions[queue] = { 282 | listeners: [], 283 | headers: headers 284 | }; 285 | new StompFrame({ 286 | command: 'SUBSCRIBE', 287 | headers: headers 288 | }).send(this.stream); 289 | } 290 | this.subscriptions[queue].listeners.push(callback); 291 | return this; 292 | }; 293 | 294 | // no need to pass a callback parameter as there is no acknowledgment for 295 | // successful UNSUBSCRIBE from the STOMP server 296 | StompClient.prototype.unsubscribe = function (queue, headers) { 297 | headers = _extend({}, headers); 298 | headers.destination = queue; 299 | new StompFrame({ 300 | command: 'UNSUBSCRIBE', 301 | headers: headers 302 | }).send(this.stream); 303 | delete this.subscriptions[queue]; 304 | return this; 305 | }; 306 | 307 | StompClient.prototype.publish = function(queue, message, headers) { 308 | headers = _extend({}, headers); 309 | headers.destination = queue; 310 | new StompFrame({ 311 | command: 'SEND', 312 | headers: headers, 313 | body: message 314 | }).send(this.stream); 315 | return this; 316 | }; 317 | 318 | function sendAckNack(acknack, messageId, subscription, transaction) { 319 | var headers = { 320 | 'message-id': messageId, 321 | 'subscription': subscription 322 | }; 323 | if(transaction) { 324 | headers['transaction'] = transaction; 325 | } 326 | new StompFrame({ 327 | command: acknack, 328 | headers: headers 329 | }).send(this.stream); 330 | } 331 | 332 | StompClient.prototype.ack = function(messageId, subscription, transaction) { 333 | sendAckNack.call(this, 'ACK', messageId, subscription, transaction); 334 | return this; 335 | }; 336 | 337 | StompClient.prototype.nack = function(messageId, subscription, transaction) { 338 | sendAckNack.call(this, 'NACK', messageId, subscription, transaction); 339 | return this; 340 | }; 341 | 342 | Object.defineProperty(StompClient.prototype, 'writable', { 343 | get: function(){ 344 | return this.stream && this.stream.writable; 345 | } 346 | }); 347 | 348 | module.exports = StompClient; 349 | module.exports.StompClient = StompClient; 350 | 351 | module.exports.Errors = { 352 | streamNotWritable: 15201 353 | }; 354 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | Events = require('events').EventEmitter, 3 | nodeunit = require('nodeunit'), 4 | testCase = require('nodeunit').testCase; 5 | 6 | var StompClient = require('../lib/client').StompClient; 7 | var connectionObserver; 8 | 9 | // surpress logs for the test 10 | util.log = function() {}; 11 | 12 | // check message headers are properties of Error object 13 | function checkError(test, er, expectedHeaders, msg) 14 | { 15 | var headers = {} 16 | for (var key in expectedHeaders) { 17 | headers[key] = er[key]; 18 | } 19 | test.deepEqual(headers, expectedHeaders, msg); 20 | } 21 | 22 | // net mockage 23 | var net = require('net'); 24 | var StompFrame = require('../lib/frame').StompFrame; 25 | 26 | // Override StompFrame send function to allow inspection of frame data inside a test 27 | var oldSend; 28 | var oldCreateConnection; 29 | var sendHook = function() {}; 30 | 31 | module.exports = testCase({ 32 | 33 | setUp: function(callback) { 34 | // Mock net object so we never try to send any real data 35 | connectionObserver = new Events(); 36 | connectionObserver.destroy = function() {}; 37 | this.stompClient = new StompClient('127.0.0.1', 2098, 'user', 'pass', '1.0'); 38 | 39 | oldCreateConnection = net.createConnection; 40 | net.createConnection = function() { 41 | return connectionObserver; 42 | }; 43 | 44 | oldSend = StompFrame.prototype.send; 45 | StompFrame.prototype.send = function(stream) { 46 | var self = this; 47 | process.nextTick(function () { 48 | sendHook(self); 49 | }); 50 | }; 51 | 52 | callback(); 53 | }, 54 | 55 | tearDown: function(callback) { 56 | delete this.stompClient; 57 | sendHook = function() {}; 58 | net.createConnection = oldCreateConnection; 59 | StompFrame.prototype.send = oldSend; 60 | callback(); 61 | }, 62 | 63 | 'check default properties are correctly set on a basic StompClient': function(test) { 64 | var stompClient = new StompClient(); 65 | 66 | test.equal(stompClient.user, ''); 67 | test.equal(stompClient.pass, ''); 68 | test.equal(stompClient.address, '127.0.0.1'); 69 | test.equal(stompClient.port, 61613); 70 | test.equal(stompClient.version, '1.0'); 71 | 72 | test.done(); 73 | }, 74 | 75 | 'check StompClient construction from paremeters': function(test) { 76 | var stompClient = new StompClient( 77 | 'test.host.net',1234,'uname','pw', '1.1', 'q1.host.net', 78 | { retries: 10, delay: 1000 }); 79 | 80 | test.equal(stompClient.user, 'uname'); 81 | test.equal(stompClient.pass, 'pw'); 82 | test.equal(stompClient.address, 'test.host.net'); 83 | test.equal(stompClient.port, 1234); 84 | test.equal(stompClient.version, '1.1'); 85 | test.equal(stompClient.vhost, 'q1.host.net'); 86 | test.equal(stompClient.reconnectOpts.retries, 10); 87 | test.equal(stompClient.reconnectOpts.delay, 1000); 88 | 89 | test.done(); 90 | }, 91 | 92 | 'check StompClient construction from options': function(test) { 93 | var stompClient = new StompClient( { 94 | address: 'test.host.net', 95 | port: 1234, 96 | user: 'uname', 97 | pass: 'pw', 98 | protocolVersion: '1.1', 99 | vhost: 'q1.host.net', 100 | reconnectOpts: { retries: 10, delay: 1000 }}); 101 | 102 | test.equal(stompClient.user, 'uname'); 103 | test.equal(stompClient.pass, 'pw'); 104 | test.equal(stompClient.address, 'test.host.net'); 105 | test.equal(stompClient.port, 1234); 106 | test.equal(stompClient.version, '1.1'); 107 | test.equal(stompClient.vhost, 'q1.host.net'); 108 | test.equal(stompClient.reconnectOpts.retries, 10); 109 | test.equal(stompClient.reconnectOpts.delay, 1000); 110 | 111 | test.done(); 112 | }, 113 | 114 | 'check StompClient TLS construction': function(test) { 115 | 116 | var stompClient = new StompClient( 117 | 'test.host.net',1234,'uname','pw', null, null, null, true); 118 | test.deepEqual(stompClient.tls, {}, 'TLS not set by parameter'); 119 | 120 | var stompClient = new StompClient( 121 | 'test.host.net',1234,'uname','pw', null, null, null, false); 122 | test.ok(!stompClient.tls, 'TLS incorrectly set by parameter'); 123 | 124 | var stompClient = new StompClient({ 125 | host: 'secure.host.net', 126 | tls: true, 127 | cert: 'dummy' 128 | }); 129 | test.equal(stompClient.address, 'secure.host.net'); 130 | test.deepEqual(stompClient.tls.cert, 'dummy', 'TLS not set by option'); 131 | 132 | var stompClient = new StompClient({ 133 | host: 'secure.host.net', 134 | tls: false, 135 | cert: 'dummy' 136 | }); 137 | test.equal(stompClient.address, 'secure.host.net'); 138 | test.ok(!stompClient.tls, 'TLS incorrectly set by option'); 139 | 140 | var stompClient = new StompClient({ 141 | host: 'secure.host.net', 142 | tls: { 143 | cert: 'dummy' 144 | }}); 145 | test.equal(stompClient.address, 'secure.host.net'); 146 | test.deepEqual(stompClient.tls.cert, 'dummy', 147 | 'TLS not set by nested option'); 148 | 149 | test.done(); 150 | }, 151 | 152 | 'check outbound CONNECT frame correctly follows protocol specification': function(test) { 153 | var self = this; 154 | test.expect(4); 155 | 156 | sendHook = function(stompFrame) { 157 | test.equal(stompFrame.command, 'CONNECT'); 158 | test.deepEqual(stompFrame.headers, { 159 | login: 'user', 160 | passcode: 'pass' 161 | }); 162 | test.equal(stompFrame.body, ''); 163 | test.equal(stompFrame.contentLength, -1); 164 | 165 | test.done(); 166 | }; 167 | 168 | //start the test 169 | this.stompClient.connect(); 170 | connectionObserver.emit('connect'); 171 | 172 | }, 173 | 174 | 'check inbound CONNECTED frame parses correctly': function(test) { 175 | var self = this; 176 | var testId = '1234'; 177 | 178 | test.expect(2); 179 | 180 | sendHook = function() { 181 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 182 | }; 183 | 184 | this.stompClient._stompFrameEmitter.on('CONNECTED', function (stompFrame) { 185 | test.equal(stompFrame.command, 'CONNECTED'); 186 | test.equal(testId, stompFrame.headers.session); 187 | test.done(); 188 | }); 189 | 190 | //start the test 191 | this.stompClient.connect(function() {}); 192 | connectionObserver.emit('connect'); 193 | }, 194 | 195 | 'check the ERROR callback fires when we receive an error frame on connection': function (test) { 196 | var self = this, 197 | expectedHeaders = { 198 | message: 'some test error', 199 | 'content-length' : 18 200 | }, 201 | expectedBody = 'Error message body'; 202 | 203 | test.expect(2); 204 | 205 | // mock that we received a CONNECTED from the stomp server in our send hook 206 | sendHook = function (stompFrame) { 207 | self.stompClient.stream.emit('data', 'ERROR\nmessage:' + expectedHeaders.message + '\ncontent-length:' + expectedHeaders['content-length'] + '\n\n' + expectedBody + '\0'); 208 | }; 209 | 210 | this.stompClient.connect(function () { 211 | test.ok(false, 'Success callback of connect() should not be called'); 212 | }, function (headers, body) { 213 | checkError(test, headers, expectedHeaders, 'passed ERROR frame headers should be as expected'); 214 | test.equal(body, expectedBody, 'passed ERROR frame body should be as expected'); 215 | test.done(); 216 | }); 217 | 218 | connectionObserver.emit('connect'); 219 | }, 220 | 221 | 'check outbound SUBSCRIBE frame correctly follows protocol specification': function(test) { 222 | var self = this; 223 | var testId = '1234'; 224 | var destination = '/queue/someQueue'; 225 | 226 | test.expect(10); 227 | 228 | //mock that we received a CONNECTED from the stomp server in our send hook 229 | sendHook = function(stompFrame) { 230 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 231 | }; 232 | 233 | // Once connected - subscribe to a fake queue 234 | this.stompClient._stompFrameEmitter.on('CONNECTED', function (stompFrame) { 235 | function unsubscribe() { 236 | sendHook = function (){}; 237 | self.stompClient.unsubscribe(destination); 238 | } 239 | // Synchronous hooking of the .send(), vastly simplifying the tests below 240 | StompFrame.prototype.send = function(stream) { 241 | var self = this; 242 | sendHook(self); 243 | }; 244 | 245 | //override the sendHook so we can test the latest stompframe to be sent 246 | sendHook = function(stompFrame) { 247 | test.equal(stompFrame.command, 'SUBSCRIBE'); 248 | test.equal(stompFrame.headers.destination, destination); 249 | test.equal(stompFrame.headers.id, 'blah'); 250 | }; 251 | 252 | // note the use of additional id header (optional in spec) below :) 253 | self.stompClient.subscribe(destination, function(){}, { id: 'blah' }); 254 | unsubscribe(); 255 | 256 | sendHook = function(stompFrame) { 257 | test.equal(stompFrame.command, 'SUBSCRIBE'); 258 | test.equal(stompFrame.headers.destination, destination); 259 | test.equal(stompFrame.headers.id, 'shucks'); 260 | }; 261 | 262 | // Note the natural argument order is used, and destination is ignored, it 263 | // gets overwritten by the real destination. 264 | self.stompClient.subscribe(destination, { id: 'shucks', destination: 'D' }, function(){}); 265 | unsubscribe(); 266 | 267 | // Subscribe without headers is valid 268 | sendHook = function(stompFrame) { 269 | test.equal(stompFrame.command, 'SUBSCRIBE'); 270 | test.equal(stompFrame.headers.destination, destination); 271 | }; 272 | 273 | self.stompClient.subscribe(destination, function(){}); 274 | unsubscribe(); 275 | 276 | // Subscribe without a callback is invalid, with or without headers 277 | try { 278 | self.stompClient.subscribe(destination, {}); 279 | } catch(er) { 280 | test.ok(true); 281 | } 282 | unsubscribe(); 283 | 284 | try { 285 | self.stompClient.subscribe(destination, {}); 286 | } catch(er) { 287 | test.ok(true); 288 | } 289 | unsubscribe(); 290 | 291 | test.done(); 292 | }); 293 | 294 | this.stompClient.connect(function() {}); 295 | connectionObserver.emit('connect'); 296 | }, 297 | 298 | 'check the SUBSCRIBE callback fires when we receive data down the destination queue': function(test) { 299 | var self = this; 300 | var testId = '1234'; 301 | var destination = '/queue/someQueue'; 302 | var messageId = 'ID:SomeID:1'; 303 | var messageToBeSent = 'oh herrow!'; 304 | 305 | test.expect(5); 306 | 307 | //mock that we received a CONNECTED from the stomp server in our send hook 308 | sendHook = function(stompFrame) { 309 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 310 | }; 311 | 312 | this.stompClient.connect(function() { 313 | 314 | // Mock inbound MESSAGE frame 315 | sendHook = function (stompFrame) { 316 | self.stompClient.stream.emit('data', 'MESSAGE\ndestination:' + destination + '\nmessage-id:' + messageId + '\n\n' + messageToBeSent + '\0'); 317 | }; 318 | 319 | // Subscribe to a queue, and upon receipt of message (wired above) test that body/headers correctly propogate to callback 320 | self.stompClient.subscribe(destination, function (body, headers) { 321 | test.equal(body, messageToBeSent, 'Received message matches the sent one'); 322 | test.equal(headers['message-id'], messageId); 323 | test.equal(headers.destination, destination); 324 | test.equal(self.stompClient.subscriptions[destination].listeners.length, 1, 'ensure callback was added to subscription stack'); 325 | 326 | // Unsubscribe and ensure queue is cleared of the subscription (and related callback) 327 | self.stompClient.unsubscribe(destination, {}); 328 | 329 | test.equal(typeof self.stompClient.subscriptions[destination], 'undefined', 'ensure queue is cleared of the subscription'); 330 | test.done(); 331 | }); 332 | 333 | }); 334 | 335 | connectionObserver.emit('connect'); 336 | }, 337 | 338 | 'check the MESSAGE callback fires when we receive a message': function(test) { 339 | var self = this; 340 | var testId = '1234'; 341 | var destination = '/queue/someQueue'; 342 | var messageId = 'ID:SomeID:1'; 343 | var messageToBeSent = 'oh herrow!'; 344 | 345 | test.expect(5); 346 | 347 | //mock that we received a CONNECTED from the stomp server in our send hook 348 | sendHook = function(stompFrame) { 349 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 350 | }; 351 | 352 | this.stompClient.connect(function() { 353 | 354 | // Mock inbound MESSAGE frame 355 | sendHook = function (stompFrame) { 356 | self.stompClient.stream.emit('data', 'MESSAGE\ndestination:' + destination + '\nmessage-id:' + messageId + '\n\n' + messageToBeSent + '\0'); 357 | }; 358 | 359 | // Subscribe to the queue, but don't do anything with the response 360 | self.stompClient.subscribe(destination, function () {}); 361 | 362 | // Subscribe to the MESSAGE hook, and upon receipt of message (wired above) test that body/headers correctly propogate to callback 363 | self.stompClient.on('message', function (body, headers) { 364 | test.equal(body, messageToBeSent, 'Received message matches the sent one'); 365 | test.equal(headers['message-id'], messageId); 366 | test.equal(headers.destination, destination); 367 | 368 | // Check the subscription has also been created 369 | test.equal(self.stompClient.subscriptions[destination].listeners.length, 1, 'ensure callback was added to subscription stack'); 370 | 371 | // Unsubscribe and ensure queue is cleared of the subscription (and related callback) 372 | self.stompClient.unsubscribe(destination, {}); 373 | 374 | test.equal(typeof self.stompClient.subscriptions[destination], 'undefined', 'ensure queue is cleared of the subscription'); 375 | test.done(); 376 | }); 377 | 378 | }); 379 | 380 | connectionObserver.emit('connect'); 381 | }, 382 | 383 | 'check the ERROR callback fires when we receive an error frame on subscription': function (test) { 384 | var self = this, 385 | testId = '1234', 386 | destination = '/queue/someQueue', 387 | expectedHeaders = { 388 | message: 'some test error', 389 | 'content-length' : 18 390 | }, 391 | expectedBody = 'Error message body', 392 | errorCallbackCalled = false; 393 | 394 | test.expect(3); 395 | 396 | //mock that we received a CONNECTED from the stomp server in our send hook 397 | sendHook = function (stompFrame) { 398 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 399 | }; 400 | 401 | this.stompClient.connect(function () { 402 | 403 | // Mock inbound ERROR frame 404 | sendHook = function (stompFrame) { 405 | self.stompClient.stream.emit('data', 'ERROR\nmessage:' + expectedHeaders.message + '\ncontent-length:' + expectedHeaders['content-length'] + '\n\n' + expectedBody + '\0'); 406 | }; 407 | 408 | // make sure the error callback hasn't been called yet 409 | test.equal(errorCallbackCalled, false, 'ERROR callback should not have been called yet'); 410 | 411 | // Subscribe to a queue, and upon receipt of message (wired above) test that body/headers correctly propogate to callback 412 | self.stompClient.subscribe(destination, function () { 413 | test.ok(false, 'Success callback of subscribe() should not be called'); 414 | }); 415 | 416 | }, function (headers, body) { 417 | errorCallbackCalled = true; 418 | checkError(test, headers, expectedHeaders, 'passed ERROR frame headers should be as expected'); 419 | test.equal(body, expectedBody, 'passed ERROR frame body should be as expected'); 420 | test.done(); 421 | }); 422 | 423 | connectionObserver.emit('connect'); 424 | }, 425 | 426 | 'check outbound UNSUBSCRIBE frame correctly follows protocol specification': function (test) { 427 | var self = this; 428 | var testId = '1234'; 429 | var destination = '/queue/someQueue'; 430 | 431 | test.expect(4); 432 | 433 | //mock that we received a CONNECTED from the stomp server in our send hook 434 | sendHook = function(stompFrame) { 435 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 436 | }; 437 | 438 | // Once connected - unsubscribe to a fake queue 439 | this.stompClient._stompFrameEmitter.on('CONNECTED', function (stompFrame) { 440 | //override the sendHook so we can test the latest stompframe to be sent 441 | sendHook = function(stompFrame) { 442 | test.equal(stompFrame.command, 'UNSUBSCRIBE'); 443 | test.equal(stompFrame.headers.destination, destination); 444 | test.equal(stompFrame.headers.id, 'specialid'); 445 | test.done(); 446 | }; 447 | 448 | var before = { id: 'specialid' }; 449 | var after = { id: 'specialid' }; 450 | self.stompClient.unsubscribe(destination, after); 451 | test.deepEqual(before, after, "methods shouldn't modify their arguments"); 452 | }); 453 | 454 | this.stompClient.connect(function(){}); 455 | connectionObserver.emit('connect'); 456 | }, 457 | 458 | 'check the ERROR callback fires when we receive an error frame when unsubscribing': function (test) { 459 | var self = this, 460 | testId = '1234', 461 | destination = '/queue/someQueue', 462 | expectedHeaders = { 463 | message: 'some test error', 464 | 'content-length' : 18 465 | }, 466 | expectedBody = 'Error message body', 467 | errorCallbackCalled = false; 468 | 469 | test.expect(4); 470 | 471 | //mock that we received a CONNECTED from the stomp server in our send hook 472 | sendHook = function (stompFrame) { 473 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 474 | }; 475 | 476 | this.stompClient.connect(function () { 477 | 478 | // Mock inbound MESSAGE frame 479 | sendHook = function (stompFrame) { 480 | self.stompClient.stream.emit('data', 'MESSAGE\ndestination:' + destination + '\nmessage-id:some message id\n\nsome message body\0'); 481 | }; 482 | 483 | // make sure the error callback hasn't been called yet 484 | test.equal(errorCallbackCalled, false, 'ERROR callback should not have been called yet'); 485 | 486 | // Subscribe to a queue, and upon receipt of message (wired above) test that body/headers correctly propogate to callback 487 | self.stompClient.subscribe(destination, function () { 488 | 489 | // Mock inbound ERROR frame 490 | sendHook = function (stompFrame) { 491 | self.stompClient.stream.emit('data', 'ERROR\nmessage:' + expectedHeaders.message + '\ncontent-length:' + expectedHeaders['content-length'] + '\n\n' + expectedBody + '\0'); 492 | }; 493 | 494 | test.equal(errorCallbackCalled, false, 'ERROR callback should not have been called yet'); 495 | 496 | self.stompClient.unsubscribe(destination, { id: 'specialid' }); 497 | 498 | }); 499 | 500 | }, function (headers, body) { 501 | errorCallbackCalled = true; 502 | checkError(test, headers, expectedHeaders, 'passed ERROR frame headers should be as expected'); 503 | test.equal(body, expectedBody, 'passed ERROR frame body should be as expected'); 504 | test.done(); 505 | }); 506 | 507 | connectionObserver.emit('connect'); 508 | }, 509 | 510 | 'check outbound SEND frame correctly follows protocol specification': function (test) { 511 | var self = this; 512 | var testId = '1234'; 513 | var destination = '/queue/someQueue'; 514 | var messageToBeSent = 'oh herrow!'; 515 | 516 | test.expect(3); 517 | 518 | //mock that we received a CONNECTED from the stomp server in our send hook 519 | sendHook = function (stompFrame) { 520 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 521 | }; 522 | 523 | this.stompClient.connect(function() { 524 | 525 | sendHook = function(stompFrame) { 526 | test.equal(stompFrame.command, 'SEND'); 527 | test.deepEqual(stompFrame.headers, { destination: destination }); 528 | test.equal(stompFrame.body, messageToBeSent); 529 | test.done(); 530 | }; 531 | 532 | self.stompClient.publish(destination, messageToBeSent); 533 | 534 | }); 535 | 536 | connectionObserver.emit('connect'); 537 | }, 538 | 539 | 'check outbound SEND header correctly follows protocol specification': function (test) { 540 | var self = this; 541 | var testId = '1234'; 542 | var destination = '/queue/someQueue'; 543 | var messageToBeSent = 'oh herrow!'; 544 | var headers = { 545 | destination: 'TO BE OVERWRITTEN', 546 | 'content-type': 'text/plain' 547 | }; 548 | 549 | test.expect(3); 550 | 551 | //mock that we received a CONNECTED from the stomp server in our send hook 552 | sendHook = function (stompFrame) { 553 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:' + testId + '\n\n\0'); 554 | }; 555 | 556 | this.stompClient.connect(function() { 557 | 558 | sendHook = function(stompFrame) { 559 | test.equal(stompFrame.command, 'SEND'); 560 | headers.destination = destination; 561 | test.deepEqual(stompFrame.headers, headers); 562 | test.equal(stompFrame.body, messageToBeSent); 563 | test.done(); 564 | }; 565 | 566 | self.stompClient.publish(destination, messageToBeSent, headers); 567 | 568 | }); 569 | 570 | connectionObserver.emit('connect'); 571 | }, 572 | 573 | 'check parseError event fires when malformed frame is received': function(test) { 574 | var self = this; 575 | 576 | test.expect(2); 577 | 578 | //mock that we received a CONNECTED from the stomp server in our send hook 579 | sendHook = function (stompFrame) { 580 | self.stompClient.stream.emit('data', 'CONNECTED\n\n\n\0'); 581 | }; 582 | 583 | this.stompClient.on('error', function (err) { 584 | test.equal(err.message, 'Header "session" is required for CONNECTED'); 585 | test.equal(err.details, 'Frame: {"command":"CONNECTED","headers":{},"body":"\\n"}'); 586 | test.done(); 587 | }); 588 | 589 | this.stompClient.connect(function() {}); 590 | connectionObserver.emit('connect'); 591 | }, 592 | 593 | 'check disconnect method correctly sends DISCONNECT frame, disconnects TCP stream, and fires callback': function (test) { 594 | var self = this; 595 | 596 | test.expect(5); 597 | 598 | //mock that we received a CONNECTED from the stomp server in our send hook 599 | sendHook = function (stompFrame) { 600 | self.stompClient.stream.emit('data', 'CONNECTED\nsession:blah\n\n\0'); 601 | }; 602 | 603 | self.stompClient.connect(function() { 604 | 605 | // Assert next outbound STOMP frame is a DISCONNECT 606 | sendHook = function (stompFrame) { 607 | test.equal(stompFrame.command, 'DISCONNECT'); 608 | test.deepEqual(stompFrame.headers, {}); 609 | test.equal(stompFrame.body, ''); 610 | }; 611 | 612 | // Set disconnection callback to ensure it is called appropriately 613 | self.stompClient.disconnect(function () { 614 | test.ok(true, 'disconnect callback executed'); 615 | test.done(); 616 | }); 617 | 618 | }); 619 | 620 | // Mock the TCP end call 621 | connectionObserver.end = function() { 622 | test.ok(true, 'TCP end call made'); 623 | connectionObserver.end = function(){}; 624 | process.nextTick(function() { connectionObserver.emit('end'); }); 625 | }; 626 | 627 | connectionObserver.emit('connect'); 628 | } 629 | 630 | }); 631 | --------------------------------------------------------------------------------