dnssd
Version:
Bonjour/Avahi-like service discovery in pure JavaScript
483 lines (392 loc) • 13.7 kB
JavaScript
'use strict';
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); } }
var Packet = require('./Packet');
var EventEmitter = require('./EventEmitter');
var RecordCollection = require('./RecordCollection');
var TimerContainer = require('./TimerContainer');
var sleep = require('./sleep');
var misc = require('./misc');
var filename = require('path').basename(__filename);
var debug = require('./debug')('dnssd:' + filename);
var RType = require('./constants').RType;
var ONE_SECOND = 1000;
var counter = 0;
var uniqueId = function uniqueId() {
return 'id#' + ++counter;
};
/**
* Creates a new MulticastResponse
* @class
* @extends EventEmitter
*
* Sends out a multicast response of records on a given interface. Responses
* can be set to repeat multiple times.
*
* @emits 'stopped'
*
* @param {NetworkInterface} intf - the interface the response will work on
* @param {EventEmitter} offswitch - emitter used to shut this response down
*/
function MulticastResponse(intf, offswitch) {
EventEmitter.call(this);
// id only used for figuring out logs
this._id = uniqueId();
debug('Creating new response (' + this._id + ')');
this._intf = intf;
this._offswitch = offswitch;
this._answers = new RecordCollection();
this._isStopped = false;
// defaults
this._repeats = 1;
this._delay = 0;
this._isDefensive = false;
// repeat responses, first at 1s apart, then increasing by a factor of 2
this._next = ONE_SECOND;
this._timers = new TimerContainer(this);
// listen to answers on interface to suppress duplicate answers
// stop on either the offswitch of an interface error
intf.using(this).on('answer', this._onAnswer).once('error', this.stop);
// waking from sleep should cause the response to stop too
sleep.using(this).on('wake', this.stop);
offswitch.using(this).once('stop', this.stop);
}
MulticastResponse.prototype = Object.create(EventEmitter.prototype);
MulticastResponse.prototype.constructor = MulticastResponse;
/**
* Adds records to be sent out.
* @param {ResourceRecords|ResourceRecords[]} arg
*/
MulticastResponse.prototype.add = function (arg) {
var records = Array.isArray(arg) ? arg : [arg];
// In any case where there may be multiple responses, like when all outgoing
// records are non-unique (like PTRs) response should be delayed 20-120 ms.
this._delay = records.some(function (record) {
return !record.isUnique;
}) ? misc.random(20, 120) : 0;
this._answers.addEach(records);
return this;
};
MulticastResponse.prototype.repeat = function (num) {
this._repeats = num;
return this;
};
/**
* Some responses are 'defensive' in that they are responding to probes or
* correcting some problem like an erroneous TTL=0.
*/
MulticastResponse.prototype.defensive = function (bool) {
this._isDefensive = !!bool;
return this;
};
/**
* Starts sending out records.
*/
MulticastResponse.prototype.start = function () {
// remove delay for defensive responses
var delay = this._isDefensive ? 0 : this._delay;
// prepare next outgoing packet in advance while listening to other answers
// on the interface so duplicate answers in this packet can be suppressed.
this._queuedPacket = this._makePacket();
this._timers.setLazy('next-response', this._send, delay);
return this;
};
/**
* Stops the response & cleans up after itself.
* @emits 'stopped' event when done
*/
MulticastResponse.prototype.stop = function () {
if (this._isStopped) return;
debug('Response stopped (' + this._id + ')');
this._isStopped = true;
this._timers.clear();
this._intf.removeListenersCreatedBy(this);
this._offswitch.removeListenersCreatedBy(this);
sleep.removeListenersCreatedBy(this);
this.emit('stopped');
};
/**
* Sends the response packets.
*
* socket.send() has a callback to know when the response was actually sent.
* Responses shut down after repeats run out.
*/
MulticastResponse.prototype._send = function () {
var _this = this;
this._repeats--;
debug('Sending response, ' + this._repeats + ' repeats left (' + this._id + ')');
var packet = this._suppressRecents(this._queuedPacket);
// send packet, stop when all responses have been sent
this._intf.send(packet, null, function () {
if (_this._repeats <= 0) _this.stop();
});
// reschedule the next response if needed. the packet is prepared in advance
// so incoming responses can be checked for duplicate answers.
if (this._repeats > 0) {
this._queuedPacket = this._makePacket();
this._timers.setLazy('next-response', this._send, this._next);
// each successive response increases delay by a factor of 2
this._next *= 2;
}
};
/**
* Create a response packet.
* @return {Packet}
*/
MulticastResponse.prototype._makePacket = function () {
var packet = new Packet();
var additionals = new RecordCollection();
this._answers.forEach(function (answer) {
additionals.addEach(answer.additionals);
});
packet.setResponseBit();
packet.setAnswers(this._answers.toArray());
packet.setAdditionals(additionals.difference(this._answers).toArray());
return packet;
};
/**
* Removes recently sent records from the outgoing packet
*
* Check the interface to for each outbound record. Records are limited to
* being sent to the multicast address once every 1s except for probe responses
* (and other defensive responses) that can be sent every 250ms.
*
* @param {Packet} packet - the outgoing packet
* @return {Packet}
*/
MulticastResponse.prototype._suppressRecents = function (packet) {
var _this2 = this;
var range = this._isDefensive ? 0.25 : 1.0;
var answers = packet.answers.filter(function (record) {
return !_this2._intf.hasRecentlySent(record, range);
});
var suppressed = packet.answers.filter(function (a) {
return !~answers.indexOf(a);
});
if (suppressed.length) {
debug('Suppressing recently sent (%s): %r', this._id, suppressed);
packet.setAnswers(answers);
}
return packet;
};
/**
* Handles incoming answer (response) packets
*
* This is solely used to do duplicate answer suppression (7.4). If another
* responder has sent the same answer as one this response is about to send,
* this response can suppress that answer since someone else already sent it.
* Modifies the next scheduled response packet only (this._queuedPacket).
*
* Note: this handle will receive this response's packets too
*
* @param {Packet} packet - the incoming probe packet
*/
MulticastResponse.prototype._onAnswer = function (packet) {
if (this._isStopped) return;
// prevent this response from accidentally suppressing itself
// (ignore packets that came from this interface)
if (packet.isLocal()) return;
// ignore goodbyes in suppression check
var incoming = packet.answers.filter(function (answer) {
return answer.ttl !== 0;
});
var outgoing = this._queuedPacket.answers;
// suppress outgoing answers that also appear in incoming records
var answers = new RecordCollection(outgoing).difference(incoming).toArray();
var suppressed = outgoing.filter(function (out) {
return !~answers.indexOf(out);
});
if (suppressed.length) {
debug('Suppressing duplicate answers (%s): %r', this._id, suppressed);
this._queuedPacket.setAnswers(answers);
}
};
/**
* Creates a new GoodbyeResponse
* @class
* @extends MulticastResponse
*
* Sends out a multicast response of records that are now dead on an interface.
* Goodbyes can be set to repeat multiple times.
*
* @emits 'stopped'
*
* @param {NetworkInterface} intf - the interface the response will work on
* @param {EventEmitter} offswitch - emitter used to shut this response down
*/
function GoodbyeResponse(intf, offswitch) {
MulticastResponse.call(this, intf, offswitch);
debug('└─ a goodbye response');
}
GoodbyeResponse.prototype = Object.create(MulticastResponse.prototype);
GoodbyeResponse.constructor = GoodbyeResponse;
/**
* Makes a goodbye packet
* @return {Packet}
*/
GoodbyeResponse.prototype._makePacket = function () {
var packet = new Packet();
// Records getting goodbye'd need a TTL=0
// Clones are used so original records (held elsewhere) don't get mutated
var answers = this._answers.map(function (record) {
var clone = record.clone();
clone.ttl = 0;
return clone;
});
packet.setResponseBit();
packet.setAnswers(answers);
return packet;
};
// Don't suppress recents on goodbyes, return provided packet unchanged
GoodbyeResponse.prototype._suppressRecents = function (p) {
return p;
};
// Don't do answer suppression on goodbyes
GoodbyeResponse.prototype._onAnswer = function () {};
/**
* Creates a new UnicastResponse
* @class
* @extends EventEmitter
*
* Sends out a unicast response to a destination. There are two types of
* unicast responses here:
* - direct responses to QU questions (mDNS rules)
* - legacy responses (normal DNS packet rules)
*
* @emits 'stopped'
*
* @param {NetworkInterface} intf - the interface the response will work on
* @param {EventEmitter} offswitch - emitter used to shut this response down
*/
function UnicastResponse(intf, offswitch) {
EventEmitter.call(this);
// id only used for figuring out logs
this._id = uniqueId();
debug('Creating a new unicast response (' + this._id + ')');
this._intf = intf;
this._offswitch = offswitch;
this._answers = new RecordCollection();
this._timers = new TimerContainer(this);
// defaults
this._delay = 0;
this._isDefensive = false;
// unicast & legacy specific
this._destination = {};
this._isLegacy = false;
this._headerID = null;
this._questions = null;
// stops on offswitch event or interface errors
intf.using(this).once('error', this.stop);
offswitch.using(this).once('stop', this.stop);
sleep.using(this).on('wake', this.stop);
}
UnicastResponse.prototype = Object.create(EventEmitter.prototype);
UnicastResponse.prototype.constructor = UnicastResponse;
/**
* Adds records to be sent out.
* @param {ResourceRecords|ResourceRecords[]} arg
*/
UnicastResponse.prototype.add = function (arg) {
var records = Array.isArray(arg) ? arg : [arg];
// In any case where there may be multiple responses, like when all outgoing
// records are non-unique (like PTRs) response should be delayed 20-120 ms.
this._delay = records.some(function (record) {
return !record.isUnique;
}) ? misc.random(20, 120) : 0;
this._answers.addEach(records);
return this;
};
UnicastResponse.prototype.defensive = function (bool) {
this._isDefensive = !!bool;
return this;
};
/**
* Sets destination info based on the query packet this response is addressing.
* Legacy responses will have to keep the questions and the packet ID for later.
*
* @param {Packet} packet - query packet to respond to
*/
UnicastResponse.prototype.respondTo = function (packet) {
this._destination.port = packet.origin.port;
this._destination.address = packet.origin.address;
if (packet.isLegacy()) {
debug('preparing legacy response (' + this._id + ')');
this._isLegacy = true;
this._headerID = packet.header.ID;
this._questions = packet.questions;
this._questions.forEach(function (question) {
question.QU = false;
});
}
return this;
};
/**
* Sends response packet to destination. Stops when packet has been sent.
* No delay for defensive or legacy responses.
*/
UnicastResponse.prototype.start = function () {
var _this3 = this;
var packet = this._makePacket();
var delay = this._isDefensive || this._isLegacy ? 0 : this._delay;
this._timers.setLazy(function () {
debug('Sending unicast response (' + _this3._id + ')');
_this3._intf.send(packet, _this3._destination, function () {
return _this3.stop();
});
}, delay);
return this;
};
/**
* Stops response and cleans up.
* @emits 'stopped' event when done
*/
UnicastResponse.prototype.stop = function () {
if (this._isStopped) return;
debug('Unicast response stopped (' + this._id + ')');
this._isStopped = true;
this._timers.clear();
this._intf.removeListenersCreatedBy(this);
this._offswitch.removeListenersCreatedBy(this);
sleep.removeListenersCreatedBy(this);
this.emit('stopped');
};
/**
* Makes response packet. Legacy response packets need special treatment.
* @return {Packet}
*/
UnicastResponse.prototype._makePacket = function () {
var packet = new Packet();
var answers = this._answers.toArray();
var additionals = answers.reduce(function (result, answer) {
return result.concat(answer.additionals);
}, []).filter(function (add) {
return !~answers.indexOf(add);
});
additionals = [].concat(_toConsumableArray(new Set(additionals)));
// Set TTL=10 on records for legacy responses. Use clones to prevent
// altering the original record set.
function legacyify(record) {
var clone = record.clone();
clone.isUnique = false;
clone.ttl = 10;
return clone;
}
if (this._isLegacy) {
packet.header.ID = this._headerID;
packet.setQuestions(this._questions);
answers = answers.filter(function (record) {
return record.rrtype !== RType.NSEC;
}).map(legacyify);
additionals = additionals.filter(function (record) {
return record.rrtype !== RType.NSEC;
}).map(legacyify);
}
packet.setResponseBit();
packet.setAnswers(answers);
packet.setAdditionals(additionals);
return packet;
};
module.exports = {
Multicast: MulticastResponse,
Goodbye: GoodbyeResponse,
Unicast: UnicastResponse
};