UNPKG

dnssd

Version:

Bonjour/Avahi-like service discovery in pure JavaScript

256 lines (198 loc) 7.74 kB
const misc = require('./misc'); const ServiceType = require('./ServiceType'); const EventEmitter = require('./EventEmitter'); let ServiceResolver = require('./ServiceResolver'); let NetworkInterface = require('./NetworkInterface'); let Query = require('./Query'); const filename = require('path').basename(__filename); const debug = require('./debug')(`dnssd:${filename}`); const RType = require('./constants').RType; const STATE = {STOPPED: 'stopped', STARTED: 'started'}; /** * Creates a new Browser * * @emits 'serviceUp' * @emits 'serviceChanged' * @emits 'serviceDown' * @emits 'error' * * @param {ServiceType|Object|String|Array} type - the service to browse * @param {Object} [options] */ function Browser(type, options = {}) { if (!(this instanceof Browser)) return new Browser(type, options); EventEmitter.call(this); // convert argument ServiceType to validate it (might throw) const serviceType = (type instanceof ServiceType) ? type : new ServiceType(type); // can't search for multiple subtypes at the same time if (serviceType.subtypes.length > 1) { throw new Error('Too many subtypes. Can only browse one at a time.'); } this._id = serviceType.toString(); debug(`Creating new browser for "${this._id}"`); this._resolvers = {}; // active service resolvers (when browsing services) this._serviceTypes = {}; // active service types (when browsing service types) this._protocol = serviceType.protocol; this._serviceName = serviceType.name; this._subtype = serviceType.subtypes[0]; this._isWildcard = serviceType.isEnumerator; this._domain = options.domain || 'local.'; this._maintain = ('maintain' in options) ? options.maintain : true; this._resolve = ('resolve' in options) ? options.resolve : true; this._interface = NetworkInterface.get(options.interface); this._state = STATE.STOPPED; // emitter used to stop child queries instead of holding onto a reference // for each one this._offswitch = new EventEmitter(); } Browser.prototype = Object.create(EventEmitter.prototype); Browser.prototype.constructor = Browser; /** * Starts browser * @return {this} */ Browser.prototype.start = function() { if (this._state === STATE.STARTED) { debug('Browser already started!'); return this; } debug(`Starting browser for "${this._id}"`); this._state = STATE.STARTED; // listen for fatal errors on interface this._interface.using(this).once('error', this._onError); this._interface.bind() .then(() => this._startQuery()) .catch(err => this._onError(err)); return this; }; /** * Stops browser. * * Browser shutdown has to: * - shut down all child service resolvers (they're no longer needed) * - stop the ongoing browsing queries on all interfaces * - remove all listeners since the browser is down * - deregister from the interfaces so they can shut down if needed */ Browser.prototype.stop = function() { debug(`Stopping browser for "${this._id}"`); this._interface.removeListenersCreatedBy(this); this._interface.stopUsing(); debug('Sending stop signal to active queries'); this._offswitch.emit('stop'); // because resolver.stop()'s will trigger serviceDown: this.removeAllListeners('serviceDown'); Object.values(this._resolvers).forEach(resolver => resolver.stop()); this._state = STATE.STOPPED; this._resolvers = {}; this._serviceTypes = {}; }; /** * Get a list of currently available services * @return {Objects[]} */ Browser.prototype.list = function() { // if browsing service types if ((this._isWildcard)) { return Object.values(this._serviceTypes); } return Object.values(this._resolvers) .filter(resolver => resolver.isResolved()) .map(resolver => resolver.service()); }; /** * Error handler * @emits 'error' */ Browser.prototype._onError = function(err) { debug(`Error on "${this._id}", shutting down. Got: \n${err}`); this.stop(); this.emit('error', err); }; /** * Starts the query for either services (like each available printer) * or service types using enumerator (listing all mDNS service on a network). * Queries are sent out on each network interface the browser uses. */ Browser.prototype._startQuery = function() { let name = misc.fqdn(this._serviceName, this._protocol, this._domain); if (this._subtype) name = misc.fqdn(this._subtype, '_sub', name); const question = { name, qtype: RType.PTR }; const answerHandler = (this._isWildcard) ? this._addServiceType.bind(this) : this._addService.bind(this); // start sending continuous, ongoing queries for services new Query(this._interface, this._offswitch) .add(question) .on('answer', answerHandler) .start(); }; /** * Answer handler for service types. Adds type and alerts user. * * @emits 'serviceUp' with new service types * @param {ResourceRecord} answer */ Browser.prototype._addServiceType = function(answer) { const name = answer.PTRDName; if (this._state === STATE.STOPPED) return debug.v('Already stopped, ignoring'); if (answer.ttl === 0) return debug.v('TTL=0, ignoring'); if (this._serviceTypes[name]) return debug.v('Already found, ignoring'); debug(`Found new service type: "${name}"`); let { service, protocol } = misc.parse(name); // remove any leading underscores for users service = service.replace(/^_/, ''); protocol = protocol.replace(/^_/, ''); const serviceType = { name: service, protocol }; this._serviceTypes[name] = serviceType; this.emit('serviceUp', serviceType); }; /** * Answer handler for services. * * New found services cause a ServiceResolve to be created. The resolver * parse the additionals and query out for an records needed to fully * describe the service (hostname, IP, port, TXT). * * @emits 'serviceUp' when a new service is found * @emits 'serviceChanged' when a resolved service changes data (IP, etc.) * @emits 'serviceDown' when a resolved service goes down * * @param {ResourceRecord} answer - the record that has service data * @param {ResourceRecord[]} [additionals] - other records that might be related */ Browser.prototype._addService = function(answer, additionals) { const name = answer.PTRDName; if (this._state === STATE.STOPPED) return debug.v('Already stopped, ignoring'); if (answer.ttl === 0) return debug.v('TTL=0, ignoring'); if (this._resolvers[name]) return debug.v('Already found, ignoring'); debug(`Found new service: "${name}"`); if (!this._resolve) { this.emit('serviceUp', misc.parse(name).instance); return; } const resolver = new ServiceResolver(name, this._interface); this._resolvers[name] = resolver; resolver.once('resolved', () => { debug('Service up'); // - stop resolvers that dont need to be maintained // - only emit 'serviceDown' events once services that have been resolved if (!this._maintain) { resolver.stop(); this._resolvers[name] = null; } else { resolver.once('down', () => this.emit('serviceDown', resolver.service())); } this.emit('serviceUp', resolver.service()); }); resolver.on('updated', () => { debug('Service updated'); this.emit('serviceChanged', resolver.service()); }); resolver.once('down', () => { debug('Service down'); delete this._resolvers[name]; }); resolver.start(additionals); }; module.exports = Browser;