UNPKG

domotz-remote-pawn

Version:

Domotz Agent

163 lines (131 loc) 4.4 kB
var dgram = require('dgram'); var util = require('util'); var os = require('os'); var EventEmitter = require('events').EventEmitter; var ssdp = exports; function Ssdp(opts) { EventEmitter.call(this); this._opts = opts || {}; this._sourcePort = this._opts.sourcePort || 0; this.multicast = '239.255.255.250'; this.port = 1900; this._bound = false; this._boundCount = 0; this._closed = false; this._queue = []; // Create sockets on all external interfaces this.createSockets(); } util.inherits(Ssdp, EventEmitter); ssdp.create = function create() { return new Ssdp(); }; Ssdp.parseMimeHeader = function (headerStr) { var lines = headerStr.split(/\r\n/g); // Parse headers from lines to hashmap return lines.reduce(function(headers, line) { line.replace(/^([^:]*)\s*:\s*(.*)$/, function (a, key, value) { headers[key.toLowerCase()] = value; }); return headers; }, {}); }; Ssdp.prototype.createSockets = function createSockets() { var self = this; var interfaces = os.networkInterfaces(); this.sockets = Object.keys(interfaces).reduce(function(a, key) { return a.concat(interfaces[key].filter(function(item) { return !item.internal; }).map(function(item) { return self.createSocket(item); })); }, []); }; Ssdp.prototype.search = function search(device, promise) { if (!promise) { promise = new EventEmitter(); promise._ended = false; promise.once('end', function() { promise._ended = true; }); } if (!this._bound) { this._queue.push({ action: 'search', device: device, promise: promise }); return promise; } // If promise was ended before binding - do not send queries if (promise._ended) return; var self = this; var query = new Buffer('M-SEARCH * HTTP/1.1\r\n' + 'HOST: ' + this.multicast + ':' + this.port + '\r\n' + 'MAN: "ssdp:discover"\r\n' + 'MX: 1\r\n' + 'ST: ' + device + '\r\n' + '\r\n'); // Send query on each socket this.sockets.forEach(function(socket) { socket.send(query, 0, query.length, this.port, this.multicast); }, this); function ondevice(info, address) { if (promise._ended) return; if (info.st !== device) return; promise.emit('device', info, address); } this.on('_device', ondevice); // Detach listener after receiving 'end' event promise.once('end', function() { self.removeListener('_device', ondevice); }); return promise; }; Ssdp.prototype.createSocket = function createSocket(interface) { var self = this; var socket = dgram.createSocket(interface.family === 'IPv4' ? 'udp4' : 'udp6'); socket.on('message', function (message, info) { // Ignore messages after closing sockets if (self._closed) return; // Parse response self.parseResponse(message.toString(), socket.address, info); }); // Bind in next tick (sockets should be me in this.sockets array) process.nextTick(function() { // Unqueue this._queue once all sockets are ready function onready() { if (self._boundCount < self.sockets.length) return; self._bound = true; self._queue.forEach(function(item) { return self[item.action](item.device, item.promise); }); } socket.on('listening', function() { self._boundCount += 1; onready(); }); // On error - remove socket from list and execute items from queue socket.once('error', function() { self.sockets.splice(self.sockets.indexOf(socket), 1); onready(); }); socket.address = interface.address; socket.bind(self._sourcePort, interface.address); }); return socket; }; // TODO create separate logic for parsing unsolicited upnp broadcasts, // if and when that need arises Ssdp.prototype.parseResponse = function parseResponse(response, addr, remote) { // Ignore incorrect packets if (!/^(HTTP|NOTIFY)/m.test(response)) return; var headers = Ssdp.parseMimeHeader(response); // We are only interested in messages that can be matched against the original // search target if (!headers.st) return; this.emit('_device', headers, addr); }; Ssdp.prototype.close = function close() { this.sockets.forEach(function(socket) { socket.close(); }); this._closed = true; };