// https://github.com/dfellis/node-udp-rpc 'use strict'; var dgram = require('dgram'); var crypto = Require('os/crypto.rand'); // var crypto = require('crypto'); var util = require('util'); var events = require('events'); var async = Require('rpc/async'); var version = "1.1.3"; // The header flag for udp-rpc packet reconstruction var flags = { // Reserved: 1, 2, 4, 8, 16, 32, 64 lastPacket: 128 }; // Helper function to generate Xor bytes function bufXor(buf) { var out = 0; for (var i = 0; i < buf.length; i++) { out = out ^ buf.readUInt8(i, true); } return out; } // Helper function for generating a unique byte for the flag/id byte function uniqId(usedList) { var id = 0; do { id = (Math.random() * (Math.pow(2, 8) - 1)) & 127; // Get a random byte and trim the top bit off } while (typeof usedList[id] != 'undefined') return id; } // Called when the client wants to call a remote rpc method function execRpc(method, address) { if (!method || !address) { throw new Error("Did not receive a valid RPC request!"); } // Construct the payload to send to the remote server var rpcParams = Array.prototype.slice.call(arguments, 2); var callback = rpcParams.length > 1 ? rpcParams.pop() : undefined; var addrArray = address.split(":"); var messageId = this.genId(); // var payload = new Buffer(([method].concat(messageId, rpcParams)).join(',')); var payload = new Buffer(JSON.stringify([method, messageId, ].concat(rpcParams))); this.emit('execRpc', method, address, rpcParams, callback); var self = this; // we have to register callback handler in advance; maybe reply arrives earlier than // sendMessage callback is executed !? this.messages[messageId] = { callTs: new Date(), callback: callback, method: method, ip: addrArray[0], port: addrArray[1], rpcParams: rpcParams // This is for debugging purposes only }; sendMessage.call(this, address, payload, function allSent(err) { self.emit('sentRpc', method, address, rpcParams, callback, err); if (err instanceof Error) { // delete callback handler delete self.messages[messageId]; callback(err) } else { // nothing to do!? } }); } function sendMessage(address, payload, callback) { var addrArray = address.split(":"); // Construct the 6 byte header elements of the udp-rpc protocol for message verification/splitting if (!this.packets[address]) { this.packets[address] = {}; } var currFlags = uniqId(this.packets[address]); var totXor = bufXor(payload); // Construct an array of payload fragments, each no larger than 494 bytes var payloads = []; if (payload.length > 494) { var i = 0; for (i = 0; i < payload.length; i += 494) { var newPayload = new Buffer(494); payload.copy(newPayload, 0, i, i + 493); payloads.push(newPayload); } if (i < payload.length) { var newPayload = new Buffer(payload.length - i); payload.copy(newPayload, 0, i, payload.length - 1); payloads.push(newPayload); } } else { payloads.push(payload); } var self = this; // For each payload fragment, generate the payload header, construct the output message, and send it async.forEach(payloads, function sendPayload(payload, callback) { var message = new Buffer(payload.length + 6); var packetNumber = payloads.indexOf(payload); currFlags &= 127; message.writeUInt32BE(packetNumber, 1, true); var curXor = bufXor(payload); if (packetNumber == payloads.length - 1) { currFlags |= flags.lastPacket; message.writeUInt8(totXor, 5, true); } else { message.writeUInt8(curXor, 5, true); } message.writeUInt8(currFlags, 0, true); payload.copy(message, 6); self.stats.send++; self.stats.sendData += message.length; self.dgram.send(message, 0, message.length, addrArray[1], addrArray[0], function(err) { if (err instanceof Error) { return callback(err); } return callback(); }); // When all are sent, or an error occurred, execute the callback }, callback); } // Called when a remote message is received. This could be an rpc request or response function receiveRpc(message, info) { this.emit('receivedPacket', message, info); // Extract the header and body of the message var header = { flagid: message.readUInt8(0, true), packetNumber: message.readUInt32BE(1, true), xor: message.readUInt8(5, true) }; var body = message.slice(6); this.stats.recv++; this.stats.recvData += message.length; var source = info.address + ":" + info.port; if (!this.packets[source]) { this.packets[source] = {}; } if (!this.packets[source][header.flagid]) { this.packets[source][header.flagid] = []; } this.packets[source][header.flagid].push({ header: header, body: body }); attemptProcessing.call(this, source, header.flagid); } function attemptProcessing(source, id) { // Get the packet array to work on var packetArray = this.packets[source][id]; // Tag the set of first and last packets in the packet array var integerSum = 0; var totalMessageLen = 0; var lastPacket = false; for (var i = 0; i < packetArray.length; i++) { integerSum += packetArray[i].header.packetNumber + 1; totalMessageLen += packetArray[i].body.length; if (!!(packetArray[i].header.flagid & flags.lastPacket)) { lastPacket = true; } } // If there is no last packet, processing impossible // Also, if the sum of packet numbers isn't n(n+1)/2, there must be a missing packet if (!lastPacket || integerSum != (packetArray.length * (packetArray.length + 1) / 2)) { return; } // Sort the array of packets based on packet number, since we can process this data packetArray = packetArray.sort(function(a, b) { return a.header.packetNumber - b.header.packetNumber; }); // Build the full message buffer from the sorted packets var fullMessage = new Buffer(totalMessageLen); for (var i = 0, j = 0; i < packetArray.length; j += packetArray[i].body.length, i++) { packetArray[i].body.copy(fullMessage, 0, j); } // Remove the saved packets from the packets object and pass the message to the processMessage delete this.packets[source][id]; processMessage.call(this, fullMessage, source); } function processMessage(message, source) { var sourceArr = source.split(':'); // var messageArr = message.toString('utf8').split(','); try { var messageArr = JSON.parse(message.toString('utf8')); } catch (e) { console.log(e) } // console.log('processMessage',messageArr[0]) // Determine type of message: RPC response, request if (!this.oneway && typeof this.messages[messageArr[0]] == 'object' && sourceArr[1] == this.messages[messageArr[0]].port) { // If a response, the message array consists of the request id and response results // Find the appropriate callback this.emit('receivedRpcResponse', message, source); this.messages[messageArr[0]].callback.apply(this, messageArr.slice(1)); delete this.messages[messageArr[0]]; } else if (typeof this.methods[messageArr[0]] == 'function') { // If a request, the message array consists of the request method, id, and parameters, in that order this.emit('receivedRpcRequest', message, source); var messageId = messageArr[1]; var params = messageArr.slice(2); // Parameters sent to the RPC method start with the source string, in case they need to know who is calling (simple state tracking) params.unshift(source); var self = this; // Automatically insert a callback to the params passed to the RPC method to handle the results if (!this.oneway) params.push(function rpcCallback() { // var payload = new Buffer(([messageId].concat(Array.prototype.slice.call(arguments, 0))).join(',')); var payload = new Buffer(JSON.stringify([messageId].concat((Array.prototype.slice.call(arguments, 0))))); sendMessage.call(self, source, payload, function(err) { if (err instanceof Error) { throw err; } // RPC server non-functional, this is fatal }); }); // Execute the requested RPC method this.methods[messageArr[0]].apply(this, params); } else { // If packet is not understood, ignore it console.log('receivedUnknownMessage',messageArr) this.emit('receivedUnknownMessage', message, source); } } // UDP-RPC object to provide a nice interface to the protocol and properly initialize/destroy it. // I can't find a way to execute a function on ``delete`` like you can with get/set, so destroy this // object with the ``die`` method. function udpRpc(ipType, srvPort, methods, oneway) { var options = {}; events.EventEmitter.call(this); if (typeof oneway == 'object') { options = oneway; oneway = options.oneway; } this.methods = {}; this.messages = {}; this.packets = {}; this.state = 'starting'; this.stats = { recv : 0, // #msgs send : 0, recvData : 0, // #Bytes sendData : 0, } for (var i in methods) { Object.defineProperty(this, methods[i].name, { enumerable: true, configurable: false, writable: false, value: execRpc.bind(this, methods[i].name) }); Object.defineProperty(this.methods, methods[i].name, { enumerable: true, configurable: false, writable: false, value: methods[i] }); } this.genId = function() { var id = ""; do { try { id = crypto.randomBytes(3).toString('base64'); } catch (e) { id = new Buffer(Math.random() * (Math.pow(2, 24) - 1)).toString('base64'); } } while (typeof this.messages[id] != 'undefined') return id; }; this.srvPort = srvPort; this.address = undefined; var self = this; process.nextTick(function() { self.dgram = dgram.createSocket(ipType); self.dgram.on('listening', function udpRpcStart() { self.address = self.dgram.address(); self.state = 'listening'; if (options.verbose) console.log('udpRpc: listening on', self.address); self.emit('init'); }); self.dgram.on('message', receiveRpc.bind(self)); self.dgram.bind(srvPort); // Currently hardwired; will look into alternatives }); this.die = function die() { this.dgram.close(); this.emit('death'); delete this; }; this.oneway = oneway || false; return this; } util.inherits(udpRpc, events.EventEmitter); exports = module.exports = udpRpc;