UNPKG

dnssd

Version:

Bonjour/Avahi-like service discovery in pure JavaScript

388 lines (320 loc) 12.9 kB
'use strict'; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 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 QueryRecord = require('./QueryRecord'); 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 counter = 0; var uniqueId = function uniqueId() { return 'id#' + ++counter; }; /** * Creates a new Probe * @class * @extends EventEmitter * * A probe will check if records are unique on a given interface. If they are * unique, the probe succeeds and the record name can be used. If any records * are found to be not unique, the probe fails and the records need to be * renamed. * * Probes send 3 probe packets out, 250ms apart. If no conflicting answers are * received after all 3 have been sent the probe is considered successful. * * @emits 'complete' * @emits 'conflict' * * @param {NetworkInterface} intf - the interface the probe will work on * @param {EventEmitter} offswitch - emitter used to shut this probe down */ function Probe(intf, offswitch) { EventEmitter.call(this); // id only used for figuring out logs this._id = uniqueId(); debug('Creating new probe (' + this._id + ')'); this._interface = intf; this._offswitch = offswitch; this._questions = new RecordCollection(); this._authorities = new RecordCollection(); this._bridgeable = new RecordCollection(); this._isStopped = false; this._numProbesSent = 0; this._timers = new TimerContainer(this); // listen on answers/probes to check for conflicts // stop on either the offswitch or an interface error intf.using(this).on('answer', this._onAnswer).on('probe', this._onProbe).on('error', this.stop); offswitch.using(this).once('stop', this.stop); // restart probing process if it was interrupted by sleep sleep.using(this).on('wake', this.stop); } Probe.prototype = Object.create(EventEmitter.prototype); Probe.prototype.constructor = Probe; /** * Add unique records to be probed * @param {ResourceRecords|ResourceRecords[]} args */ Probe.prototype.add = function (args) { var _this = this; var records = Array.isArray(args) ? args : [args]; records.forEach(function (record) { _this._authorities.add(record); _this._questions.add(new QueryRecord({ name: record.name })); }); return this; }; /** * Sets the record set getting probed across all interfaces, not just this one. * Membership in the set helps let us know if a record is getting bridged from * one interface to another. */ Probe.prototype.bridgeable = function (bridgeable) { this._bridgeable = new RecordCollection(bridgeable); return this; }; /** * Starts probing records. * The first probe should be delayed 0-250ms to prevent collisions. */ Probe.prototype.start = function () { if (this._isStopped) return; this._timers.setLazy('next-probe', this._send, misc.random(0, 250)); return this; }; /** * Stops the probe. Has to remove any timers that might exist because of this * probe, like the next queued timer. */ Probe.prototype.stop = function () { if (this._isStopped) return; debug('Probe stopped (' + this._id + ')'); this._isStopped = true; this._timers.clear(); this._interface.removeListenersCreatedBy(this); this._offswitch.removeListenersCreatedBy(this); sleep.removeListenersCreatedBy(this); }; /** * Restarts the probing process */ Probe.prototype._restart = function () { this._numProbesSent = 0; this._timers.clear(); this._send(); }; /** * Sends the probe packets. Gets called repeatedly. */ Probe.prototype._send = function () { var _this2 = this; var packet = this._makePacket(); this._numProbesSent++; debug('Sending probe #' + this._numProbesSent + '/3 (' + this._id + ')'); this._interface.send(packet); // Queue next action // - if 3 probes have been sent, 750ms with no conflicts, probing is complete // - otherwise queue next outgoing probe this._timers.setLazy('next-probe', function () { _this2._numProbesSent === 3 ? _this2._complete() : _this2._send(); }, 250); }; /** * Gets called when the probe completes successfully. If the probe finished * early without having to send all 3 probes, completeEarly is set to true. * * @emits 'complete' with true/false * * @param {boolean} [completedEarly] */ Probe.prototype._complete = function (completedEarly) { debug('Probe (' + this._id + ') complete, early: ' + !!completedEarly); this.stop(); this.emit('complete', completedEarly); }; /** * Create probe packets. Probe packets are the same as query packets but they * have records in the authority section. */ Probe.prototype._makePacket = function () { var packet = new Packet(); packet.setQuestions(this._questions.toArray()); packet.setAuthorities(this._authorities.toArray()); return packet; }; /** * Handles incoming answer packets from other mDNS responders * * Any answer that conflicts with one of the proposed records causes a conflict * and stops the probe. If the answer packet matches all proposed records exactly, * it means someone else has already probed the record set and the probe can * finish early. * * Biggest issue here is A/AAAA answer records from bonjour getting bridged. * * Note: don't need to worry about *our* bridged interface answers here. Probes * within a single responder are synchronized and the responder will not * transition into a 'responding' state until all the probes are done. * * @emits 'conflict' when there is a conflict * * @param {Packet} packet - the incoming answer packet */ Probe.prototype._onAnswer = function (packet) { if (this._isStopped) return; var incoming = new RecordCollection([].concat(_toConsumableArray(packet.answers), _toConsumableArray(packet.additionals))); // if incoming records match the probes records exactly, including rdata, // then the record set has already been probed and verified by someone else if (incoming.hasEach(this._authorities)) { debug('All probe records found in answer, completing early (' + this._id + ')'); return this._complete(true); } // check each of our proposed records // check if any of the incoming records conflict with the current record // check each for a conflict but ignore if we think the record was // bridged from another interface (if the record set has the record on // some other interface, the packet was probably bridged) var conflicts = this._authorities.getConflicts(incoming); var hasConflict = conflicts.length && !this._bridgeable.hasEach(conflicts); // a conflicting response from an authoritative responder is fatal and means // the record set needs to be renamed if (hasConflict) { debug('Found conflict on incoming records (' + this._id + ')'); this.stop(); this.emit('conflict'); } }; /** * Handles incoming probe packets * * Checks for conflicts with simultaneous probes (a rare race condition). If * the two probes have conflicting data for the same record set, they are * compared and the losing probe has to wait 1 second and try again. * (See: 8.2.1. Simultaneous Probe Tiebreaking for Multiple Records) * * Note: this handle will receive this probe's packets too * * @param {Packet} packet - the incoming probe packet */ Probe.prototype._onProbe = function (packet) { var _this3 = this; if (this._isStopped) return; debug('Checking probe for conflicts (' + this._id + ')'); // Prevent probe from choking on cooperating probe packets in the event that // they get bridged over another interface. (Eg: AAAA record from interface 1 // shouldn't conflict with a bridged AAAA record from interface 2, even though // the interfaces have different addresses.) Just ignore simultaneous probes // from the same machine and not deal with it. if (packet.isLocal()) { return debug('Local probe, ignoring (' + this._id + ')'); } // Prep records: // - split into groups by record name // - uppercase name so they can be compared case-insensitively // - sort record array by ascending rrtype // // { // 'NAME1': [records], // 'NAME2': [records] // } var local = {}; var incoming = {}; var has = function has(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }; this._authorities.toArray().forEach(function (r) { var key = r.name.toUpperCase(); if (has(local, key)) local[key].push(r);else local[key] = [r]; }); packet.authorities.forEach(function (r) { var key = r.name.toUpperCase(); // only include those that appear in the other group if (has(local, key)) { if (has(incoming, key)) incoming[key].push(r);else incoming[key] = [r]; } }); Object.keys(local).forEach(function (key) { local[key] = local[key].sort(function (a, b) { return a.rrtype - b.rrtype; }); }); Object.keys(incoming).forEach(function (key) { incoming[key] = incoming[key].sort(function (a, b) { return a.rrtype - b.rrtype; }); }); // Look for conflicts in each group of records. IE, if there are records // named 'A' and records named 'B', look at each set. 'A' records first, // and then 'B' records. Stops at the first conflict. var hasConflict = Object.keys(local).some(function (name) { if (!incoming[name]) return false; return _this3._recordsHaveConflict(local[name], incoming[name]); }); // If this probe is found to be in conflict it has to pause for 1 second // before trying again. A legitimate competing probe should have completed // by then and can then authoritatively respond to this probe, causing this // one to fail. if (hasConflict) { this._timers.clear(); this._timers.setLazy('restart', this._restart, 1000); } }; /** * Compares two records sets lexicographically * * Records are compared, pairwise, in their sorted order, until a difference * is found or until one of the lists runs out. If no differences are found, * and record lists are the same length, then there is no conflict. * * Returns true if there was a conflict with this probe's records and false * if this probe is ok. * * @param {ResourceRecords[]} records * @param {ResourceRecords[]} incomingRecords * @return {Boolean} */ Probe.prototype._recordsHaveConflict = function (records, incomingRecords) { debug('Checking for lexicographic conflicts with other probe:'); var hasConflict = false; var pairs = []; for (var i = 0; i < Math.max(records.length, incomingRecords.length); i++) { pairs.push([records[i], incomingRecords[i]]); } pairs.forEach(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), record = _ref2[0], incoming = _ref2[1]; debug('Comparing: %s', record); debug(' with: %s', incoming); // this probe has LESS records than other probe, this probe LOST if (typeof record === 'undefined') { hasConflict = true; return false; // stop comparing } // this probe has MORE records than other probe, this probe WON if (typeof incoming === 'undefined') { hasConflict = false; return false; // stop comparing } var comparison = record.compare(incoming); // record is lexicographically earlier than incoming, this probe LOST if (comparison === -1) { hasConflict = true; return false; // stop comparing } // record is lexicographically later than incoming, this probe WON if (comparison === 1) { hasConflict = false; return false; // stop comparing } // otherwise, if records are lexicographically equal, continue and // check the next record pair }); debug('Lexicographic conflict %s', hasConflict ? 'found' : 'not found'); return hasConflict; }; module.exports = Probe;