205 lines
5.4 KiB
JavaScript
205 lines
5.4 KiB
JavaScript
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'
|
|
}
|