dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
309 lines (259 loc) • 8.22 kB
JavaScript
/**
* Wraps a buffer for easier reading / writing without keeping track of offsets.
* @class
*
* instead of:
* buffer.writeUInt8(1, 0);
* buffer.writeUInt8(2, 1);
* buffer.writeUInt8(3, 2);
*
* do:
* wrapper.writeUInt8(1);
* wrapper.writeUInt8(2);
* wrapper.writeUInt8(3);
*/
class BufferWrapper {
/**
* @param {Buffer} [buffer]
* @param {integer} [position]
*/
constructor(buffer, position = 0) {
this.buffer = buffer || Buffer.alloc(512);
this.position = position;
}
readUInt8() {
const value = this.buffer.readUInt8(this.position);
this.position += 1;
return value;
}
writeUInt8(value) {
this._checkLength(1);
this.buffer.writeUInt8(value, this.position);
this.position += 1;
}
readUInt16BE() {
const value = this.buffer.readUInt16BE(this.position);
this.position += 2;
return value;
}
writeUInt16BE(value) {
this._checkLength(2);
this.buffer.writeUInt16BE(value, this.position);
this.position += 2;
}
readUInt32BE() {
const value = this.buffer.readUInt32BE(this.position);
this.position += 4;
return value;
}
writeUInt32BE(value) {
this._checkLength(4);
this.buffer.writeUInt32BE(value, this.position);
this.position += 4;
}
readUIntBE(len) {
const value = this.buffer.readUIntBE(this.position, len);
this.position += len;
return value;
}
writeUIntBE(value, len) {
this._checkLength(len);
this.buffer.writeUIntBE(value, this.position, len);
this.position += len;
}
readString(len) {
const str = this.buffer.toString('utf8', this.position, this.position + len);
this.position += len;
return str;
}
writeString(str) {
const len = Buffer.byteLength(str);
this._checkLength(len);
this.buffer.write(str, this.position);
this.position += len;
}
/**
* Returns a sub portion of the wrapped buffer
* @param {integer} len
* @return {Buffer}
*/
read(len) {
const buf = Buffer.alloc(len).fill(0);
this.buffer.copy(buf, 0, this.position);
this.position += len;
return buf;
}
/**
* Writes another buffer onto the wrapped buffer
* @param {Buffer} buffer
*/
add(buffer) {
this._checkLength(buffer.length);
buffer.copy(this.buffer, this.position);
this.position += buffer.length;
}
seek(position) {
this.position = position;
}
skip(len) {
this.position += len;
}
tell() {
return this.position;
}
remaining() {
return this.buffer.length - this.position;
}
unwrap() {
return this.buffer.slice(0, this.position);
}
_checkLength(len) {
const needed = len - this.remaining();
const amount = (needed > 512) ? needed * 1.5 : 512;
if (needed > 0) this._grow(amount);
}
_grow(amount) {
this.buffer = Buffer.concat([this.buffer, Buffer.alloc(amount).fill(0)]);
}
indexOf(needle) {
// limit indexOf search up to current position in buffer, no need to
// search for stuff after this.position
const haystack = this.buffer.slice(0, this.position);
if (!haystack.length || !needle.length) return -1;
if (needle.length > haystack.length) return -1;
// use node's indexof if this version has it
if (typeof Buffer.prototype.indexOf === 'function') {
return haystack.indexOf(needle);
}
// otherwise do naive search
const maxIndex = haystack.length - needle.length;
let index = 0;
let pos = 0;
for (; index <= maxIndex; index++, pos = 0) {
while (haystack[index + pos] === needle[pos]) {
if (++pos === needle.length) return index;
}
}
return -1;
}
/**
* Reads a fully qualified domain name from the buffer following the dns
* message format / compression style.
*
* Basic:
* Each label is preceded by an uint8 specifying the length of the label,
* finishing with a 0 which indicates the root label.
*
* +---+------+---+--------+---+-----+---+
* | 3 | wwww | 6 | google | 3 | com | 0 | --> www.google.com.
* +---+------+---+--------+---+-----+---+
*
* Compression:
* A pointer is used to point to the location of the previously written labels.
* If a length byte is > 192 (0xC0) then it means its a pointer to other
* labels and not a length marker. The pointer is 2 octets long.
*
* +---+------+-------------+
* | 3 | wwww | 0xC000 + 34 | --> www.google.com.
* +---+------+-------------+
* ^-- the "google.com." part can be found @ offset 34
*
* @return {string}
*/
readFQDN() {
const labels = [];
let len, farthest;
while (this.remaining() >= 0 && (len = this.readUInt8())) {
// Handle dns compression. If the length is > 192, it means its a pointer.
// The pointer points to a previous position in the buffer to move to and
// read from. Pointer (a int16be) = 0xC000 + position
if (len < 192) {
labels.push(this.readString(len));
} else {
const position = (len << 8) + this.readUInt8() - 0xC000;
// If a pointer was found, keep track of the farthest position reached
// (the current position) before following the pointers so we can return
// to it later after following all the compression pointers
if (!farthest) farthest = this.position;
this.seek(position);
}
}
// reset to correct position after following pointers (if any)
if (farthest) this.seek(farthest);
return labels.join('.') + '.'; // + root label
}
/**
* Writes a fully qualified domain name
* Same rules as readFQDN above. Does compression.
*
* @param {string} name
*/
writeFQDN(name) {
// convert name into an array of buffers
const labels = name.split('.').filter(s => !!s).map((label) => {
const len = Buffer.byteLength(label);
const buf = Buffer.alloc(1 + len);
buf.writeUInt8(len, 0);
buf.write(label, 1);
return buf;
});
// add root label (a single ".") to the end (zero length label = 0)
labels.push(Buffer.alloc(1));
// compress
const compressed = this._getCompressedLabels(labels);
compressed.forEach(label => this.add(label));
}
/**
* Finds a compressed version of given labels within the buffer
*
* Checks if a sub section has been written before, starting with all labels
* and removing the first label on each successive search until a match (index)
* is found, or until NO match is found.
*
* Ex:
*
* 1st pass: Instance._service._tcp.local
* 2nd pass: _service._tcp.local
* 3rd pass: _tcp.local
* ^-- found "_tcp.local" @ 34, try to compress more
*
* 4th pass: Instance._service.[0xC000 + 34]
* 5th pass: _service.[0xC000 + 34]
* ^-- found "_service.[0xC000 + 34]" @ 52, try to compress more
*
* 6th pass: Instance.[0xC000 + 52]
*
* Nothing else found, returns [Instance, 0xC000+52]
*
* @param {Buffer[]} labels
* @return {Buffer[]} - compressed version
*/
_getCompressedLabels(labels) {
const copy = [...labels];
const wrapper = this;
function compress(lastPointer) {
// re-loop on each compression attempt
copy.forEach((label, index) => {
// if a pointer was found on the last compress call, don't bother trying
// to find a previous instance of a pointer, it doesn't do any good.
// no need to change [0xC000 + 54] pointer to a [0xC000 + 23] pointer
if (lastPointer && label === lastPointer) return;
if (label.length === 1 && label[0] === 0) return;
const subset = copy.slice(index);
const pos = wrapper.indexOf(Buffer.concat(subset));
if (!!~pos) {
const pointer = Buffer.alloc(2);
pointer.writeUInt16BE(0xC000 + pos, 0);
// drop this label and everything after it (stopping forEach loop)
// put the pointer there instead
copy.splice(index, copy.length - index);
copy.push(pointer);
compress(pointer); // try to compress some more
}
});
}
compress();
return copy;
}
}
module.exports = BufferWrapper;