You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
5.4 KiB
205 lines
5.4 KiB
var packet = require('dns-packet') |
|
var dgram = require('dgram') |
|
var thunky = require('thunky') |
|
var events = require('events') |
|
var os = require('os') |
|
|
|
var noop = function () {} |
|
|
|
module.exports = function (opts) { |
|
if (!opts) opts = {} |
|
|
|
var that = new events.EventEmitter() |
|
var port = typeof opts.port === 'number' ? opts.port : 5353 |
|
var type = opts.type || 'udp4' |
|
var ip = opts.ip || opts.host || (type === 'udp4' ? '224.0.0.251' : null) |
|
var me = {address: ip, port: port} |
|
var memberships = {} |
|
var destroyed = false |
|
var interval = null |
|
|
|
if (type === 'udp6' && (!ip || !opts.interface)) { |
|
throw new Error('For IPv6 multicast you must specify `ip` and `interface`') |
|
} |
|
|
|
var socket = opts.socket || dgram.createSocket({ |
|
type: type, |
|
reuseAddr: opts.reuseAddr !== false, |
|
toString: function () { |
|
return type |
|
} |
|
}) |
|
|
|
socket.on('error', function (err) { |
|
if (err.code === 'EACCES' || err.code === 'EADDRINUSE') that.emit('error', err) |
|
else that.emit('warning', err) |
|
}) |
|
|
|
socket.on('message', function (message, rinfo) { |
|
try { |
|
message = packet.decode(message) |
|
} catch (err) { |
|
that.emit('warning', err) |
|
return |
|
} |
|
|
|
that.emit('packet', message, rinfo) |
|
|
|
if (message.type === 'query') that.emit('query', message, rinfo) |
|
if (message.type === 'response') that.emit('response', message, rinfo) |
|
}) |
|
|
|
socket.on('listening', function () { |
|
if (!port) port = me.port = socket.address().port |
|
if (opts.multicast !== false) { |
|
that.update() |
|
interval = setInterval(that.update, 5000) |
|
socket.setMulticastTTL(opts.ttl || 255) |
|
socket.setMulticastLoopback(opts.loopback !== false) |
|
} |
|
}) |
|
|
|
var bind = thunky(function (cb) { |
|
if (!port || opts.bind === false) return cb(null) |
|
socket.once('error', cb) |
|
socket.bind(port, opts.bind || opts.interface, function () { |
|
socket.removeListener('error', cb) |
|
cb(null) |
|
}) |
|
}) |
|
|
|
bind(function (err) { |
|
if (err) return that.emit('error', err) |
|
that.emit('ready') |
|
}) |
|
|
|
that.send = function (value, rinfo, cb) { |
|
if (typeof rinfo === 'function') return that.send(value, null, rinfo) |
|
if (!cb) cb = noop |
|
if (!rinfo) rinfo = me |
|
else if (!rinfo.host && !rinfo.address) rinfo.address = me.address |
|
|
|
bind(onbind) |
|
|
|
function onbind (err) { |
|
if (destroyed) return cb() |
|
if (err) return cb(err) |
|
var message = packet.encode(value) |
|
socket.send(message, 0, message.length, rinfo.port, rinfo.address || rinfo.host, cb) |
|
} |
|
} |
|
|
|
that.response = |
|
that.respond = function (res, rinfo, cb) { |
|
if (Array.isArray(res)) res = {answers: res} |
|
|
|
res.type = 'response' |
|
res.flags = (res.flags || 0) | packet.AUTHORITATIVE_ANSWER |
|
that.send(res, rinfo, cb) |
|
} |
|
|
|
that.query = function (q, type, rinfo, cb) { |
|
if (typeof type === 'function') return that.query(q, null, null, type) |
|
if (typeof type === 'object' && type && type.port) return that.query(q, null, type, rinfo) |
|
if (typeof rinfo === 'function') return that.query(q, type, null, rinfo) |
|
if (!cb) cb = noop |
|
|
|
if (typeof q === 'string') q = [{name: q, type: type || 'ANY'}] |
|
if (Array.isArray(q)) q = {type: 'query', questions: q} |
|
|
|
q.type = 'query' |
|
that.send(q, rinfo, cb) |
|
} |
|
|
|
that.destroy = function (cb) { |
|
if (!cb) cb = noop |
|
if (destroyed) return process.nextTick(cb) |
|
destroyed = true |
|
clearInterval(interval) |
|
|
|
// Need to drop memberships by hand and ignore errors. |
|
// socket.close() does not cope with errors. |
|
for (var iface in memberships) { |
|
try { |
|
socket.dropMembership(ip, iface) |
|
} catch (e) { |
|
// eat it |
|
} |
|
} |
|
memberships = {} |
|
socket.close(cb) |
|
} |
|
|
|
that.update = function () { |
|
var ifaces = opts.interface ? [].concat(opts.interface) : allInterfaces() |
|
var updated = false |
|
|
|
for (var i = 0; i < ifaces.length; i++) { |
|
var addr = ifaces[i] |
|
if (memberships[addr]) continue |
|
|
|
try { |
|
socket.addMembership(ip, addr) |
|
memberships[addr] = true |
|
updated = true |
|
} catch (err) { |
|
that.emit('warning', err) |
|
} |
|
} |
|
|
|
if (updated) { |
|
if (socket.setMulticastInterface) { |
|
try { |
|
socket.setMulticastInterface(opts.interface || defaultInterface()) |
|
} catch (err) { |
|
that.emit('warning', err) |
|
} |
|
} |
|
that.emit('networkInterface') |
|
} |
|
} |
|
|
|
return that |
|
} |
|
|
|
function defaultInterface () { |
|
var networks = os.networkInterfaces() |
|
var names = Object.keys(networks) |
|
|
|
for (var i = 0; i < names.length; i++) { |
|
var net = networks[names[i]] |
|
for (var j = 0; j < net.length; j++) { |
|
var iface = net[j] |
|
if (isIPv4(iface.family) && !iface.internal) { |
|
if (os.platform() === 'darwin' && names[i] === 'en0') return iface.address |
|
return '0.0.0.0' |
|
} |
|
} |
|
} |
|
|
|
return '127.0.0.1' |
|
} |
|
|
|
function allInterfaces () { |
|
var networks = os.networkInterfaces() |
|
var names = Object.keys(networks) |
|
var res = [] |
|
|
|
for (var i = 0; i < names.length; i++) { |
|
var net = networks[names[i]] |
|
for (var j = 0; j < net.length; j++) { |
|
var iface = net[j] |
|
if (isIPv4(iface.family)) { |
|
res.push(iface.address) |
|
// could only addMembership once per interface (https://nodejs.org/api/dgram.html#dgram_socket_addmembership_multicastaddress_multicastinterface) |
|
break |
|
} |
|
} |
|
} |
|
|
|
return res |
|
} |
|
|
|
function isIPv4 (family) { // for backwards compat |
|
return family === 4 || family === 'IPv4' |
|
}
|
|
|