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.
198 lines
5.5 KiB
198 lines
5.5 KiB
'use strict' |
|
|
|
var util = require('util') |
|
var EventEmitter = require('events').EventEmitter |
|
var serviceName = require('multicast-dns-service-types') |
|
var dnsEqual = require('dns-equal') |
|
var dnsTxt = require('dns-txt') |
|
|
|
var TLD = '.local' |
|
var WILDCARD = '_services._dns-sd._udp' + TLD |
|
|
|
module.exports = Browser |
|
|
|
util.inherits(Browser, EventEmitter) |
|
|
|
/** |
|
* Start a browser |
|
* |
|
* The browser listens for services by querying for PTR records of a given |
|
* type, protocol and domain, e.g. _http._tcp.local. |
|
* |
|
* If no type is given, a wild card search is performed. |
|
* |
|
* An internal list of online services is kept which starts out empty. When |
|
* ever a new service is discovered, it's added to the list and an "up" event |
|
* is emitted with that service. When it's discovered that the service is no |
|
* longer available, it is removed from the list and a "down" event is emitted |
|
* with that service. |
|
*/ |
|
function Browser (mdns, opts, onup) { |
|
if (typeof opts === 'function') return new Browser(mdns, null, opts) |
|
|
|
EventEmitter.call(this) |
|
|
|
this._mdns = mdns |
|
this._onresponse = null |
|
this._serviceMap = {} |
|
this._txt = dnsTxt(opts.txt) |
|
|
|
if (!opts || !opts.type) { |
|
this._name = WILDCARD |
|
this._wildcard = true |
|
} else { |
|
this._name = serviceName.stringify(opts.type, opts.protocol || 'tcp') + TLD |
|
if (opts.name) this._name = opts.name + '.' + this._name |
|
this._wildcard = false |
|
} |
|
|
|
this.services = [] |
|
|
|
if (onup) this.on('up', onup) |
|
|
|
this.start() |
|
} |
|
|
|
Browser.prototype.start = function () { |
|
if (this._onresponse) return |
|
|
|
var self = this |
|
|
|
// List of names for the browser to listen for. In a normal search this will |
|
// be the primary name stored on the browser. In case of a wildcard search |
|
// the names will be determined at runtime as responses come in. |
|
var nameMap = {} |
|
if (!this._wildcard) nameMap[this._name] = true |
|
|
|
this._onresponse = function (packet, rinfo) { |
|
if (self._wildcard) { |
|
packet.answers.forEach(function (answer) { |
|
if (answer.type !== 'PTR' || answer.name !== self._name || answer.name in nameMap) return |
|
nameMap[answer.data] = true |
|
self._mdns.query(answer.data, 'PTR') |
|
}) |
|
} |
|
|
|
Object.keys(nameMap).forEach(function (name) { |
|
// unregister all services shutting down |
|
goodbyes(name, packet).forEach(self._removeService.bind(self)) |
|
|
|
// register all new services |
|
var matches = buildServicesFor(name, packet, self._txt, rinfo) |
|
if (matches.length === 0) return |
|
|
|
matches.forEach(function (service) { |
|
if (self._serviceMap[service.fqdn]) return // ignore already registered services |
|
self._addService(service) |
|
}) |
|
}) |
|
} |
|
|
|
this._mdns.on('response', this._onresponse) |
|
this.update() |
|
} |
|
|
|
Browser.prototype.stop = function () { |
|
if (!this._onresponse) return |
|
|
|
this._mdns.removeListener('response', this._onresponse) |
|
this._onresponse = null |
|
} |
|
|
|
Browser.prototype.update = function () { |
|
this._mdns.query(this._name, 'PTR') |
|
} |
|
|
|
Browser.prototype._addService = function (service) { |
|
this.services.push(service) |
|
this._serviceMap[service.fqdn] = true |
|
this.emit('up', service) |
|
} |
|
|
|
Browser.prototype._removeService = function (fqdn) { |
|
var service, index |
|
this.services.some(function (s, i) { |
|
if (dnsEqual(s.fqdn, fqdn)) { |
|
service = s |
|
index = i |
|
return true |
|
} |
|
}) |
|
if (!service) return |
|
this.services.splice(index, 1) |
|
delete this._serviceMap[fqdn] |
|
this.emit('down', service) |
|
} |
|
|
|
// PTR records with a TTL of 0 is considered a "goodbye" announcement. I.e. a |
|
// DNS response broadcasted when a service shuts down in order to let the |
|
// network know that the service is no longer going to be available. |
|
// |
|
// For more info see: |
|
// https://tools.ietf.org/html/rfc6762#section-8.4 |
|
// |
|
// This function returns an array of all resource records considered a goodbye |
|
// record |
|
function goodbyes (name, packet) { |
|
return packet.answers.concat(packet.additionals) |
|
.filter(function (rr) { |
|
return rr.type === 'PTR' && rr.ttl === 0 && dnsEqual(rr.name, name) |
|
}) |
|
.map(function (rr) { |
|
return rr.data |
|
}) |
|
} |
|
|
|
function buildServicesFor (name, packet, txt, referer) { |
|
var records = packet.answers.concat(packet.additionals).filter(function (rr) { |
|
return rr.ttl > 0 // ignore goodbye messages |
|
}) |
|
|
|
return records |
|
.filter(function (rr) { |
|
return rr.type === 'PTR' && dnsEqual(rr.name, name) |
|
}) |
|
.map(function (ptr) { |
|
var service = { |
|
addresses: [] |
|
} |
|
|
|
records |
|
.filter(function (rr) { |
|
return (rr.type === 'SRV' || rr.type === 'TXT') && dnsEqual(rr.name, ptr.data) |
|
}) |
|
.forEach(function (rr) { |
|
if (rr.type === 'SRV') { |
|
var parts = rr.name.split('.') |
|
var name = parts[0] |
|
var types = serviceName.parse(parts.slice(1, -1).join('.')) |
|
service.name = name |
|
service.fqdn = rr.name |
|
service.host = rr.data.target |
|
service.referer = referer |
|
service.port = rr.data.port |
|
service.type = types.name |
|
service.protocol = types.protocol |
|
service.subtypes = types.subtypes |
|
} else if (rr.type === 'TXT') { |
|
service.rawTxt = rr.data |
|
service.txt = txt.decode(rr.data) |
|
} |
|
}) |
|
|
|
if (!service.name) return |
|
|
|
records |
|
.filter(function (rr) { |
|
return (rr.type === 'A' || rr.type === 'AAAA') && dnsEqual(rr.name, service.host) |
|
}) |
|
.forEach(function (rr) { |
|
service.addresses.push(rr.data) |
|
}) |
|
|
|
return service |
|
}) |
|
.filter(function (rr) { |
|
return !!rr |
|
}) |
|
}
|
|
|