diff --git a/lib/browser.js b/lib/browser.js index ba34e8b..9e94805 100644 --- a/lib/browser.js +++ b/lib/browser.js @@ -29,6 +29,7 @@ util.inherits(Browser, EventEmitter) */ function Browser (mdns, opts, onup) { if (typeof opts === 'function') return new Browser(mdns, null, opts) + var self = this EventEmitter.call(this) @@ -38,11 +39,21 @@ function Browser (mdns, opts, onup) { this._txt = dnsTxt(opts.txt) if (!opts || !opts.type) { - this._name = WILDCARD + this._names = [ WILDCARD ] this._wildcard = true } else { - this._name = serviceName.stringify(opts.type, opts.protocol || 'tcp') + TLD - if (opts.name) this._name = opts.name + '.' + this._name + // When requesting specific subtypes, add those to the name map, else + // use the single service type and any optional name prefix. + if ((opts.subtypes === undefined) || (opts.subtypes.length === 0)) { + this._names = [ serviceName.stringify(opts.type, opts.protocol || 'tcp') + TLD ] + if (opts.name) this._name = opts.name + '.' + this._name + } else { + self._names = [] + opts.subtypes.forEach(function (subtype) { + self._names.push('_' + subtype + '._sub.' + + serviceName.stringify(opts.type, opts.protocol || 'tcp') + TLD) + }) + } this._wildcard = false } @@ -55,19 +66,31 @@ function Browser (mdns, opts, onup) { Browser.prototype.start = function () { if (this._onresponse) return - var self = this + var nameMap = {} + var nameInMap = function (recordName) { + var nameMatch = false + self._names.forEach(function (name) { + if (name === recordName) { + nameMatch = true + } + }) + return nameMatch + } // 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 + if (!this._wildcard) { + this._names.forEach(function (name) { + nameMap[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 + if (answer.type !== 'PTR' || nameInMap(answer.map) || answer.name in nameMap) return nameMap[answer.data] = true self._mdns.query(answer.data, 'PTR') }) @@ -82,8 +105,27 @@ Browser.prototype.start = function () { if (matches.length === 0) return matches.forEach(function (service) { - if (self._serviceMap[service.fqdn]) return // ignore already registered services - self._addService(service) + var serviceIndex = 0 + if (self._serviceMap[service.fqdn]) { + // ignore already registered services, which exist is the new service + // has no subtype + if (service.subtypes.length === 0) return + + // Check to see if this includes a subtype that didn't exist previously + // If so, add it to the service already cached and emit a CB + for (serviceIndex = 0; serviceIndex < self.services.length; serviceIndex += 1) { + if (self.services[serviceIndex].fqdn === service.fqdn) { + break + } + } + // If the service subtype type already exists in the service, ignore it. + if (self.services[serviceIndex].subtypes.indexOf(service.subtypes[0]) !== -1) return + + self.services[serviceIndex].subtypes.push(service.subtypes[0]) + self.emit('up', self.services[serviceIndex]) + } else { + self._addService(service) + } }) }) } @@ -100,7 +142,10 @@ Browser.prototype.stop = function () { } Browser.prototype.update = function () { - this._mdns.query(this._name, 'PTR') + var self = this + this._names.forEach(function (name) { + self._mdns.query(name, 'PTR') + }) } Browser.prototype._addService = function (service) { @@ -166,6 +211,7 @@ function buildServicesFor (name, packet, txt, referer) { var parts = rr.name.split('.') var name = parts[0] var types = serviceName.parse(parts.slice(1, -1).join('.')) + var subparts = ptr.name.split('.') service.name = name service.fqdn = rr.name service.host = rr.data.target @@ -173,7 +219,15 @@ function buildServicesFor (name, packet, txt, referer) { service.port = rr.data.port service.type = types.name service.protocol = types.protocol - service.subtypes = types.subtypes + + // If the subparts length is larger than the parts length, then + // there does indeed exist a subtype and we add that to the main + // service record. + if (subparts.length > (parts.length - 1)) { + service.subtypes = [ subparts[0].slice(1) ] + } else { + service.subtypes = [] + } } else if (rr.type === 'TXT') { service.rawTxt = rr.data service.txt = txt.decode(rr.data) diff --git a/lib/service.js b/lib/service.js index 52a1606..a34952a 100644 --- a/lib/service.js +++ b/lib/service.js @@ -22,6 +22,7 @@ function Service (opts) { this.type = serviceName.stringify(opts.type, this.protocol) this.host = opts.host || os.hostname() this.port = opts.port + this.addresses = opts.addresses this.fqdn = this.name + '.' + this.type + TLD this.subtypes = opts.subtypes || null this.txt = opts.txt || null @@ -31,27 +32,56 @@ function Service (opts) { } Service.prototype._records = function () { - var records = [rrPtr(this), rrSrv(this), rrTxt(this)] + var records = [ rrPtrServices(this), rrPtr(this), rrSrv(this), rrTxt(this) ] + + if (this.subtypes) { + for (var subtypeIndex = 0; subtypeIndex < this.subtypes.length; subtypeIndex += 1) { + records.push(rrPtr(this, subtypeIndex)) + } + } var self = this - var interfaces = os.networkInterfaces() - Object.keys(interfaces).forEach(function (name) { - interfaces[name].forEach(function (addr) { - if (addr.internal) return - if (addr.family === 'IPv4') { - records.push(rrA(self, addr.address)) - } else { - records.push(rrAaaa(self, addr.address)) - } + if (!this.addresses) { + var interfaces = os.networkInterfaces() + Object.keys(interfaces).forEach(function (name) { + interfaces[name].forEach(function (addr) { + if (addr.internal) return + if (addr.family === 'IPv4') { + records.push(rrA(self, addr.address)) + } else { + records.push(rrAaaa(self, addr.address)) + } + }) }) - }) + } else { + if (this.addresses.ipv4) { + this.addresses.ipv4.forEach(function (addr) { + records.push(rrA(self, addr)) + }) + } + if (this.addresses.ipv6) { + this.addresses.ipv6.forEach(function (addr) { + records.push(rrAaaa(self, addr)) + }) + } + } return records } -function rrPtr (service) { +function rrPtrServices (service) { + return { + name: '_services._dns-sd._udp.local', + type: 'PTR', + ttl: 28800, + data: service.type + TLD + } +} + +function rrPtr (service, subtypeIndex) { return { - name: service.type + TLD, + name: (subtypeIndex !== undefined) ? '_' + service.subtypes[subtypeIndex] + '._sub.' + + service.type + TLD : service.type + TLD, type: 'PTR', ttl: 28800, data: service.fqdn diff --git a/test/bonjour.js b/test/bonjour.js index 1b87080..b049731 100644 --- a/test/bonjour.js +++ b/test/bonjour.js @@ -78,6 +78,8 @@ test('bonjour.find', function (bonjour, t) { var ups = 0 browser.on('up', function (s) { + if (s.name === 'Sub Foo') return + if (s.name === 'Foo Bar') { t.equal(s.name, 'Foo Bar') t.equal(s.fqdn, 'Foo Bar._test._tcp.local') @@ -113,6 +115,79 @@ test('bonjour.find', function (bonjour, t) { bonjour.publish({ name: 'Foo Bar', type: 'test', port: 3000 }).on('up', next()) bonjour.publish({ name: 'Invalid', type: 'test2', port: 3000 }).on('up', next()) + bonjour.publish({ name: 'Sub Foo', type: 'test', subtypes: [ 'stOne', 'stTwo' ], port: 3000 }).on('up', next()) + bonjour.publish({ name: 'Baz', type: 'test', port: 3000, txt: { foo: 'bar' } }).on('up', next()) +}) + +test('bonjour.find - all services', function (bonjour, t) { + var next = afterAll(function () { + var browserServices = bonjour.find({}) + var ups = 0 + var found = [] + + browserServices.on('up', function (s) { + // Ensures that bonjour responds to the '_services._dns-sd._udp.local' + // request. + found.push(s.name) + + if (++ups === 4) { + found.sort() + t.equal(found[0], 'Baz') + t.equal(found[1], 'Foo Bar') + t.equal(found[2], 'Invalid') + t.equal(found[3], 'Sub Foo') + setTimeout(function () { + bonjour.destroy() + t.end() + }, 50) + } + }) + }) + + bonjour.publish({ name: 'Foo Bar', type: 'test', port: 3000 }).on('up', next()) + bonjour.publish({ name: 'Invalid', type: 'test2', port: 3000 }).on('up', next()) + bonjour.publish({ name: 'Sub Foo', type: 'test', subtypes: [ 'stOne', 'stTwo' ], port: 3000 }).on('up', next()) + bonjour.publish({ name: 'Baz', type: 'test', port: 3000, txt: { foo: 'bar' } }).on('up', next()) +}) + +test('bonjour.find - subtypes', function (bonjour, t) { + var next = afterAll(function () { + var browserSubtypes = bonjour.find({ type: 'test', subtypes: [ 'stOne', 'stTwo' ] }) + var subUp = 0 + + browserSubtypes.on('up', function (s) { + t.equal(s.name, 'Sub Foo') + t.equal(s.fqdn, 'Sub Foo._test._tcp.local') + t.deepEqual(s.txt, {}) + t.deepEqual(s.rawTxt, new Buffer('00', 'hex')) + t.equal(s.host, os.hostname()) + t.equal(s.port, 3000) + t.equal(s.type, 'test') + t.equal(s.protocol, 'tcp') + t.equal(s.referer.address, '127.0.0.1') + t.equal(s.referer.family, 'IPv4') + t.ok(Number.isFinite(s.referer.port)) + t.ok(Number.isFinite(s.referer.size)) + if (++subUp === 2) { + // Subtypes may be out of order depending on order records were + // received in. + var testCount = 0 + s.subtypes.forEach(function (subtype) { + if ((subtype === 'stTwo') || (subtype === 'stOne')) { + testCount += 1 + } + }) + setTimeout(function () { + bonjour.destroy() + t.end() + }, 50) + } + }) + }) + + bonjour.publish({ name: 'Foo Bar', type: 'test', port: 3000 }).on('up', next()) + bonjour.publish({ name: 'Invalid', type: 'test2', port: 3000 }).on('up', next()) + bonjour.publish({ name: 'Sub Foo', type: 'test', subtypes: [ 'stOne', 'stTwo' ], port: 3000 }).on('up', next()) bonjour.publish({ name: 'Baz', type: 'test', port: 3000, txt: { foo: 'bar' } }).on('up', next()) }) diff --git a/test/service.js b/test/service.js index 9903153..1f9594c 100644 --- a/test/service.js +++ b/test/service.js @@ -71,9 +71,19 @@ test('txt', function (t) { t.end() }) +test('addresses', function (t) { + var s1 = new Service({ name: 'Foo Bar', type: 'http', port: 3000, host: 'testhost1.com', addresses: { ipv4: [ '1.2.3.4' ], ipv6: [ '2001:db8::01:02', 'fe80::01:02' ] } }) + var s2 = new Service({ name: 'Foo Bar', type: 'http', port: 3000, host: 'testhost2.com', addresses: { ipv4: [ '5.6.7.8', '9.10.11.12' ] } }) + + t.deepEqual(s1.addresses, { ipv4: [ '1.2.3.4' ], ipv6: [ '2001:db8::01:02', 'fe80::01:02' ] }) + t.deepEqual(s2.addresses, { ipv4: [ '5.6.7.8', '9.10.11.12' ] }) + t.end() +}) + test('_records() - minimal', function (t) { var s = new Service({ name: 'Foo Bar', type: 'http', protocol: 'tcp', port: 3000 }) t.deepEqual(s._records(), [ + { data: '_http._tcp.local', name: '_services._dns-sd._udp.local', ttl: 28800, type: 'PTR' }, { data: s.fqdn, name: '_http._tcp.local', ttl: 28800, type: 'PTR' }, { data: { port: 3000, target: os.hostname() }, name: s.fqdn, ttl: 120, type: 'SRV' }, { data: new Buffer('00', 'hex'), name: s.fqdn, ttl: 4500, type: 'TXT' } @@ -81,12 +91,28 @@ test('_records() - minimal', function (t) { t.end() }) -test('_records() - everything', function (t) { +test('_records() - everything bar addresses', function (t) { var s = new Service({ name: 'Foo Bar', type: 'http', protocol: 'tcp', port: 3000, host: 'example.com', txt: { foo: 'bar' } }) t.deepEqual(s._records(), [ + { data: '_http._tcp.local', name: '_services._dns-sd._udp.local', ttl: 28800, type: 'PTR' }, { data: s.fqdn, name: '_http._tcp.local', ttl: 28800, type: 'PTR' }, { data: { port: 3000, target: 'example.com' }, name: s.fqdn, ttl: 120, type: 'SRV' }, { data: new Buffer('07666f6f3d626172', 'hex'), name: s.fqdn, ttl: 4500, type: 'TXT' } ].concat(getAddressesRecords(s.host))) t.end() }) + +test('_records() - everything including addresses', function (t) { + var s = new Service({ name: 'Foo Bar', type: 'http', protocol: 'tcp', port: 3000, host: 'example.com', txt: { foo: 'bar' }, + addresses: { ipv4: [ '13.14.15.16' ], ipv6: [ '2001:db8::01:03', 'fe80::01:03' ] } }) + t.deepEqual(s._records(), [ + { data: '_http._tcp.local', name: '_services._dns-sd._udp.local', ttl: 28800, type: 'PTR' }, + { data: s.fqdn, name: '_http._tcp.local', ttl: 28800, type: 'PTR' }, + { data: { port: 3000, target: 'example.com' }, name: s.fqdn, ttl: 120, type: 'SRV' }, + { data: new Buffer('07666f6f3d626172', 'hex'), name: s.fqdn, ttl: 4500, type: 'TXT' }, + { data: '13.14.15.16', name: 'example.com', ttl: 120, type: 'A' }, + { data: '2001:db8::01:03', name: 'example.com', ttl: 120, type: 'AAAA' }, + { data: 'fe80::01:03', name: 'example.com', ttl: 120, type: 'AAAA' } + ]) + t.end() +})