domotz-remote-pawn
Version:
Domotz Agent
163 lines (131 loc) • 4.4 kB
JavaScript
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;
};