[
  {
    "path": ".gitignore",
    "content": "node_modules\ndiagnostics-server.log\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - '4.7.3'\n  - '5.10'\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Mathias Buus\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# dns-discovery\n\nDiscovery peers in a distributed system using regular dns and multicast dns.\n\n```\nnpm install dns-discovery\n```\n\n[![build status](http://img.shields.io/travis/mafintosh/dns-discovery.svg?style=flat)](http://travis-ci.org/mafintosh/dns-discovery)\n\n## Usage\n\n``` js\nvar discovery = require('dns-discovery')\n\nvar disc1 = discovery()\nvar disc2 = discovery()\n\ndisc1.on('peer', function (name, peer) {\n  console.log(name, peer)\n})\n\n// announce an app\ndisc2.announce('test-app', 9090)\n```\n\n## API\n\n#### `var disc = discovery([options])`\n\nCreate a new discovery instance. Options include:\n\n``` js\n{\n  server: 'discovery.example.com:9090', // put a centralized dns discovery server here\n  ttl: someSeconds, // ttl for records in seconds. defaults to Infinity.\n  limit: someLimit, // max number of records stored. defaults to 10000.\n  multicast: true, // use multicast-dns. defaults to true.\n  domain: 'my-domain.com', // top-level domain to use for records. defaults to dns-discovery.local\n  socket: someUdpSocket, // use this udp socket as the client socket\n  loopback: false // discover yourself over multicast\n}\n```\n\nIf you have more than one discovery server you can specify an array\n\n``` js\n{\n  server: [\n    'discovery.example.com:9090',\n    'another.discovery.example.com'\n  ]\n}\n```\n\n#### `disc.lookup(name, [callback])`\n\nDo a lookup for a specific app name. When new peers are discovered for this name peer events will be emitted. The callback will be called when the query is complete.\n\n``` js\ndisc.on('peer', function (name, peer) {\n  console.log(name) // app name this peer was discovered for (ie 'example')\n  console.log(peer) // {host: 'some-ip', port: 1234}\n})\ndisc.lookup('example')\n```\n\n#### `disc.announce(name, port, [options], [callback])`\n\nAnnounce a new port for a specific app name. Announce also does a lookup so you don't need to do that afterwards.\n\nIf you want to specify a public port (a port that is reachable from outside your firewall) you can set the `publicPort: port`\noption. This will announce the public port to your list of dns servers and use the other port over multicast.\n\nYou can also set `impliedPort: true` to announce the public port of the dns socket to the list of dns servers.\n\n#### `disc.unannounce(name, port, [options], [callback])`\n\nStop announcing a port for an app. Has the same options as .announce\n\n#### `disc.listen([port], [callback])`\n\nListen for dns records on a specific port. You *only* need to call this if you want to turn your peer into a discovery server that other peers can use to store peer objects on.\n\n``` js\nvar server = discovery()\nserver.listen(9090, function () {\n  var disc = discovery({server: 'localhost:9090'})\n  disc.announce('test-app', 8080) // will announce this record to the above discovery server\n})\n```\n\nYou can setup a discovery server to announce records on the internet as multicast-dns only works on a local network.\nThe port defaults to `53` which is the standard dns port. Additionally it tries to bind to `5300` to support networks that filter dns traffic.\n\n#### `disc.destroy([onclose])`\n\nDestroy the discovery instance. Will destroy the underlying udp socket as well.\n\n#### `Event: \"listening\"`\n\nEmitted after a successful `listen()`.\n\n#### `Event: \"close\"`\n\nEmitted after a successful `destroy()`.\n\n#### `Event: \"peer\" (name, {host, port})\n\nEmitted when a peer has been discovered.\n\n - **name** The app name the peer was discovered for.\n - **host** The address of the peer.\n - **port** The port the peer is listening on.\n\n#### `Event: \"announced\" (name, {port})`\n\nEmitted after a successful `announce()`.\n\n - **name** The app name that was announced.\n - **port** The port that was announced.\n\n#### `Event: \"unannounced\" (name, {port})`\n\nEmitted after a successful `unannounce()`.\n\n - **name** The app name that was unannounced.\n - **port** The port that was unannounced.\n\n#### `Event: \"traffic\" (type, details)`\n\nEmitted when any kind of message event occurs. The `type` will be prefixed with `'in:'` to indicate inbound, and `'out:'` to indicate outbound messages. This event is mostly useful for debugging.\n\n#### `Event: \"secrets-rotated\"`\n\nEmitted when the internal secrets used to generate session tokens have been rotated. This event is mostly useful for debugging.\n\n#### `Event: \"error\" (err)`\n\nEmitted when networking errors occur, such as failures to bind the socket (EACCES, EADDRINUSE).\n\n## CLI\n\nThere is a cli tool available as well\n\n``` sh\nnpm install -g dns-discovery\ndns-discovery help\n```\n\nTo announce a service do\n\n``` sh\n# will announce test-app over multicast-dns\ndns-discovery announce test-app --port=8080\n```\n\nTo look it up\n\n``` sh\n# will print services when they are found\ndns-discovery lookup test-app\n```\n\nTo run a discovery server\n\n``` sh\n# listen for services and store them with a ttl of 30s\ndns-discovery listen --port=9090 --ttl=30\n```\n\nAnd to announce to that discovery server (and over multicast-dns)\n\n``` sh\n# replace example.com with the host of the server running the discovery server\ndns-discovery announce test-app --server=example.com:9090 --port=9090\n```\n\nAnd finally to lookup using that discovery server (and multicast-dns)\n\n``` sh\ndns-discovery lookup test-app --server=example.com:9090\n```\n\nYou can use any other dns client to resolve the records as well. For example using `dig`.\n\n``` sh\n# dig requires the discovery server to run on port 53\ndig @discovery.example.com test-app SRV\n```\n\n## License\n\nMIT\n"
  },
  {
    "path": "bin.js",
    "content": "#!/usr/bin/env node\n\nvar discovery = require('./')\nvar minimist = require('minimist')\n\nvar argv = minimist(process.argv.slice(2), {\n  alias: {port: 'p', host: 'h', server: 's', domain: 'd'}\n})\n\nvar rcvd = {}\nvar cmd = argv._[0]\nvar disc = discovery(argv)\n\nif (cmd === 'listen') {\n  disc.listen(argv.port, onlisten)\n  if (argv.diag) {\n    var diagServer = require('./diagnostics-server').createServer(disc, {password: argv.diagpw})\n    diagServer.listen((typeof argv.diag === 'number' ? argv.diag : 3030), ondiaglisten)\n  }\n} else if (cmd === 'lookup') {\n  disc.on('peer', onpeer)\n  lookup()\n  setInterval(lookup, 1000)\n} else if (cmd === 'announce') {\n  if (!argv.port) throw new Error('You need to specify --port')\n  announce()\n  setInterval(announce, 1000)\n} else {\n  console.error(\n    'dns-discovery [command]\\n' +\n    '  announce [name]\\n' +\n    '    --port=(port)\\n' +\n    '    --host=(optional host)\\n' +\n    '    --server=(optional discovery server)\\n' +\n    '    --domain=(optional authoritative domain)\\n' +\n    '  lookup [name]\\n' +\n    '    --server=(optional discovery server)\\n' +\n    '    --domain=(optional authoritative domain)\\n' +\n    '  listen\\n' +\n    '    --port=(optional port)\\n' +\n    '    --ttl=(optional ttl in seconds)\\n' +\n    '    --domain=(optional authoritative domain)\\n' +\n    '    --diag=(enable diagnostic server, optional port, default 3030)\\n' +\n    '    --diagpw=(optional password for diagnostic server)'\n  )\n  process.exit(1)\n}\n\nfunction lookup () {\n  disc.lookup(argv._[1])\n}\n\nfunction announce () {\n  disc.announce(argv._[1], argv.port)\n}\n\nfunction onpeer (name, peer) {\n  var addr = peer.host + ':' + peer.host\n  if (rcvd[addr]) return\n  rcvd[addr] = true\n  console.log(name, peer)\n}\n\nfunction onlisten (err) {\n  if (err) throw err\n  console.log('Server is listening on port %d', argv.port || 53)\n}\n\nfunction ondiaglisten (err) {\n  if (err) throw err\n  console.log('Diagnostics server is listening on port %d', (typeof argv.diag === 'number' ? argv.diag : 3030))\n}\n"
  },
  {
    "path": "diagnostics-server/index.css",
    "content": "html {\n  font-family: sans-serif;\n}\n\nh3 {\n  font-weight: normal;\n}\n\ntd {\n  border: 1px solid #ccc;\n  padding: 5px;\n}\n\n#nav {\n  font-size: 19px;\n  border-bottom: 1px solid gray;\n  padding-bottom: 10px;\n  margin-bottom: 10px;\n}\n#nav a {\n  color: gray;\n  margin-right: 10px;\n}\n#nav a:not(.active):hover {\n  text-decoration: underline;\n  cursor: pointer;\n}\n#nav a.active {\n  color: black;\n  text-decoration: underline;\n}\n\n#views > div {\n  display: none;\n}\n#views > div.active {\n  display: block;\n}\n\n.collapsable {\n  display: none;\n}\n.collapsable.open {\n  display: block;\n}\n.toggle-collapsable {\n  color: blue;\n  cursor: pointer;\n}\n.toggle-collapsable:hover {\n  text-decoration: underline;\n}"
  },
  {
    "path": "diagnostics-server/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>DNS Discovery Server Diagnostics Panel</title>\n    <link rel=\"stylesheet\" href=\"/index.css\">\n  </head>\n\n  <body>\n    <div id=\"nav\">\n      <a id=\"nav-state\" class=\"active\" data-view=\"state\">State</a>\n      <a id=\"nav-log\" data-view=\"log\">Log</a>\n    </div>\n\n    <div id=\"views\">\n      <div id=\"view-state\"></div>\n      <div id=\"view-log\"></div>\n    </div>\n\n    <script src=\"/index.js\"></script>\n  </body>\n</html>"
  },
  {
    "path": "diagnostics-server/index.js",
    "content": "/* globals fetch */\n\nconst navAs = Array.from(document.querySelectorAll('#nav > a'))\nconst viewDivs = Array.from(document.querySelectorAll('#views > div'))\nvar currentPoll\n\n// register ui events\nnavAs.forEach(a => {\n  a.addEventListener('click', () => {\n    setView(a.dataset.view)\n  })\n})\ndocument.body.addEventListener('click', e => {\n  console.log('click', e.target, e.target.classList.contains('toggle-collapsable'))\n  if (e.target.classList.contains('toggle-collapsable')) {\n    document.getElementById(e.target.dataset.target).classList.toggle('open')\n  }\n})\n\nfunction setView (view) {\n  navAs.forEach(a => a.classList.remove('active'))\n  viewDivs.forEach(div => div.classList.remove('active'))\n  document.querySelector('#nav-' + view).classList.add('active')\n  document.querySelector('#view-' + view).classList.add('active')\n  clearInterval(currentPoll)\n  setupView[view]()\n}\n\nconst setupView = {\n  'state': fetchAndRenderPeers,\n  'log': fetchAndRenderLog\n}\n\nasync function fetchAndRenderPeers () {\n  let state = await (await fetch('/state.json', {credentials: 'include'})).json()\n  let html = `\n    <h2>Stats</h2>\n\n    <h3>Loadavg: [ ${state.stats.loadavg[0]} (1m), ${state.stats.loadavg[1]} (5m), ${state.stats.loadavg[2]} (15m) ]</h3>\n\n    <h3>Queries/sec: ${state.stats.queriesPS[0]} <small><a class=\"toggle-collapsable\" data-target=\"queriesPS\">toggle history</a></small></h3>\n    <div class=\"collapsable\" id=\"queriesPS\">\n      <table>${state.stats.queriesPS.map(renderHistoryItem).join('')}</table>\n    </div>\n\n    <h3>Multicast Queries/sec: ${state.stats.multicastQueriesPS[0]} <small><a class=\"toggle-collapsable\" data-target=\"multicastQueriesPS\">toggle history</a></small></h3>\n    <div class=\"collapsable\" id=\"multicastQueriesPS\">\n      <table>${state.stats.multicastQueriesPS.map(renderHistoryItem).join('')}</table>\n    </div>\n\n    <h2>Top keys</h2>\n    <table>${state.stats.topKeys.map(entry => `<tr><td>${safen(entry.name)}</td><td>${safen(entry.numRecords)} peers</td></tr>`).join('')}</table>\n    \n    <h2>Peer tables</h2>\n    ${state.peers.map(peerGroup => `\n      <h3>Key: ${safen(peerGroup.name)}</h3>\n      <table>\n        ${peerGroup.records.map(record => (`\n          <tr><td>${safen(record.address)}</td></tr>\n        `)).join('')}\n      </table>\n    `).join('')}\n  `\n  document.querySelector('#view-state').innerHTML = html\n}\n\nfunction renderHistoryItem (value, i) {\n  return `<tr><td>-${i * 10}s</td><td>${value}</td></tr>`\n}\n\nasync function fetchAndRenderLog () {\n  let log = await (await fetch('/log.txt', {credentials: 'include'})).text()\n  let html = `<pre>${log}</pre>`\n  document.querySelector('#view-log').innerHTML = html\n}\n\nsetView('state')\n\nfunction safen (str) {\n  return ('' + str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/&/g, '&amp;').replace(/\"/g, '')\n}\n"
  },
  {
    "path": "diagnostics-server.js",
    "content": "var os = require('os')\nvar http = require('http')\nvar fs = require('fs')\nvar pump = require('pump')\nvar speedometer = require('speedometer')\nvar CircularAppendFile = require('circular-append-file')\n\n// capture the last 10 minutes of stats\nconst HISTORY_LIMIT = 60\nconst HISTORY_INTERVAL = 10e3\nconst LOG_FILE_PATH = './diagnostics-server.log'\nconst LOG_SIZE_LIMIT = 1024 /* 1kb */ * 1024 /* 1mb */ * 32 /* 32mb */\n\nvar queriesSpeed = speedometer()\nvar multicastQueriesSpeed = speedometer()\nvar queriesPS = []\nvar multicastQueriesPS = []\n\nexports.createServer = function (disc, opts = {}) {\n  // logging\n  var logFile = CircularAppendFile(LOG_FILE_PATH, {maxSize: LOG_SIZE_LIMIT})\n  function track (evt) {\n    disc.on(evt, (...args) => {\n      logFile.append(renderLogEntry(evt, (new Date()).toLocaleString(), args))\n    })\n  }\n  track('traffic')\n  track('secrets-rotated')\n  track('error')\n  track('listening')\n  track('close')\n  track('peer')\n\n  // stats\n  disc.on('traffic', (type) => {\n    if (type === 'in:query') {\n      queriesSpeed(1)\n    }\n    if (type === 'in:multicastquery') {\n      multicastQueriesSpeed(1)\n    }\n  })\n  setInterval(() => {\n    queriesPS.unshift(queriesSpeed())\n    if (queriesPS.length > HISTORY_LIMIT) queriesPS.pop()\n    multicastQueriesPS.unshift(multicastQueriesSpeed())\n    if (multicastQueriesPS.length > HISTORY_LIMIT) multicastQueriesPS.pop()\n  }, HISTORY_INTERVAL)\n\n  // server\n  return http.createServer((req, res) => {\n    // auth\n    if (opts.password) {\n      var auth = req.headers.authorization\n      if (!auth) {\n        res.writeHead(401, {'WWW-Authenticate': 'Basic realm=\"Password needed\"', 'Content-Type': 'text/plain'})\n        return res.end('need password')\n      }\n      var givenPW = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':')[1]\n      if (givenPW !== opts.password) {\n        res.writeHead(401, {'WWW-Authenticate': 'Basic realm=\"Password needed\"', 'Content-Type': 'text/plain'})\n        return res.end('bad password')\n      }\n    }\n\n    // serve\n    if (req.url === '/' || req.url === '/index.html') {\n      res.writeHead(200, {'Content-Type': 'text/html'})\n      pump(fs.createReadStream('./diagnostics-server/index.html'), res)\n    } else if (req.url === '/index.css') {\n      res.writeHead(200, {'Content-Type': 'text/css'})\n      pump(fs.createReadStream('./diagnostics-server/index.css'), res)\n    } else if (req.url === '/index.js') {\n      res.writeHead(200, {'Content-Type': 'application/javascript'})\n      pump(fs.createReadStream('./diagnostics-server/index.js'), res)\n    } else if (req.url === '/state.json') {\n      res.writeHead(200, {'Content-Type': 'application/json'})\n      res.end(JSON.stringify({\n        stats: {\n          queriesPS,\n          multicastQueriesPS,\n          loadavg: os.loadavg(),\n          topKeys: disc._domainStore.getTopKeyStats()\n        },\n        peers: disc.toJSON()\n      }))\n    } else if (req.url === '/log.txt') {\n      res.writeHead(200, {'Content-Type': 'text/plain'})\n      pump(logFile.createReadStream(), res)\n    } else {\n      res.writeHead(404, { 'Content-Type': 'text/plain' })\n      res.end('Not found')\n    }\n  })\n}\n\nfunction renderLogEntry (evt, ts, args) {\n  switch (evt) {\n    case 'listening':\n      return `${ts} Listening\\n`\n    case 'traffic':\n      let info = args[1]\n      switch (args[0]) {\n        case 'in:query':\n          return `${ts} <- query              (from: ${info.peer.host}:${info.peer.port})    ${renderDNSMsg(info.message)}\\n`\n        case 'in:response':\n          return `${ts} <- response           (to: ${info.peer.host}:${info.peer.port})      ${renderDNSMsg(info.message)}\\n`\n        case 'in:multicastquery':\n          return `${ts} <- multicast query    (from: ${info.peer.address}:${info.peer.port})   ${renderDNSMsg(info.message)}\\n`\n        case 'in:multicastresponse':\n          return `${ts} <- multicast response (from: ${info.peer.address}:${info.peer.port})   ${renderDNSMsg(info.message)}\\n`\n        case 'out:response':\n          return `${ts} -> response           (to: ${info.peer.host}:${info.peer.port})      ${renderDNSMsg(info.message)}\\n`\n        case 'out:multicastresponse':\n          return `${ts} -> multicast response                            ${renderDNSMsg(info.message)}\\n`\n        case 'out:query':\n          return `${ts} -> query              (to: ${info.peer.host}:${info.peer.port})      ${renderDNSMsg(info.message)}\\n`\n        case 'out:multicastquery':\n          return `${ts} -> multicast query                               ${renderDNSMsg(info.message)}\\n`\n        default:\n          return `${ts} TODO ${JSON.stringify(args)}\\n`\n      }\n    case 'peer':\n      return `${ts} Peer for \"${args[0]}\" at ${args[1].host}:${args[1].port}\\n`\n    case 'close':\n      return `${ts} Closed\\n`\n    case 'secrets-rotated':\n      return `${ts} Secrets rotated\\n`\n    default:\n      return `${ts} Unknown event: ${JSON.stringify({evt, args})}\\n`\n  }\n}\n\nfunction renderDNSMsg ({id, questions, answers, additionals}) {\n  function item (prefix) {\n    return ({type, name}) => {\n      return `${safen(prefix)}.${safen(type)}:${safen(name)}`\n    }\n  }\n  function list (l, prefix) {\n    if (!l || !l.length) return ''\n    return l.map(item(prefix)).join(' ')\n  }\n  return ((id) ? `id=${safen(id)} ` : '') + `${list(questions, 'Q')} ${list(answers, 'A')} ${list(additionals, 'ADD')}`\n}\n\nfunction safen (str) {\n  return ('' + str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/&/g, '&amp;').replace(/\"/g, '')\n}\n"
  },
  {
    "path": "example.js",
    "content": "var discovery = require('dns-discovery')\n\nvar disc1 = discovery()\nvar disc2 = discovery()\n\ndisc2.on('peer', function (name, peer) {\n  console.log(name, peer)\n})\n\ndisc1.announce('test', 4244)\n"
  },
  {
    "path": "index.js",
    "content": "var dns = require('dns-socket')\nvar events = require('events')\nvar util = require('util')\nvar crypto = require('crypto')\nvar network = require('network-address')\nvar multicast = require('multicast-dns')\nvar debug = require('debug')('dns-discovery')\nvar store = require('./store')\n\nvar IPv4 = /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}.\\d{1,3}$/\nvar PORT = /^\\d{1,5}$/\n\nconst TYPE_LOOKUP = 1\nconst TYPE_ANNOUNCE = 2\nconst TYPE_UNANNOUNCE = 3\n\nmodule.exports = DNSDiscovery\n\nfunction DNSDiscovery (opts) {\n  if (!(this instanceof DNSDiscovery)) return new DNSDiscovery(opts)\n  if (!opts) opts = {}\n\n  events.EventEmitter.call(this)\n\n  var self = this\n\n  this.socket = dns(opts)\n  this.servers = [].concat(opts.servers || opts.server || []).map(parseAddr)\n\n  this._sockets = []\n  this._onsocket(this.socket)\n\n  this.multicast = opts.multicast !== false ? (isMulticaster(opts.multicast) ? opts.multicast : multicast()) : null\n  if (this.multicast) {\n    this.multicast.on('query', onmulticastquery)\n    this.multicast.on('response', onmulticastresponse)\n    this.multicast.on('error', onerror)\n  }\n\n  this._loopback = !!opts.loopback\n  this._listening = false\n  this._id = crypto.randomBytes(32).toString('base64')\n  this._domain = opts.domain || 'dns-discovery.local'\n  this._pushDomain = 'push.' + this._domain\n  this._tokens = new Array(this.servers.length)\n  this._tokensAge = []\n  this._secrets = [\n    crypto.randomBytes(32),\n    crypto.randomBytes(32)\n  ]\n\n  while (this._tokensAge.length < this._tokens.length) this._tokensAge.push(0)\n\n  this._interval = setInterval(rotateSecrets, 5 * 60 * 1000)\n  if (this._interval.unref) this._interval.unref()\n\n  this._ttl = opts.ttl || 0\n  this._tick = 1\n\n  var push = opts.push || {}\n  if (!push.ttl) push.ttl = opts.ttl || 60\n  if (!push.limit) push.limit = opts.limit\n\n  this._domainStore = store(opts)\n  this._pushStore = store(push)\n\n  function rotateSecrets () {\n    self._rotateSecrets()\n  }\n\n  function onerror (err) {\n    debug('Error', err)\n    self.emit('error', err)\n  }\n\n  function onmulticastquery (message, rinfo) {\n    debug(\n      'MDNS query %s:%s %dQ %dA +%d',\n      rinfo.address, rinfo.port,\n      message.questions.length,\n      message.answers.length,\n      message.additionals.length\n    )\n    self.emit('traffic', 'in:multicastquery', {message: message, peer: rinfo})\n    self._onmulticastquery(message, rinfo.port, rinfo.address)\n  }\n\n  function onmulticastresponse (message, rinfo) {\n    debug(\n      'MDNS response %s:%s %dA +%d',\n      rinfo.address, rinfo.port,\n      message.answers.length,\n      message.additionals.length\n    )\n    self.emit('traffic', 'in:multicastresponse', {message: message, peer: rinfo})\n    self._onmulticastresponse(message, rinfo.port, rinfo.address)\n  }\n}\n\nutil.inherits(DNSDiscovery, events.EventEmitter)\n\nDNSDiscovery.prototype.toJSON = function () {\n  return this._domainStore.toJSON()\n}\n\nDNSDiscovery.prototype._onsocket = function (socket) {\n  var self = this\n\n  this._sockets.push(socket)\n  socket.on('query', onquery)\n  socket.on('error', onerror)\n\n  function onerror (err) {\n    debug('Error', err)\n    self.emit('error', err)\n  }\n\n  function onquery (message, port, host) {\n    debug(\n      'DNS query %s:%s %dQ %dA +%d',\n      host, port,\n      message.questions.length,\n      message.answers.length,\n      message.additionals.length\n    )\n    self.emit('traffic', 'in:query', {message: message, peer: {port: port, host: host}})\n    self._onquery(message, port, host, socket)\n  }\n}\n\nDNSDiscovery.prototype._rotateSecrets = function () {\n  if (this._listening) {\n    debug('Rotating secrets')\n    this._secrets.shift()\n    this._secrets.push(crypto.randomBytes(32))\n  }\n\n  for (var i = 0; i < this._tokensAge.length; i++) {\n    if (this._tokensAge[i] < this._tick) {\n      this._tokens[i] = null\n      this._tokensAge[i] = 0\n    }\n  }\n\n  this.emit('secrets-rotated')\n  this._tick++\n}\n\nDNSDiscovery.prototype._onmulticastquery = function (query, port, host) {\n  var reply = {questions: query.questions, answers: []}\n  var i = 0\n\n  for (i = 0; i < query.questions.length; i++) {\n    this._onquestion(query.questions[i], port, host, reply.answers, true)\n  }\n  for (i = 0; i < query.answers.length; i++) {\n    this._onanswer(query.answers[i], port, host, null)\n  }\n  for (i = 0; i < query.additionals.length; i++) {\n    this._onanswer(query.additionals[i], port, host, null)\n  }\n\n  if (reply.answers.length) {\n    this.emit('traffic', 'out:multicastresponse', {message: reply})\n    this.multicast.response(reply, {port: port})\n  }\n}\n\nDNSDiscovery.prototype._onmulticastresponse = function (response, port, host) {\n  var i = 0\n\n  for (i = 0; i < response.answers.length; i++) {\n    this._onanswer(response.answers[i], port, host, null)\n  }\n  for (i = 0; i < response.additionals.length; i++) {\n    this._onanswer(response.additionals[i], port, host, null)\n  }\n}\n\nDNSDiscovery.prototype._onanswer = function (answer, port, host, socket) {\n  var domain = parseDomain(answer.name)\n  var id = parseId(answer.name, domain)\n  if (!id) {\n    debug('Invalid ID in answer, discarding', { name: answer.name, domain: domain, host: host, port: port })\n    return\n  }\n\n  if (answer.type === 'SRV') {\n    if (!IPv4.test(answer.data.target)) return\n    var peer = {\n      port: answer.data.port || port,\n      host: answer.data.target === '0.0.0.0' ? host : answer.data.target\n    }\n    debug('Announce received via SRV', id, peer.host + ':' + 'peer.port')\n    this.emit('peer', id, peer)\n    return\n  }\n\n  if (answer.type === 'TXT') {\n    try {\n      var data = decodeTxt(answer.data)\n    } catch (err) {\n      return\n    }\n\n    var tokenMatch = data.token === hash(this._secrets[1], host)\n\n    if (!tokenMatch || this._loopback) {\n      // not an echo\n      this._parsePeers(id, data, host)\n    }\n\n    if (!this._listening) {\n      return\n    }\n\n    // We are in server mode now. Add the record to the cache\n\n    if (!tokenMatch) {\n      // check if old token matches\n      if (data.token !== hash(this._secrets[0], host)) {\n        debug('Invalid token in TXT answer, discarding')\n        return\n      }\n    }\n\n    if (PORT.test(data.announce)) {\n      var announce = Number(data.announce) || port\n      debug('Announce received via TXT', id, host + ':' + announce)\n      this.emit('peer', id, {port: announce, host: host})\n      if (this._domainStore.add(id, announce, host) && socket) {\n        this._push(id, announce, host, socket)\n      }\n    }\n\n    if (PORT.test(data.unannounce)) {\n      var unannounce = Number(data.unannounce) || port\n      this._domainStore.remove(id, unannounce, host)\n      debug('Un-announce received via TXT', id, host + ':' + unannounce)\n    }\n\n    if (data.subscribe) {\n      debug('Subscribe-to-push received via TXT', id, host + ':' + port)\n      this._pushStore.add(id, port, host)\n    } else {\n      debug('Unsubscribe-from-push received via TXT', id, host + ':' + port)\n      this._pushStore.remove(id, port, host)\n    }\n  }\n}\n\nDNSDiscovery.prototype._push = function (id, port, host, socket) {\n  var subs = this._pushStore.get(id, 16)\n  var query = {\n    additionals: [{\n      type: 'SRV',\n      name: id + '.' + this._domain,\n      ttl: this._ttl,\n      data: {\n        port: port,\n        target: host\n      }\n    }]\n  }\n\n  if (subs.length) debug('Pushing announcement to', subs.length, 'subscribers')\n  for (var i = 0; i < subs.length; i++) {\n    var peer = subs[i]\n    var tid = socket.query(query, peer.port, peer.host)\n    socket.setRetries(tid, 2)\n  }\n}\n\nDNSDiscovery.prototype._onquestion = function (query, port, host, answers, multicast) {\n  var domain = parseDomain(query.name)\n\n  if (domain !== this._domain) return\n\n  if (query.type === 'TXT' && domain === query.name) {\n    debug('Replying state-info via TXT to %s:%s', host, port)\n    answers.push({\n      type: 'TXT',\n      name: query.name,\n      ttl: this._ttl,\n      data: encodeTxt({\n        token: hash(this._secrets[1], host),\n        host: host,\n        port: '' + port\n      })\n    })\n    return\n  }\n\n  var id = parseId(query.name, domain)\n  if (!id) {\n    debug('Invalid ID in question, discarding', { name: query.name, domain: domain, host: host, port: port })\n    return\n  }\n\n  if (query.type === 'TXT') {\n    var buf = toBuffer(this._domainStore.get(id, 100))\n    var token = hash(this._secrets[1], host)\n    if (multicast && !buf.length) return // just an optimization\n    debug('Replying known peers via TXT to', host + ':' + port)\n    answers.push({\n      type: 'TXT',\n      name: query.name,\n      ttl: this._ttl,\n      data: encodeTxt(buf.length ? {\n        token: token,\n        peers: buf.toString('base64')\n      } : {\n        token: token\n      })\n    })\n    return\n  }\n\n  var peers = this._domainStore.get(id, 10)\n  debug('Replying announce via', query.type, ' to', host + ':' + port)\n\n  for (var i = 0; i < peers.length; i++) {\n    var peer = peers[i]\n\n    if (query.type === 'A') {\n      answers.push({\n        type: 'A',\n        name: query.name,\n        ttl: this._ttl,\n        data: peer.host === '0.0.0.0' ? network() : peer.host\n      })\n    }\n    if (query.type === 'SRV') {\n      answers.push({\n        type: 'SRV',\n        name: query.name,\n        ttl: this._ttl,\n        data: {\n          port: peer.port,\n          target: peer.host\n        }\n      })\n    }\n  }\n}\n\nDNSDiscovery.prototype._onquery = function (query, port, host, socket) {\n  var reply = {questions: query.questions, answers: []}\n  var i = 0\n\n  for (i = 0; i < query.questions.length; i++) {\n    this._onquestion(query.questions[i], port, host, reply.answers)\n  }\n  for (i = 0; i < query.answers.length; i++) {\n    this._onanswer(query.answers[i], port, host, socket)\n  }\n  for (i = 0; i < query.additionals.length; i++) {\n    this._onanswer(query.additionals[i], port, host, socket)\n  }\n  socket.response(query, reply, port, host)\n  // note: emit 'traffic' after calling .response() because socket.response() modifies `reply`\n  this.emit('traffic', 'out:response', {message: reply, peer: {port: port, host: host}})\n}\n\nDNSDiscovery.prototype._probeAndSend = function (type, i, id, port, cb) {\n  var self = this\n  this._probe(i, 0, function (err) {\n    if (err) return cb(err)\n    self._send(type, i, id, port, cb)\n  })\n}\n\nDNSDiscovery.prototype._send = function (type, i, id, port, cb) {\n  var s = this.servers[i]\n  var token = this._tokens[i]\n  var data = null\n\n  switch (type) {\n    case TYPE_LOOKUP:\n      data = {subscribe: true, token: token}\n      break\n\n    case TYPE_ANNOUNCE:\n      data = {subscribe: true, token: token, announce: '' + port}\n      break\n\n    case TYPE_UNANNOUNCE:\n      data = {token: token, unannounce: '' + port}\n      break\n  }\n\n  var query = {\n    index: i,\n    questions: [{\n      type: 'TXT',\n      name: id + '.' + this._domain\n    }],\n    additionals: [{\n      type: 'TXT',\n      name: id + '.' + this._domain,\n      ttl: this._ttl,\n      data: encodeTxt(data)\n    }]\n  }\n\n  this.socket.query(query, s.port, s.host, cb)\n  this.emit('traffic', 'out:query', {message: query, peer: s})\n}\n\nDNSDiscovery.prototype.lookup = function (id, opts, cb) {\n  debug('lookup()', id)\n  this._visit(TYPE_LOOKUP, id, 0, opts, cb)\n}\n\nDNSDiscovery.prototype.announce = function (id, port, opts, cb) {\n  debug('announce()', id)\n  this._visit(TYPE_ANNOUNCE, id, port, opts, cb)\n}\n\nDNSDiscovery.prototype.unannounce = function (id, port, opts, cb) {\n  debug('unannounce()', id)\n  this._visit(TYPE_UNANNOUNCE, id, port, opts, cb)\n}\n\nDNSDiscovery.prototype._visit = function (type, id, port, opts, cb) {\n  if (typeof opts === 'function') return this._visit(type, id, port, null, opts)\n  if (typeof port === 'function') return this._visit(type, id, 0, port)\n  if (!cb) cb = noop\n  if (Buffer.isBuffer(id)) id = id.toString('hex')\n  if (!opts) opts = {}\n\n  var self = this\n  var missing = this.servers.length\n  var success = false\n\n  if (opts.server !== false) {\n    var publicPort = opts.publicPort || (opts.impliedPort ? 0 : port)\n    for (var i = 0; i < this.servers.length; i++) {\n      if (this._tokens[i]) this._send(type, i, id, publicPort, done)\n      else this._probeAndSend(type, i, id, publicPort, done)\n    }\n  }\n\n  if (type === TYPE_ANNOUNCE) this._domainStore.add(id, port, '0.0.0.0')\n  if (type === TYPE_UNANNOUNCE) this._domainStore.remove(id, port, '0.0.0.0')\n\n  if (opts.multicast !== false && this.multicast) {\n    if (type !== TYPE_UNANNOUNCE) {\n      missing++\n      var message = {\n        questions: [{\n          type: 'TXT',\n          name: id + '.' + this._domain\n        }]\n      }\n      this.multicast.query(message, done)\n      this.emit('traffic', 'out:multicastquery', {message: message})\n    }\n  }\n\n  if (!missing) {\n    missing++\n    process.nextTick(done)\n  }\n\n  function done (_, res, q, _port, _host) {\n    if (res) {\n      success = true\n      self.emit('traffic', 'in:response', {message: res, peer: {host: _host, port: _port}})\n      try {\n        var data = res.answers.length && decodeTxt(res.answers[0].data)\n      } catch (err) {\n        // do nothing\n      }\n      if (data) self._parseData(id, data, q.index, _host)\n      if (type === TYPE_ANNOUNCE) self.emit('announced', id, {port: port})\n      if (type === TYPE_UNANNOUNCE) self.emit('unannounced', id, {port: port})\n    }\n\n    if (!--missing) cb(success ? null : new Error('Query failed'))\n  }\n}\n\nDNSDiscovery.prototype._parsePeers = function (id, data, host) {\n  try {\n    var buf = Buffer.from(data.peers, 'base64')\n  } catch (err) {\n    return\n  }\n\n  for (var i = 0; i < buf.length; i += 6) {\n    var peer = decodePeer(buf, i)\n    if (!peer) continue\n    if (peer.host === '0.0.0.0') peer.host = host\n    this.emit('peer', id, peer)\n  }\n}\n\nDNSDiscovery.prototype._parseData = function (id, data, index, host) {\n  if (data.token) {\n    this._tokens[index] = data.token\n    this._tokensAge[index] = this._tick\n  }\n  if (data && data.peers && id) this._parsePeers(id, data, host)\n}\n\nDNSDiscovery.prototype.whoami = function (cb) {\n  var missing = this.servers.length\n  var prevData = null\n  var prevHost = null\n  var called = false\n\n  if (this.servers.length) {\n    for (var i = 0; i < this.servers.length; i++) this._probe(i, 2, done)\n  } else {\n    debug('whoami() failed - no servers to ping')\n    missing = 1\n    process.nextTick(done)\n  }\n\n  function done (_, data, port, host) {\n    if (data) {\n      if (!called && IPv4.test(data.host) && PORT.test(data.port)) {\n        if (prevHost && prevHost !== host) {\n          called = true\n          if (prevData.host === data.host && prevData.port === data.port) {\n            cb(null, {port: Number(data.port), host: data.host})\n          } else if (prevData.host === data.host) {\n            cb(null, {port: 0, host: data.host})\n          } else {\n            cb(new Error('Inconsistent remote port/host'))\n          }\n        }\n        prevData = data\n        prevHost = host\n      }\n    }\n\n    if (--missing || called) {\n      if (!called) {\n        debug('whoami() probe got response; waiting for a confirmation from %d other(s)', missing)\n      }\n      return\n    }\n    if (data) cb(null, {port: 0, host: data.host})\n    else cb(new Error('Probe failed'))\n  }\n}\n\nDNSDiscovery.prototype._probe = function (i, retries, cb) {\n  var self = this\n  var s = this.servers[i]\n  var q = {\n    questions: [{\n      type: 'TXT',\n      name: this._domain\n    }]\n  }\n  debug('probing %s:%d', s.host, s.port)\n\n  var first = true\n  var result = null\n  var id = this.socket.query(q, s.port, s.host, done)\n\n  if (retries) this.socket.setRetries(id, retries)\n\n  function done (_, res, query, port, host) {\n    if (res) {\n      self.emit('traffic', 'in:response', {message: res, peer: {host: host, port: port}})\n      try {\n        var data = res.answers.length && decodeTxt(res.answers[0].data)\n      } catch (err) {\n        // do nothing\n      }\n      if (data && data.token) {\n        self._parseData(null, data, i, host)\n        result = data\n      }\n    }\n\n    if (result) {\n      if (!first) {\n        s.port = port\n        s.secondaryPort = 0\n      } else {\n        s.secondaryPort = 0\n      }\n\n      debug('probe of %s:%d succeeded', host, port)\n      return cb(null, result, port, host)\n    }\n\n    if (!first || !s.secondaryPort) {\n      debug('probe of %s:%d failed', host, port)\n      return cb(new Error('Probe failed'))\n    }\n\n    first = false\n    debug('retrying probe of %s at secondary port %d', host, s.secondaryPort)\n    id = self.socket.query(q, s.secondaryPort, s.host, done)\n    if (retries) self.socket.setRetries(id, retries)\n  }\n}\n\nDNSDiscovery.prototype.destroy = function (onclose) {\n  debug('destroy()')\n  if (onclose) this.once('close', onclose)\n\n  var self = this\n  var missing = this._sockets.length\n  clearInterval(this._interval)\n\n  if (this.multicast) this.multicast.destroy(onmulticastclose)\n  else onmulticastclose()\n\n  function onmulticastclose () {\n    for (var i = 0; i < self._sockets.length; i++) {\n      self._sockets[i].destroy(onsocketclose)\n    }\n  }\n\n  function onsocketclose () {\n    if (!--missing) self.emit('close')\n  }\n}\n\nDNSDiscovery.prototype.listen = function (ports, onlistening) {\n  if (onlistening) this.once('listening', onlistening)\n  if (this._listening) throw new Error('Server is already listening')\n  this._listening = true\n\n  if (!ports) ports = [53, 5300]\n  if (!Array.isArray(ports)) ports = [ports]\n\n  debug('Listening on port(s)', ports.join(', '))\n\n  var self = this\n  var missing = ports.length\n\n  for (var i = 0; i < ports.length; i++) {\n    var socket = dns()\n    socket.bind(ports[i], onbind)\n    this._onsocket(socket)\n  }\n\n  function onbind () {\n    if (!--missing) self.emit('listening')\n  }\n}\n\nfunction noop () {}\n\nfunction parseAddr (addr) {\n  if (addr.indexOf(':') === -1) addr += ':5300,53'\n  var match = addr.match(/^([^:]+)(?::(\\d{1,5})(?:,(\\d{1,5}))?)?$/)\n  if (!match) throw new Error('Could not parse ' + addr)\n\n  return {\n    port: Number(match[2] || 53),\n    secondaryPort: Number(match[3] || 0),\n    host: match[1]\n  }\n}\n\nfunction hash (secret, host) {\n  return crypto.createHash('sha256').update(secret).update(host).digest('base64')\n}\n\nfunction parseId (name, domain) {\n  if (!domain || name.length === domain.length) return null\n  return name.slice(0, -domain.length - 1)\n}\n\nfunction parseDomain (name) {\n  var i = name.lastIndexOf('.')\n  if (i === -1) return null\n  i = name.lastIndexOf('.', i - 1)\n  return i === -1 ? name : name.slice(i + 1)\n}\n\nfunction toBuffer (peers) {\n  var buf = Buffer.alloc(peers.length * 6)\n  for (var i = 0; i < peers.length; i++) {\n    if (!peers[i].buffer) peers[i].buffer = encodePeer(peers[i])\n    peers[i].buffer.copy(buf, i * 6)\n  }\n  return buf\n}\n\nfunction encodePeer (peer) {\n  var buf = Buffer.alloc(6)\n  var parts = peer.host.split('.')\n  buf[0] = Number(parts[0] || 0)\n  buf[1] = Number(parts[1] || 0)\n  buf[2] = Number(parts[2] || 0)\n  buf[3] = Number(parts[3] || 0)\n  buf.writeUInt16BE(peer.port || 0, 4)\n  return buf\n}\n\nfunction decodePeer (buf, offset) {\n  if (buf.length - offset < 6) return null\n  var host = buf[offset++] + '.' + buf[offset++] + '.' + buf[offset++] + '.' + buf[offset++]\n  var port = buf.readUInt16BE(offset)\n  offset += 2\n  return {port: port, host: host}\n}\n\nfunction decodeTxt (bufs) {\n  var data = {}\n\n  for (var i = 0; i < bufs.length; i++) {\n    var buf = bufs[i]\n    var j = buf.indexOf(61) // '='\n    if (j === -1) data[buf.toString()] = true\n    else data[buf.slice(0, j).toString()] = buf.slice(j + 1).toString()\n  }\n\n  return data\n}\n\nfunction encodeTxt (data) {\n  var keys = Object.keys(data)\n  var bufs = []\n\n  for (var i = 0; i < keys.length; i++) {\n    bufs.push(Buffer.from(keys[i] + '=' + data[keys[i]]))\n  }\n\n  return bufs\n}\n\nfunction isMulticaster (m) {\n  return typeof m === 'object' && m && typeof m.query === 'function'\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"dns-discovery\",\n  \"version\": \"6.2.3\",\n  \"description\": \"Discovery peers in a distributed system using regular dns and multicast dns.\",\n  \"main\": \"index.js\",\n  \"dependencies\": {\n    \"circular-append-file\": \"^1.0.1\",\n    \"debug\": \"^2.6.9\",\n    \"dns-socket\": \"^3.0.0\",\n    \"lru\": \"^2.0.0\",\n    \"minimist\": \"^1.2.0\",\n    \"multicast-dns\": \"^7.1.1\",\n    \"network-address\": \"^1.1.2\",\n    \"pump\": \"^3.0.0\",\n    \"speedometer\": \"^1.0.0\",\n    \"unordered-set\": \"^1.1.0\"\n  },\n  \"devDependencies\": {\n    \"standard\": \"^11.0.0\",\n    \"tape\": \"^4.9.0\"\n  },\n  \"bin\": {\n    \"dns-discovery\": \"./bin.js\"\n  },\n  \"scripts\": {\n    \"test\": \"standard && tape test.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/mafintosh/dns-discovery.git\"\n  },\n  \"author\": \"Mathias Buus (@mafintosh)\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/mafintosh/dns-discovery/issues\"\n  },\n  \"homepage\": \"https://github.com/mafintosh/dns-discovery\"\n}\n"
  },
  {
    "path": "store.js",
    "content": "var set = require('unordered-set')\nvar lru = require('lru')\n\nmodule.exports = Store\n\nfunction Store (opts) {\n  if (!(this instanceof Store)) return new Store(opts)\n  if (!opts) opts = {}\n\n  this.maxValues = opts.values || Infinity\n  this.maxEntries = opts.records || Infinity\n  this.entries = lru(this.maxEntries)\n  this.limit = opts.limit || 10000\n  this.ttl = (opts.ttl || 0) * 1000\n  this.used = 0\n}\n\nStore.prototype.get = function (name, max) {\n  var entry = this.entries.get(name)\n  var result = []\n\n  if (!entry) return result\n  if (!max) max = entry.values.length\n\n  while (result.length < max) {\n    var i = result.length\n    if (i >= entry.values.length) return result\n\n    var missing = entry.values.length - i\n    var next = i + (Math.random() * missing) | 0\n    var val = entry.values[next]\n\n    if (this.ttl && (Date.now() - val._modified) > this.ttl) {\n      set.remove(entry.values, val)\n      this.used--\n\n      if (!entry.values.length) {\n        this.entries.remove(name)\n        return result\n      }\n    } else {\n      set.swap(entry.values, entry.values[i], val)\n      result.push(val)\n    }\n  }\n\n  return result\n}\n\nStore.prototype.remove = function (name, port, host) {\n  var address = host + ':' + port\n  var entry = this.entries.peek(name)\n  if (!entry) return\n\n  var peer = entry.byAddr.remove(address)\n  if (!peer) return\n\n  set.remove(entry.values, peer)\n  this.used--\n  if (!entry.values.length) this.entries.remove(name)\n}\n\nStore.prototype.add = function (name, port, host) {\n  var peer = new Peer(port, host)\n\n  if (this.used >= this.limit) this.evict()\n\n  var entry = this.entries.get(name)\n\n  if (!entry) {\n    entry = this.entries.set(name, new Record(name, this.maxValues))\n  }\n\n  var prev = entry.byAddr.get(peer.address)\n  var old = !!prev\n  if (!old) {\n    prev = peer\n    set.add(entry.values, peer)\n    entry.byAddr.set(peer.address, peer)\n    this.used++\n  }\n  if (this.ttl) prev._modified = Date.now()\n\n  return !old\n}\n\nStore.prototype.evict = function () {\n  var oldest = this.entries.tail && this.entries.peek(this.entries.tail)\n  if (!oldest) return\n\n  var oldestPeer = oldest.byAddr.tail && oldest.byAddr.remove(oldest.byAddr.tail)\n  if (!oldestPeer) return\n\n  set.remove(oldest.values, oldestPeer)\n  this.used--\n\n  if (!oldest.values.length) {\n    this.entries.remove(this.entries.tail)\n  }\n}\n\nStore.prototype.toJSON = function () {\n  var entries = []\n  var keys = Object.keys(this.entries.cache)\n  for (var i = 0; i < keys.length; i++) {\n    entries.push({\n      name: keys[i],\n      records: this.entries.peek(keys[i]).values\n    })\n  }\n  return entries\n}\n\nStore.prototype.getTopKeyStats = function (n) {\n  n = n || 10\n  var entries = []\n  var keys = Object.keys(this.entries.cache)\n  for (var i = 0; i < keys.length; i++) {\n    entries.push({\n      name: keys[i],\n      numRecords: this.entries.peek(keys[i]).values.length\n    })\n  }\n  entries.sort(function (a, b) {\n    return b.numRecords - a.numRecords\n  })\n  return entries.slice(0, n)\n}\n\nfunction Peer (port, host) {\n  this.host = host || '127.0.0.1'\n  this.port = port\n  this.address = this.host + ':' + this.port\n  this.buffer = null\n\n  this._modified = 0\n  this._index = 0\n}\n\nfunction Record (name, limit) {\n  this.name = name\n  this.values = []\n  this.byAddr = lru(limit || Infinity)\n}\n"
  },
  {
    "path": "test.js",
    "content": "var dgram = require('dgram')\nvar tape = require('tape')\nvar discovery = require('./')\n\nfreePort(function (port) {\n  tape('discovers', function (t) {\n    var disc1 = discovery()\n    var disc2 = discovery()\n    var ns = Math.random().toString(16) + '-' + process.pid\n    var appName = 'dns-discovery-' + ns\n\n    disc2.on('peer', function (name, peer) {\n      disc1.destroy()\n      disc2.destroy()\n      t.same(name, appName)\n      t.same(peer.port, 8080)\n      t.same(typeof peer.host, 'string')\n      t.end()\n    })\n\n    disc1.announce(appName, 8080)\n  })\n\n  tape('discovers only using server', function (t) {\n    t.plan(4)\n\n    var server = discovery({multicast: false})\n    var client2 = discovery({multicast: false, server: 'localhost:' + port})\n    var client1 = discovery({multicast: false, server: 'localhost:' + port})\n\n    server.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, 8080)\n    })\n\n    client2.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, 8080)\n      server.destroy()\n      client1.destroy()\n      client2.destroy()\n    })\n\n    server.listen(port, function () {\n      client1.announce('hello-world', 8080, function () {\n        client2.lookup('hello-world')\n      })\n    })\n  })\n\n  tape('discovers only using server with secondary port', function (t) {\n    t.plan(4)\n\n    var server = discovery({multicast: false})\n    var client2 = discovery({multicast: false, server: 'localhost:9999,' + port})\n    var client1 = discovery({multicast: false, server: 'localhost:9998,' + port})\n\n    server.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, 8080)\n    })\n\n    client2.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, 8080)\n      server.destroy()\n      client1.destroy()\n      client2.destroy()\n    })\n\n    server.listen(port, function () {\n      client1.announce('hello-world', 8080, function () {\n        client2.lookup('hello-world')\n      })\n    })\n  })\n\n  tape('discovers only using multiple servers', function (t) {\n    t.plan(6)\n\n    var server = discovery({multicast: false})\n    var client1 = discovery({multicast: false, server: ['localhost:' + port, 'localhost:' + port]})\n    var client2 = discovery({multicast: false, server: ['localhost:' + port, 'localhost:' + port]})\n\n    server.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, 8080)\n    })\n\n    client2.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, 8080)\n      server.destroy()\n      client1.destroy()\n      client2.destroy()\n    })\n\n    server.listen(port, function () {\n      client1.announce('hello-world', 8080, function () {\n        client2.lookup('hello-world')\n      })\n    })\n  })\n\n  tape('limit', function (t) {\n    var server = discovery({multicast: false, limit: 1})\n    var ns = Math.random().toString(16) + '-' + process.pid\n\n    server.announce(ns + 'hello-world', 8080)\n    server.announce(ns + 'hello-world-2', 8081)\n\n    var domains = server.toJSON()\n    t.same(domains.length, 1)\n    t.same(domains[0].records.length, 1)\n    t.end()\n  })\n\n  tape('push', function (t) {\n    var server = discovery({multicast: false})\n    var client1 = discovery({multicast: false, server: 'localhost:' + port})\n    var client2 = discovery({multicast: false, server: 'localhost:' + port})\n\n    server.listen(port, function () {\n      server.once('peer', function () {\n        client2.announce('hello-world', 8081)\n      })\n      client1.lookup('hello-world')\n      client1.announce('hello-world', 8080)\n      client1.on('peer', function (id, peer) {\n        if (peer.port === 8081) {\n          client1.destroy()\n          client2.destroy()\n          server.destroy()\n          t.pass('got peer')\n          t.end()\n        }\n      })\n    })\n  })\n\n  tape('unannounce', function (t) {\n    var server = discovery({multicast: false})\n    var client1 = discovery({multicast: false, server: 'localhost:' + port})\n    var client2 = discovery({multicast: false, server: 'localhost:' + port})\n\n    client2.on('peer', function () {\n      t.fail('no peers should be discovered')\n    })\n\n    server.listen(port, function () {\n      client1.announce('test', 8080, function () {\n        client1.unannounce('test', 8080, function () {\n          client2.lookup('test', function () {\n            setTimeout(function () {\n              client2.destroy()\n              client1.destroy()\n              server.destroy()\n              t.end()\n            }, 100)\n          })\n        })\n      })\n    })\n  })\n\n  tape('custom socket + server', function (t) {\n    t.plan(5)\n\n    var socket = dgram.createSocket('udp4')\n\n    socket.once('message', function () {\n      t.pass('used custom socket')\n    })\n\n    var server = discovery({multicast: false})\n    var client2 = discovery({multicast: false, server: 'localhost:' + port, socket: socket})\n    var client1 = discovery({multicast: false, server: 'localhost:' + port})\n\n    server.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, 8080)\n    })\n\n    client2.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, 8080)\n      server.destroy()\n      client1.destroy()\n      client2.destroy()\n    })\n\n    server.listen(port, function () {\n      client1.announce('hello-world', 8080, function () {\n        client2.lookup('hello-world')\n      })\n    })\n  })\n\n  tape('implied port', function (t) {\n    t.plan(4)\n\n    var socket = dgram.createSocket('udp4')\n\n    var server = discovery({multicast: false})\n    var client2 = discovery({multicast: false, server: 'localhost:' + port})\n    var client1 = discovery({multicast: false, server: 'localhost:' + port, socket: socket})\n\n    server.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, socket.address().port)\n    })\n\n    client2.on('peer', function (name, peer) {\n      t.same(name, 'hello-world')\n      t.same(peer.port, socket.address().port)\n      server.destroy()\n      client1.destroy()\n      client2.destroy()\n    })\n\n    server.listen(port, function () {\n      client1.announce('hello-world', 8080, {impliedPort: true}, function () {\n        client2.lookup('hello-world')\n      })\n    })\n  })\n\n  tape('loopback', function (t) {\n    var client = discovery({loopback: true})\n\n    client.on('peer', function () {\n      client.destroy()\n      t.end()\n    })\n\n    client.announce('test', 8080)\n  })\n\n  tape('public port', function (t) {\n    var server = discovery({multicast: false})\n    var client2 = discovery({server: 'localhost:' + port})\n    var client1 = discovery({server: 'localhost:' + port})\n    var ns = Math.random().toString(16) + '-' + process.pid\n    var appName = 'dns-discovery-' + ns\n    var missing = 2\n\n    server.on('peer', function (name, peer) {\n      t.same(name, appName)\n      t.same(peer.port, 9090, 'server port')\n    })\n\n    client2.on('peer', function (name, peer) {\n      t.same(name, appName)\n      t.same(peer.port, peer.host === '127.0.0.1' ? 9090 : 8080)\n      if (--missing) return\n      server.destroy()\n      client1.destroy()\n      client2.destroy()\n      t.end()\n    })\n\n    server.listen(port, function () {\n      client1.announce(appName, 8080, {publicPort: 9090}, function () {\n        client2.lookup(appName)\n      })\n    })\n  })\n})\n\nfunction freePort (cb) {\n  var socket = dgram.createSocket('udp4')\n  socket.bind(0, function () {\n    socket.on('close', cb.bind(null, socket.address().port))\n    socket.close()\n  })\n}\n"
  }
]