UNPKG

dnssd

Version:

Bonjour/Avahi-like service discovery in pure JavaScript

356 lines (305 loc) 13.1 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 _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var os = require('os'); var util = require('util'); var misc = require('./misc'); var QueryRecord = require('./QueryRecord'); var ResourceRecord = require('./ResourceRecord'); var BufferWrapper = require('./BufferWrapper'); var RecordCollection = require('./RecordCollection'); var filename = require('path').basename(__filename); var debug = require('./debug')('dnssd:' + filename); /** * mDNS Packet * @class * * Make new empty packets with `new Packet()` * or parse a packet from a buffer with `new Packet(buffer)` * * Check if there were problems parsing a buffer by checking `packet.isValid()` * isValid() will return false if buffer parsing failed or if something is wrong * with the packet's header. * */ var Packet = function () { /** * @param {Buffer} [buffer] - optional buffer to parse * @param {Object} [origin] - optional msg info */ function Packet(buffer) { var origin = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, Packet); this.header = { ID: 0, QR: 0, OPCODE: 0, AA: 0, TC: 0, RD: 0, RA: 0, Z: 0, AD: 0, CD: 0, RCODE: 0, QDCount: 0, ANCount: 0, NSCount: 0, ARCount: 0 }; this.questions = []; this.answers = []; this.authorities = []; this.additionals = []; this.origin = { address: origin.address, port: origin.port }; // wrap parse in try/catch because it could throw // if it does, make packet.isValid() always return false if (buffer) { try { this.parseBuffer(buffer); } catch (err) { debug('Packet parse error: ' + err + ' \n' + err.stack); this.isValid = function () { return false; }; } } } _createClass(Packet, [{ key: 'parseBuffer', value: function parseBuffer(buffer) { var wrapper = new BufferWrapper(buffer); var readQuestion = function readQuestion() { return QueryRecord.fromBuffer(wrapper); }; var readRecord = function readRecord() { return ResourceRecord.fromBuffer(wrapper); }; this.header = this.parseHeader(wrapper); this.questions = misc.map_n(readQuestion, this.header.QDCount); this.answers = misc.map_n(readRecord, this.header.ANCount); this.authorities = misc.map_n(readRecord, this.header.NSCount); this.additionals = misc.map_n(readRecord, this.header.ARCount); } /** * Header: * +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ * | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | * +----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+ * | Identifier | * +----+-------------------+----+----+----+----+----+----+----+-------------------+ * | QR | OPCODE | AA | TC | RD | RA | Z | AD | CD | RCODE | * +----+-------------------+----+----+----+----+----+----+----+-------------------+ * | QDCount (Number of questions) | * +-------------------------------------------------------------------------------+ * | ANCount (Number of answer records) | * +-------------------------------------------------------------------------------+ * | NSCount (Number of authority records) | * +-------------------------------------------------------------------------------+ * | ARCount (Number of additional records) | * +-------------------------------------------------------------------------------+ * * For mDNS, RD, RA, Z, AD and CD MUST be zero on transmission, and MUST be ignored * on reception. Responses with OPCODEs or RCODEs =/= 0 should be silently ignored. */ }, { key: 'parseHeader', value: function parseHeader(wrapper) { var header = {}; header.ID = wrapper.readUInt16BE(); var flags = wrapper.readUInt16BE(); header.QR = (flags & 1 << 15) >> 15; header.OPCODE = (flags & 0xF << 11) >> 11; header.AA = (flags & 1 << 10) >> 10; header.TC = (flags & 1 << 9) >> 9; header.RD = 0; header.RA = 0; header.Z = 0; header.AD = 0; header.CD = 0; header.RCODE = flags & 0xF; header.QDCount = wrapper.readUInt16BE(); header.ANCount = wrapper.readUInt16BE(); header.NSCount = wrapper.readUInt16BE(); header.ARCount = wrapper.readUInt16BE(); return header; } }, { key: 'toBuffer', value: function toBuffer() { var wrapper = new BufferWrapper(); var writeRecord = function writeRecord(record) { return record.writeTo(wrapper); }; this.writeHeader(wrapper); this.questions.forEach(writeRecord); this.answers.forEach(writeRecord); this.authorities.forEach(writeRecord); this.additionals.forEach(writeRecord); return wrapper.unwrap(); } }, { key: 'writeHeader', value: function writeHeader(wrapper) { var flags = 0 + (this.header.QR << 15) + (this.header.OPCODE << 11) + (this.header.AA << 10) + (this.header.TC << 9) + (this.header.RD << 8) + (this.header.RA << 7) + (this.header.Z << 6) + (this.header.AD << 5) + (this.header.CD << 4) + this.header.RCODE; wrapper.writeUInt16BE(this.header.ID); wrapper.writeUInt16BE(flags); wrapper.writeUInt16BE(this.questions.length); // QDCount wrapper.writeUInt16BE(this.answers.length); // ANCount wrapper.writeUInt16BE(this.authorities.length); // NSCount wrapper.writeUInt16BE(this.additionals.length); // ARCount } }, { key: 'setQuestions', value: function setQuestions(questions) { this.questions = questions; this.header.QDCount = this.questions.length; } }, { key: 'setAnswers', value: function setAnswers(answers) { this.answers = answers; this.header.ANCount = this.answers.length; } }, { key: 'setAuthorities', value: function setAuthorities(authorities) { this.authorities = authorities; this.header.NSCount = this.authorities.length; } }, { key: 'setAdditionals', value: function setAdditionals(additionals) { this.additionals = additionals; this.header.ARCount = this.additionals.length; } }, { key: 'setResponseBit', value: function setResponseBit() { this.header.QR = 1; // response this.header.AA = 1; // authoritative (all responses must be) } }, { key: 'isValid', value: function isValid() { return this.header.OPCODE === 0 && this.header.RCODE === 0 && (!this.isAnswer() || this.header.AA === 1); // must be authoritative } }, { key: 'isEmpty', value: function isEmpty() { return this.isAnswer() ? !this.answers.length // responses have to have answers : !this.questions.length; // queries/probes have to have questions } }, { key: 'isLegacy', value: function isLegacy() { return !!this.origin.port && this.origin.port !== 5353; } }, { key: 'isLocal', value: function isLocal() { var _ref, _this = this; return !!this.origin.address && (_ref = []).concat.apply(_ref, _toConsumableArray(Object.values(os.networkInterfaces()))).some(function (_ref2) { var address = _ref2.address; return address === _this.origin.address; }); } }, { key: 'isProbe', value: function isProbe() { return !!(!this.header.QR && this.authorities.length); } }, { key: 'isQuery', value: function isQuery() { return !!(!this.header.QR && !this.authorities.length); } }, { key: 'isAnswer', value: function isAnswer() { return !!this.header.QR; } }, { key: 'equals', value: function equals(other) { return misc.equals(this.header, other.header) && new RecordCollection(this.questions).equals(other.questions) && new RecordCollection(this.answers).equals(other.answers) && new RecordCollection(this.additionals).equals(other.additionals) && new RecordCollection(this.authorities).equals(other.authorities); } }, { key: 'split', value: function split() { var one = new Packet(); var two = new Packet(); one.header = Object.assign({}, this.header); two.header = Object.assign({}, this.header); if (this.isQuery()) { one.header.TC = 1; one.setQuestions(this.questions); two.setQuestions([]); one.setAnswers(this.answers.slice(0, Math.ceil(this.answers.length / 2))); two.setAnswers(this.answers.slice(Math.ceil(this.answers.length / 2))); } if (this.isAnswer()) { var _ref3, _ref4; one.setAnswers(this.answers.slice(0, Math.ceil(this.answers.length / 2))); two.setAnswers(this.answers.slice(Math.ceil(this.answers.length / 2))); one.setAdditionals((_ref3 = []).concat.apply(_ref3, _toConsumableArray(one.answers.map(function (a) { return a.additionals; })))); two.setAdditionals((_ref4 = []).concat.apply(_ref4, _toConsumableArray(two.answers.map(function (a) { return a.additionals; })))); } // if it can't split packet, just return empties and hope for the best... return [one, two]; } /** * Makes a nice string for looking at packets. Makes something like: * * ANSWER * ├─┬ Questions[2] * │ └── record.local. ANY QM * ├─┬ Answer RRs[1] * │ └── record.local. A ... * ├─┬ Authority RRs[1] * │ └── record.local. A ... * └─┬ Additional RRs[1] * └── record.local. A ... */ }, { key: 'toString', value: function toString() { var str = ''; if (this.isAnswer()) str += misc.bg(' ANSWER ', 'blue', true) + '\n'; if (this.isProbe()) str += misc.bg(' PROBE ', 'magenta', true) + '\n'; if (this.isQuery()) str += misc.bg(' QUERY ', 'yellow', true) + '\n'; var recordGroups = []; var aligned = misc.alignRecords(this.questions, this.answers, this.authorities, this.additionals); if (this.questions.length) recordGroups.push(['Questions', aligned[0]]); if (this.answers.length) recordGroups.push(['Answer RRs', aligned[1]]); if (this.authorities.length) recordGroups.push(['Authority RRs', aligned[2]]); if (this.additionals.length) recordGroups.push(['Additional RRs', aligned[3]]); recordGroups.forEach(function (_ref5, i) { var _ref6 = _slicedToArray(_ref5, 2), name = _ref6[0], records = _ref6[1]; var isLastSection = i === recordGroups.length - 1; // add record group header str += util.format(' %s─┬ %s [%s]\n', isLastSection ? '└' : '├', name, records.length); // add record strings records.forEach(function (record, j) { var isLastRecord = j === records.length - 1; str += util.format(' %s %s── %s\n', isLastSection ? ' ' : '│', isLastRecord ? '└' : '├', record); }); }); return str; } }]); return Packet; }(); module.exports = Packet;