UNPKG

electron-dns-sd

Version:

The electron-dns-sd is a Node.js module which is a pure javascript implementation of mDNS/DNS-SD (Apple Bonjour) browser and packet parser. It allows you to discover IPv4 addresses in the local network specifying a service name such as `_http._tcp.local`.

602 lines (557 loc) 15.4 kB
/* ------------------------------------------------------------------ * node-dns-sd - dns-sd.js * * Copyright (c) 2018 - 2020, Futomi Hatano, All rights reserved. * Released under the MIT license * Date: 2020-09-30 * ---------------------------------------------------------------- */ 'use strict'; let mDgram; if (typeof window !== 'undefined' && typeof window.dgram !== undefined) mDgram = window.dgram; else mDgram = require('dgram'); // dgram is UDP let mOs; if (typeof window !== 'undefined' && typeof window.os !== undefined) mOs = window.os; else mOs = require('os'); const mDnsSdParser = require('./dns-sd-parser.js'); const mDnsSdComposer = require('./dns-sd-composer.js'); /* ------------------------------------------------------------------ * Constructor: DnsSd() * ---------------------------------------------------------------- */ const DnsSd = function () { // Public this.ondata = () => {}; // Private this._MULTICAST_ADDR = '224.0.0.251'; this._UDP_PORT = 5353; this._DISCOVERY_WAIT_DEFAULT = 3; // sec this._udp = null; this._source_address_list = []; this._discovered_devices = {}; this._is_discovering = false; this._is_monitoring = false; this._is_listening = false; this._onreceive = () => {}; }; /* ------------------------------------------------------------------ * Method: discover(params) * - params | Object | Required | * - name | String or | Required | Servcie name.(e.g., "_googlecast._tcp.local") * | Array | | * - type | String | Optional | Query Type (e.g., "PTR"). The default value is "*". * - key | String | Optional | "address" (default) or "fqdn". * | | | - "address": IP address based discovery * | | | - "fqdn": FQDN (service) based discovery * - wait | Integer | Optional | Duration of monitoring. The default value is 3 (sec). * - quick | Boolean | Optional | If `true`, this method returns immediately after * | | | a device was found ignoring the value of the `wait`. * | | | The default value is `false`. * - filter | String or | Optional | If specified as a string, this method discovers only * | Function | | devices which the string is found in the `fqdn`, * | | | `address`, `modelName` or `familyName`. * | | | If specified as a function, this method discovers * | | | only devices for which the function returns `true`. * ---------------------------------------------------------------- */ DnsSd.prototype.discover = function (params) { let promise = new Promise((resolve, reject) => { if (this._is_discovering === true) { reject(new Error('The discovery process is running.')); return; } // Check the parameters let res = this._checkDiscoveryParameters(params); if (res['error']) { reject(res['error']); return; } let device_list = []; this._startListening() .then(() => { return this._startDiscovery(res['params']); }) .then(() => { for (let addr in this._discovered_devices) { let device = this._discovered_devices[addr]; device_list.push(device); } this._stopDiscovery().then(() => { resolve(device_list); }); }) .catch((error) => { this._stopDiscovery().then(() => { reject(error); }); }); }); return promise; }; DnsSd.prototype._createDeviceObject = function (packet) { let o = {}; let trecs = {}; ['answers', 'authorities', 'additionals'].forEach((k) => { packet[k].forEach((r) => { let type = r['type']; if (!trecs[type]) { trecs[type] = []; } trecs[type].push(r); }); }); o['address'] = null; if (trecs['A']) { o['address'] = trecs['A'][0]['rdata']; } if (!o['address']) { o['address'] = packet['address']; } o['fqdn'] = null; let hostname = null; if (trecs['PTR']) { let rec = trecs['PTR'][0]; o['fqdn'] = rec['rdata']; } o['modelName'] = null; o['familyName'] = null; if (trecs['TXT'] && trecs['TXT'][0] && trecs['TXT'][0]['rdata']) { let r = trecs['TXT'][0]; let d = r['rdata'] || {}; let name = r['name'] || ''; if (/Apple TV/.test(name)) { o['modelName'] = 'Apple TV'; if (trecs['TXT']) { for (let i = 0; i < trecs['TXT'].length; i++) { let r = trecs['TXT'][i]; if (/_device-info/.test(r['name']) && r['rdata'] && r['rdata']['model']) { o['modelName'] = 'Apple TV ' + r['rdata']['model']; break; } } } } else if (/_googlecast/.test(name)) { o['modelName'] = d['md'] || null; o['familyName'] = d['fn'] || null; } else if (/Philips hue/.test(name)) { o['modelName'] = 'Philips hue'; if (d['md']) { o['modelName'] += ' ' + d['md']; } } else if (/Canon/.test(name)) { o['modelName'] = d['ty'] || null; } } if (!o['modelName']) { if (trecs['A'] && trecs['A'][0]) { let r = trecs['A'][0]; let name = r['name']; if (/Apple\-TV/.test(name)) { o['modelName'] = 'Apple TV'; } else if (/iPad/.test(name)) { o['modelName'] = 'iPad'; } } } if (!o['modelName']) { if (o['fqdn']) { let hostname = o['fqdn'].split('.').shift(); if (hostname && / /.test(hostname)) { o['modelName'] = hostname; } } } o['service'] = null; if (trecs['SRV']) { let rec = trecs['SRV'][0]; let name_parts = rec['name'].split('.'); name_parts.reverse(); o['service'] = { port: rec['rdata']['port'], protocol: name_parts[1].replace(/^_/, ''), type: name_parts[2].replace(/^_/, ''), }; } o['packet'] = packet; return o; }; DnsSd.prototype._checkDiscoveryParameters = function (params) { let p = {}; if (params) { if (typeof params !== 'object') { return { error: new Error('The argument `params` is invalid.') }; } } else { return { error: new Error('The argument `params` is required.') }; } if ('name' in params) { let v = params['name']; if (typeof v === 'string') { if (v === '') { return { error: new Error('The `name` must be an non-empty string.') }; } p['name'] = [v]; } else if (Array.isArray(v)) { if (v.length === 0) { return { error: new Error('The `name` must be a non-empty array.') }; } else if (v.length > 255) { return { error: new Error('The `name` can include up to 255 elements.') }; } let err = null; let list = []; for (let i = 0; i < v.length; i++) { if (typeof v[i] === 'string' && v[i] !== '') { list.push(v[i]); } else { err = new Error('The `name` must be an Array object including non-empty strings.'); break; } } if (err) { return { error: err }; } p['name'] = list; } else { return { error: new Error('The `name` must be a string or an Array object.') }; } } else { return { error: new Error('The `name` is required.') }; } if ('type' in params) { let v = params['type']; if (typeof v !== 'string' || !(/^[a-zA-Z0-9]{1,10}$/.test(v) || v === '*')) { return { error: new Error('The `type` is invalid.') }; } p['type'] = v.toUpperCase(); } if ('key' in params) { let v = params['key']; if (typeof v !== 'string' || !/^(address|fqdn)$/.test(v)) { return { error: new Error('The `key` is invalid.') }; } if (!v) { v = 'address'; } p['key'] = v; } if ('wait' in params) { let v = params['wait']; if (typeof v !== 'number' || v <= 0 || v % 1 !== 0) { return { error: new Error('The `wait` is invalid.') }; } p['wait'] = v; } if ('quick' in params) { let v = params['quick']; if (typeof v !== 'boolean') { return { error: new Error('The `quick` must be a boolean.') }; } p['quick'] = v; } else { p['quick'] = false; } if (`filter` in params) { let v = params['filter']; if (typeof v !== 'string' && typeof v !== 'function') { return { error: new Error('The `filter` must be a string.') }; } if (v) { p['filter'] = v; } } return { params: p }; }; DnsSd.prototype._startDiscovery = function (params) { let promise = new Promise((resolve, reject) => { this._discovered_devices = {}; this._is_discovering = true; let wait = params && params['wait'] ? params['wait'] : this._DISCOVERY_WAIT_DEFAULT; // Create a request packet let buf = mDnsSdComposer.compose({ name: params['name'], type: params['type'], }); // Timer let send_timer = null; let wait_timer = null; let clearTimer = () => { if (send_timer) { clearTimeout(send_timer); send_timer = null; } if (wait_timer) { clearTimeout(wait_timer); wait_timer = null; } this._onreceive = () => {}; }; wait_timer = setTimeout(() => { clearTimer(); resolve(); }, wait * 1000); let quick = params['quick']; let key = params['key']; if (!key) { key = 'address'; } this._onreceive = (addr, packet) => { if (!this._isTargettedDevice(packet, params['name'])) { return; } let device = this._createDeviceObject(packet); if (!this._evaluateDeviceFilter(device, params['filter'])) { return; } if (key === 'fqdn') { let fqdn = device['fqdn']; this._discovered_devices[fqdn] = device; } else { this._discovered_devices[addr] = device; } if (quick) { clearTimer(); resolve(); } }; // Send a packet let send_num = 0; let sendQueryPacket = () => { this._udp.send(buf, 0, buf.length, this._UDP_PORT, this._MULTICAST_ADDR, (error, bytes) => { if (error) { clearTimer(); reject(error); } else { send_num++; if (send_num < 3) { send_timer = setTimeout(() => { sendQueryPacket(); }, 1000); } else { send_timer = null; } } }); }; sendQueryPacket(); }); return promise; }; DnsSd.prototype._isTargettedDevice = function (packet, name_list) { let hit = false; packet['answers'].forEach((ans) => { let name = ans['name']; if (name && name_list.indexOf(name) >= 0) { hit = true; } }); return hit; }; DnsSd.prototype._evaluateDeviceFilter = function (device, filter) { if (filter) { let filter_type = typeof filter; if (filter_type === 'string') { return this._evaluateDeviceFilterString(device, filter); } else if (filter_type === 'function') { return this._evaluateDeviceFilterFunction(device, filter); } else { return false; } } else { return true; } }; DnsSd.prototype._evaluateDeviceFilterString = function (device, filter) { if (device['fqdn'] && device['fqdn'].indexOf(filter) >= 0) { return true; } if (device['address'] && device['address'].indexOf(filter) >= 0) { return true; } if (device['modelName'] && device['modelName'].indexOf(filter) >= 0) { return true; } if (device['familyName'] && device['familyName'].indexOf(filter) >= 0) { return true; } return false; }; DnsSd.prototype._evaluateDeviceFilterFunction = function (device, filter) { let res = false; try { res = filter(device); } catch (e) {} res = res ? true : false; return res; }; DnsSd.prototype._stopDiscovery = function () { let promise = new Promise((resolve, reject) => { this._discovered_devices = {}; this._is_discovering = false; this._stopListening() .then(() => { resolve(); }) .catch((error) => { resolve(); }); }); return promise; }; DnsSd.prototype._addMembership = function () { this._source_address_list.forEach((netif) => { try { this._udp.addMembership(this._MULTICAST_ADDR, netif); } catch (e) { console.log(`Catching error on address already in use: ${JSON.stringify(e)}`); } }); }; DnsSd.prototype._dropMembership = function () { this._source_address_list.forEach((netif) => { try { this._udp.dropMembership(this._MULTICAST_ADDR, netif); } catch (e) { console.log(`Catching error on dropMembership: ${JSON.stringify(e)}`); } }); }; DnsSd.prototype._getSourceAddressList = function () { let list = []; let netifs = mOs.networkInterfaces(); for (let dev in netifs) { netifs[dev].forEach((info) => { if (info.family === 'IPv4' && info.internal === false) { let m = info.address.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/); if (m) { list.push(m[1]); } } }); } return list; }; /* ------------------------------------------------------------------ * Method: startMonitoring() * ---------------------------------------------------------------- */ DnsSd.prototype.startMonitoring = function () { let promise = new Promise((resolve, reject) => { if (this._is_monitoring === true) { resolve(); return; } this._startListening() .then(() => { this._is_monitoring = true; resolve(); }) .catch((error) => { this._is_monitoring = false; this._stopListening().then(() => { reject(error); }); }); }); return promise; }; /* ------------------------------------------------------------------ * Method: stopMonitoring() * ---------------------------------------------------------------- */ DnsSd.prototype.stopMonitoring = function () { let promise = new Promise((resolve, reject) => { this._is_monitoring = false; this._stopListening() .then(() => { resolve(); }) .catch((error) => { resolve(); }); }); return promise; }; DnsSd.prototype._startListening = function () { let promise = new Promise((resolve, reject) => { if (this._is_listening === true) { resolve(); return; } // Get the source IP address this._source_address_list = this._getSourceAddressList(); // Set up a UDP tranceiver this._udp = mDgram.createSocket({ type: 'udp4', reuseAddr: true, }); this._udp.once('error', (error) => { this._is_listening = false; reject(error); return; }); this._udp.once('listening', () => { this._udp.setMulticastLoopback(false); this._addMembership(); this._is_listening = true; resolve(); return; }); this._udp.on('message', (buf, rinfo) => { this._receivePacket(buf, rinfo); }); this._udp.bind({ port: this._UDP_PORT }, () => { this._udp.removeAllListeners('error'); }); }); return promise; }; DnsSd.prototype._stopListening = function () { let promise = new Promise((resolve, reject) => { this._dropMembership(); if (this._is_discovering || this._is_monitoring) { resolve(); } else { let cleanObj = () => { if (this._udp) { this._udp.unref(); this._udp = null; } this._is_listening = false; resolve(); }; if (this._udp) { this._udp.removeAllListeners('message'); this._udp.removeAllListeners('error'); this._udp.removeAllListeners('listening'); this._udp.close(() => { cleanObj(); }); } else { cleanObj(); } } }); return promise; }; DnsSd.prototype._receivePacket = function (buf, rinfo) { let p = mDnsSdParser.parse(buf); if (!p) { return; } p['address'] = rinfo.address; if (this._is_discovering) { if (this._isAnswerPacket(p, rinfo.address)) { this._onreceive(rinfo.address, p); } } if (this._is_monitoring) { if (typeof this.ondata === 'function') { this.ondata(p); } } }; DnsSd.prototype._isAnswerPacket = function (p, address) { if (this._source_address_list.indexOf(address) >= 0) { return false; } if (!(p['header']['qr'] === 1 && p['header']['op'] === 0)) { return false; } return true; }; module.exports = new DnsSd();