UNPKG

@homebridge/ciao

Version:

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

324 lines 16.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NonCompressionLabelCoder = exports.DNSLabelCoder = void 0; const tslib_1 = require("tslib"); const assert_1 = tslib_1.__importDefault(require("assert")); class DNSLabelCoder { constructor(legacyUnicastEncoding) { this.trackedLengths = []; this.writtenNames = []; this.legacyUnicastEncoding = legacyUnicastEncoding || false; } initBuf(buffer) { this.buffer = buffer; } initRRLocation(recordOffset, rDataOffset, rDataLength) { this.startOfRR = recordOffset; this.startOfRData = rDataOffset; this.rDataLength = rDataLength; } clearRRLocation() { this.startOfRR = undefined; this.startOfRData = undefined; this.rDataLength = undefined; } getUncompressedNameLength(name) { if (name === ".") { return 1; // root label takes one zero byte } (0, assert_1.default)(name.endsWith("."), "Supplied illegal name which doesn't end with the root label!"); let length = 0; const labels = name.split("."); for (let i = 0; i < labels.length; i++) { const label = labels[i]; if (!label && i < labels.length - 1) { assert_1.default.fail("Label " + i + " in name '" + name + "' was empty"); } length += DNSLabelCoder.getLabelLength(label); } return length; } getNameLength(name) { if (DNSLabelCoder.DISABLE_COMPRESSION) { return this.getUncompressedNameLength(name); } if (name === ".") { return 1; // root label takes one zero byte and is not compressible } (0, assert_1.default)(name.endsWith("."), "Supplied illegal name which doesn't end with the root label!"); const labelLengths = name.split(".") .map(label => DNSLabelCoder.getLabelLength(label)); const nameLength = { name: name, length: 0, // total length needed for encoding (with compression enabled) labelLengths: labelLengths, }; let candidateSharingLongestSuffix = undefined; let longestSuffixLength = 0; // amount of labels which are identical // pointers MUST only point to PRIOR label locations for (let i = 0; i < this.trackedLengths.length; i++) { const element = this.trackedLengths[i]; const suffixLength = DNSLabelCoder.computeLabelSuffixLength(element.name, name); // it is very important that this is an GREATER and not just a GREATER EQUAL!!!! // don't change anything unless you fully understand all implications (0, and big comment block below) if (suffixLength > longestSuffixLength) { candidateSharingLongestSuffix = element; longestSuffixLength = suffixLength; } } let length = 0; if (candidateSharingLongestSuffix) { // in theory, it is possible that the candidate has a pointer which "fromIndex" is smaller than // the "toIndex" we are pointing to below. This could result in that we point to a location which // never gets written into the buffer, thus we can't point to it. // But as we always start in order (with the first element in our array; see for loop above) // we will always find the label first, which such a theoretical candidate is also pointing at const pointingFromIndex = labelLengths.length - 1 - longestSuffixLength; // -1 as the empty root label is always included for (let i = 0; i < pointingFromIndex; i++) { length += labelLengths[i]; } length += 2; // 2 byte for the pointer } else { for (let i = 0; i < labelLengths.length; i++) { length += labelLengths[i]; } } nameLength.length = length; this.trackedLengths.push(nameLength); return nameLength.length; } encodeUncompressedName(name, offset) { if (!this.buffer) { assert_1.default.fail("Illegal state. Buffer not initialized!"); } return DNSLabelCoder.encodeUncompressedName(name, this.buffer, offset); } static encodeUncompressedName(name, buffer, offset) { (0, assert_1.default)(name.endsWith("."), "Name does not end with the root label"); const oldOffset = offset; const labels = name === "." ? [""] : name.split("."); for (let i = 0; i < labels.length; i++) { const label = labels[i]; if (label === "") { (0, assert_1.default)(i === labels.length - 1, "Encountered root label being not at the end of the domain name"); buffer.writeUInt8(0, offset++); // write a terminating zero break; } // write length byte followed by the label data const length = buffer.write(label, offset + 1); buffer.writeUInt8(length, offset); offset += length + 1; } return offset - oldOffset; // written bytes } encodeName(name, offset) { if (DNSLabelCoder.DISABLE_COMPRESSION) { return this.encodeUncompressedName(name, offset); } if (!this.buffer) { assert_1.default.fail("Illegal state. Buffer not initialized!"); } if (name === ".") { this.buffer.writeUInt8(0, offset); // write a terminating zero return 1; } const oldOffset = offset; const labels = name.split("."); const writtenName = { name: name, writtenLabels: new Array(labels.length).fill(-1), // init with "-1" meaning unknown location }; let candidateSharingLongestSuffix = undefined; let longestSuffixLength = 0; // amount of labels which are identical for (let i = 0; i < this.writtenNames.length; i++) { const element = this.writtenNames[i]; const suffixLength = DNSLabelCoder.computeLabelSuffixLength(element.name, name); // it is very important that this is an GREATER and not just a GREATER EQUAL!!!! // don't change anything unless you fully understand all implications (0, and big comment block below) if (suffixLength > longestSuffixLength) { candidateSharingLongestSuffix = element; longestSuffixLength = suffixLength; } } if (candidateSharingLongestSuffix) { // in theory, it is possible that the candidate has a pointer which "fromIndex" is smaller than // the "toIndex" we are pointing to below. This could result in that we point to a location which // never gets written into the buffer, thus we can't point to it. // But as we always start in order (with the first element in our array; see for loop above) // we will always find the label first, which such a theoretical candidate is also pointing at const pointingFromIndex = labels.length - 1 - longestSuffixLength; // -1 as the empty root label is always included const pointingToIndex = candidateSharingLongestSuffix.writtenLabels.length - 1 - longestSuffixLength; for (let i = 0; i < pointingFromIndex; i++) { writtenName.writtenLabels[i] = offset; offset += DNSLabelCoder.writeLabel(labels[i], this.buffer, offset); } const pointerDestination = candidateSharingLongestSuffix.writtenLabels[pointingToIndex]; (0, assert_1.default)(pointerDestination !== -1, "Label which was pointed at wasn't yet written to the buffer!"); (0, assert_1.default)(pointerDestination <= DNSLabelCoder.NOT_POINTER_MASK, "Pointer exceeds to length of a maximum of 14 bits"); (0, assert_1.default)(pointerDestination < offset, "Pointer can only point to a prior location"); const pointer = DNSLabelCoder.POINTER_MASK | pointerDestination; this.buffer.writeUInt16BE(pointer, offset); offset += 2; } else { for (let i = 0; i < labels.length; i++) { writtenName.writtenLabels[i] = offset; offset += DNSLabelCoder.writeLabel(labels[i], this.buffer, offset); } } this.writtenNames.push(writtenName); return offset - oldOffset; // written bytes } decodeName(offset, resolvePointers = true) { if (!this.buffer) { assert_1.default.fail("Illegal state. Buffer not initialized!"); } const oldOffset = offset; let name = ""; for (;;) { const length = this.buffer.readUInt8(offset++); if (length === 0) { // zero byte to terminate the name name += "."; break; // root label marks end of name } const labelTypePattern = length & DNSLabelCoder.POINTER_MASK_ONE_BYTE; if (labelTypePattern) { if (labelTypePattern === DNSLabelCoder.POINTER_MASK_ONE_BYTE) { // we got a pointer here const pointer = this.buffer.readUInt16BE(offset - 1) & DNSLabelCoder.NOT_POINTER_MASK; // extract the offset offset++; // increment for the second byte of the pointer if (!resolvePointers) { name += name ? ".~" : "~"; break; } // if we would allow pointers to a later location, we MUST ensure that we don't end up in an endless loop (0, assert_1.default)(pointer < oldOffset, "Pointer at " + (offset - 2) + " MUST point to a prior location!"); name += (name ? "." : "") + this.decodeName(pointer).data; // recursively decode the rest of the name break; // pointer marks end of name } else if (labelTypePattern === DNSLabelCoder.LOCAL_COMPRESSION_ONE_BYTE) { let localPointer = this.buffer.readUInt16BE(offset - 1) & DNSLabelCoder.NOT_POINTER_MASK; offset++; // increment for the second byte of the pointer; if (!resolvePointers) { name += name ? ".~" : "~"; break; } if (localPointer >= 0 && localPointer < 255) { // 255 is reserved (0, assert_1.default)(this.startOfRR !== undefined, "Cannot decompress locally compressed name as record is not initialized!"); localPointer += this.startOfRR; (0, assert_1.default)(localPointer < oldOffset, "LocalPointer <255 at " + (offset - 2) + " MUST point to a prior location!"); name += (name ? "." : "") + this.decodeName(localPointer).data; // recursively decode the rest of the name } else if (localPointer >= 256) { (0, assert_1.default)(this.startOfRData !== undefined && this.rDataLength !== undefined, "Cannot decompress locally compressed name as record is not initialized!"); localPointer -= 256; // subtract the offset 256 localPointer += this.startOfRData; (0, assert_1.default)(localPointer < oldOffset, "LocalPointer >=256 at " + (offset - 2) + " MUST point to a prior location!"); name += (name ? "." : "") + this.decodeName(localPointer).data; // recursively decode the rest of the name } else { assert_1.default.fail("Encountered unknown pointer range " + localPointer); } break; // pointer marks end of name } else if (labelTypePattern === DNSLabelCoder.EXTENDED_LABEL_TYPE_ONE_BYTE) { const extendedLabelType = length & DNSLabelCoder.NOT_POINTER_MASK_ONE_BYTE; assert_1.default.fail("Received extended label type " + extendedLabelType + " at " + (offset - 1)); } else { assert_1.default.fail("Encountered unknown pointer type: " + Buffer.from([labelTypePattern >> 6]).toString("hex") + " (with original byte " + Buffer.from([length]).toString("hex") + ")"); } } const label = this.buffer.toString("utf-8", offset, offset + length); offset += length; name += (name ? "." : "") + label; } return { data: name, readBytes: offset - oldOffset, }; } static getLabelLength(label) { if (!label) { // empty label aka root label return 1; // root label takes one zero byte } else { const byteLength = Buffer.byteLength(label); (0, assert_1.default)(byteLength <= 63, "Label cannot be longer than 63 bytes (" + label + ")"); return 1 + byteLength; // length byte + label data } } static writeLabel(label, buffer, offset) { if (!label) { buffer.writeUInt8(0, offset); return 1; } else { const length = buffer.write(label, offset + 1); buffer.writeUInt8(length, offset); return length + 1; } } static computeLabelSuffixLength(a, b) { (0, assert_1.default)(a.length !== 0 && b.length !== 0, "Encountered empty name when comparing suffixes!"); const lastAIndex = a.length - 1; const lastBIndex = b.length - 1; let equalLabels = 0; let exitByBreak = false; // we start with i=1 as the last character will always be the root label terminator "." for (let i = 1; i <= lastAIndex && i <= lastBIndex; i++) { // we are comparing both strings backwards const aChar = a.charAt(lastAIndex - i); const bChar = b.charAt(lastBIndex - i); (0, assert_1.default)(!!aChar && !!bChar, "Seemingly encountered out of bounds trying to calculate suffixes"); if (aChar !== bChar) { exitByBreak = true; break; // encountered the first character to differ } else if (aChar === ".") { // we reached the label terminator, thus we count up the labels which are equal equalLabels++; } } if (!exitByBreak) { equalLabels++; // accommodate for the top level label (fqdn doesn't start with a dot) } return equalLabels; } } exports.DNSLabelCoder = DNSLabelCoder; DNSLabelCoder.DISABLE_COMPRESSION = false; // RFC 1035 4.1.4. Message compression: // In order to reduce the size of messages, the domain system utilizes a // compression scheme which eliminates the repetition of domain names in a // message. In this scheme, an entire domain name or a list of labels at // the end of a domain name is replaced with a pointer to a PRIOR occurrence // of the same name. // // The compression scheme allows a domain name in a message to be // represented as either: // - a sequence of labels ending in a zero octet // - a pointer // - a sequence of labels ending with a pointer // RFC 6762 name compression for rdata should be used in: NS, CNAME, PTR, DNAME, SOA, MX, AFSDB, RT, KX, RP, PX, SRV, NSEC DNSLabelCoder.POINTER_MASK = 0xC000; // 2 bytes, starting with 11 DNSLabelCoder.POINTER_MASK_ONE_BYTE = 0xC0; // same deal as above, just on a 1 byte level DNSLabelCoder.LOCAL_COMPRESSION_ONE_BYTE = 0x80; // "10" label type https://tools.ietf.org/html/draft-ietf-dnsind-local-compression-05#section-4 DNSLabelCoder.EXTENDED_LABEL_TYPE_ONE_BYTE = 0x40; // "01" edns extended label type https://tools.ietf.org/html/rfc6891#section-4.2 DNSLabelCoder.NOT_POINTER_MASK = 0x3FFF; DNSLabelCoder.NOT_POINTER_MASK_ONE_BYTE = 0x3F; class NonCompressionLabelCoder extends DNSLabelCoder { getNameLength(name) { return this.getUncompressedNameLength(name); } encodeName(name, offset) { return this.encodeUncompressedName(name, offset); } } exports.NonCompressionLabelCoder = NonCompressionLabelCoder; NonCompressionLabelCoder.INSTANCE = new NonCompressionLabelCoder(); //# sourceMappingURL=DNSLabelCoder.js.map