dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
441 lines (354 loc) • 12.5 kB
JavaScript
;
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
var os = require('os');
var dgram = require('dgram');
var Packet = require('./Packet');
var EventEmitter = require('./EventEmitter');
var ExpiringRecordCollection = require('./ExpiringRecordCollection');
var Mutex = require('./Mutex');
var misc = require('./misc');
var hex = require('./hex');
var filename = require('path').basename(__filename);
var debug = require('./debug')('dnssd:' + filename);
var MDNS_PORT = 5353;
var MDNS_ADDRESS = { IPv4: '224.0.0.251', IPv6: 'FF02::FB' };
/**
* IP should be considered as internal when:
* ::1 - IPv6 loopback
* fc00::/8
* fd00::/8
* fe80::/8
* 10.0.0.0 -> 10.255.255.255 (10/8 prefix)
* 127.0.0.0 -> 127.255.255.255 (127/8 prefix)
* 172.16.0.0 -> 172.31.255.255 (172.16/12 prefix)
* 192.168.0.0 -> 192.168.255.255 (192.168/16 prefix)
*
*/
function isLocal(ip) {
// IPv6
if (!!~ip.indexOf(':')) {
return (/^::1$/.test(ip) || /^fe80/i.test(ip) || /^fc[0-9a-f]{2}/i.test(ip) || /^fd[0-9a-f]{2}/i.test(ip)
);
}
// IPv4
var parts = ip.split('.').map(function (n) {
return parseInt(n, 10);
});
return parts[0] === 10 || parts[0] === 192 && parts[1] === 168 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31;
}
function isIPv4(ip) {
return (/(?:[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$)/.test(ip)
);
}
function findInterfaceName(address) {
var interfaces = os.networkInterfaces();
return Object.keys(interfaces).find(function (name) {
return interfaces[name].some(function (addr) {
return addr.address === address;
});
});
}
/**
* Maps interface names to a previously created NetworkInterfaces
*/
var activeInterfaces = {};
/**
* Creates a new NetworkInterface
* @class
* @extends EventEmitter
*
* @param {string} name
*/
function NetworkInterface(name, address) {
this._id = name || 'INADDR_ANY';
this._multicastAddr = address;
debug('Creating new NetworkInterface on `%s`', this._id);
EventEmitter.call(this);
// socket binding
this._usingMe = 0;
this._isBound = false;
this._sockets = [];
this._mutex = new Mutex();
// incoming / outgoing records
this.cache = new ExpiringRecordCollection([], this._id + '\'s cache');
this._history = new ExpiringRecordCollection([], this._id + '\'s history');
// outgoing packet buffers (debugging)
this._buffers = [];
}
NetworkInterface.prototype = Object.create(EventEmitter.prototype);
NetworkInterface.prototype.constructor = NetworkInterface;
/**
* Creates/returns NetworkInterfaces from a name or address of interface.
* Active interfaces get reused.
*
* @static
*
* Ex:
* > const interfaces = NetworkInterface.get('eth0');
* > const interfaces = NetworkInterface.get('111.222.333.444');
*
* @param {string} arg
* @return {NetworkInterface}
*/
NetworkInterface.get = function get() {
var specific = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
// doesn't set a specific multicast send address
if (!specific) {
if (!activeInterfaces.any) {
activeInterfaces.any = new NetworkInterface();
}
return activeInterfaces.any;
}
// sets multicast send address
var name = void 0;
var address = void 0;
// arg is an IP address
if (isIPv4(specific)) {
name = findInterfaceName(specific);
address = specific;
// arg is the name of an interface
} else {
if (!os.networkInterfaces()[specific]) {
throw new Error('Can\'t find an interface named \'' + specific + '\'');
}
name = specific;
address = os.networkInterfaces()[name].find(function (a) {
return a.family === 'IPv4';
}).address;
}
if (!name || !address) {
throw new Error('Interface matching \'' + specific + '\' not found');
}
if (!activeInterfaces[name]) {
activeInterfaces[name] = new NetworkInterface(name, address);
}
return activeInterfaces[name];
};
/**
* Returns the name of the loopback interface (if there is one)
* @static
*/
NetworkInterface.getLoopback = function getLoopback() {
var interfaces = os.networkInterfaces();
return Object.keys(interfaces).find(function (name) {
var addresses = interfaces[name];
return addresses.every(function (address) {
return address.internal;
});
});
};
/**
* Binds each address the interface uses to the multicast address/port
* Increments `this._usingMe` to keep track of how many browsers/advertisements
* are using it.
*/
NetworkInterface.prototype.bind = function () {
var _this = this;
return new Promise(function (resolve, reject) {
_this._usingMe++;
// prevent concurrent binds:
_this._mutex.lock(function (unlock) {
if (_this._isBound) {
unlock();
resolve();
return;
}
// create & bind socket
_this._bindSocket().then(function () {
debug('Interface ' + _this._id + ' now bound');
_this._isBound = true;
unlock();
resolve();
}).catch(function (err) {
_this._usingMe--;
reject(err);
unlock();
});
});
});
};
NetworkInterface.prototype._bindSocket = function () {
var _this2 = this;
var isPending = true;
var promise = new Promise(function (resolve, reject) {
var socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
socket.on('error', function (err) {
if (isPending) reject(err);else _this2._onError(err);
});
socket.on('close', function () {
_this2._onError(new Error('Socket closed unexpectedly'));
});
socket.on('message', function (msg, rinfo) {
_this2._onMessage(msg, rinfo);
});
socket.on('listening', function () {
var _ref;
var sinfo = socket.address();
debug(_this2._id + ' listening on ' + sinfo.address + ':' + sinfo.port);
// Make sure loopback is set to ensure we can communicate with any other
// responders on the same machine. IP_MULTICAST_LOOP might default to
// true so this may be redundant on some platforms.
socket.setMulticastLoopback(true);
socket.setTTL(255);
// set a specific multicast interface to use for outgoing packets
if (_this2._multicastAddr) socket.setMulticastInterface(_this2._multicastAddr);
// add membership on each unique IPv4 interface address
var addresses = (_ref = []).concat.apply(_ref, _toConsumableArray(Object.values(os.networkInterfaces()))).filter(function (addr) {
return addr.family === 'IPv4';
}).map(function (addr) {
return addr.address;
});
[].concat(_toConsumableArray(new Set(addresses))).forEach(function (address) {
try {
socket.addMembership(MDNS_ADDRESS.IPv4, address);
} catch (e) {
console.log('OUCH! - could not add membership to interface ' + address, e);
}
});
_this2._sockets.push(socket);
resolve();
});
socket.bind({ address: '0.0.0.0', port: MDNS_PORT });
});
return promise.then(function () {
isPending = false;
});
};
/**
* Handles incoming messages.
*
* @emtis 'answer' w/ answer packet
* @emtis 'probe' w/ probe packet
* @emtis 'query' w/ query packet
*
* @param {Buffer} msg
* @param {object} origin
*/
NetworkInterface.prototype._onMessage = function (msg, origin) {
if (debug.verbose.isEnabled) {
debug.verbose('Incoming message on interface %s from %s:%s \n\n%s\n\n', this._id, origin.address, origin.port, hex.view(msg));
}
var packet = new Packet(msg, origin);
if (debug.isEnabled) {
var index = this._buffers.findIndex(function (buf) {
return msg.equals(buf);
});
var address = origin.address,
port = origin.port;
if (index !== -1) {
this._buffers.splice(index, 1); // remove buf @index
debug(address + ':' + port + ' -> ' + this._id + ' *** Ours: \n\n<-- ' + packet + '\n\n');
} else {
debug(address + ':' + port + ' -> ' + this._id + ' \n\n<-- ' + packet + '\n\n');
}
}
if (!packet.isValid()) return debug('Bad packet, ignoring');
// must silently ignore responses where source UDP port is not 5353
if (packet.isAnswer() && origin.port === 5353) {
this._addToCache(packet);
this.emit('answer', packet);
}
if (packet.isProbe() && origin.port === 5353) {
this.emit('probe', packet);
}
if (packet.isQuery()) {
this.emit('query', packet);
}
};
/**
* Adds records from incoming packet to interface cache. Also flushes records
* (sets them to expire in 1s) if the cache flush bit is set.
*/
NetworkInterface.prototype._addToCache = function (packet) {
var _this3 = this;
debug('Adding records to interface (%s) cache', this._id);
var incomingRecords = [].concat(_toConsumableArray(packet.answers), _toConsumableArray(packet.additionals));
incomingRecords.forEach(function (record) {
if (record.isUnique) _this3.cache.flushRelated(record);
_this3.cache.add(record);
});
};
NetworkInterface.prototype.hasRecentlySent = function (record) {
var range = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
return this._history.hasAddedWithin(record, range);
};
/**
* Send the packet on each socket for this interface.
* If no unicast destination address/port is given the packet is sent to the
* multicast address/port.
*/
NetworkInterface.prototype.send = function (packet, destination, callback) {
var _this4 = this;
if (!this._isBound) {
debug('Interface not bound yet, can\'t send');
return callback && callback();
}
if (packet.isEmpty()) {
debug('Packet is empty, not sending');
return callback && callback();
}
if (destination && !isLocal(destination.address)) {
debug('Destination ' + destination.address + ' not link-local, not sending');
return callback && callback();
}
if (packet.isAnswer() && !destination) {
debug.verbose('Adding outgoing multicast records to history');
this._history.addEach([].concat(_toConsumableArray(packet.answers), _toConsumableArray(packet.additionals)));
}
var done = callback && misc.after_n(callback, this._sockets.length);
var buf = packet.toBuffer();
// send packet on each socket
this._sockets.forEach(function (socket) {
var family = socket.address().family;
var port = destination ? destination.port : MDNS_PORT;
var address = destination ? destination.address : MDNS_ADDRESS[family];
// don't try to send to IPv4 on an IPv6 & vice versa
if (destination && family === 'IPv4' && !isIPv4(address) || destination && family === 'IPv6' && isIPv4(address)) {
debug('Mismatched sockets, (' + family + ' to ' + destination.address + '), skipping');
return;
}
// the outgoing list _should_ only have a few at any given time
// but just in case, make sure it doesn't grow indefinitely
if (debug.isEnabled && _this4._buffers.length < 10) _this4._buffers.push(buf);
debug('%s (%s) -> %s:%s\n\n--> %s\n\n', _this4._id, family, address, port, packet);
socket.send(buf, 0, buf.length, port, address, function (err) {
if (!err) return done && done();
// any other error goes to the handler:
if (err.code !== 'EMSGSIZE') return _this4._onError(err);
// split big packets up and resend:
debug('Packet too big to send, splitting');
packet.split().forEach(function (half) {
_this4.send(half, destination, callback);
});
});
});
};
/**
* Browsers/Advertisements use this instead of using stop()
*/
NetworkInterface.prototype.stopUsing = function () {
this._usingMe--;
if (this._usingMe <= 0) this.stop();
};
NetworkInterface.prototype.stop = function () {
debug('Shutting down ' + this._id + '...');
this._sockets.forEach(function (socket) {
socket.removeAllListeners(); // do first to prevent close events
try {
socket.close();
} catch (e) {/**/}
});
this.cache.clear();
this._history.clear();
this._usingMe = 0;
this._isBound = false;
this._sockets = [];
this._buffers = [];
debug('Done.');
};
NetworkInterface.prototype._onError = function (err) {
debug(this._id + ' had an error: ' + err + '\n' + err.stack);
this.stop();
this.emit('error', err);
};
module.exports = NetworkInterface;