UNPKG

dnssd

Version:

Bonjour/Avahi-like service discovery in pure JavaScript

801 lines (628 loc) 25.2 kB
const fs = require('fs'); const path = require('path'); const chai = require('chai'); const expect = chai.expect; const sinon = require('sinon'); const sinonChai = require('sinon-chai'); chai.use(sinonChai); const dir = process['test-dir'] || '../../src'; const BufferWrapper = require(dir + '/BufferWrapper'); const QueryRecord = require(dir + '/QueryRecord'); const hex = require(dir + '/hex'); const RType = require(dir + '/constants').RType; const RClass = require(dir + '/constants').RClass; const filename = require('path').basename(__filename); const debug = require(dir + '/debug')('dnssd:' + filename); const ResourceRecord = require(dir + '/ResourceRecord'); describe('ResourceRecord', function() { const packetDir = path.resolve(__dirname, '../data/records/'); function getFile(file) { const buffer = fs.readFileSync(packetDir + '/' + file); return new BufferWrapper(buffer); } describe('#constructor', function() { it('should throw an error, use ::fromBuffer instead', function() { expect(() => new ResourceRecord()).to.throw(Error); }); }); describe('ResourceRecord.A', function() { describe('#constructor', function() { it('should make new record from given fields & defaults', function() { const record = new ResourceRecord.A({ name: 'test.local.', address: '1.1.1.1', additionals: ['fake record'], }); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.A); expect(record).to.include({ rrtype : RType.A, rrclass : RClass.IN, name : 'test.local.', ttl : 120, isUnique : true, address : '1.1.1.1', }); expect(record.additionals).to.have.members(['fake record']); }); it('should throw if record not given a name', function() { expect(() => new ResourceRecord.A()).to.throw(Error); expect(() => new ResourceRecord.A({})).to.throw(Error); expect(() => new ResourceRecord.A({name: ''})).to.throw(Error); }); }); describe('::fromBuffer', function() { it('A.bin', function() { const wrapper = getFile('A.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.A); expect(record).to.include({ rrtype : RType.A, rrclass : RClass.IN, name : 'box.local.', ttl : 120, isUnique: true, address : '169.254.22.58', }); }); }); }); describe('ResourceRecord.PTR', function() { describe('#constructor', function() { it('should make new record from given fields & defaults', function() { const record = new ResourceRecord.PTR({ name: '_service._tcp.local.', PTRDName: 'test._service._tcp.local.', }); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.PTR); expect(record).to.include({ rrtype : RType.PTR, rrclass : RClass.IN, name : '_service._tcp.local.', ttl : 4500, isUnique: false, PTRDName: 'test._service._tcp.local.', }); }); }); describe('::fromBuffer', function() { it('PTR-service.bin', function() { const wrapper = getFile('PTR-service.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.PTR); expect(record).to.include({ rrtype : RType.PTR, rrclass : RClass.IN, name : '_service._tcp.local.', ttl : 4500, isUnique: false, PTRDName: 'test._service._tcp.local.', }); }); it('PTR-enumerator.bin', function() { const wrapper = getFile('PTR-enumerator.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.PTR); expect(record).to.include({ rrtype : RType.PTR, rrclass : RClass.IN, name : '_services._dns-sd._udp.local.', ttl : 4500, isUnique: false, PTRDName: '_service._tcp.local.', }); }); it('PTR-goodbye.bin', function() { const wrapper = getFile('PTR-goodbye.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.PTR); expect(record).to.include({ rrtype : RType.PTR, rrclass : RClass.IN, name : '_service._tcp.local.', ttl : 0, isUnique: false, PTRDName: 'test._service._tcp.local.', }); }); }); }); describe('ResourceRecord.TXT', function() { describe('#constructor', function() { it('should make new record from given fields & defaults', function() { const record = new ResourceRecord.TXT({ name: 'test._service._tcp.local.', txt: {key: new Buffer('value')}, }); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.TXT); expect(record).to.include({ rrtype : RType.TXT, rrclass : RClass.IN, name : 'test._service._tcp.local.', ttl : 4500, isUnique: true, }); expect(record.txt).to.eql({key: 'value'}); expect(record.txtRaw).to.eql({key: new Buffer('value')}); }); }); describe('::fromBuffer', function() { it('TXT-empty.bin', function() { const wrapper = getFile('TXT-empty.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.TXT); expect(record).to.include({ rrtype : RType.TXT, rrclass : RClass.IN, name : 'test._service._tcp.local.', ttl : 4500, isUnique: true, }); expect(record.txt).to.be.empty; expect(record.txtRaw).to.be.empty; }); it('TXT-false.bin', function() { const wrapper = getFile('TXT-false.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.TXT); expect(record).to.include({ rrtype : RType.TXT, rrclass : RClass.IN, name : 'BOX@TuneBlade._http._tcp.local.', ttl : 4500, isUnique: true, }); expect(record.txt).to.eql({Password: 'False'}); // <- a string! expect(record.txtRaw).to.eql({Password: new Buffer('False')}); }); it('TXT-large.bin', function() { const wrapper = getFile('TXT-large.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.TXT); expect(record).to.include({ rrtype : RType.TXT, rrclass : RClass.IN, name : 'Test._testlargetxt._tcp.local.', ttl : 4500, isUnique: true, }); const expected = {GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG: true}; expect(record.txt).to.include(expected); expect(record.txtRaw).to.include(expected); }); }); describe('#_readRData', function() { function makeTXT(data) { const wrapper = new BufferWrapper(); wrapper.writeFQDN('Test.'); wrapper.writeUInt16BE(RType.TXT); wrapper.writeUInt16BE(RClass.IN); wrapper.writeUInt32BE(4500); const rdataStartPos = wrapper.tell(); wrapper.skip(2); // <- rdata length goes here wrapper.writeUInt8(data.length); wrapper.writeString(data); const endRData = wrapper.tell(); wrapper.seek(rdataStartPos); wrapper.writeUInt16BE(endRData - rdataStartPos - 2); // <- rdata length wrapper.seek(0); // <- reset position return wrapper; } it('key=value -> {key: value}', function() { const wrapper = makeTXT('key=value'); const record = ResourceRecord.fromBuffer(wrapper); expect(record.txt).to.eql({key: 'value'}); expect(record.txtRaw).to.eql({key: new Buffer('value')}); }); it('key= -> {key: null}', function() { const wrapper = makeTXT('key='); const record = ResourceRecord.fromBuffer(wrapper); expect(record.txt).to.eql({key: null}); expect(record.txtRaw).to.eql({key: null}); }); it('key -> {key: true}', function() { const wrapper = makeTXT('key'); const record = ResourceRecord.fromBuffer(wrapper); expect(record.txt).to.eql({key: true}); expect(record.txtRaw).to.eql({key: true}); }); }); }); describe('ResourceRecord.AAAA', function() { describe('#constructor', function() { it('should make new record from given fields & defaults', function() { const record = new ResourceRecord.AAAA({ name : 'test.local.', address: '::1', ttl : 333, }); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.AAAA); expect(record).to.include({ rrtype : RType.AAAA, rrclass : RClass.IN, name : 'test.local.', ttl : 333, isUnique: true, address: '::1', }); }); }); describe('::fromBuffer', function() { it('AAAA.bin', function() { const wrapper = getFile('AAAA.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.AAAA); expect(record).to.include({ rrtype : RType.AAAA, rrclass : RClass.IN, name : 'box.local.', ttl : 120, isUnique: true, address : 'fe80::c5b:7534:952d:163a', }); }); }); }); describe('ResourceRecord.SRV', function() { describe('#constructor', function() { it('should make new record from given fields & defaults', function() { const record = new ResourceRecord.SRV({ name : 'test._service._tcp.local.', target: 'box.local.', port : 9000, }); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.SRV); expect(record).to.include({ rrtype : RType.SRV, rrclass : RClass.IN, name : 'test._service._tcp.local.', ttl : 120, isUnique: true, target : 'box.local.', port : 9000, }); }); }); describe('::fromBuffer', function() { it('SRV.bin', function() { const wrapper = getFile('SRV.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.SRV); expect(record).to.include({ rrtype : RType.SRV, rrclass : RClass.IN, name : 'test._service._tcp.local.', ttl : 120, isUnique: true, target : 'box.local.', port : 9090, priority: 0, weight : 0, }); }); }); }); describe('ResourceRecord.NSEC', function() { describe('#constructor', function() { it('should make new record from given fields & defaults', function() { const record = new ResourceRecord.NSEC({ name : 'test._service._tcp.local.', existing: [RType.SRV, RType.TXT], }); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.NSEC); expect(record).to.include({ rrtype : RType.NSEC, rrclass : RClass.IN, name : 'test._service._tcp.local.', ttl : 120, isUnique: true, }); expect(record.existing).to.have.members([RType.TXT, RType.SRV]); }); }); describe('::fromBuffer', function() { it('NSEC-addresses.bin', function() { const wrapper = getFile('NSEC-addresses.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.NSEC); expect(record).to.include({ rrtype : RType.NSEC, rrclass : RClass.IN, name : 'box.local.', ttl : 120, isUnique: true, }); expect(record.existing).to.have.members([RType.A, RType.AAAA]); }); it('NSEC-service.bin', function() { const wrapper = getFile('NSEC-service.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.NSEC); expect(record).to.include({ rrtype : RType.NSEC, rrclass : RClass.IN, name : 'test._service._tcp.local.', ttl : 4500, isUnique: true, }); expect(record.existing).to.have.members([RType.TXT, RType.SRV]); }); }); describe('#_readRData', function() { it('should only parse restricted form and ignore blocks > 255', function() { const wrapper = new BufferWrapper(); wrapper.writeFQDN('Test.'); wrapper.writeUInt16BE(RType.NSEC); wrapper.writeUInt16BE(RClass.IN); wrapper.writeUInt32BE(4500); const rdataStartPos = wrapper.tell(); wrapper.skip(2); // <- rdata length goes here wrapper.writeFQDN('Test.'); wrapper.writeUInt8(1); // block 1 wrapper.writeUInt8(1); // bitfield length (1 octet) wrapper.writeUInt8(1 << 6); // RType.A (01000000) const finalPos = wrapper.tell(); wrapper.seek(rdataStartPos); wrapper.writeUInt16BE(finalPos - rdataStartPos - 2); // <- rdata length wrapper.seek(0); // <- reset position const record = ResourceRecord.fromBuffer(wrapper); expect(record.existing).to.be.empty; expect(wrapper.tell()).to.equal(finalPos); }); it('should ignore bad records with bitfield length > 32', function() { const wrapper = new BufferWrapper(); wrapper.writeFQDN('Test.'); wrapper.writeUInt16BE(RType.NSEC); wrapper.writeUInt16BE(RClass.IN); wrapper.writeUInt32BE(4500); const rdataStartPos = wrapper.tell(); wrapper.skip(2); // <- rdata length goes here wrapper.writeFQDN('Test.'); wrapper.writeUInt8(0); // block 0 wrapper.writeUInt8(44); // bitfield length (44 octets) wrapper.writeUInt8(1 << 6); // RType.A (01000000) const finalPos = wrapper.tell(); wrapper.seek(rdataStartPos); wrapper.writeUInt16BE(finalPos - rdataStartPos - 2); // <- rdata length wrapper.seek(0); // <- reset position const record = ResourceRecord.fromBuffer(wrapper); expect(record.existing).to.be.empty; expect(wrapper.tell()).to.equal(finalPos); }); }); describe('#_writeRData', function() { it('should not throw if `existing` is empty', function() { const wrapper = new BufferWrapper(); const record = new ResourceRecord.NSEC({name: 'Empty'}); expect(() => record._writeRData(wrapper)).to.not.throw(Error); }); }); }); describe('ResourceRecord.Unknown', function() { describe('#constructor', function() { it('should make new record from given fields & defaults', function() { const rdata = new Buffer('rdata'); const record = new ResourceRecord.Unknown({ name : 'test._service._tcp.local.', rrtype: 127, rdata : rdata, }); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.Unknown); expect(record).to.include({ rrtype : 127, rrclass : RClass.IN, name : 'test._service._tcp.local.', ttl : 120, isUnique: true, rdata : rdata, }); }); }); describe('::fromBuffer', function() { it('HINFO-unknown.bin', function() { const wrapper = getFile('HINFO-unknown.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.Unknown); expect(record).to.include({ rrtype : 13, rrclass : RClass.IN, name : 'Test._testupdate._tcp.local.', ttl : 4500, isUnique: true, }); expect(record.RData).to.be.a.buffer; }); it('OPT-unknown.bin', function() { const wrapper = getFile('OPT-unknown.bin'); const record = ResourceRecord.fromBuffer(wrapper); expect(record).to.be.instanceof(ResourceRecord); expect(record).to.be.instanceof(ResourceRecord.Unknown); expect(record).to.include({ rrtype: 41, name : '.', ttl : 4500, }); expect(record.RData).to.be.a.buffer; }); }); }); describe('#writeTo', function() { const files = fs.readdirSync(packetDir); files.forEach((file) => { it(file, function() { const input = getFile(file); const output = new BufferWrapper(); const record = ResourceRecord.fromBuffer(input); record.writeTo(output); if (debug.v.isEnabled) { debug.v('%s:\n%s\n\nINPUT: \n%s\n\nOUTPUT: \n%s\n\nAre equal?: %s', file, record, hex.view(input.unwrap()), hex.view(output.unwrap()), output.unwrap().equals(input.unwrap())); } expect( output.unwrap().equals(input.unwrap()) ).to.be.true; }); }); }); describe('#conflictsWith', function() { const SRV_1 = new ResourceRecord.SRV({name: 'Same', target: 'Something'}); const SRV_2 = new ResourceRecord.SRV({name: 'Same', target: 'Else'}); const PTR_1 = new ResourceRecord.PTR({name: 'Same'}); const PTR_2 = new ResourceRecord.PTR({name: 'Different'}); it('should be true if there is a conflict', function() { expect(SRV_1.conflictsWith(SRV_2)).to.be.true; // different rdata }); it('should be false if no conflict', function() { expect(SRV_1.conflictsWith(SRV_1)).to.be.false; // same rdata expect(SRV_1.conflictsWith(PTR_1)).to.be.false; // different rrtype expect(SRV_1.conflictsWith(PTR_2)).to.be.false; // different name expect(PTR_1.conflictsWith(PTR_2)).to.be.false; // not unique }); }); describe('#canAnswer', function() { const specific = new QueryRecord({name: 'NAME', qtype: RType.SRV}); const anyRType = new QueryRecord({name: 'name', qtype: RType.ANY}); const anyClass = new QueryRecord({name: 'name', qclass: RClass.ANY}); const noMatch = new QueryRecord({name: 'Test', qtype: RType.ANY}); const SRV = new ResourceRecord.SRV({name: 'Name'}); // <-- case insensitive const PTR = new ResourceRecord.PTR({name: 'Name'}); const TXT = new ResourceRecord.TXT({name: 'Name', rrclass: 123}); it('should be true if record can answer the query record', function() { expect(SRV.canAnswer(specific)).to.be.true; expect(SRV.canAnswer(anyRType)).to.be.true; expect(PTR.canAnswer(anyRType)).to.be.true; expect(TXT.canAnswer(anyClass)).to.be.true; }); it('should be false record can\'t answer question', function() { expect(PTR.canAnswer(specific)).to.be.false; expect(TXT.canAnswer(anyRType)).to.be.false; expect(PTR.canAnswer(noMatch)).to.be.false; expect(PTR.canAnswer(noMatch)).to.be.false; expect(SRV.canAnswer(noMatch)).to.be.false; }); }); describe('#equals', function() { const SRV_1 = new ResourceRecord.SRV({name: 'Same', port: 9000}); const SRV_2 = new ResourceRecord.SRV({name: 'Same', port: 9000}); const TXT_1 = new ResourceRecord.TXT({name: 'Same', txt: {key: true}}); const TXT_2 = new ResourceRecord.TXT({name: 'Same', txt: {key: true}}); it('should be true if record can answer the query record', function() { expect(SRV_1.equals(SRV_2)).to.be.true; expect(TXT_1.equals(TXT_2)).to.be.true; }); it('should be false record can\'t answer question', function() { expect(SRV_1.equals(TXT_1)).to.be.false; expect(SRV_2.equals(TXT_2)).to.be.false; }); }); describe('#compare', function() { // different rrclasses const rrclass_1 = new ResourceRecord.A({name: 'A', rrclass: 1}); const rrclass_2 = new ResourceRecord.A({name: 'A', rrclass: 2}); // different rrtypes const A = new ResourceRecord.A({name: 'A'}); const AAAA = new ResourceRecord.AAAA({name: 'AAAA'}); // different rdata const TXT_1 = new ResourceRecord.TXT({name: 'TXT', txt: {key: '1'}}); const TXT_2 = new ResourceRecord.TXT({name: 'TXT', txt: {key: '2'}}); it('should first compare records base on rrclass', function() { expect(rrclass_1.compare(rrclass_2)).to.equal(-1); expect(rrclass_2.compare(rrclass_1)).to.equal(1); }); it('should then compare based on rrtypes', function() { expect(A.compare(AAAA)).to.equal(-1); expect(AAAA.compare(A)).to.equal(1); }); it('should then compared on rdata buffers', function() { expect(TXT_1.compare(TXT_2)).to.equal(-1); expect(TXT_2.compare(TXT_1)).to.equal(1); expect(TXT_2.compare(TXT_2)).to.equal(0); expect(TXT_1.compare(TXT_1)).to.equal(0); }); }); describe('#matches', function() { const SRV_1 = new ResourceRecord.SRV({name: 'SRV Record', target: 'box.local.'}); const SRV_2 = new ResourceRecord.SRV({name: 'SRV Record', target: 'test.local.'}); it('should return true/false if record matches all properties', function() { const properties_1 = {name: 'SRV Record', target: 'box.local.'}; const properties_2 = {name: 'SRV Record', target: 'test.local.'}; expect(SRV_1.matches(properties_1)).to.be.true; expect(SRV_1.matches(properties_2)).to.be.false; expect(SRV_2.matches(properties_1)).to.be.false; expect(SRV_2.matches(properties_2)).to.be.true; }); it('should match strings case insensitive', function() { const properties = {name: 'srv RECORD', rrtype: RType.SRV}; expect(SRV_1.matches(properties)).to.be.true; expect(SRV_2.matches(properties)).to.be.true; }); }); describe('#clone', function() { const SRV = new ResourceRecord.SRV({name: 'SRV Record', target: 'box.local.'}); const unknown = new ResourceRecord.Unknown({ name: 'Unknown.type.', rrtype: 127, rdata: new Buffer('rdata'), }); it('should return a clone', function() { const clone = SRV.clone(); expect(clone.equals(SRV)).to.be.true; // same data expect(clone).to.not.equal(SRV); // different object }); it('should work for unknown types too', function() { const clone = unknown.clone(); expect(clone.equals(unknown)).to.be.true; // same data expect(clone).to.not.equal(unknown); // different object }); }); describe('#updateWith', function() { const SRV = new ResourceRecord.SRV({name: 'SRV Record', target: 'box.local.'}); const clone = SRV.clone(); it('should update record and rehash', function() { SRV.updateWith(function(record) { record.target = 'new.local.'; }); expect(SRV.target).to.equal('new.local.'); expect(SRV.equals(clone)).to.be.false; }); }); describe('#canGoodbye', function() { const PTR = new ResourceRecord.PTR({name: 'PTR'}); const reserved = new ResourceRecord.PTR({name: 'db._dns-sd._udp.example.com.'}); it('should return false for reserved record names', function() { expect(PTR.canGoodbye()).to.be.true; expect(reserved.canGoodbye()).to.be.false; }); }); describe('#toString', function() { describe('should look nice and not throw', function() { const files = fs.readdirSync(packetDir); files.forEach((file) => { it(file, function() { const input = getFile(file); const record = ResourceRecord.fromBuffer(input); debug(record.toString()); // dont throw }); }); }); }); });