UNPKG

5.13 kBJavaScriptView Raw
1'use strict'
2
3var dnsEqual = require('dns-equal')
4var flatten = require('array-flatten')
5var Service = require('./service')
6
7var REANNOUNCE_MAX_MS = 60 * 60 * 1000
8var REANNOUNCE_FACTOR = 3
9
10module.exports = Registry
11
12function Registry (server) {
13 this._server = server
14 this._services = []
15}
16
17Registry.prototype.publish = function (opts) {
18 var service = new Service(opts)
19 service.start = start.bind(service, this)
20 service.stop = stop.bind(service, this)
21 service.start({ probe: opts.probe !== false })
22 return service
23}
24
25Registry.prototype.unpublishAll = function (cb) {
26 teardown(this._server, this._services, cb)
27 this._services = []
28}
29
30Registry.prototype.destroy = function () {
31 this._services.forEach(function (service) {
32 service._destroyed = true
33 })
34}
35
36function start (registry, opts) {
37 if (this._activated) return
38 this._activated = true
39
40 registry._services.push(this)
41
42 if (opts.probe) {
43 var service = this
44 probe(registry._server.mdns, this, function (exists) {
45 if (exists) {
46 service.stop()
47 service.emit('error', new Error('Service name is already in use on the network'))
48 return
49 }
50 announce(registry._server, service)
51 })
52 } else {
53 announce(registry._server, this)
54 }
55}
56
57function stop (registry, cb) {
58 if (!this._activated) return // TODO: What about the callback?
59
60 teardown(registry._server, this, cb)
61
62 var index = registry._services.indexOf(this)
63 if (index !== -1) registry._services.splice(index, 1)
64}
65
66/**
67 * Check if a service name is already in use on the network.
68 *
69 * Used before announcing the new service.
70 *
71 * To guard against race conditions where multiple services are started
72 * simultaneously on the network, wait a random amount of time (between
73 * 0 and 250 ms) before probing.
74 *
75 * TODO: Add support for Simultaneous Probe Tiebreaking:
76 * https://tools.ietf.org/html/rfc6762#section-8.2
77 */
78function probe (mdns, service, cb) {
79 var sent = false
80 var retries = 0
81 var timer
82
83 mdns.on('response', onresponse)
84 setTimeout(send, Math.random() * 250)
85
86 function send () {
87 // abort if the service have or is being stopped in the meantime
88 if (!service._activated || service._destroyed) return
89
90 mdns.query(service.fqdn, 'ANY', function () {
91 // This function will optionally be called with an error object. We'll
92 // just silently ignore it and retry as we normally would
93 sent = true
94 timer = setTimeout(++retries < 3 ? send : done, 250)
95 timer.unref()
96 })
97 }
98
99 function onresponse (packet) {
100 // Apparently conflicting Multicast DNS responses received *before*
101 // the first probe packet is sent MUST be silently ignored (see
102 // discussion of stale probe packets in RFC 6762 Section 8.2,
103 // "Simultaneous Probe Tiebreaking" at
104 // https://tools.ietf.org/html/rfc6762#section-8.2
105 if (!sent) return
106
107 if (packet.answers.some(matchRR) || packet.additionals.some(matchRR)) done(true)
108 }
109
110 function matchRR (rr) {
111 return dnsEqual(rr.name, service.fqdn)
112 }
113
114 function done (exists) {
115 mdns.removeListener('response', onresponse)
116 clearTimeout(timer)
117 cb(!!exists)
118 }
119}
120
121/**
122 * Initial service announcement
123 *
124 * Used to announce new services when they are first registered.
125 *
126 * Broadcasts right away, then after 3 seconds, 9 seconds, 27 seconds,
127 * and so on, up to a maximum interval of one hour.
128 */
129function announce (server, service) {
130 var delay = 1000
131 var packet = service._records()
132
133 server.register(packet)
134
135 ;(function broadcast () {
136 // abort if the service have or is being stopped in the meantime
137 if (!service._activated || service._destroyed) return
138
139 server.mdns.respond(packet, function () {
140 // This function will optionally be called with an error object. We'll
141 // just silently ignore it and retry as we normally would
142 if (!service.published) {
143 service._activated = true
144 service.published = true
145 service.emit('up')
146 }
147 delay = delay * REANNOUNCE_FACTOR
148 if (delay < REANNOUNCE_MAX_MS && !service._destroyed) {
149 setTimeout(broadcast, delay).unref()
150 }
151 })
152 })()
153}
154
155/**
156 * Stop the given services
157 *
158 * Besides removing a service from the mDNS registry, a "goodbye"
159 * message is sent for each service to let the network know about the
160 * shutdown.
161 */
162function teardown (server, services, cb) {
163 if (!Array.isArray(services)) services = [services]
164
165 services = services.filter(function (service) {
166 return service._activated // ignore services not currently starting or started
167 })
168
169 var records = flatten.depth(services.map(function (service) {
170 service._activated = false
171 var records = service._records()
172 records.forEach(function (record) {
173 record.ttl = 0 // prepare goodbye message
174 })
175 return records
176 }), 1)
177
178 if (records.length === 0) return cb && cb()
179
180 server.unregister(records)
181
182 // send goodbye message
183 server.mdns.respond(records, function () {
184 services.forEach(function (service) {
185 service.published = false
186 })
187 if (cb) cb.apply(null, arguments)
188 })
189}