├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── gulpfile.js ├── index.js ├── lib ├── broadcast.js ├── index.js ├── protocol.js ├── request.js ├── response.js ├── router.js └── socket.js ├── package.json └── specs ├── api.spec.js └── socket.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | .zedstate -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "validateIndentation": 4, 4 | "disallowSpacesInFunctionDeclaration": null, 5 | "disallowSpacesInNamedFunctionExpression": null, 6 | "disallowSpacesInFunctionExpression": null, 7 | "disallowSpacesInAnonymousFunctionExpression": null, 8 | "safeContextKeyword": "self", 9 | "requireMultipleVarDecl": null, 10 | "disallowAnonymousFunctions": true 11 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "trailing": true, 12 | "boss": true, 13 | "eqnull": true, 14 | "strict": true, 15 | "immed": true, 16 | "expr": true, 17 | "latedef": "true", 18 | "quotmark": "single", 19 | "indent": 4, 20 | "node": true, 21 | "globals": { 22 | "describe": false, 23 | "it": false 24 | } 25 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.12 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.0 (2015-04-08) 2 | 3 | * cade8a2 feature: better random port selection (broadcast) 4 | 5 | # 1.0.0 (2014-12-30) 6 | 7 | * Implemented possibility for executing a `command` on several hosts within a multicast group and collecting results afterwards. 8 | * Support for registering commands (`server.command`). 9 | * Implemented API for creating `kast` servers (`server.listen`). 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 André König, Germany 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kast [![Build Status](https://travis-ci.org/akoenig/kast.svg)](https://travis-ci.org/akoenig/kast) 2 | 3 | An UDP multicast framework. 4 | 5 | ## Usage example 6 | 7 | Let's say we want to start two `kast` servers, each of which provides a command to check if the respective host is alive. 8 | 9 | ### Host A 10 | 11 | ```javascript 12 | var kast = require('kast'); 13 | 14 | var server = kast(); 15 | 16 | server.command('/alive', function (req, res) { 17 | res.send('Hey! Host A is alive!'); 18 | }); 19 | 20 | server.listen(5000); 21 | ``` 22 | 23 | ### Host B 24 | 25 | ```javascript 26 | var kast = require('kast'); 27 | 28 | var server = kast(); 29 | 30 | server.command('/alive', function (req, res) { 31 | res.send('This is Host B speaking! How can I help you?'); 32 | }); 33 | 34 | server.listen(5000); 35 | ``` 36 | 37 | ### Client 38 | 39 | A client can now send a _broadcast request_ to all the hosts without knowing each individual ip address / host name. 40 | 41 | ```javascript 42 | var kast = require('kast'); 43 | 44 | kast.broadcast({ 45 | port: 5000, 46 | command: '/alive', 47 | }, function onResults (err, results) { 48 | console.log(results); 49 | }); 50 | ``` 51 | 52 | The results of the hosts which have responded in a timely manner will be collected in the `results` argument, like: 53 | 54 | ```javascript 55 | { 56 | '10.0.0.1': 'Hey! Host A is alive!', 57 | '10.0.0.23': 'This is Host B speaking! How can I help you?' 58 | } 59 | ``` 60 | 61 | ## API 62 | 63 | ### Install 64 | 65 | ```sh 66 | $ npm install --save kast 67 | ``` 68 | 69 | ### Basic usage 70 | 71 | ```javascript 72 | 73 | var kast = require('kast'); 74 | 75 | var server = kast(); 76 | ``` 77 | 78 | ### Creating a server instance 79 | 80 | #### server.listen(port, [host], [callback]) 81 | 82 | Binds and listens for new connections on the given host and post. Please note that the `host` parameter is optional. `kast` uses `224.1.1.1` as a default multicast group. 83 | 84 | The `callback` function will be executed as `callback(err)` after the binding process has been finished. 85 | 86 | This method will return an instance of `Socket` which exposes the following API: 87 | 88 | ##### socket.close(callback) 89 | 90 | Possibility for shutting the whole `kast` server down. 91 | 92 | ### Registering commands 93 | 94 | #### server.command(name, handlerFn) 95 | 96 | The `handlerFn` will receive two ordinary parameters: A `req`uest and a `res`ponse parameter. Pretty much like [Express](http://expressjs.com/) - You're welcome :). 97 | 98 | ##### Request 99 | 100 | An object with the following information: 101 | 102 | * `req.id`: An unique request id (will be generated by `kast`). 103 | * `req.connection.remoteAddress`: The ip address of the host which has sent the request. 104 | * `req.connection.remotePort`: The remote port of the host which has sent the request. 105 | * `req.command`: The `name` of the command. 106 | * `req.body`: A possible request body with additional data (comparable with a HTTP request body). 107 | 108 | ##### Response 109 | 110 | While the `req` object provides pure information without any kind of functionality, the `res` object exposes a method with which you can _answer_ a command and send the result back to the client. 111 | 112 | ###### res.send(body) 113 | 114 | Sends a response with the given `body` as string. 115 | 116 | ### Send a broadcast to the servers 117 | 118 | #### kast.broadcast(options, callback) 119 | 120 | The broadcast method will usually be called by a client which wants to execute several commands on the respective hosts (within the multicast group). 121 | 122 | Possible options are: 123 | 124 | * `port`: The port on which the other hosts are listening. 125 | * `command`: The name of the command which should be executed. 126 | * `body`: (optional) Additional data that should be passed to the command. 127 | * `timeout: (optional; default=2000ms) The timeout after which the broadcast should stop waiting for responses. 128 | * `host`: (optional; default='224.1.1.1') The ip address of the multicast group. 129 | 130 | The callback function will be executed as `callback(err, results)`. `results` will be an object with the ip addresses of the hosts as `keys` and the respective responses as `values`. 131 | 132 | ## Development 133 | 134 | ### Tests 135 | 136 | In order to run the test suite, install the dependencies first, then run `npm test`: 137 | 138 | ```sh 139 | $ npm install 140 | $ npm test 141 | ``` 142 | 143 | ## License 144 | 145 | MIT © 2014, [André König](http://andrekoenig.info) (andre.koenig@posteo.de) 146 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var path = require('path'); 17 | 18 | var gulp = require('gulp'); 19 | var jshint = require('gulp-jshint'); 20 | var jscs = require('gulp-jscs'); 21 | var mocha = require('gulp-mocha'); 22 | var sequence = require('run-sequence'); 23 | 24 | var paths = {}; 25 | 26 | paths.sources = [ 27 | path.join(__dirname, '*.js'), 28 | path.join(__dirname, 'lib', '**', '*.js'), 29 | path.join(__dirname, 'specs', '**', '*.spec.js') 30 | ]; 31 | 32 | paths.specs = [path.join(__dirname, 'specs', '**', '*.spec.js')]; 33 | 34 | gulp.task('specs', function specs () { 35 | return gulp.src(paths.specs, {read: false}) 36 | .pipe(mocha({reporter: 'nyan'})); 37 | }); 38 | 39 | gulp.task('lint', function lint () { 40 | return gulp.src(paths.sources) 41 | .pipe(jshint()) 42 | .pipe(jshint.reporter('jshint-stylish')) 43 | .pipe(jshint.reporter('fail')); 44 | }); 45 | 46 | gulp.task('checkstyle', function checkstyle () { 47 | return gulp.src(paths.sources) 48 | .pipe(jscs()); 49 | }); 50 | 51 | gulp.task('default', function defaultTask (callback) { 52 | return sequence('lint', 'checkstyle', 'specs', callback); 53 | }); 54 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | module.exports = require('./lib/'); 17 | -------------------------------------------------------------------------------- /lib/broadcast.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var net = require('net'); 17 | 18 | var debug = require('debug')('kart:Broadcast'); 19 | var mandatory = require('mandatory'); 20 | var VError = require('verror'); 21 | 22 | var socket = require('./socket'); 23 | var protocol = require('./protocol')(); 24 | 25 | /** 26 | * 27 | * Provides an interface for sending a multicast request. 28 | * 29 | * @param {object} options 30 | * Broadcast request options. An example: 31 | * 32 | * `port`: The multicast port on which the other clients are listening. 33 | * `command`: The command which should be executed on the client's side. 34 | * 35 | * `body`: (optional) The request body that should be passed to the client's command handler. 36 | * `timeout`: (optional; default: 2000) The timeout after which the broadcast should stop waiting for responses. 37 | * 38 | * `host`: (optional; default: '224.1.1.1') The multicast group ip address. 39 | * 40 | * @param {function} callback 41 | * Will be executed when the responses has been collected. Executed as `callback(err, results)` 42 | * whereas results is an {array} that contains the ip address of the remote host as key 43 | * and the response body as value. Example: 44 | * 45 | * { 46 | * '192.168.178.0.65': '{"uptime": "20h"}', 47 | * '192.168.178.0.12': '{"uptime": "47h"}' 48 | * } 49 | * 50 | */ 51 | module.exports = function broadcast (options, callback) { 52 | var bcast; 53 | 54 | mandatory(options).is('object', 'Please provide a broadcast configuration object.'); 55 | mandatory(options.port).is('number', 'Please provide a port of the remote multicast sockets.'); 56 | mandatory(options.command).is('string', 'Please provide a command which should be executed.'); 57 | 58 | mandatory(callback).is('function', 'Please provide a proper callback function.'); 59 | 60 | if (options.body) { 61 | mandatory(options.body).is('string', 'Please make sure that the request body is a string.'); 62 | } 63 | 64 | options.timeout = options.timeout || 2000; 65 | 66 | bcast = new Broadcast(options); 67 | 68 | bcast.spread(callback); 69 | }; 70 | 71 | function Broadcast (options) { 72 | var hrtime = process.hrtime(); 73 | 74 | this.$timeout = options.timeout; 75 | 76 | this.$dto = { 77 | id: Date.now().toString() + hrtime[0] + hrtime[1], 78 | command: options.command, 79 | body: options.body 80 | }; 81 | 82 | // 83 | // Save the port of the multicast group. 84 | // This broadcast will create a separate socket on a random port. 85 | // 86 | this.$remotePort = options.port; 87 | this.$host = options.host; 88 | } 89 | 90 | /** 91 | * @private 92 | * 93 | * Helper method which determines an open port on this host. 94 | * 95 | * @param {function} callback 96 | * Will be executed as `callback(err, port)`. 97 | * 98 | */ 99 | Broadcast.prototype.$getRandomPort = function $getRandomPort (callback) { 100 | var range = [49152, 65535]; 101 | 102 | function roll () { 103 | return Math.floor(Math.random() * (range[1] - range[0]) + range[0]); 104 | } 105 | 106 | (function getPort (cb) { 107 | var port = roll(); 108 | var server = net.createServer(); 109 | 110 | range = range + 1; 111 | 112 | server.listen(port, function onListen () { 113 | server.once('close', function onClose () { 114 | return cb(null, port); 115 | }); 116 | 117 | server.close(); 118 | }); 119 | 120 | server.on('error', function onError () { 121 | getPort(cb); 122 | }); 123 | })(callback); 124 | }; 125 | 126 | /** 127 | * Sends the actual broadcast request over the wire. The method will create a 128 | * separate socket in order to not interfere with a possible multicast socket. 129 | * 130 | * @param {function} callback 131 | * Will be executed when the defined timeout has reached. Will be executed as 132 | * `callback(err, results)` whereas `results` provides the following exemplary structure: 133 | * 134 | * { 135 | * '192.168.178.0.65': '{"uptime": "20h"}', 136 | * '192.168.178.0.12': '{"uptime": "47h"}' 137 | * } 138 | * 139 | */ 140 | Broadcast.prototype.spread = function spread (callback) { 141 | var self = this; 142 | var sock = null; 143 | var results = {}; 144 | 145 | function onMessage (buffer, rinfo) { 146 | var response = protocol.parse('response', buffer); 147 | 148 | if (response.id === self.$dto.id) { 149 | results[rinfo.address] = response.body; 150 | } 151 | } 152 | 153 | function onClose () { 154 | debug('Port closed.'); 155 | 156 | callback(null, results); 157 | } 158 | 159 | function onTimeout () { 160 | debug('Timeout.'); 161 | 162 | sock.close(onClose); 163 | } 164 | 165 | function onOpen (err) { 166 | var request = null; 167 | 168 | if (err) { 169 | return callback(new VError(err, 'Failed to open the socket for broadcasting.')); 170 | } 171 | 172 | debug('Port opened.'); 173 | 174 | request = new Buffer(protocol.serialize('request', self.$dto)); 175 | 176 | sock.send(request, self.$remotePort); 177 | 178 | debug('Sent request. Waiting for responses (%dms).', self.$timeout); 179 | 180 | setTimeout(onTimeout, self.$timeout); 181 | } 182 | 183 | function onRandomPort (err, port) { 184 | var options = {}; 185 | 186 | if (err) { 187 | return callback(new VError(err, 'Failed to grab a new random port for the broadcast socket.')); 188 | } 189 | 190 | debug('Fetched a new random port at %d', port); 191 | 192 | options.host = self.$host; 193 | options.port = port; 194 | 195 | options.dispatcher = onMessage; 196 | 197 | sock = socket(options); 198 | sock.open(onOpen); 199 | } 200 | 201 | this.$getRandomPort(onRandomPort); 202 | }; 203 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var debug = require('debug')('kast:Kast'); 17 | var mandatory = require('mandatory'); 18 | var VError = require('verror'); 19 | 20 | var socket = require('./socket'); 21 | var request = require('./request'); 22 | var response = require('./response'); 23 | var router = require('./router'); 24 | var broadcast = require('./broadcast'); 25 | 26 | module.exports = function initialize () { 27 | var kast = new Kast(); 28 | 29 | // 30 | // Prepare the front-facing API. 31 | // 32 | return { 33 | command: kast.command.bind(kast), 34 | listen: kast.listen.bind(kast) 35 | }; 36 | }; 37 | 38 | module.exports.broadcast = broadcast; 39 | 40 | function Kast () { 41 | // Will be set in the `listen` method. 42 | this.$socket = null; 43 | 44 | this.$router = router(); 45 | } 46 | 47 | /** 48 | * @private 49 | * 50 | * This dispatcher method will be executed when a new request has been received 51 | * through the multicast socket. It is responsible for creating the respective 52 | * request and response object, extracting the intended command and passing the 53 | * request to the `router` object which is responsible for executing the registered 54 | * route. 55 | * 56 | * @param {Buffer} message 57 | * The raw buffer message from the multicast socket. 58 | * 59 | * @param {object} rinfo 60 | * Contains client connection details (ip address and remote port) 61 | * 62 | */ 63 | Kast.prototype.$dispatch = function $dispatch (message, rinfo) { 64 | var req = request(message, rinfo); 65 | var res = response(req, this.$socket); 66 | 67 | if (!req) { 68 | return debug('Received invalid request.'); 69 | } 70 | 71 | this.$router.exec(req.command, req, res); 72 | }; 73 | 74 | /** 75 | * Mounts a command handler. 76 | * 77 | * Usage example: 78 | * 79 | * app.command('/users', function (req, res) { 80 | * res.send('Hello world.'); 81 | * }); 82 | * 83 | */ 84 | Kast.prototype.command = function command (commandName, handler) { 85 | 86 | mandatory(commandName).is('string', 'Please define a command name.'); 87 | mandatory(handler).is('function', 'Please define a command handler function.'); 88 | 89 | this.$router.mount(commandName, handler); 90 | }; 91 | 92 | /** 93 | * Listen for incoming requests on this multicast socket. 94 | * 95 | * @param {number} port 96 | * The port on which the multicast socket should be opened. 97 | * 98 | * @param {string} host 99 | * The multicast ip address (optional; default: `224.1.1.1`). 100 | * 101 | * @param {string} callback 102 | * An optional callback which will be executed when the socket has been opened. 103 | * Executed as `callback(err)`. 104 | * 105 | */ 106 | Kast.prototype.listen = function listen (port, host, callback) { 107 | var options = {}; 108 | 109 | mandatory(port).is('number', 'Please define a port for the multicast socket.'); 110 | 111 | function onListen (err) { 112 | if (err) { 113 | return callback(new VError(err, 'Unable to create multicast socket.')); 114 | } 115 | 116 | debug('Bound socket.'); 117 | 118 | callback(null); 119 | } 120 | 121 | if (typeof host === 'function') { 122 | callback = host; 123 | host = null; 124 | } 125 | 126 | // TODO: Implement additional check if the passed `host` represents 127 | // a valid multicast ip address. 128 | options.host = host; 129 | options.port = port; 130 | options.dispatcher = this.$dispatch.bind(this); 131 | 132 | callback = callback || function noop () {}; 133 | 134 | if (this.$socket) { 135 | this.$socket.close(); 136 | } 137 | 138 | this.$socket = socket(options); 139 | this.$socket.open(onListen); 140 | 141 | return this.$socket; 142 | }; 143 | -------------------------------------------------------------------------------- /lib/protocol.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var debug = require('debug')('kast:Protocol'); 17 | 18 | module.exports = function initialize () { 19 | var protocol = new Protocol(); 20 | 21 | // 22 | // Preparing the front-facing API. 23 | // 24 | return { 25 | parse: protocol.parse.bind(protocol), 26 | serialize: protocol.serialize.bind(protocol) 27 | }; 28 | }; 29 | 30 | function Protocol () { 31 | this.$DELIMITER = ';'; 32 | } 33 | 34 | /** 35 | * Method for parsing the string representation of `requests`/`responses`. 36 | * 37 | * ;;; 38 | * 39 | * Example: 40 | * 41 | * request;123123234234;/users;uptime=10h 42 | * 43 | * @param {string} type 44 | * The parser type, e.g. `request`. 45 | * 46 | * @param {Buffer} raw 47 | * The raw `request`/`response` message. 48 | * 49 | * @returns {object} An example: 50 | * 51 | * { 52 | * id: 123123234234, 53 | * command: '/users', 54 | * body: uptime=10h 55 | * } 56 | * 57 | */ 58 | Protocol.prototype.parse = function parse (type, raw) { 59 | var result = {}; 60 | var parts; 61 | 62 | if (!/(request|response)/.test(type)) { 63 | return result; 64 | } 65 | 66 | raw = raw.toString('utf-8'); 67 | 68 | parts = raw.split(this.$DELIMITER); 69 | 70 | result.id = parts[1]; 71 | result.command = parts[2]; 72 | result.body = parts[3]; 73 | 74 | return result; 75 | }; 76 | 77 | /** 78 | * Method for serializing a `request`/`response` DTO to a string representation. 79 | * 80 | * Example: 81 | * 82 | * { 83 | * id: 123123234234, 84 | * command: '/users', 85 | * body: '192.168.0.1' 86 | * } 87 | * 88 | * => response;123123234234;/users;192.168.0.1 89 | * 90 | * @param {string} type 91 | * The serializer type, e.g. `response`. 92 | * 93 | * @returns {string} 94 | * 95 | */ 96 | Protocol.prototype.serialize = function serialize (type, dto) { 97 | var result = ''; 98 | var parts = [type, dto.id, dto.command, dto.body]; 99 | 100 | if (!/(request|response)/.test(type)) { 101 | return result; 102 | } 103 | 104 | result = parts.join(this.$DELIMITER); 105 | 106 | debug('Serialized request from DTO to: %s', result); 107 | 108 | return result; 109 | }; 110 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var protocol = require('./protocol')(); 17 | 18 | /** 19 | * Creates a request DTO out of the raw request buffer and 20 | * the client's connection details. 21 | * 22 | * The created object will be used within the user's command handler. 23 | * 24 | * @param {Buffer} raw 25 | * The raw request buffer 26 | * 27 | * @param {object} rinfo 28 | * The client's connection details 29 | * 30 | * @returns {object} The request DTO. 31 | * 32 | */ 33 | module.exports = function initialize (raw, rinfo) { 34 | var request = protocol.parse('request', raw); 35 | 36 | if (!request.id) { 37 | return null; 38 | } 39 | 40 | return { 41 | id: request.id, 42 | connection: { 43 | remoteAddress: rinfo.address, 44 | remotePort: rinfo.port 45 | }, 46 | command: request.command, 47 | body: request.body 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var protocol = require('./protocol')(); 17 | 18 | /** 19 | * Creates a response object which will be used within the user's command handler. 20 | * 21 | * @param {Request} request 22 | * The request DTO (see `Request`). 23 | * 24 | * @param {Socket} socket 25 | * The socket that will be used within the response `send` function. 26 | * 27 | * @returns {object} 28 | * Response object that provides an API with which the user can answer a request. 29 | * 30 | */ 31 | module.exports = function initialize (request, socket) { 32 | 33 | return { 34 | send: function send (body) { 35 | var dto = {}; 36 | var buffer = null; 37 | 38 | dto.id = request.id; 39 | dto.command = request.command; 40 | dto.body = body; 41 | 42 | buffer = new Buffer(protocol.serialize('response', dto)); 43 | 44 | socket.send(buffer, request.connection.remotePort, request.connection.remoteAddress); 45 | } 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var debug = require('debug')('kast:Router'); 17 | 18 | module.exports = function instantiate () { 19 | var router = new Router(); 20 | 21 | return { 22 | mount: router.mount.bind(router), 23 | exec: router.exec.bind(router) 24 | }; 25 | }; 26 | 27 | function Router () { 28 | this.$routes = {}; 29 | } 30 | 31 | /** 32 | * Mounts a handler to a defined command. 33 | * 34 | * Example: 35 | * 36 | * app.mount('/users', function (req, res) {...}); 37 | * 38 | * @param {string} command 39 | * The name of the command. 40 | * 41 | * @param {function} handler 42 | * A handler function which will be executed when the respective command should 43 | * be executed. Please note that a `Request` and a `Response` object will be passed 44 | * to the handler function. 45 | * 46 | */ 47 | Router.prototype.mount = function mount (command, handler) { 48 | this.$routes[command] = handler; 49 | 50 | debug('Mounted %s', command); 51 | }; 52 | 53 | /** 54 | * Executes the handler of a given command (if mounted). 55 | * 56 | * @param {string} command 57 | * The name of the handler that should be executed. 58 | * 59 | * @param {Request} req (see `Request`) 60 | * @param {Response} req (see `Response`) 61 | * 62 | */ 63 | Router.prototype.exec = function exec (command, req, res) { 64 | var route = this.$routes[command]; 65 | 66 | debug('Asked for executing route %s', command); 67 | 68 | if (route) { 69 | debug('About to execute route "%s".', command); 70 | 71 | return route(req, res); 72 | } else { 73 | debug('Route not found: %s', command); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /lib/socket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var dgram = require('dgram'); 17 | 18 | var debug = require('debug')('kast:Socket'); 19 | var ip = require('ip'); 20 | var VError = require('verror'); 21 | 22 | module.exports = function initialize (options) { 23 | var socket = new Socket(options); 24 | 25 | return { 26 | open: socket.open.bind(socket), 27 | send: socket.send.bind(socket), 28 | close: socket.close.bind(socket) 29 | }; 30 | }; 31 | 32 | /** 33 | * Wrapper around the socket implementation of Node.js with some 34 | * proxy functionality for incoming messages. 35 | * 36 | * @param {object} options 37 | * A configuration object Should have the following structure: 38 | * 39 | * `port`: The respective port on which the multicast socket should be opened. 40 | * `host`: The multicast ip address (optional; default: `224.1.1.1`). 41 | * `dispatcher`: A function which will be executed on every request. 42 | * Params that will be passed to this dispatcher: 43 | * - `{Buffer} message` - the raw request string 44 | * - `{object} rinfo` - client connection details 45 | * `ttl`: The number of IP hops that a packet is allowed to go through (optional; default: 128). 46 | * 47 | */ 48 | function Socket (options) { 49 | this.$ipaddress = ip.address(); 50 | 51 | this.$port = options.port; 52 | this.$host = options.host || '224.1.1.1'; 53 | this.$ttl = options.ttl || 128; 54 | this.$dispatcher = options.dispatcher; 55 | 56 | // 57 | // The native socket implementation. 58 | // Will be filled on opening the socket. 59 | // 60 | this.$impl = null; 61 | } 62 | 63 | /** 64 | * @private 65 | * 66 | * Callback function which will be executed when a new message arrived on the socket. 67 | * The function acts as a proxy, ignores own messages on the multicast socket and 68 | * executes the defined dispatcher. 69 | * 70 | */ 71 | Socket.prototype.$message = function $message (message, rinfo) { 72 | 73 | // 74 | // Only emit the messages that comes from a different host. 75 | // 76 | if (rinfo.address !== this.$ipaddress) { 77 | debug('Received a message.'); 78 | this.$dispatcher(message, rinfo); 79 | } 80 | }; 81 | 82 | /** 83 | * Opens the socket. 84 | * 85 | * @param {function} callback 86 | * Will be executed when the socket has been opened successfully. Executed as `callback(err)`. 87 | * 88 | */ 89 | Socket.prototype.open = function open (callback) { 90 | var self = this; 91 | 92 | function onListening () { 93 | self.$impl.setBroadcast(true); 94 | self.$impl.setMulticastTTL(self.$ttl); 95 | self.$impl.addMembership(self.$host); 96 | 97 | debug('Bound the port.'); 98 | 99 | callback(null); 100 | } 101 | 102 | function onError (err) { 103 | callback(new VError(err, 'Failed to bind the port.')); 104 | 105 | return self.$impl.close(); 106 | } 107 | 108 | if (!this.$impl) { 109 | debug('Opening the multicast socket.'); 110 | 111 | this.$impl = dgram.createSocket('udp4'); 112 | 113 | this.$impl.on('listening', onListening); 114 | this.$impl.on('message', this.$message.bind(this)); 115 | 116 | this.$impl.on('error', onError); 117 | 118 | this.$impl.bind(this.$port); 119 | } 120 | }; 121 | 122 | /** 123 | * Closes the socket. 124 | * 125 | */ 126 | Socket.prototype.close = function close (callback) { 127 | callback = callback || function noop() {}; 128 | 129 | this.$impl.once('close', callback); 130 | 131 | this.$impl.close(); 132 | this.$impl = null; 133 | }; 134 | 135 | /** 136 | * Sends a buffer to a given host and port (if defined; to the multicast group 137 | * otherwise). 138 | * 139 | * @param {Buffer} buffer 140 | * The buffer that should be sent. 141 | * 142 | * @param {number} port 143 | * optional; default = the multicast socket port. 144 | * 145 | * @param {string} host 146 | * optional; default = the multicast ip address. 147 | * 148 | */ 149 | Socket.prototype.send = function send (buffer, port, host) { 150 | port = port || this.$port; 151 | host = host || this.$host; 152 | 153 | if (this.$impl) { 154 | this.$impl.send(buffer, 0, buffer.length, port, host); 155 | } 156 | }; 157 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kast", 3 | "version": "1.1.0", 4 | "description": "An UDP multicast RPC framework.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "gulp", 8 | "lint": "gulp lint", 9 | "specs": "gulp specs", 10 | "checkstyle": "gulp checkstyle" 11 | }, 12 | "pre-commit": [ 13 | "lint", 14 | "checkstyle", 15 | "specs" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:akoenig/kast.git" 20 | }, 21 | "keywords": [ 22 | "rpc", 23 | "multicast" 24 | ], 25 | "author": "André König ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/akoenig/kast/issues" 29 | }, 30 | "homepage": "https://github.com/akoenig/kast", 31 | "dependencies": { 32 | "debug": "^2.1.0", 33 | "ip": "^0.3.2", 34 | "mandatory": "^1.0.0", 35 | "verror": "^1.6.0" 36 | }, 37 | "devDependencies": { 38 | "expect.js": "^0.3.1", 39 | "gulp": "^3.8.10", 40 | "gulp-jscs": "^1.3.1", 41 | "gulp-jshint": "^1.9.0", 42 | "gulp-mocha": "^2.0.0", 43 | "jshint-stylish": "^1.0.0", 44 | "pre-commit": "0.0.9", 45 | "run-sequence": "^1.0.2" 46 | } 47 | } -------------------------------------------------------------------------------- /specs/api.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var expect = require('expect.js'); 17 | 18 | var kast = require('../'); 19 | 20 | describe('The front-facing API', function suite () { 21 | 22 | this.timeout(4000); 23 | 24 | it('should have a `listen` method with proper argument checks', function test (done) { 25 | var app = kast(); 26 | 27 | expect(app.listen).not.to.be(undefined); 28 | expect(typeof app.listen).to.be('function'); 29 | 30 | try { 31 | app.listen(); 32 | } catch (e) { 33 | expect(e).not.to.be(undefined); 34 | } 35 | 36 | done(); 37 | }); 38 | 39 | it('should have a `command` method with proper argument checks', function test (done) { 40 | var app = kast(); 41 | 42 | expect(app.command).not.to.be(undefined); 43 | expect(typeof app.command).to.be('function'); 44 | 45 | try { 46 | app.command(); 47 | } catch (e) { 48 | expect(e).not.to.be(undefined); 49 | } 50 | 51 | try { 52 | app.command('/foo'); 53 | } catch (e) { 54 | expect(e).not.to.be(undefined); 55 | } 56 | 57 | done(); 58 | }); 59 | 60 | it('should have a `broadcast` method with proper argument checks', function test (done) { 61 | var success = true; 62 | 63 | expect(kast.broadcast).not.to.be(undefined); 64 | expect(typeof kast.broadcast).to.be('function'); 65 | 66 | try { 67 | kast.broadcast(); 68 | } catch (e) { 69 | expect(e).not.to.be(undefined); 70 | } 71 | 72 | // 73 | // Missing required params 74 | // 75 | try { 76 | kast.broadcast({}); 77 | } catch (e) { 78 | expect(e).not.to.be(undefined); 79 | } 80 | 81 | try { 82 | kast.broadcast({ 83 | port: 5000 84 | }); 85 | } catch (e) { 86 | expect(e).not.to.be(undefined); 87 | } 88 | 89 | try { 90 | kast.broadcast({ 91 | port: 5000, 92 | command: '/foo' 93 | }, function onResponse () { 94 | done(); 95 | }); 96 | } catch (e) { 97 | success = false; 98 | } 99 | 100 | expect(success).to.be(true); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /specs/socket.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * kast 3 | * 4 | * Copyright(c) 2014 André König 5 | * MIT Licensed 6 | * 7 | */ 8 | 9 | /** 10 | * @author André König 11 | * 12 | */ 13 | 14 | 'use strict'; 15 | 16 | var expect = require('expect.js'); 17 | 18 | var kast = require('../'); 19 | 20 | describe('The socket implementation', function suite () { 21 | 22 | this.timeout(4000); 23 | 24 | it('should be able to start listening on a defined port without errors', function test (done) { 25 | var app = kast(); 26 | 27 | var socket = app.listen(5000, function onListen (err) { 28 | expect(err).to.be(null); 29 | 30 | expect(socket).not.to.be(undefined); 31 | 32 | socket.close(function onClose () { 33 | done(); 34 | }); 35 | }); 36 | }); 37 | 38 | }); 39 | --------------------------------------------------------------------------------