UNPKG

5.67 kBJavaScriptView Raw
1'use strict'
2
3var util = require('util')
4var EventEmitter = require('events').EventEmitter
5var serviceName = require('multicast-dns-service-types')
6var dnsEqual = require('dns-equal')
7var dnsTxt = require('dns-txt')
8
9var TLD = '.local'
10var WILDCARD = '_services._dns-sd._udp' + TLD
11
12module.exports = Browser
13
14util.inherits(Browser, EventEmitter)
15
16/**
17 * Start a browser
18 *
19 * The browser listens for services by querying for PTR records of a given
20 * type, protocol and domain, e.g. _http._tcp.local.
21 *
22 * If no type is given, a wild card search is performed.
23 *
24 * An internal list of online services is kept which starts out empty. When
25 * ever a new service is discovered, it's added to the list and an "up" event
26 * is emitted with that service. When it's discovered that the service is no
27 * longer available, it is removed from the list and a "down" event is emitted
28 * with that service.
29 */
30function Browser (mdns, opts, onup) {
31 if (typeof opts === 'function') return new Browser(mdns, null, opts)
32
33 EventEmitter.call(this)
34
35 this._mdns = mdns
36 this._onresponse = null
37 this._serviceMap = {}
38 this._txt = dnsTxt(opts.txt)
39
40 if (!opts || !opts.type) {
41 this._name = WILDCARD
42 this._wildcard = true
43 } else {
44 this._name = serviceName.stringify(opts.type, opts.protocol || 'tcp') + TLD
45 if (opts.name) this._name = opts.name + '.' + this._name
46 this._wildcard = false
47 }
48
49 this.services = []
50
51 if (onup) this.on('up', onup)
52
53 this.start()
54}
55
56Browser.prototype.start = function () {
57 if (this._onresponse) return
58
59 var self = this
60
61 // List of names for the browser to listen for. In a normal search this will
62 // be the primary name stored on the browser. In case of a wildcard search
63 // the names will be determined at runtime as responses come in.
64 var nameMap = {}
65 if (!this._wildcard) nameMap[this._name] = true
66
67 this._onresponse = function (packet, rinfo) {
68 if (self._wildcard) {
69 packet.answers.forEach(function (answer) {
70 if (answer.type !== 'PTR' || answer.name !== self._name || answer.name in nameMap) return
71 nameMap[answer.data] = true
72 self._mdns.query(answer.data, 'PTR')
73 })
74 }
75
76 Object.keys(nameMap).forEach(function (name) {
77 // unregister all services shutting down
78 goodbyes(name, packet).forEach(self._removeService.bind(self))
79
80 // register all new services
81 var matches = buildServicesFor(name, packet, self._txt, rinfo)
82 if (matches.length === 0) return
83
84 matches.forEach(function (service) {
85 if (self._serviceMap[service.fqdn]) return // ignore already registered services
86 self._addService(service)
87 })
88 })
89 }
90
91 this._mdns.on('response', this._onresponse)
92 this.update()
93}
94
95Browser.prototype.stop = function () {
96 if (!this._onresponse) return
97
98 this._mdns.removeListener('response', this._onresponse)
99 this._onresponse = null
100}
101
102Browser.prototype.update = function () {
103 this._mdns.query(this._name, 'PTR')
104}
105
106Browser.prototype._addService = function (service) {
107 this.services.push(service)
108 this._serviceMap[service.fqdn] = true
109 this.emit('up', service)
110}
111
112Browser.prototype._removeService = function (fqdn) {
113 var service, index
114 this.services.some(function (s, i) {
115 if (dnsEqual(s.fqdn, fqdn)) {
116 service = s
117 index = i
118 return true
119 }
120 })
121 if (!service) return
122 this.services.splice(index, 1)
123 delete this._serviceMap[fqdn]
124 this.emit('down', service)
125}
126
127// PTR records with a TTL of 0 is considered a "goodbye" announcement. I.e. a
128// DNS response broadcasted when a service shuts down in order to let the
129// network know that the service is no longer going to be available.
130//
131// For more info see:
132// https://tools.ietf.org/html/rfc6762#section-8.4
133//
134// This function returns an array of all resource records considered a goodbye
135// record
136function goodbyes (name, packet) {
137 return packet.answers.concat(packet.additionals)
138 .filter(function (rr) {
139 return rr.type === 'PTR' && rr.ttl === 0 && dnsEqual(rr.name, name)
140 })
141 .map(function (rr) {
142 return rr.data
143 })
144}
145
146function buildServicesFor (name, packet, txt, referer) {
147 var records = packet.answers.concat(packet.additionals).filter(function (rr) {
148 return rr.ttl > 0 // ignore goodbye messages
149 })
150
151 return records
152 .filter(function (rr) {
153 return rr.type === 'PTR' && dnsEqual(rr.name, name)
154 })
155 .map(function (ptr) {
156 var service = {
157 addresses: []
158 }
159
160 records
161 .filter(function (rr) {
162 return (rr.type === 'SRV' || rr.type === 'TXT') && dnsEqual(rr.name, ptr.data)
163 })
164 .forEach(function (rr) {
165 if (rr.type === 'SRV') {
166 var parts = rr.name.split('.')
167 var name = parts[0]
168 var types = serviceName.parse(parts.slice(1, -1).join('.'))
169 service.name = name
170 service.fqdn = rr.name
171 service.host = rr.data.target
172 service.referer = referer
173 service.port = rr.data.port
174 service.type = types.name
175 service.protocol = types.protocol
176 service.subtypes = types.subtypes
177 } else if (rr.type === 'TXT') {
178 service.rawTxt = rr.data
179 service.txt = txt.decode(rr.data)
180 }
181 })
182
183 if (!service.name) return
184
185 records
186 .filter(function (rr) {
187 return (rr.type === 'A' || rr.type === 'AAAA') && dnsEqual(rr.name, service.host)
188 })
189 .forEach(function (rr) {
190 service.addresses.push(rr.data)
191 })
192
193 return service
194 })
195 .filter(function (rr) {
196 return !!rr
197 })
198}