udp-service-discovery
Version:
Simple and cheap service discovery broadcast and listener
237 lines (236 loc) • 8.66 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const dgram = require("dgram");
const events_1 = require("events");
const netmask_1 = require("netmask");
class UDPServiceDiscovery extends events_1.EventEmitter {
constructor(opts) {
super();
this._status = 'INITIALIZING';
this._serviceListenFor = null;
this._serviceListenOnce = false;
this._opts = {
port: 12345,
address: null,
announceInterval: 1000,
retryInterval: 999,
debug: false
};
if (typeof opts !== 'undefined') {
Object.assign(this._opts, opts);
}
this._start();
}
get status() {
return this._status;
}
broadcast(name, ip, port, msg, repeat = 0) {
let service = { host: null };
let interval;
let runs = 0;
let netmask;
let host;
let broadcastAddress;
let announceMessage;
switch (arguments.length) {
case 5:
case 4:
if (typeof msg === 'string') {
service = this._getJSONObjFromString(msg);
}
else {
service = msg;
}
case 3:
service.port = port;
case 2:
service.host = ip;
service.name = name;
break;
case 1:
service = this._getJSONObjFromString(arguments[0]);
break;
}
announceMessage = new Buffer(JSON.stringify(service));
const announce = (() => {
if (repeat !== 0 && runs >= repeat) {
clearInterval(interval);
this._log('Finished ' + repeat + ' broadcasts.');
return;
}
const mask = this._getLocalIPAndNetmask();
if (mask.length > 0 && mask[0].length > 0) {
netmask = mask[0][1];
host = mask[0][0];
broadcastAddress = this._getBroadcastAddress(this, host, netmask);
if (host !== service.host) {
service.host = host;
announceMessage = new Buffer(JSON.stringify(service));
this.emit('statusChanged', this.status);
}
if (host) {
this._socket.send(announceMessage, 0, announceMessage.length, this._opts.port, broadcastAddress, (err, bytes) => {
runs++;
if (err) {
this._log('UDP - Error announcing!', err);
throw err;
}
else {
if (this.status !== 'BROADCASTING') {
this._status = 'BROADCASTING';
this.emit('statusChanged', this.status);
}
}
});
}
}
else {
// Not connected
this._status = 'NOT_CONNECTED';
this.emit('statusChanged', this.status);
}
});
// announce once per second
interval = setInterval(announce, this._opts.announceInterval);
}
listen(listenFor) {
this._serviceListenOnce = false;
if (listenFor) {
if (typeof listenFor === 'string' && !this._getJSONObjFromString(listenFor)) {
this._serviceListenFor = {};
this._serviceListenFor.name = listenFor;
}
else {
if (typeof listenFor === 'string') {
this._serviceListenFor = this._getJSONObjFromString(listenFor);
}
else {
this._serviceListenFor = listenFor;
}
}
}
this.tryBinding();
}
listenOnce(listenFor) {
this._serviceListenOnce = true;
if (listenFor) {
if (typeof listenFor === 'string' && !this._getJSONObjFromString(listenFor)) {
this._serviceListenFor = {};
this._serviceListenFor.name = listenFor;
}
else {
if (typeof listenFor === 'string') {
this._serviceListenFor = this._getJSONObjFromString(listenFor);
}
else {
this._serviceListenFor = listenFor;
}
}
}
this.tryBinding();
}
tryBinding() {
this._socket.bind(this._opts.port, this._opts.address);
}
_start() {
// Setup UDP socket
this._socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
// Setup message handler
this._socket.on('message', data => {
const announcedService = this._getJSONObjFromString(data.toString());
if (announcedService) {
if (this._serviceListenFor) {
let match = false;
Object.keys(this._serviceListenFor).some(key => {
if (this._serviceListenFor[key] !== announcedService[key]) {
match = false;
return true; // returning true on the first element that doesn't match to break the loop
}
else {
match = true;
}
});
if (match) {
this.emit('discovery', announcedService);
if (this._serviceListenOnce) {
this.close();
}
}
else {
// no match
}
}
else {
this.emit('discovery', announcedService);
if (this._serviceListenOnce) {
this.close();
}
}
}
});
this._socket.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
this._log('UDPServiceDiscovery address/port ' + e.address + ':' + e.port + ' in use, retrying in ' +
this._opts.retryInterval + ' ms');
setTimeout(this.tryBinding.bind(this), this._opts.retryInterval);
}
else {
this._log('UDPServiceDiscovery SocketError: ' + e);
}
});
this._socket.on('listening', () => {
this._socket.setBroadcast(true);
const address = this._socket.address();
if (this.listenOnce) {
this._log('UDPServiceDiscovery listening (once) on ' + address.address + ':' + address.port);
}
else {
this._log('UDPServiceDiscovery listening (forever) on ' + address.address + ':' + address.port);
}
});
this._socket.on('close', () => {
this._log('UDPServiceDiscovery Closing socket.');
});
}
close() {
this._socket.close();
}
_log(...msg) {
if (this._opts.debug) {
console.log(...msg);
}
}
_getJSONObjFromString(jsonString) {
try {
const o = JSON.parse(jsonString);
// Handle non-exception-throwing cases:
// Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
// but... JSON.parse(null) returns null, and typeof null === "object",
// so we must check for that, too. Thankfully, null is falsey, so this suffices:
if (o && typeof o === 'object') {
return o;
}
}
catch (err) {
}
return null;
}
_getLocalIPAndNetmask() {
const networkInterfaces = require('os').networkInterfaces();
const matches = [];
Object.keys(networkInterfaces).forEach(function (item) {
networkInterfaces[item].forEach(function (address) {
if (address.internal === false && address.family === 'IPv4') {
matches.push([address.address, address.netmask]);
}
});
});
return matches;
}
_getBroadcastAddress(sender, ip, netmask) {
sender._log(ip, netmask);
const block = new netmask_1.Netmask(ip + '/' + netmask);
return block.broadcast;
}
}
exports.UDPServiceDiscovery = UDPServiceDiscovery;