From 8f7deb68538823bf3a43438ee9f2da1e3b757c45 Mon Sep 17 00:00:00 2001 From: Peter Magnusson Date: Thu, 31 Jul 2014 14:34:13 +0200 Subject: [PATCH] Adding record types etc. Closes #4 Some work towards #5 --- .jshintrc | 4 +- README.md | 1 + examples/simple.js | 45 ++++---- index.js | 17 ++- lib/browser.js | 234 ++++++++++++++++++++++++++++++++++++++++++ lib/bufferconsumer.js | 6 +- lib/dnspacket.js | 64 +++++++++--- lib/dnsrecord.js | 35 +++++-- lib/index.js | 200 ------------------------------------ lib/service_type.js | 7 +- test/dns.test.js | 63 +++++++++++- test/mdns.test.js | 2 +- test/packets.json | 2 + 13 files changed, 427 insertions(+), 253 deletions(-) create mode 100644 lib/browser.js delete mode 100644 lib/index.js diff --git a/.jshintrc b/.jshintrc index 78fb22e..d810a14 100644 --- a/.jshintrc +++ b/.jshintrc @@ -4,5 +4,7 @@ "unused": true, "quotmark": true, "undef": true, - "laxcomma": true + "laxcomma": true, + "curly": true, + "eqeqeq": true } \ No newline at end of file diff --git a/README.md b/README.md index 2476158..73ca3bb 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,4 @@ References * http://en.wikipedia.org/wiki/Zero_configuration_networking#Service_discovery * RFC 6762 - mDNS - http://tools.ietf.org/html/rfc6762 * RFC 6763 - DNS Based Service Discovery - http://tools.ietf.org/html/rfc6763 +* http://www.tcpipguide.com/free/t_DNSMessageHeaderandQuestionSectionFormat.htm diff --git a/examples/simple.js b/examples/simple.js index f0ca81d..ca476c0 100644 --- a/examples/simple.js +++ b/examples/simple.js @@ -1,21 +1,24 @@ -var mdns = require('../'); - - -// var browser = new mdns.Mdns(mdns.tcp("googlecast")); -// console.log(mdns.ServiceType.wildcard); -var browser = new mdns.Mdns(mdns.ServiceType.wildcard); - -browser.on('ready', function () { - console.log('browser is ready') - browser.discover(); -}); - - -browser.on('update', function (data) { - var device = { - address: data.addresses[0], - name: data.name, - servicename: data.type - } - console.log('device:', data); -}); \ No newline at end of file +var mdns = require('../'); + + + + +var browser = new mdns.createBrowser(); //defaults to mdns.ServiceType.wildcard +//var browser = new mdns.Mdns(mdns.tcp("googlecast")); +//var browser = mdns.createBrowser(mdns.tcp("workstation")); + +browser.on('ready', function () { + console.log('browser is ready'); + browser.discover(); +}); + + +browser.on('update', function (data) { + + console.log('data:', data); +}); + +//stop after 60 seconds +setTimeout(function () { + browser.stop(); +}, 6000); \ No newline at end of file diff --git a/index.js b/index.js index 15179a5..717778b 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,26 @@ var config = require('./package.json'); var st = require('./lib/service_type'); +var Browser = module.exports.Browser = require('./lib/browser'); //just for convenience -module.exports.Mdns = require('./lib'); module.exports.version = config.version; module.exports.name = config.name; +/** + * Create a browser instance + * @param {string} [serviceType] - The Service type to browse for. Defaults to ServiceType.wildcard + * @return {Browser} + */ +module.exports.createBrowser = function (serviceType) { + if (typeof serviceType === 'undefined') { + serviceType = st.ServiceType.wildcard + } + return new Browser(serviceType); +} + + + + module.exports.ServiceType = st.ServiceType; module.exports.makeServiceType = st.makeServiceType; module.exports.tcp = st.protocolHelper('tcp'); diff --git a/lib/browser.js b/lib/browser.js new file mode 100644 index 0000000..a09b9d4 --- /dev/null +++ b/lib/browser.js @@ -0,0 +1,234 @@ +var debug = require('debug')('mdns:browser'); +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var dgram = require('dgram'); +var os = require('os'); + +var DNSPacket = require('./dnspacket'); +var DNSRecord = require('./dnsrecord'); +var sorter = require('./sorter'); + +var ServiceType = require('./service_type').ServiceType; + +var internal = {}; + + +internal.broadcast = function(sock, serviceType) { + debug('broadcasting to', sock.address()); + var packet = new DNSPacket(); + packet.push('qd', new DNSRecord(serviceType.toString() + '.local', DNSRecord.Type.PTR, 1)); + var buf = packet.toBuffer(); + debug('created buffer with length', buf.length); + sock.send(buf, 0, buf.length, 5353, '224.0.0.251', function (err, bytes) { + debug('%s sent %d bytes with err:%s', sock.address().address, bytes, err); + }); +}; + +/** + * Handles incoming UDP traffic. + * @private + */ +internal.onMessage = function (message, remote, connection) { + debug('got packet from remote', remote); + var packet = DNSPacket.parse(message); + + var data = { + interfaceIndex: connection.interfaceIndex, + networkInterface: connection.networkInterface, + addresses: [remote.address] + }; + + packet.each('qd', DNSRecord.Type.PTR, function (rec) { + data.query = rec.name; + }.bind(this)); + + ['an', 'ns', 'ar'].forEach(function(section) { + packet.each(section, DNSRecord.Type.PTR, function (rec) { + var name = rec.asName(); + if(!data.hasOwnProperty('type')) { + data.type = []; + } + try { + var type = new ServiceType(name); + data.type.push({ + name: type.name, + protocol: type.protocol, + subtypes: type.subtypes + }); + } catch (e) { + debug('error', e); + data.type.push({name: name}); + } + }); + + packet.each(section, DNSRecord.Type.SRV, function(rec) { + var srv = rec.asSrv(); + data.port = srv.port; + data.host = srv.target + '.local'; + }); + + packet.each(section, DNSRecord.Type.TXT, function(rec) { + data.txt = rec.asTxt(); + }); + + packet.each(section, DNSRecord.Type.A, function(rec) { + var value = rec.asA(); + if(data.addresses.indexOf(value) < 0) { + data.addresses.push(value); + } + }); + sorter.sortIps(data.addresses); + + packet.each(section, DNSRecord.Type.AAAA, function(rec) { + var value = rec.asAAAA(); + if(data.addresses.indexOf(value) < 0) { + data.addresses.push(value); + } + }); + + });//-forEach section + + this.emit('update', data); +}; + +/** + * mDNS Browser class + * @class + * @param {string|ServiceType} serviceType - The service type to browse for. + */ +var Browser = module.exports = function (serviceType) { + if(!(this instanceof Browser)) { return new Browser(serviceType); } + if (typeof serviceType !== 'string' && !(serviceType instanceof ServiceType)) { + debug('serviceType type:', typeof serviceType); + debug('serviceType is ServiceType:', serviceType instanceof ServiceType); + debug('serviceType=', serviceType); + throw new Error('argument must be instance of ServiceType or valid string'); + } + this.serviceType = serviceType; + var self = this; + this._all = new EventEmitter(); + + var connections = []; + var created = 0; + process.nextTick(function () { + var interfaces = os.networkInterfaces(); + var index = 0; + for(var key in interfaces) { + if(interfaces.hasOwnProperty(key)) { + for(var i = 0; i < interfaces[key].length; i++) { + var address = interfaces[key][i].address; + debug('interface', key, interfaces[key]); + //no IPv6 addresses + if (address.indexOf(':') !== -1) { + continue; + } + createSocket(index++, key, address, bindToAddress.bind(self)); + } + } + } + }.bind(this)); + + + function createSocket (interfaceIndex, networkInterface, address, callback) { + var sock = dgram.createSocket('udp4'); + debug('creating socket for interface %s', address); + created++; + sock.bind(0, address, function (err) { + callback(err, interfaceIndex, networkInterface, sock); + }); + } + + + function bindToAddress (err, interfaceIndex, networkInterface, sock) { + if(err) { + debug('there was an error binding %s', err); + return; + } + debug('bindToAddress'); + var info = sock.address(); + var connection = {socket:sock, hasTraffic: false, + interfaceIndex: interfaceIndex, + networkInterface: networkInterface}; + + connections.push(connection); + + sock.on('message', function(){ + connection.hasTraffic = true; + [].push.call(arguments, connection); + internal.onMessage.apply(this, arguments); + }.bind(this)); + + sock.on('error', _onError); + sock.on('close', function () { + debug('socket closed', info); + }); + + self._all.on('broadcast', function () { + internal.broadcast(sock, serviceType); + }.bind(this)); + + if(created === connections.length) { + this.emit('ready', connections.length); + } + }//--bindToAddress + + + function _onError (err) { + debug('socket error', err); + self.emit('error', err); + } + + + this.stop = function () { + debug('stopping'); + for(var i=0; i < connections.length; i++) { + var socket = connections[i].socket; + socket.close(); + socket.unref(); + } + connections = []; + };//--start + + + + this.closeUnused = function () { + var i; + debug('closing sockets without traffic'); + var closed = []; + for(i=0; i < connections.length; i++) { + var connection = connections[i]; + if (!connection.hasTraffic) { + connection.socket.close(); + connection.socket.unref(); + closed.push(connection); + } + } + for(i=0; i). */ BufferConsumer.prototype.name = function(join) { - if (typeof join === 'undefined') join = true; + if (typeof join === 'undefined') { join = true; } var parts = []; var len; this.ref = undefined; @@ -51,11 +51,11 @@ BufferConsumer.prototype.name = function(join) { try { len = this.byte(); } catch (e) { - if (e instanceof RangeError) break; + if (e instanceof RangeError) { break; } } if (!len) { break; - } else if (len == 0xc0) { + } else if (len === 0xc0) { // TODO: This indicates a pointer to another valid name inside the // DNSPacket, and is always a suffix: we're at the end of the name. // We should probably hold onto this value instead of discarding it. diff --git a/lib/dnspacket.js b/lib/dnspacket.js index b271af5..a520bca 100644 --- a/lib/dnspacket.js +++ b/lib/dnspacket.js @@ -3,6 +3,17 @@ var BufferWriter = require('./bufferwriter'); var DataConsumer = require('./bufferconsumer'); var DNSRecord = require('./dnsrecord'); +/** + * This callback is used for "each" methods + * @callback DNSPacket~eachCallback + * @param {DNSRecord} rec - DNSRecord that was found + */ + +var FLAG_RESPONSE=0x8000; +var FLAG_AUTHORATIVE=0x400; +var FLAG_TRUNCATED=0x200; +var FLAG_RECURSION=0x100; + /** * DNSPacket holds the state of a DNS packet. It can be modified or serialized * in-place. @@ -12,23 +23,46 @@ var DNSRecord = require('./dnsrecord'); var DNSPacket = module.exports = function(opt_flags) { this.flags_ = opt_flags || 0; /* uint16 */ this.data_ = {'qd': [], 'an': [], 'ns': [], 'ar': []}; + + debug('Response', (opt_flags & FLAG_RESPONSE) === FLAG_RESPONSE); + debug('Authorative', (opt_flags & FLAG_AUTHORATIVE) === FLAG_AUTHORATIVE); + debug('Truncated', (opt_flags & FLAG_TRUNCATED) === FLAG_TRUNCATED); + debug('Recursion', (opt_flags & FLAG_RECURSION) === FLAG_RECURSION); + }; + +/** + * Enum identifying DNSPacket sections + * @readonly + * @enum {string} + */ +DNSPacket.Section = { + QUESTION: 'qd', + ANSWER: 'an', + AUTHORITY: 'ns', + ADDITIONAL: 'ar' +}; + + + /** * Parse a DNSPacket from an Buffer + * @param {Buffer} buffer - A Node.js Buffer instance + * @returns {DNSPacket} Instance of DNSPacket */ DNSPacket.parse = function(buffer) { var consumer = new DataConsumer(buffer); - if (consumer.short()) { + if (consumer.short()) { //transaction id throw new Error('DNS packet must start with 00 00'); } var flags = consumer.short(); var count = { - 'qd': consumer.short(), - 'an': consumer.short(), - 'ns': consumer.short(), - 'ar': consumer.short(), + 'qd': consumer.short(), //Question Count + 'an': consumer.short(), //Answer Record Count + 'ns': consumer.short(), //Authority Record Count + 'ar': consumer.short(), //Additional record count }; debug('parsing flags: 0x%s, count: %j', flags.toString('16'), count); @@ -40,7 +74,7 @@ DNSPacket.parse = function(buffer) { consumer.name(), consumer.short(), // type consumer.short()); // class - packet.push('qd', part); + packet.push(DNSPacket.Section.QUESTION, part); debug('new qd dnsrecord: %j', part); } @@ -59,8 +93,8 @@ DNSPacket.parse = function(buffer) { } }); - debug("qd: %d/%d, an: %d/%d", count.qd, packet.data_.qd.length, - count.an, packet.data_.an.length) + debug('qd: %d/%d, an: %d/%d', count.qd, packet.data_.qd.length, + count.an, packet.data_.an.length); consumer.isEOF() || console.warn('was not EOF on incoming packet'); return packet; @@ -70,18 +104,23 @@ DNSPacket.prototype.push = function(section, record) { this.data_[section].push(record); }; +/** + * Get records from packet + * @param {DNSPacket.Section} section - record section [qd|an|ns|ar], + * @param {DNSRecord.Type} [filter] - DNSRecord.Type to filter on + * @param {DNSPacket~eachCallback} callback - Function callback + */ DNSPacket.prototype.each = function(section /*[filter] callback*/) { var filter = false; var cb; - if (arguments.length == 2) { + if (arguments.length === 2) { cb = arguments[1]; } else { filter = arguments[1]; cb = arguments[2]; } this.data_[section].forEach(function(rec) { - if (!filter || rec.type == filter) { - debug('found record for %s, %s', section, filter); + if (!filter || rec.type === filter) { cb(rec); } }); @@ -90,6 +129,7 @@ DNSPacket.prototype.each = function(section /*[filter] callback*/) { /** * Serialize this DNSPacket into an Buffer for sending over UDP. + * @returns {Buffer} A Node.js Buffer */ DNSPacket.prototype.toBuffer = function() { var out = new BufferWriter(); @@ -105,7 +145,7 @@ DNSPacket.prototype.toBuffer = function() { this.data_[section].forEach(function (rec) { out.name(rec.name).short(rec.type).short(rec.cl); - if (section != 'qd') { + if (section !== 'qd') { throw new Error('can\'t yet serialize non-QD records'); } }); diff --git a/lib/dnsrecord.js b/lib/dnsrecord.js index 4f82204..bd23f75 100644 --- a/lib/dnsrecord.js +++ b/lib/dnsrecord.js @@ -3,24 +3,30 @@ var DataConsumer = require('./bufferconsumer'); * DNSRecord is a record inside a DNS packet; e.g. a QUESTION, or an ANSWER, * AUTHORITY, or ADDITIONAL record. Note that QUESTION records are special, * and do not have ttl or data. + * @class + * @param {string} name + * @param {number} type + * @param {number} cl - class + * @param {number} [opt_ttl] - time to live in seconds + * @param {Buffer} [opt_data] - additional data ad Node.js Buffer. */ var DNSRecord = module.exports = function(name, type, cl, opt_ttl, opt_data) { this.name = name; this.type = type; this.cl = cl; - this.isQD = (arguments.length == 3); + this.isQD = (arguments.length === 3); if (!this.isQD) { this.ttl = opt_ttl; this.data_ = opt_data; } }; -DNSRecord.prototype.asName = function() { - return new DataConsumer(this.data_).name(); -}; - - +/** + * Enum for record type values + * @readonly + * @enum {number} + */ DNSRecord.Type = { A: 0x01, // 1 PTR: 0x0c, // 12 @@ -30,6 +36,11 @@ DNSRecord.Type = { }; +DNSRecord.prototype.asName = function() { + return new DataConsumer(this.data_).name(); +}; + + DNSRecord.prototype.asSrv = function() { var consumer = new DataConsumer(this.data_); return { @@ -40,6 +51,7 @@ DNSRecord.prototype.asSrv = function() { }; }; + DNSRecord.prototype.asTxt = function() { var consumer = new DataConsumer(this.data_); var data = {}; @@ -51,15 +63,22 @@ DNSRecord.prototype.asTxt = function() { return data; }; + DNSRecord.prototype.asA = function() { var consumer = new DataConsumer(this.data_); var data = ''; - for(var i = 0; i < 3; i++) + for(var i = 0; i < 3; i++) { data += consumer.byte() + '.'; + } data += consumer.byte(); return data; }; + +/* + * Parse data into a IPV6 address string + * @returns {string} + */ DNSRecord.prototype.asAAAA = function () { var consumer = new DataConsumer(this.data_); var data = ''; @@ -68,5 +87,5 @@ DNSRecord.prototype.asAAAA = function () { } data += consumer.short().toString(16); return data; -} +}; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 986932b..0000000 --- a/lib/index.js +++ /dev/null @@ -1,200 +0,0 @@ -var debug = require('debug')('mdns:mdns'); -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var dgram = require('dgram'); -var os = require('os'); - -var DNSPacket = require('./dnspacket'); -var DNSRecord = require('./dnsrecord'); -var sorter = require('./sorter'); - -var ServiceType = require('./service_type').ServiceType; - -var internal = {}; - - -internal.broadcast = function(sock, serviceType) { - debug('broadcasting to', sock.address()); - var packet = new DNSPacket(); - - packet.push('qd', new DNSRecord(serviceType.toString() + '.local', DNSRecord.Type.PTR, 1)); - var buf = packet.toBuffer(); - debug('created buffer with length', buf.length); - sock.send(buf, 0, buf.length, 5353, '224.0.0.251', function (err, bytes) { - debug('%s sent %d bytes with err:%s', sock.address().address, bytes, err); - }); -}; - -/** - * Handles incoming UDP traffic. - * @private - */ -internal.onMessage = function (message, remote) { - - debug('got packet from remote', remote); - var packet = DNSPacket.parse(message); - - var data = {}; - data.remote = remote; - packet.each('qd', DNSRecord.Type.PTR, function (rec) { - data.query = rec.name; - }.bind(this)); - - packet.each('an', DNSRecord.Type.PTR, function (rec) { - var name = rec.asName(); - try { - var type = new ServiceType(name); - data.type = { - name: type.name, - protocol: type.protocol, - subtypes: type.subtypes - }; - } catch (e) { - data.name = name; - } - }.bind(this)); - - packet.each('ar', DNSRecord.Type.SRV, function(rec) { - var srv = rec.asSrv(); - data.port = srv.port; - data.host = srv.target + '.local'; - }); - - packet.each('ar', DNSRecord.Type.TXT, function(rec) { - data.txt = rec.asTxt(); - }); - - data.addresses = []; - packet.each('ar', DNSRecord.Type.A, function(rec) { - data.addresses.push(rec.asA()); - }); - sorter.sortIps(data.addresses); - - this.emit('update', data); -}; - - -var Mdns = module.exports = function (serviceType) { - if(!(this instanceof Mdns)) return new Mdns(serviceType); - var self = this; - this.serviceType = serviceType; - this._all = new EventEmitter(); - - var connections = []; - var created = 0; - process.nextTick(function () { - var interfaces = os.networkInterfaces(); - for(var key in interfaces) { - if(interfaces.hasOwnProperty(key)) { - for(var i = 0; i < interfaces[key].length; i++) { - var address = interfaces[key][i].address; - //no IPv6 addresses - if (address.indexOf(':') != -1) { - continue; - } - createSocket(address, bindToAddress.bind(self)); - } - } - } - }.bind(this)); - - function createSocket (address, callback) { - var sock = dgram.createSocket('udp4'); - debug('creating socket for interface %s', address); - created++; - sock.bind(0, address, function (err) { - callback(err, sock); - }); - } - - function bindToAddress (err, sock) { - if(err) { - debug('there was an error binding %s', err); - return; - } - debug('bindToAddress'); - var info = sock.address(); - var connection = {socket:sock, hasTraffic: false}; - connections.push(connection); - - sock.on('message', function(){ - connection.hasTraffic = true; - [].push.call(arguments, connection); - internal.onMessage.apply(this, arguments); - }.bind(this)); - - sock.on('error', _onError); - sock.on('close', function () { - debug('socket closed', info); - }); - - self._all.on('broadcast', function () { - internal.broadcast(sock, serviceType); - }.bind(this)); - - if(created == connections.length) { - this.emit('ready', connections.length); - } - }//--bindToAddress - - - function _onError (err) { - debug('socket error', err); - self.emit('error', err); - } - - - - this.shutdown = function () { - debug('shutting down'); - for(var i=0; i < connections.length; i++) { - var socket = connections[i].socket; - socket.close(); - socket.unref(); - } - connections = []; - };//--shutdown - - - - this.closeUnused = function () { - var i; - debug('closing sockets without traffic'); - var closed = []; - for(i=0; i < connections.length; i++) { - var connection = connections[i]; - if (!connection.hasTraffic) { - connection.socket.close(); - connection.socket.unref(); - closed.push(connection); - } - } - for(i=0; i