UNPKG

@homebridge/ciao

Version:

ciao is a RFC 6763 compliant dns-sd library, advertising on multicast dns (RFC 6762) implemented in plain Typescript/JavaScript

455 lines 21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DNSPacket = exports.PacketType = exports.QClass = exports.RClass = exports.QType = exports.RType = exports.RCode = exports.OpCode = void 0; const tslib_1 = require("tslib"); const assert_1 = tslib_1.__importDefault(require("assert")); const fast_deep_equal_1 = tslib_1.__importDefault(require("fast-deep-equal")); const dns_string_utils_1 = require("./dns-string-utils"); const DNSLabelCoder_1 = require("./DNSLabelCoder"); const Question_1 = require("./Question"); require("./records"); const ResourceRecord_1 = require("./ResourceRecord"); var OpCode; (function (OpCode) { OpCode[OpCode["QUERY"] = 0] = "QUERY"; // incomplete list })(OpCode || (exports.OpCode = OpCode = {})); var RCode; (function (RCode) { RCode[RCode["NoError"] = 0] = "NoError"; // incomplete list })(RCode || (exports.RCode = RCode = {})); var RType; (function (RType) { RType[RType["A"] = 1] = "A"; RType[RType["CNAME"] = 5] = "CNAME"; RType[RType["PTR"] = 12] = "PTR"; RType[RType["TXT"] = 16] = "TXT"; RType[RType["AAAA"] = 28] = "AAAA"; RType[RType["SRV"] = 33] = "SRV"; RType[RType["OPT"] = 41] = "OPT"; RType[RType["NSEC"] = 47] = "NSEC"; // incomplete list })(RType || (exports.RType = RType = {})); var QType; (function (QType) { QType[QType["A"] = 1] = "A"; QType[QType["CNAME"] = 5] = "CNAME"; QType[QType["PTR"] = 12] = "PTR"; QType[QType["TXT"] = 16] = "TXT"; QType[QType["AAAA"] = 28] = "AAAA"; QType[QType["SRV"] = 33] = "SRV"; // OPT = 41, // RFC 6891 QType[QType["NSEC"] = 47] = "NSEC"; QType[QType["ANY"] = 255] = "ANY"; // incomplete list })(QType || (exports.QType = QType = {})); var RClass; (function (RClass) { RClass[RClass["IN"] = 1] = "IN"; // incomplete list })(RClass || (exports.RClass = RClass = {})); var QClass; (function (QClass) { QClass[QClass["IN"] = 1] = "IN"; QClass[QClass["ANY"] = 255] = "ANY"; // incomplete list })(QClass || (exports.QClass = QClass = {})); var PacketType; (function (PacketType) { PacketType[PacketType["QUERY"] = 0] = "QUERY"; PacketType[PacketType["RESPONSE"] = 1] = "RESPONSE"; })(PacketType || (exports.PacketType = PacketType = {})); function isQuery(query) { return "answers" in query; } function isProbeQuery(query) { return "authorities" in query; } class DNSPacket { constructor(definition) { this.questions = new Map(); this.answers = new Map(); this.authorities = new Map(); this.additionals = new Map(); this.estimatedEncodingLength = 0; // upper bound for the resulting encoding length, should only be called via the getter this.lastCalculatedLength = 0; this.lengthDirty = true; this.id = definition.id || 0; this.legacyUnicastEncoding = definition.legacyUnicast || false; this.type = definition.type; this.opcode = definition.opcode || 0 /* OpCode.QUERY */; this.flags = definition.flags || {}; this.rcode = definition.rCode || 0 /* RCode.NoError */; if (this.type === 1 /* PacketType.RESPONSE */) { this.flags.authoritativeAnswer = true; // RFC 6763 18.4 AA is always set for responses in mdns } if (definition.questions) { this.addQuestions(...definition.questions); } if (definition.answers) { this.addAnswers(...definition.answers); } if (definition.authorities) { this.addAuthorities(...definition.authorities); } if (definition.additionals) { this.addAdditionals(...definition.additionals); } } static createDNSQueryPacket(definition, udpPayloadSize = this.UDP_PAYLOAD_SIZE_IPV4) { const packets = this.createDNSQueryPackets(definition, udpPayloadSize); (0, assert_1.default)(packets.length === 1, "Cannot user short method createDNSQueryPacket when query packets are more than one: is " + packets.length); return packets[0]; } static createDNSQueryPackets(definition, udpPayloadSize = this.UDP_PAYLOAD_SIZE_IPV4) { const packets = []; // packet is like the "main" packet const packet = new DNSPacket({ type: 0 /* PacketType.QUERY */, questions: definition.questions, additionals: isQuery(definition) ? definition.additionals : undefined, // OPT record is included in additionals section }); packets.push(packet); if (packet.getEstimatedEncodingLength() > udpPayloadSize) { const compressedLength = packet.getEncodingLength(); // calculating the real length will update the estimated property as well if (compressedLength > udpPayloadSize) { // if we are still above the payload size we have a problem assert_1.default.fail("Cannot send query where already the query section is exceeding the udpPayloadSize (" + compressedLength + ">" + udpPayloadSize + ")!"); } } // related https://en.wikipedia.org/wiki/Knapsack_problem if (isQuery(definition) && definition.answers) { let currentPacket = packet; let i = 0; const answers = definition.answers.concat([]); // concat basically creates a copy of the array // sort the answers ascending on their encoding length; otherwise we would need to check if a packets fits in a previously created packet answers.sort((a, b) => { return a.getEncodingLength(DNSLabelCoder_1.NonCompressionLabelCoder.INSTANCE) - b.getEncodingLength(DNSLabelCoder_1.NonCompressionLabelCoder.INSTANCE); }); // in the loop below, we check if we need to truncate the list of known-answers in the query while (i < answers.length) { for (; i < answers.length; i++) { const answer = answers[i]; const estimatedSize = answer.getEncodingLength(DNSLabelCoder_1.NonCompressionLabelCoder.INSTANCE); if (packet.getEstimatedEncodingLength() + estimatedSize <= udpPayloadSize) { // size check on estimated calculations currentPacket.addAnswers(answer); } else if (packet.getEncodingLength() + estimatedSize <= udpPayloadSize) { // check if the record may fit when message compression is used. // we may still have a false positive here, as they currently can't compute the REAL encoding for the answer // record, thus we rely on the estimated size currentPacket.addAnswers(answer); } else { if (currentPacket.questions.size === 0 && currentPacket.answers.size === 0) { // we encountered a record which is too big and can't fit in a udpPayloadSize sized packet // RFC 6762 17. In the case of a single Multicast DNS resource record that is too // large to fit in a single MTU-sized multicast response packet, a // Multicast DNS responder SHOULD send the resource record alone, in a // single IP datagram, using multiple IP fragments. packet.addAnswers(answer); } break; } } if (i < answers.length) { // if there are more records left, we need to truncate the packet again currentPacket.flags.truncation = true; // first of all, mark the previous packet as truncated currentPacket = new DNSPacket({ type: 0 /* PacketType.QUERY */ }); packets.push(currentPacket); } } } else if (isProbeQuery(definition) && definition.authorities) { packet.addAuthorities(...definition.authorities); const compressedLength = packet.getEncodingLength(); if (compressedLength > udpPayloadSize) { assert_1.default.fail(`Probe query packet exceeds the mtu size (${compressedLength}>${udpPayloadSize}). Can't split probe queries at the moment!`); } } // otherwise, the packet consist of only questions return packets; } static createDNSResponsePacketsFromRRSet(definition, udpPayloadSize = this.UDP_PAYLOAD_SIZE_IPV4) { const packet = new DNSPacket({ id: definition.id, legacyUnicast: definition.legacyUnicast, type: 1 /* PacketType.RESPONSE */, flags: { authoritativeAnswer: true }, // RFC 6763 18.4 AA is always set for responses in mdns // possible questions sent back to an unicast querier (unicast dns contain only one question, so no size problem here) questions: definition.questions, answers: definition.answers, additionals: definition.additionals, }); if (packet.getEncodingLength() > udpPayloadSize) { assert_1.default.fail("Couldn't construct a dns response packet from a rr set which fits in an udp payload sized packet!"); } return packet; } canBeCombinedWith(packet, udpPayloadSize = DNSPacket.UDP_PAYLOAD_SIZE_IPV4) { // packet header must be identical return this.id === packet.id && this.type === packet.type && this.opcode === packet.opcode && (0, fast_deep_equal_1.default)(this.flags, packet.flags) && this.rcode === packet.rcode // and the data must fit into a udpPayloadSize sized packet && this.getEncodingLength() + packet.getEncodingLength() <= udpPayloadSize; } combineWith(packet) { this.setLegacyUnicastEncoding(this.legacyUnicastEncoding || packet.legacyUnicastEncoding); this.addRecords(this.questions, packet.questions.values()); this.addRecords(this.answers, packet.answers.values(), this.additionals); this.addRecords(this.authorities, packet.authorities.values()); this.addRecords(this.additionals, packet.additionals.values()); } addQuestions(...questions) { return this.addRecords(this.questions, questions); } addAnswers(...answers) { return this.addRecords(this.answers, answers, this.additionals); } addAuthorities(...authorities) { return this.addRecords(this.authorities, authorities); } addAdditionals(...additionals) { return this.addRecords(this.additionals, additionals); } addRecords(recordList, added, removeFromWhenAdded) { let addedAny = false; for (const record of added) { if (recordList.has(record.asString())) { continue; } if (this.estimatedEncodingLength) { this.estimatedEncodingLength += record.getEncodingLength(DNSLabelCoder_1.NonCompressionLabelCoder.INSTANCE); } recordList.set(record.asString(), record); addedAny = true; this.lengthDirty = true; if (removeFromWhenAdded) { removeFromWhenAdded.delete(record.asString()); } } return addedAny; } setLegacyUnicastEncoding(legacyUnicastEncoding) { if (this.legacyUnicastEncoding !== legacyUnicastEncoding) { this.lengthDirty = true; // above option changes length of SRV records } this.legacyUnicastEncoding = legacyUnicastEncoding; } legacyUnicastEncodingEnabled() { return this.legacyUnicastEncoding; } getEstimatedEncodingLength() { if (this.estimatedEncodingLength) { return this.estimatedEncodingLength; } const labelCoder = DNSLabelCoder_1.NonCompressionLabelCoder.INSTANCE; let length = DNSPacket.DNS_PACKET_HEADER_SIZE; for (const record of this.questions.values()) { length += record.getEncodingLength(labelCoder); } for (const record of this.answers.values()) { length += record.getEncodingLength(labelCoder); } for (const record of this.authorities.values()) { length += record.getEncodingLength(labelCoder); } for (const record of this.additionals.values()) { length += record.getEncodingLength(labelCoder); } this.estimatedEncodingLength = length; return length; } getEncodingLength(coder) { if (!this.lengthDirty) { return this.lastCalculatedLength; } const labelCoder = coder || new DNSLabelCoder_1.DNSLabelCoder(this.legacyUnicastEncoding); let length = DNSPacket.DNS_PACKET_HEADER_SIZE; for (const record of this.questions.values()) { length += record.getEncodingLength(labelCoder); } for (const record of this.answers.values()) { length += record.getEncodingLength(labelCoder); } for (const record of this.authorities.values()) { length += record.getEncodingLength(labelCoder); } for (const record of this.additionals.values()) { length += record.getEncodingLength(labelCoder); } this.lengthDirty = false; // reset dirty flag this.lastCalculatedLength = length; this.estimatedEncodingLength = length; return length; } encode() { const labelCoder = new DNSLabelCoder_1.DNSLabelCoder(this.legacyUnicastEncoding); const length = this.getEncodingLength(labelCoder); const buffer = Buffer.allocUnsafe(length); labelCoder.initBuf(buffer); let offset = 0; buffer.writeUInt16BE(this.id, offset); offset += 2; let flags = (this.type << 15) | (this.opcode << 11) | this.rcode; if (this.flags.authoritativeAnswer) { flags |= DNSPacket.AUTHORITATIVE_ANSWER_MASK; } if (this.flags.truncation) { flags |= DNSPacket.TRUNCATION_MASK; } if (this.flags.recursionDesired) { flags |= DNSPacket.RECURSION_DESIRED_MASK; } if (this.flags.recursionAvailable) { flags |= DNSPacket.RECURSION_AVAILABLE_MASK; } if (this.flags.zero) { flags |= DNSPacket.ZERO_HEADER_MASK; } if (this.flags.authenticData) { flags |= DNSPacket.AUTHENTIC_DATA_MASK; } if (this.flags.checkingDisabled) { flags |= DNSPacket.CHECKING_DISABLED_MASK; } buffer.writeUInt16BE(flags, offset); offset += 2; buffer.writeUInt16BE(this.questions.size, offset); offset += 2; buffer.writeUInt16BE(this.answers.size, offset); offset += 2; buffer.writeUInt16BE(this.authorities.size, offset); offset += 2; buffer.writeUInt16BE(this.additionals.size, offset); offset += 2; for (const question of this.questions.values()) { const length = question.encode(labelCoder, buffer, offset); offset += length; } for (const record of this.answers.values()) { const length = record.encode(labelCoder, buffer, offset); offset += length; } for (const record of this.authorities.values()) { const length = record.encode(labelCoder, buffer, offset); offset += length; } for (const record of this.additionals.values()) { const length = record.encode(labelCoder, buffer, offset); offset += length; } (0, assert_1.default)(offset === buffer.length, "Bytes written didn't match the buffer size!"); return buffer; } static decode(context, buffer, offset = 0) { const labelCoder = new DNSLabelCoder_1.DNSLabelCoder(); labelCoder.initBuf(buffer); const id = buffer.readUInt16BE(offset); offset += 2; const flags = buffer.readUInt16BE(offset); offset += 2; const questionLength = buffer.readUInt16BE(offset); offset += 2; const answerLength = buffer.readUInt16BE(offset); offset += 2; const authoritiesLength = buffer.readUInt16BE(offset); offset += 2; const additionalsLength = buffer.readUInt16BE(offset); offset += 2; const questions = []; const answers = []; const authorities = []; const additionals = []; offset += DNSPacket.decodeList(context, labelCoder, buffer, offset, questionLength, Question_1.Question.decode.bind(Question_1.Question), questions); offset += DNSPacket.decodeList(context, labelCoder, buffer, offset, answerLength, ResourceRecord_1.ResourceRecord.decode.bind(ResourceRecord_1.ResourceRecord), answers); offset += DNSPacket.decodeList(context, labelCoder, buffer, offset, authoritiesLength, ResourceRecord_1.ResourceRecord.decode.bind(ResourceRecord_1.ResourceRecord), authorities); offset += DNSPacket.decodeList(context, labelCoder, buffer, offset, additionalsLength, ResourceRecord_1.ResourceRecord.decode.bind(ResourceRecord_1.ResourceRecord), additionals); (0, assert_1.default)(offset === buffer.length, "Didn't read the full buffer (offset=" + offset + ", length=" + buffer.length + ")"); const qr = (flags >> 15); const opcode = ((flags >> 11) & 0xf); const rCode = (flags & 0xf); const packetFlags = {}; if (flags & this.AUTHORITATIVE_ANSWER_MASK) { packetFlags.authoritativeAnswer = true; } if (flags & this.TRUNCATION_MASK) { packetFlags.truncation = true; } if (flags & this.RECURSION_DESIRED_MASK) { packetFlags.recursionDesired = true; } if (flags & this.RECURSION_AVAILABLE_MASK) { packetFlags.recursionAvailable = true; } if (flags & this.ZERO_HEADER_MASK) { packetFlags.zero = true; } if (flags & this.AUTHENTIC_DATA_MASK) { packetFlags.authenticData = true; } if (flags & this.CHECKING_DISABLED_MASK) { packetFlags.checkingDisabled = true; } return new DNSPacket({ id: id, type: qr, opcode: opcode, rCode: rCode, flags: packetFlags, questions: questions, answers: answers, authorities: authorities, additionals: additionals, }); } static decodeList(context, coder, buffer, offset, length, decoder, destination) { const oldOffset = offset; for (let i = 0; i < length; i++) { const decoded = decoder(context, coder, buffer, offset); offset += decoded.readBytes; if (decoded.data) { // if the rdata is not supported by us, or we encountered a parsing error, we ignore the record destination.push(decoded.data); } } return offset - oldOffset; } asLoggingString(udpPayloadSize) { let answerString = ""; let additionalsString = ""; for (const record of this.answers.values()) { if (answerString) { answerString += ","; } answerString += (0, dns_string_utils_1.dnsTypeToString)(record.type); } for (const record of this.additionals.values()) { if (additionalsString) { additionalsString += ","; } additionalsString += (0, dns_string_utils_1.dnsTypeToString)(record.type); } const optionsStrings = []; if (this.legacyUnicastEncodingEnabled()) { optionsStrings.push("U"); } if (udpPayloadSize) { optionsStrings.push("UPS: " + udpPayloadSize); } const optionsString = optionsStrings.length !== 0 ? ` (${optionsStrings})` : ""; return `[${answerString}] answers and [${additionalsString}] additionals with size ${this.getEncodingLength()}B${optionsString}`; } } exports.DNSPacket = DNSPacket; DNSPacket.UDP_PAYLOAD_SIZE_IPV4 = (process.env.CIAO_UPS ? parseInt(process.env.CIAO_UPS) : 1440); // noinspection JSUnusedGlobalSymbols DNSPacket.UDP_PAYLOAD_SIZE_IPV6 = (process.env.CIAO_UPS ? parseInt(process.env.CIAO_UPS) : 1440); DNSPacket.AUTHORITATIVE_ANSWER_MASK = 0x400; DNSPacket.TRUNCATION_MASK = 0x200; DNSPacket.RECURSION_DESIRED_MASK = 0x100; DNSPacket.RECURSION_AVAILABLE_MASK = 0x80; DNSPacket.ZERO_HEADER_MASK = 0x40; DNSPacket.AUTHENTIC_DATA_MASK = 0x20; DNSPacket.CHECKING_DISABLED_MASK = 0x10; // 2 bytes ID, 2 bytes flags, 2 bytes question count, 2 bytes answer count, 2 bytes authorities count; 2 bytes additionals count DNSPacket.DNS_PACKET_HEADER_SIZE = 12; //# sourceMappingURL=DNSPacket.js.map