dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
319 lines (263 loc) • 9.94 kB
JavaScript
const os = require('os');
const util = require('util');
const misc = require('./misc');
const QueryRecord = require('./QueryRecord');
const ResourceRecord = require('./ResourceRecord');
const BufferWrapper = require('./BufferWrapper');
const RecordCollection = require('./RecordCollection');
const filename = require('path').basename(__filename);
const 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.
*
*/
class Packet {
/**
* @param {Buffer} [buffer] - optional buffer to parse
* @param {Object} [origin] - optional msg info
*/
constructor(buffer, origin = {}) {
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 = () => false;
}
}
}
parseBuffer(buffer) {
const wrapper = new BufferWrapper(buffer);
const readQuestion = () => QueryRecord.fromBuffer(wrapper);
const readRecord = () => 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.
*/
parseHeader(wrapper) {
const header = {};
header.ID = wrapper.readUInt16BE();
const 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;
}
toBuffer() {
const wrapper = new BufferWrapper();
const writeRecord = record => 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();
}
writeHeader(wrapper) {
const 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
}
setQuestions(questions) {
this.questions = questions;
this.header.QDCount = this.questions.length;
}
setAnswers(answers) {
this.answers = answers;
this.header.ANCount = this.answers.length;
}
setAuthorities(authorities) {
this.authorities = authorities;
this.header.NSCount = this.authorities.length;
}
setAdditionals(additionals) {
this.additionals = additionals;
this.header.ARCount = this.additionals.length;
}
setResponseBit() {
this.header.QR = 1; // response
this.header.AA = 1; // authoritative (all responses must be)
}
isValid() {
return this.header.OPCODE === 0 &&
this.header.RCODE === 0 &&
(!this.isAnswer() || this.header.AA === 1); // must be authoritative
}
isEmpty() {
return (this.isAnswer())
? !this.answers.length // responses have to have answers
: !this.questions.length; // queries/probes have to have questions
}
isLegacy() {
return (!!this.origin.port && this.origin.port !== 5353);
}
isLocal() {
return !!this.origin.address &&
[].concat(...Object.values(os.networkInterfaces()))
.some(({ address }) => address === this.origin.address);
}
isProbe() {
return !!(!this.header.QR && this.authorities.length);
}
isQuery() {
return !!(!this.header.QR && !this.authorities.length);
}
isAnswer() {
return !!(this.header.QR);
}
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);
}
split() {
const one = new Packet();
const 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()) {
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([].concat(...one.answers.map(a => a.additionals)));
two.setAdditionals([].concat(...two.answers.map(a => 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 ...
*/
toString() {
let 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';
const recordGroups = [];
const 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(([name, records], i) => {
const 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((record, j) => {
const isLastRecord = (j === records.length-1);
str += util.format(' %s %s── %s\n',
(isLastSection) ? ' ' : '│',
(isLastRecord) ? '└' : '├',
record);
});
});
return str;
}
}
module.exports = Packet;