UNPKG

dnssd

Version:

Bonjour/Avahi-like service discovery in pure JavaScript

875 lines (706 loc) 25.9 kB
'use strict'; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var hash = require('./hash'); var misc = require('./misc'); var BufferWrapper = require('./BufferWrapper'); var filename = require('path').basename(__filename); var debug = require('./debug')('dnssd:' + filename); var RClass = require('./constants').RClass; var RType = require('./constants').RType; var RNums = require('./constants').RNums; /** * Create/parse resource records * @class * * Create a specific ResourceRecord (AAAA): * > const record = new ResourceRecord.AAAA({name: 'Target.local.', address: '::1'}); * * Parse a ResourceRecord from a buffer (a wrapped buffer): * > const record = ResourceRecord.fromBuffer(wrapper); * */ var ResourceRecord = function () { function ResourceRecord(fields) { _classCallCheck(this, ResourceRecord); if (this.constructor === ResourceRecord) throw new Error('Abstract only!'); if (!fields || !fields.name) throw new Error('Record must have a name'); this.name = fields.name; this.rrtype = fields.rrtype || RType[this.constructor.name]; this.rrclass = fields.rrclass || RClass.IN; if ('ttl' in fields) this.ttl = fields.ttl; if ('isUnique' in fields) this.isUnique = fields.isUnique; this.additionals = fields.additionals || []; } /** * Parse a record from a buffer. Starts reading the wrapped buffer at w/e * position its at when fromBuffer is called. * * @param {BufferWrapper} wrapper * @return {ResourceRecord} */ _createClass(ResourceRecord, [{ key: '_makehashes', /** * Makes a couple hashes of record properties so records can get compared * easier. */ value: function _makehashes() { // a hash for name/rrtype/rrclass (records like PTRs might share name/type // but have different rdata) this.namehash = hash(this.name, this.rrtype, this.rrclass); // hash for comparing rdata this.rdatahash = this._hashRData(); // a unique hash for a given name/type/class *AND* rdata this.hash = hash(this.namehash, this.rdatahash); } /** * Writes the record to a wrapped buffer at the wrapper's current position. * @param {BufferWrapper} wrapper */ }, { key: 'writeTo', value: function writeTo(wrapper) { var classField = this.isUnique ? this.rrclass | 0x8000 : this.rrclass; // record info wrapper.writeFQDN(this.name); wrapper.writeUInt16BE(this.rrtype); wrapper.writeUInt16BE(classField); wrapper.writeUInt32BE(this.ttl); // leave UInt16BE gap to write rdataLen var rdataLenPos = wrapper.tell(); wrapper.skip(2); // record specific rdata this._writeRData(wrapper); // go back and add rdata length var rdataLen = wrapper.tell() - rdataLenPos - 2; wrapper.buffer.writeUInt16BE(rdataLen, rdataLenPos); } /** * Checks if this record conflicts with another. Records conflict if they * 1) are both unique (shared record sets can't conflict) * 2) have the same name/type/class * 3) but have different rdata * * @param {ResourceRecord} record * @return {boolean} */ }, { key: 'conflictsWith', value: function conflictsWith(record) { var hasConflict = this.isUnique && record.isUnique && this.namehash === record.namehash && this.rdatahash !== record.rdatahash; if (hasConflict) { debug('Found conflict: \nRecord: %s\nIncoming: %s', this, record); } return hasConflict; } /** * Checks if this record can answer the question. Record names are compared * case insensitively. * * @param {QueryRecord} question * @return {boolean} */ }, { key: 'canAnswer', value: function canAnswer(question) { return (this.rrclass === question.qclass || question.qclass === RClass.ANY) && (this.rrtype === question.qtype || question.qtype === RType.ANY) && this.name.toUpperCase() === question.name.toUpperCase(); } /** * Records are equal if name/type/class and rdata are the same */ }, { key: 'equals', value: function equals(record) { return this.hash === record.hash; } /** * Determines which record is lexicographically later. Used to determine * which probe wins when two competing probes are sent at the same time. * (see https://tools.ietf.org/html/rfc6762#section-8.2) * * means comparing, in order, * - rrclass * - rrtype * - rdata, byte by byte * * Rdata has to be written to a buffer first and then compared. * The cache flush bit has to be excluded as well when comparing * rrclass. * * 1 = this record comes later than the other record * -1 = this record comes earlier than the other record * 0 = records are equal * * @param {ResourceRecord} record * @return {number} */ }, { key: 'compare', value: function compare(record) { if (this.equals(record)) return 0; if (this.rrclass > record.rrclass) return 1; if (this.rrclass < record.rrclass) return -1; if (this.rrtype > record.rrtype) return 1; if (this.rrtype < record.rrtype) return -1; // make buffers out of em so we can compare byte by byte // this also prevents data from being name compressed, since // we are only writing a single rdata, and nothing else var rdata_1 = new BufferWrapper(); var rdata_2 = new BufferWrapper(); this._writeRData(rdata_1); record._writeRData(rdata_2); return rdata_1.unwrap().compare(rdata_2.unwrap()); } /** * Test if a record matches some properties. String values are compared * case insensitively. * * Ex: * > const isMatch = record.matches({name: 'test.', priority: 12}) * * @param {object} properties * @return {boolean} */ }, { key: 'matches', value: function matches(properties) { var _this = this; return Object.keys(properties).map(function (key) { return [key, properties[key]]; }).every(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), key = _ref2[0], value = _ref2[1]; return typeof _this[key] === 'string' && typeof value === 'string' ? _this[key].toUpperCase() === value.toUpperCase() : misc.equals(_this[key], value); }); } /** * Returns a clone of the record, making a new object */ }, { key: 'clone', value: function clone() { var type = this.constructor.name; var fields = this; return new ResourceRecord[type](fields); } /** * If anything changes on a record it needs to be re-hashed. Otherwise * all the comparisons won't work with the new changes. * * Bad: record.target = 'new.local.'; * Good: record.updateWith(() => {record.target = 'new.local.'}); * */ }, { key: 'updateWith', value: function updateWith(fn) { // give record to updater function to modify fn(this); // rehash in case name/rdata changed this._makehashes(); } /** * Records with reserved names shouldn't be goodbye'd * * _services._dns-sd._udp.<domain>. * b._dns-sd._udp.<domain>. * db._dns-sd._udp.<domain>. * r._dns-sd._udp.<domain>. * dr._dns-sd._udp.<domain>. * lb._dns-sd._udp.<domain>. */ }, { key: 'canGoodbye', value: function canGoodbye() { var name = this.name.toLowerCase(); return name.indexOf('._dns-sd._udp.') === -1; } /** * Breaks up the record into an array of parts. Used in misc.alignRecords * so stuff can get printed nicely in columns. Only ever used in debugging. */ }, { key: 'toParts', value: function toParts() { var parts = []; var type = this.constructor.name === 'Unknown' ? this.rrtype : this.constructor.name; var ttl = this.ttl === 0 ? misc.color(this.ttl, 'red') : String(this.ttl); parts.push(this.name); parts.push(this.ttl === 0 ? misc.color(type, 'red') : misc.color(type, 'blue')); parts.push(ttl); parts.push(String(this._getRDataStr())); if (this.isUnique) parts.push(misc.color('(flush)', 'grey')); return parts; } }, { key: 'toString', value: function toString() { return this.toParts().join(' '); } }], [{ key: 'fromBuffer', value: function fromBuffer(wrapper) { var name = wrapper.readFQDN(); var rrtype = wrapper.readUInt16BE(); var rrclass = wrapper.readUInt16BE(); var ttl = wrapper.readUInt32BE(); // top-bit in rrclass is reused as the cache-flush bit var fields = { name: name, rrtype: rrtype, rrclass: rrclass & ~0x8000, isUnique: !!(rrclass & 0x8000), ttl: ttl }; if (rrtype === RType.A) return new ResourceRecord.A(fields, wrapper); if (rrtype === RType.PTR) return new ResourceRecord.PTR(fields, wrapper); if (rrtype === RType.TXT) return new ResourceRecord.TXT(fields, wrapper); if (rrtype === RType.AAAA) return new ResourceRecord.AAAA(fields, wrapper); if (rrtype === RType.SRV) return new ResourceRecord.SRV(fields, wrapper); if (rrtype === RType.NSEC) return new ResourceRecord.NSEC(fields, wrapper); return new ResourceRecord.Unknown(fields, wrapper); } }]); return ResourceRecord; }(); /** * A record (IPv4 address) */ var A = function (_ResourceRecord) { _inherits(A, _ResourceRecord); /** * @param {object} fields * @param {BufferWrapper} [wrapper] - only used by the .fromBuffer method */ function A(fields, wrapper) { _classCallCheck(this, A); // defaults: var _this2 = _possibleConstructorReturn(this, (A.__proto__ || Object.getPrototypeOf(A)).call(this, fields)); misc.defaults(_this2, { ttl: 120, isUnique: true }); // rdata: _this2.address = fields.address || ''; if (wrapper) _this2._readRData(wrapper); _this2._makehashes(); return _this2; } _createClass(A, [{ key: '_readRData', value: function _readRData(wrapper) { var _len = wrapper.readUInt16BE(); var n1 = wrapper.readUInt8(); var n2 = wrapper.readUInt8(); var n3 = wrapper.readUInt8(); var n4 = wrapper.readUInt8(); this.address = n1 + '.' + n2 + '.' + n3 + '.' + n4; } }, { key: '_writeRData', value: function _writeRData(wrapper) { this.address.split('.').forEach(function (str) { var n = parseInt(str, 10); wrapper.writeUInt8(n); }); } }, { key: '_hashRData', value: function _hashRData() { return hash(this.address); } }, { key: '_getRDataStr', value: function _getRDataStr() { return this.address; } }]); return A; }(ResourceRecord); ResourceRecord.A = A; /** * PTR record */ var PTR = function (_ResourceRecord2) { _inherits(PTR, _ResourceRecord2); function PTR(fields, wrapper) { _classCallCheck(this, PTR); // defaults: var _this3 = _possibleConstructorReturn(this, (PTR.__proto__ || Object.getPrototypeOf(PTR)).call(this, fields)); misc.defaults(_this3, { ttl: 4500, isUnique: false }); // rdata: _this3.PTRDName = fields.PTRDName || ''; if (wrapper) _this3._readRData(wrapper); _this3._makehashes(); return _this3; } _createClass(PTR, [{ key: '_readRData', value: function _readRData(wrapper) { var _len = wrapper.readUInt16BE(); this.PTRDName = wrapper.readFQDN(); } }, { key: '_writeRData', value: function _writeRData(wrapper) { wrapper.writeFQDN(this.PTRDName); } }, { key: '_hashRData', value: function _hashRData() { return hash(this.PTRDName); } }, { key: '_getRDataStr', value: function _getRDataStr() { return this.PTRDName; } }]); return PTR; }(ResourceRecord); ResourceRecord.PTR = PTR; /** * TXT record * * key/value conventions: * - Key present with value * 'key=value' -> {key: value} * * - Key present, _empty_ value: * 'key=' -> {key: null} * * - Key present, but no value: * 'key' -> {key: true} * * Important note: keys are case insensitive */ var TXT = function (_ResourceRecord3) { _inherits(TXT, _ResourceRecord3); function TXT(fields, wrapper) { _classCallCheck(this, TXT); // defaults: var _this4 = _possibleConstructorReturn(this, (TXT.__proto__ || Object.getPrototypeOf(TXT)).call(this, fields)); misc.defaults(_this4, { ttl: 4500, isUnique: true }); // rdata: _this4.txtRaw = misc.makeRawTXT(fields.txt || {}); _this4.txt = misc.makeReadableTXT(fields.txt || {}); if (wrapper) _this4._readRData(wrapper); _this4._makehashes(); return _this4; } _createClass(TXT, [{ key: '_readRData', value: function _readRData(wrapper) { var rdataLength = wrapper.readUInt16BE(); var end = wrapper.tell() + rdataLength; var len = void 0; // read each key: value pair while (wrapper.tell() < end && (len = wrapper.readUInt8())) { var key = ''; var chr = void 0, value = void 0; while (len-- > 0 && (chr = wrapper.readString(1)) !== '=') { key += chr; } if (len > 0) value = wrapper.read(len);else if (chr === '=') value = null;else value = true; this.txtRaw[key] = value; this.txt[key] = Buffer.isBuffer(value) ? value.toString() : value; } } }, { key: '_writeRData', value: function _writeRData(wrapper) { var _this5 = this; // need to at least put a 0 byte if no txt data if (!Object.keys(this.txtRaw).length) { return wrapper.writeUInt8(0); } // value is either true, null, or a buffer Object.keys(this.txtRaw).forEach(function (key) { var value = _this5.txtRaw[key]; var str = value === true ? key : key + '='; var len = Buffer.byteLength(str); if (Buffer.isBuffer(value)) len += value.length; wrapper.writeUInt8(len); wrapper.writeString(str); if (Buffer.isBuffer(value)) wrapper.add(value); }); } }, { key: '_hashRData', value: function _hashRData() { return hash(this.txtRaw); } }, { key: '_getRDataStr', value: function _getRDataStr() { return misc.truncate(JSON.stringify(this.txt), 30); } }]); return TXT; }(ResourceRecord); ResourceRecord.TXT = TXT; /** * AAAA record (IPv6 address) */ var AAAA = function (_ResourceRecord4) { _inherits(AAAA, _ResourceRecord4); function AAAA(fields, wrapper) { _classCallCheck(this, AAAA); // defaults: var _this6 = _possibleConstructorReturn(this, (AAAA.__proto__ || Object.getPrototypeOf(AAAA)).call(this, fields)); misc.defaults(_this6, { ttl: 120, isUnique: true }); // rdata: _this6.address = fields.address || ''; if (wrapper) _this6._readRData(wrapper); _this6._makehashes(); return _this6; } _createClass(AAAA, [{ key: '_readRData', value: function _readRData(wrapper) { var _len = wrapper.readUInt16BE(); var raw = wrapper.read(16); var parts = []; for (var i = 0; i < raw.length; i += 2) { parts.push(raw.readUInt16BE(i).toString(16)); } this.address = parts.join(':').replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3').replace(/:{3,4}/, '::'); } }, { key: '_writeRData', value: function _writeRData(wrapper) { function expandIPv6(str) { var ip = str; // replace ipv4 address if any var ipv4_match = ip.match(/(.*:)([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$)/); if (ipv4_match) { ip = ipv4_match[1]; var ipv4 = ipv4_match[2].match(/[0-9]+/g); for (var i = 0; i < 4; i++) { ipv4[i] = parseInt(ipv4[i], 10).toString(16); } ip += ipv4[0] + ipv4[1] + ':' + ipv4[2] + ipv4[3]; } // take care of leading and trailing :: ip = ip.replace(/^:|:$/g, ''); var ipv6 = ip.split(':'); for (var _i2 = 0; _i2 < ipv6.length; _i2++) { // normalize grouped zeros :: if (ipv6[_i2] === '') { ipv6[_i2] = new Array(9 - ipv6.length).fill(0).join(':'); } } return ipv6.join(':'); } expandIPv6(this.address).split(':').forEach(function (str) { var u16 = parseInt(str, 16); wrapper.writeUInt16BE(u16); }); } }, { key: '_hashRData', value: function _hashRData() { return hash(this.address); } }, { key: '_getRDataStr', value: function _getRDataStr() { return this.address; } }]); return AAAA; }(ResourceRecord); ResourceRecord.AAAA = AAAA; /** * SRV record */ var SRV = function (_ResourceRecord5) { _inherits(SRV, _ResourceRecord5); function SRV(fields, wrapper) { _classCallCheck(this, SRV); // defaults: var _this7 = _possibleConstructorReturn(this, (SRV.__proto__ || Object.getPrototypeOf(SRV)).call(this, fields)); misc.defaults(_this7, { ttl: 120, isUnique: true }); // rdata: _this7.target = fields.target || ''; _this7.port = fields.port || 0; _this7.priority = fields.priority || 0; _this7.weight = fields.weight || 0; if (wrapper) _this7._readRData(wrapper); _this7._makehashes(); return _this7; } _createClass(SRV, [{ key: '_readRData', value: function _readRData(wrapper) { var _len = wrapper.readUInt16BE(); this.priority = wrapper.readUInt16BE(); this.weight = wrapper.readUInt16BE(); this.port = wrapper.readUInt16BE(); this.target = wrapper.readFQDN(); } }, { key: '_writeRData', value: function _writeRData(wrapper) { wrapper.writeUInt16BE(this.priority); wrapper.writeUInt16BE(this.weight); wrapper.writeUInt16BE(this.port); wrapper.writeFQDN(this.target); } }, { key: '_hashRData', value: function _hashRData() { return hash(this.priority, this.weight, this.port, this.target); } }, { key: '_getRDataStr', value: function _getRDataStr() { return this.target + ' ' + this.port + ' P:' + this.priority + ' W:' + this.weight; } }]); return SRV; }(ResourceRecord); ResourceRecord.SRV = SRV; /** * NSEC record * Only handles the limited 'restricted' form (record rrtypes < 255) */ var NSEC = function (_ResourceRecord6) { _inherits(NSEC, _ResourceRecord6); function NSEC(fields, wrapper) { _classCallCheck(this, NSEC); // defaults: var _this8 = _possibleConstructorReturn(this, (NSEC.__proto__ || Object.getPrototypeOf(NSEC)).call(this, fields)); misc.defaults(_this8, { ttl: 120, isUnique: true }); // rdata: _this8.existing = (fields.existing || []).sort(function (a, b) { return a - b; }); if (wrapper) _this8._readRData(wrapper); _this8._makehashes(); return _this8; } _createClass(NSEC, [{ key: '_readRData', value: function _readRData(wrapper) { var rdataLength = wrapper.readUInt16BE(); var rdataEnd = wrapper.tell() + rdataLength; var _name = wrapper.readFQDN(); // doesn't matter, ignored var block = wrapper.readUInt8(); // window block for rrtype bitfield var len = wrapper.readUInt8(); // number of octets in bitfield // Ignore rrtypes over 255 (only implementing the restricted form) // Bitfield length must always be < 32, otherwise skip parsing if (block !== 0 || len > 32) return wrapper.seek(rdataEnd); // NSEC rrtype bitfields can be up to 256 bits (32 bytes), BUT // - js bitwise operators are only do 32 bits // - node's buffer.readIntBE() can only read up to 6 bytes // // So here we're doing 1 byte of the field at a time // for (var maskNum = 0; maskNum < len; maskNum++) { var mask = wrapper.readUInt8(1); if (mask === 0) continue; for (var bit = 0; bit < 8; bit++) { if (mask & 1 << bit) { // rrtypes in bitfields are in network bit order // 01000000 => 1 === RType.A (bit 6) // 00000000 00000000 00000000 00001000 => 28 === RType.AAAA (bit 3) var rrtype = 8 * maskNum + (7 - bit); this.existing.push(rrtype); } } } } }, { key: '_writeRData', value: function _writeRData(wrapper) { // restricted form, only rrtypes up to 255 var rrtypes = [].concat(_toConsumableArray(new Set(this.existing))).filter(function (x) { return x <= 255; }); // Same problems as _readRData, 32 bit operators and can't write big ints, // so bitfields are broken up into 1 byte segments and handled one at a time var len = !rrtypes.length ? 0 : Math.ceil(Math.max.apply(Math, _toConsumableArray(rrtypes)) / 8); var masks = Array(len).fill(0); rrtypes.forEach(function (rrtype) { var index = ~~(rrtype / 8); // which mask this rrtype is on var bit = 7 - rrtype % 8; // convert to network bit order masks[index] |= 1 << bit; }); wrapper.writeFQDN(this.name); // "next domain name", ignored for mdns wrapper.writeUInt8(0); // block number, always 0 for restricted form wrapper.writeUInt8(len); // bitfield length in octets // write masks byte by byte since node buffers can only write 42 bit numbers masks.forEach(function (mask) { return wrapper.writeUInt8(mask); }); } }, { key: '_hashRData', value: function _hashRData() { return hash(this.existing); } }, { key: '_getRDataStr', value: function _getRDataStr() { return this.existing.map(function (rrtype) { return RNums[rrtype] || rrtype; }).join(', '); } }]); return NSEC; }(ResourceRecord); ResourceRecord.NSEC = NSEC; /** * Unknown record, anything not describe above. Could be OPT records, etc. */ var Unknown = function (_ResourceRecord7) { _inherits(Unknown, _ResourceRecord7); function Unknown(fields, wrapper) { _classCallCheck(this, Unknown); // defaults: var _this9 = _possibleConstructorReturn(this, (Unknown.__proto__ || Object.getPrototypeOf(Unknown)).call(this, fields)); misc.defaults(_this9, { ttl: 120, isUnique: true }); // rdata: _this9.rdata = fields.rdata || Buffer.alloc(0); if (wrapper) _this9._readRData(wrapper); _this9._makehashes(); return _this9; } _createClass(Unknown, [{ key: '_readRData', value: function _readRData(wrapper) { var rdataLength = wrapper.readUInt16BE(); this.RData = wrapper.read(rdataLength); } }, { key: '_writeRData', value: function _writeRData(wrapper) { wrapper.add(this.RData); } }, { key: '_hashRData', value: function _hashRData() { return hash(this.RData); } }, { key: '_getRDataStr', value: function _getRDataStr() { // replace non-ascii characters w/ gray dots function ascii(chr) { return (/[ -~]/.test(chr) ? chr : misc.color('.', 'grey') ); } var chars = this.RData.toString().split(''); var str = chars.slice(0, 30).map(ascii).join(''); return chars.length <= 30 ? str : str + '…'; } }]); return Unknown; }(ResourceRecord); ResourceRecord.Unknown = Unknown; module.exports = ResourceRecord;