Repository: mafintosh/dns-discovery
Branch: master
Commit: 1b0f61366a55
Files: 14
Total size: 48.7 KB
Directory structure:
gitextract_4lt9pael/
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── bin.js
├── diagnostics-server/
│ ├── index.css
│ ├── index.html
│ └── index.js
├── diagnostics-server.js
├── example.js
├── index.js
├── package.json
├── store.js
└── test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
diagnostics-server.log
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- '4.7.3'
- '5.10'
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Mathias Buus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# dns-discovery
Discovery peers in a distributed system using regular dns and multicast dns.
```
npm install dns-discovery
```
[](http://travis-ci.org/mafintosh/dns-discovery)
## Usage
``` js
var discovery = require('dns-discovery')
var disc1 = discovery()
var disc2 = discovery()
disc1.on('peer', function (name, peer) {
console.log(name, peer)
})
// announce an app
disc2.announce('test-app', 9090)
```
## API
#### `var disc = discovery([options])`
Create a new discovery instance. Options include:
``` js
{
server: 'discovery.example.com:9090', // put a centralized dns discovery server here
ttl: someSeconds, // ttl for records in seconds. defaults to Infinity.
limit: someLimit, // max number of records stored. defaults to 10000.
multicast: true, // use multicast-dns. defaults to true.
domain: 'my-domain.com', // top-level domain to use for records. defaults to dns-discovery.local
socket: someUdpSocket, // use this udp socket as the client socket
loopback: false // discover yourself over multicast
}
```
If you have more than one discovery server you can specify an array
``` js
{
server: [
'discovery.example.com:9090',
'another.discovery.example.com'
]
}
```
#### `disc.lookup(name, [callback])`
Do 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.
``` js
disc.on('peer', function (name, peer) {
console.log(name) // app name this peer was discovered for (ie 'example')
console.log(peer) // {host: 'some-ip', port: 1234}
})
disc.lookup('example')
```
#### `disc.announce(name, port, [options], [callback])`
Announce a new port for a specific app name. Announce also does a lookup so you don't need to do that afterwards.
If you want to specify a public port (a port that is reachable from outside your firewall) you can set the `publicPort: port`
option. This will announce the public port to your list of dns servers and use the other port over multicast.
You can also set `impliedPort: true` to announce the public port of the dns socket to the list of dns servers.
#### `disc.unannounce(name, port, [options], [callback])`
Stop announcing a port for an app. Has the same options as .announce
#### `disc.listen([port], [callback])`
Listen 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.
``` js
var server = discovery()
server.listen(9090, function () {
var disc = discovery({server: 'localhost:9090'})
disc.announce('test-app', 8080) // will announce this record to the above discovery server
})
```
You can setup a discovery server to announce records on the internet as multicast-dns only works on a local network.
The port defaults to `53` which is the standard dns port. Additionally it tries to bind to `5300` to support networks that filter dns traffic.
#### `disc.destroy([onclose])`
Destroy the discovery instance. Will destroy the underlying udp socket as well.
#### `Event: "listening"`
Emitted after a successful `listen()`.
#### `Event: "close"`
Emitted after a successful `destroy()`.
#### `Event: "peer" (name, {host, port})
Emitted when a peer has been discovered.
- **name** The app name the peer was discovered for.
- **host** The address of the peer.
- **port** The port the peer is listening on.
#### `Event: "announced" (name, {port})`
Emitted after a successful `announce()`.
- **name** The app name that was announced.
- **port** The port that was announced.
#### `Event: "unannounced" (name, {port})`
Emitted after a successful `unannounce()`.
- **name** The app name that was unannounced.
- **port** The port that was unannounced.
#### `Event: "traffic" (type, details)`
Emitted 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.
#### `Event: "secrets-rotated"`
Emitted when the internal secrets used to generate session tokens have been rotated. This event is mostly useful for debugging.
#### `Event: "error" (err)`
Emitted when networking errors occur, such as failures to bind the socket (EACCES, EADDRINUSE).
## CLI
There is a cli tool available as well
``` sh
npm install -g dns-discovery
dns-discovery help
```
To announce a service do
``` sh
# will announce test-app over multicast-dns
dns-discovery announce test-app --port=8080
```
To look it up
``` sh
# will print services when they are found
dns-discovery lookup test-app
```
To run a discovery server
``` sh
# listen for services and store them with a ttl of 30s
dns-discovery listen --port=9090 --ttl=30
```
And to announce to that discovery server (and over multicast-dns)
``` sh
# replace example.com with the host of the server running the discovery server
dns-discovery announce test-app --server=example.com:9090 --port=9090
```
And finally to lookup using that discovery server (and multicast-dns)
``` sh
dns-discovery lookup test-app --server=example.com:9090
```
You can use any other dns client to resolve the records as well. For example using `dig`.
``` sh
# dig requires the discovery server to run on port 53
dig @discovery.example.com test-app SRV
```
## License
MIT
================================================
FILE: bin.js
================================================
#!/usr/bin/env node
var discovery = require('./')
var minimist = require('minimist')
var argv = minimist(process.argv.slice(2), {
alias: {port: 'p', host: 'h', server: 's', domain: 'd'}
})
var rcvd = {}
var cmd = argv._[0]
var disc = discovery(argv)
if (cmd === 'listen') {
disc.listen(argv.port, onlisten)
if (argv.diag) {
var diagServer = require('./diagnostics-server').createServer(disc, {password: argv.diagpw})
diagServer.listen((typeof argv.diag === 'number' ? argv.diag : 3030), ondiaglisten)
}
} else if (cmd === 'lookup') {
disc.on('peer', onpeer)
lookup()
setInterval(lookup, 1000)
} else if (cmd === 'announce') {
if (!argv.port) throw new Error('You need to specify --port')
announce()
setInterval(announce, 1000)
} else {
console.error(
'dns-discovery [command]\n' +
' announce [name]\n' +
' --port=(port)\n' +
' --host=(optional host)\n' +
' --server=(optional discovery server)\n' +
' --domain=(optional authoritative domain)\n' +
' lookup [name]\n' +
' --server=(optional discovery server)\n' +
' --domain=(optional authoritative domain)\n' +
' listen\n' +
' --port=(optional port)\n' +
' --ttl=(optional ttl in seconds)\n' +
' --domain=(optional authoritative domain)\n' +
' --diag=(enable diagnostic server, optional port, default 3030)\n' +
' --diagpw=(optional password for diagnostic server)'
)
process.exit(1)
}
function lookup () {
disc.lookup(argv._[1])
}
function announce () {
disc.announce(argv._[1], argv.port)
}
function onpeer (name, peer) {
var addr = peer.host + ':' + peer.host
if (rcvd[addr]) return
rcvd[addr] = true
console.log(name, peer)
}
function onlisten (err) {
if (err) throw err
console.log('Server is listening on port %d', argv.port || 53)
}
function ondiaglisten (err) {
if (err) throw err
console.log('Diagnostics server is listening on port %d', (typeof argv.diag === 'number' ? argv.diag : 3030))
}
================================================
FILE: diagnostics-server/index.css
================================================
html {
font-family: sans-serif;
}
h3 {
font-weight: normal;
}
td {
border: 1px solid #ccc;
padding: 5px;
}
#nav {
font-size: 19px;
border-bottom: 1px solid gray;
padding-bottom: 10px;
margin-bottom: 10px;
}
#nav a {
color: gray;
margin-right: 10px;
}
#nav a:not(.active):hover {
text-decoration: underline;
cursor: pointer;
}
#nav a.active {
color: black;
text-decoration: underline;
}
#views > div {
display: none;
}
#views > div.active {
display: block;
}
.collapsable {
display: none;
}
.collapsable.open {
display: block;
}
.toggle-collapsable {
color: blue;
cursor: pointer;
}
.toggle-collapsable:hover {
text-decoration: underline;
}
================================================
FILE: diagnostics-server/index.html
================================================
<!doctype html>
<html>
<head>
<title>DNS Discovery Server Diagnostics Panel</title>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="nav">
<a id="nav-state" class="active" data-view="state">State</a>
<a id="nav-log" data-view="log">Log</a>
</div>
<div id="views">
<div id="view-state"></div>
<div id="view-log"></div>
</div>
<script src="/index.js"></script>
</body>
</html>
================================================
FILE: diagnostics-server/index.js
================================================
/* globals fetch */
const navAs = Array.from(document.querySelectorAll('#nav > a'))
const viewDivs = Array.from(document.querySelectorAll('#views > div'))
var currentPoll
// register ui events
navAs.forEach(a => {
a.addEventListener('click', () => {
setView(a.dataset.view)
})
})
document.body.addEventListener('click', e => {
console.log('click', e.target, e.target.classList.contains('toggle-collapsable'))
if (e.target.classList.contains('toggle-collapsable')) {
document.getElementById(e.target.dataset.target).classList.toggle('open')
}
})
function setView (view) {
navAs.forEach(a => a.classList.remove('active'))
viewDivs.forEach(div => div.classList.remove('active'))
document.querySelector('#nav-' + view).classList.add('active')
document.querySelector('#view-' + view).classList.add('active')
clearInterval(currentPoll)
setupView[view]()
}
const setupView = {
'state': fetchAndRenderPeers,
'log': fetchAndRenderLog
}
async function fetchAndRenderPeers () {
let state = await (await fetch('/state.json', {credentials: 'include'})).json()
let html = `
<h2>Stats</h2>
<h3>Loadavg: [ ${state.stats.loadavg[0]} (1m), ${state.stats.loadavg[1]} (5m), ${state.stats.loadavg[2]} (15m) ]</h3>
<h3>Queries/sec: ${state.stats.queriesPS[0]} <small><a class="toggle-collapsable" data-target="queriesPS">toggle history</a></small></h3>
<div class="collapsable" id="queriesPS">
<table>${state.stats.queriesPS.map(renderHistoryItem).join('')}</table>
</div>
<h3>Multicast Queries/sec: ${state.stats.multicastQueriesPS[0]} <small><a class="toggle-collapsable" data-target="multicastQueriesPS">toggle history</a></small></h3>
<div class="collapsable" id="multicastQueriesPS">
<table>${state.stats.multicastQueriesPS.map(renderHistoryItem).join('')}</table>
</div>
<h2>Top keys</h2>
<table>${state.stats.topKeys.map(entry => `<tr><td>${safen(entry.name)}</td><td>${safen(entry.numRecords)} peers</td></tr>`).join('')}</table>
<h2>Peer tables</h2>
${state.peers.map(peerGroup => `
<h3>Key: ${safen(peerGroup.name)}</h3>
<table>
${peerGroup.records.map(record => (`
<tr><td>${safen(record.address)}</td></tr>
`)).join('')}
</table>
`).join('')}
`
document.querySelector('#view-state').innerHTML = html
}
function renderHistoryItem (value, i) {
return `<tr><td>-${i * 10}s</td><td>${value}</td></tr>`
}
async function fetchAndRenderLog () {
let log = await (await fetch('/log.txt', {credentials: 'include'})).text()
let html = `<pre>${log}</pre>`
document.querySelector('#view-log').innerHTML = html
}
setView('state')
function safen (str) {
return ('' + str).replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '')
}
================================================
FILE: diagnostics-server.js
================================================
var os = require('os')
var http = require('http')
var fs = require('fs')
var pump = require('pump')
var speedometer = require('speedometer')
var CircularAppendFile = require('circular-append-file')
// capture the last 10 minutes of stats
const HISTORY_LIMIT = 60
const HISTORY_INTERVAL = 10e3
const LOG_FILE_PATH = './diagnostics-server.log'
const LOG_SIZE_LIMIT = 1024 /* 1kb */ * 1024 /* 1mb */ * 32 /* 32mb */
var queriesSpeed = speedometer()
var multicastQueriesSpeed = speedometer()
var queriesPS = []
var multicastQueriesPS = []
exports.createServer = function (disc, opts = {}) {
// logging
var logFile = CircularAppendFile(LOG_FILE_PATH, {maxSize: LOG_SIZE_LIMIT})
function track (evt) {
disc.on(evt, (...args) => {
logFile.append(renderLogEntry(evt, (new Date()).toLocaleString(), args))
})
}
track('traffic')
track('secrets-rotated')
track('error')
track('listening')
track('close')
track('peer')
// stats
disc.on('traffic', (type) => {
if (type === 'in:query') {
queriesSpeed(1)
}
if (type === 'in:multicastquery') {
multicastQueriesSpeed(1)
}
})
setInterval(() => {
queriesPS.unshift(queriesSpeed())
if (queriesPS.length > HISTORY_LIMIT) queriesPS.pop()
multicastQueriesPS.unshift(multicastQueriesSpeed())
if (multicastQueriesPS.length > HISTORY_LIMIT) multicastQueriesPS.pop()
}, HISTORY_INTERVAL)
// server
return http.createServer((req, res) => {
// auth
if (opts.password) {
var auth = req.headers.authorization
if (!auth) {
res.writeHead(401, {'WWW-Authenticate': 'Basic realm="Password needed"', 'Content-Type': 'text/plain'})
return res.end('need password')
}
var givenPW = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':')[1]
if (givenPW !== opts.password) {
res.writeHead(401, {'WWW-Authenticate': 'Basic realm="Password needed"', 'Content-Type': 'text/plain'})
return res.end('bad password')
}
}
// serve
if (req.url === '/' || req.url === '/index.html') {
res.writeHead(200, {'Content-Type': 'text/html'})
pump(fs.createReadStream('./diagnostics-server/index.html'), res)
} else if (req.url === '/index.css') {
res.writeHead(200, {'Content-Type': 'text/css'})
pump(fs.createReadStream('./diagnostics-server/index.css'), res)
} else if (req.url === '/index.js') {
res.writeHead(200, {'Content-Type': 'application/javascript'})
pump(fs.createReadStream('./diagnostics-server/index.js'), res)
} else if (req.url === '/state.json') {
res.writeHead(200, {'Content-Type': 'application/json'})
res.end(JSON.stringify({
stats: {
queriesPS,
multicastQueriesPS,
loadavg: os.loadavg(),
topKeys: disc._domainStore.getTopKeyStats()
},
peers: disc.toJSON()
}))
} else if (req.url === '/log.txt') {
res.writeHead(200, {'Content-Type': 'text/plain'})
pump(logFile.createReadStream(), res)
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' })
res.end('Not found')
}
})
}
function renderLogEntry (evt, ts, args) {
switch (evt) {
case 'listening':
return `${ts} Listening\n`
case 'traffic':
let info = args[1]
switch (args[0]) {
case 'in:query':
return `${ts} <- query (from: ${info.peer.host}:${info.peer.port}) ${renderDNSMsg(info.message)}\n`
case 'in:response':
return `${ts} <- response (to: ${info.peer.host}:${info.peer.port}) ${renderDNSMsg(info.message)}\n`
case 'in:multicastquery':
return `${ts} <- multicast query (from: ${info.peer.address}:${info.peer.port}) ${renderDNSMsg(info.message)}\n`
case 'in:multicastresponse':
return `${ts} <- multicast response (from: ${info.peer.address}:${info.peer.port}) ${renderDNSMsg(info.message)}\n`
case 'out:response':
return `${ts} -> response (to: ${info.peer.host}:${info.peer.port}) ${renderDNSMsg(info.message)}\n`
case 'out:multicastresponse':
return `${ts} -> multicast response ${renderDNSMsg(info.message)}\n`
case 'out:query':
return `${ts} -> query (to: ${info.peer.host}:${info.peer.port}) ${renderDNSMsg(info.message)}\n`
case 'out:multicastquery':
return `${ts} -> multicast query ${renderDNSMsg(info.message)}\n`
default:
return `${ts} TODO ${JSON.stringify(args)}\n`
}
case 'peer':
return `${ts} Peer for "${args[0]}" at ${args[1].host}:${args[1].port}\n`
case 'close':
return `${ts} Closed\n`
case 'secrets-rotated':
return `${ts} Secrets rotated\n`
default:
return `${ts} Unknown event: ${JSON.stringify({evt, args})}\n`
}
}
function renderDNSMsg ({id, questions, answers, additionals}) {
function item (prefix) {
return ({type, name}) => {
return `${safen(prefix)}.${safen(type)}:${safen(name)}`
}
}
function list (l, prefix) {
if (!l || !l.length) return ''
return l.map(item(prefix)).join(' ')
}
return ((id) ? `id=${safen(id)} ` : '') + `${list(questions, 'Q')} ${list(answers, 'A')} ${list(additionals, 'ADD')}`
}
function safen (str) {
return ('' + str).replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '')
}
================================================
FILE: example.js
================================================
var discovery = require('dns-discovery')
var disc1 = discovery()
var disc2 = discovery()
disc2.on('peer', function (name, peer) {
console.log(name, peer)
})
disc1.announce('test', 4244)
================================================
FILE: index.js
================================================
var dns = require('dns-socket')
var events = require('events')
var util = require('util')
var crypto = require('crypto')
var network = require('network-address')
var multicast = require('multicast-dns')
var debug = require('debug')('dns-discovery')
var store = require('./store')
var IPv4 = /^\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}$/
var PORT = /^\d{1,5}$/
const TYPE_LOOKUP = 1
const TYPE_ANNOUNCE = 2
const TYPE_UNANNOUNCE = 3
module.exports = DNSDiscovery
function DNSDiscovery (opts) {
if (!(this instanceof DNSDiscovery)) return new DNSDiscovery(opts)
if (!opts) opts = {}
events.EventEmitter.call(this)
var self = this
this.socket = dns(opts)
this.servers = [].concat(opts.servers || opts.server || []).map(parseAddr)
this._sockets = []
this._onsocket(this.socket)
this.multicast = opts.multicast !== false ? (isMulticaster(opts.multicast) ? opts.multicast : multicast()) : null
if (this.multicast) {
this.multicast.on('query', onmulticastquery)
this.multicast.on('response', onmulticastresponse)
this.multicast.on('error', onerror)
}
this._loopback = !!opts.loopback
this._listening = false
this._id = crypto.randomBytes(32).toString('base64')
this._domain = opts.domain || 'dns-discovery.local'
this._pushDomain = 'push.' + this._domain
this._tokens = new Array(this.servers.length)
this._tokensAge = []
this._secrets = [
crypto.randomBytes(32),
crypto.randomBytes(32)
]
while (this._tokensAge.length < this._tokens.length) this._tokensAge.push(0)
this._interval = setInterval(rotateSecrets, 5 * 60 * 1000)
if (this._interval.unref) this._interval.unref()
this._ttl = opts.ttl || 0
this._tick = 1
var push = opts.push || {}
if (!push.ttl) push.ttl = opts.ttl || 60
if (!push.limit) push.limit = opts.limit
this._domainStore = store(opts)
this._pushStore = store(push)
function rotateSecrets () {
self._rotateSecrets()
}
function onerror (err) {
debug('Error', err)
self.emit('error', err)
}
function onmulticastquery (message, rinfo) {
debug(
'MDNS query %s:%s %dQ %dA +%d',
rinfo.address, rinfo.port,
message.questions.length,
message.answers.length,
message.additionals.length
)
self.emit('traffic', 'in:multicastquery', {message: message, peer: rinfo})
self._onmulticastquery(message, rinfo.port, rinfo.address)
}
function onmulticastresponse (message, rinfo) {
debug(
'MDNS response %s:%s %dA +%d',
rinfo.address, rinfo.port,
message.answers.length,
message.additionals.length
)
self.emit('traffic', 'in:multicastresponse', {message: message, peer: rinfo})
self._onmulticastresponse(message, rinfo.port, rinfo.address)
}
}
util.inherits(DNSDiscovery, events.EventEmitter)
DNSDiscovery.prototype.toJSON = function () {
return this._domainStore.toJSON()
}
DNSDiscovery.prototype._onsocket = function (socket) {
var self = this
this._sockets.push(socket)
socket.on('query', onquery)
socket.on('error', onerror)
function onerror (err) {
debug('Error', err)
self.emit('error', err)
}
function onquery (message, port, host) {
debug(
'DNS query %s:%s %dQ %dA +%d',
host, port,
message.questions.length,
message.answers.length,
message.additionals.length
)
self.emit('traffic', 'in:query', {message: message, peer: {port: port, host: host}})
self._onquery(message, port, host, socket)
}
}
DNSDiscovery.prototype._rotateSecrets = function () {
if (this._listening) {
debug('Rotating secrets')
this._secrets.shift()
this._secrets.push(crypto.randomBytes(32))
}
for (var i = 0; i < this._tokensAge.length; i++) {
if (this._tokensAge[i] < this._tick) {
this._tokens[i] = null
this._tokensAge[i] = 0
}
}
this.emit('secrets-rotated')
this._tick++
}
DNSDiscovery.prototype._onmulticastquery = function (query, port, host) {
var reply = {questions: query.questions, answers: []}
var i = 0
for (i = 0; i < query.questions.length; i++) {
this._onquestion(query.questions[i], port, host, reply.answers, true)
}
for (i = 0; i < query.answers.length; i++) {
this._onanswer(query.answers[i], port, host, null)
}
for (i = 0; i < query.additionals.length; i++) {
this._onanswer(query.additionals[i], port, host, null)
}
if (reply.answers.length) {
this.emit('traffic', 'out:multicastresponse', {message: reply})
this.multicast.response(reply, {port: port})
}
}
DNSDiscovery.prototype._onmulticastresponse = function (response, port, host) {
var i = 0
for (i = 0; i < response.answers.length; i++) {
this._onanswer(response.answers[i], port, host, null)
}
for (i = 0; i < response.additionals.length; i++) {
this._onanswer(response.additionals[i], port, host, null)
}
}
DNSDiscovery.prototype._onanswer = function (answer, port, host, socket) {
var domain = parseDomain(answer.name)
var id = parseId(answer.name, domain)
if (!id) {
debug('Invalid ID in answer, discarding', { name: answer.name, domain: domain, host: host, port: port })
return
}
if (answer.type === 'SRV') {
if (!IPv4.test(answer.data.target)) return
var peer = {
port: answer.data.port || port,
host: answer.data.target === '0.0.0.0' ? host : answer.data.target
}
debug('Announce received via SRV', id, peer.host + ':' + 'peer.port')
this.emit('peer', id, peer)
return
}
if (answer.type === 'TXT') {
try {
var data = decodeTxt(answer.data)
} catch (err) {
return
}
var tokenMatch = data.token === hash(this._secrets[1], host)
if (!tokenMatch || this._loopback) {
// not an echo
this._parsePeers(id, data, host)
}
if (!this._listening) {
return
}
// We are in server mode now. Add the record to the cache
if (!tokenMatch) {
// check if old token matches
if (data.token !== hash(this._secrets[0], host)) {
debug('Invalid token in TXT answer, discarding')
return
}
}
if (PORT.test(data.announce)) {
var announce = Number(data.announce) || port
debug('Announce received via TXT', id, host + ':' + announce)
this.emit('peer', id, {port: announce, host: host})
if (this._domainStore.add(id, announce, host) && socket) {
this._push(id, announce, host, socket)
}
}
if (PORT.test(data.unannounce)) {
var unannounce = Number(data.unannounce) || port
this._domainStore.remove(id, unannounce, host)
debug('Un-announce received via TXT', id, host + ':' + unannounce)
}
if (data.subscribe) {
debug('Subscribe-to-push received via TXT', id, host + ':' + port)
this._pushStore.add(id, port, host)
} else {
debug('Unsubscribe-from-push received via TXT', id, host + ':' + port)
this._pushStore.remove(id, port, host)
}
}
}
DNSDiscovery.prototype._push = function (id, port, host, socket) {
var subs = this._pushStore.get(id, 16)
var query = {
additionals: [{
type: 'SRV',
name: id + '.' + this._domain,
ttl: this._ttl,
data: {
port: port,
target: host
}
}]
}
if (subs.length) debug('Pushing announcement to', subs.length, 'subscribers')
for (var i = 0; i < subs.length; i++) {
var peer = subs[i]
var tid = socket.query(query, peer.port, peer.host)
socket.setRetries(tid, 2)
}
}
DNSDiscovery.prototype._onquestion = function (query, port, host, answers, multicast) {
var domain = parseDomain(query.name)
if (domain !== this._domain) return
if (query.type === 'TXT' && domain === query.name) {
debug('Replying state-info via TXT to %s:%s', host, port)
answers.push({
type: 'TXT',
name: query.name,
ttl: this._ttl,
data: encodeTxt({
token: hash(this._secrets[1], host),
host: host,
port: '' + port
})
})
return
}
var id = parseId(query.name, domain)
if (!id) {
debug('Invalid ID in question, discarding', { name: query.name, domain: domain, host: host, port: port })
return
}
if (query.type === 'TXT') {
var buf = toBuffer(this._domainStore.get(id, 100))
var token = hash(this._secrets[1], host)
if (multicast && !buf.length) return // just an optimization
debug('Replying known peers via TXT to', host + ':' + port)
answers.push({
type: 'TXT',
name: query.name,
ttl: this._ttl,
data: encodeTxt(buf.length ? {
token: token,
peers: buf.toString('base64')
} : {
token: token
})
})
return
}
var peers = this._domainStore.get(id, 10)
debug('Replying announce via', query.type, ' to', host + ':' + port)
for (var i = 0; i < peers.length; i++) {
var peer = peers[i]
if (query.type === 'A') {
answers.push({
type: 'A',
name: query.name,
ttl: this._ttl,
data: peer.host === '0.0.0.0' ? network() : peer.host
})
}
if (query.type === 'SRV') {
answers.push({
type: 'SRV',
name: query.name,
ttl: this._ttl,
data: {
port: peer.port,
target: peer.host
}
})
}
}
}
DNSDiscovery.prototype._onquery = function (query, port, host, socket) {
var reply = {questions: query.questions, answers: []}
var i = 0
for (i = 0; i < query.questions.length; i++) {
this._onquestion(query.questions[i], port, host, reply.answers)
}
for (i = 0; i < query.answers.length; i++) {
this._onanswer(query.answers[i], port, host, socket)
}
for (i = 0; i < query.additionals.length; i++) {
this._onanswer(query.additionals[i], port, host, socket)
}
socket.response(query, reply, port, host)
// note: emit 'traffic' after calling .response() because socket.response() modifies `reply`
this.emit('traffic', 'out:response', {message: reply, peer: {port: port, host: host}})
}
DNSDiscovery.prototype._probeAndSend = function (type, i, id, port, cb) {
var self = this
this._probe(i, 0, function (err) {
if (err) return cb(err)
self._send(type, i, id, port, cb)
})
}
DNSDiscovery.prototype._send = function (type, i, id, port, cb) {
var s = this.servers[i]
var token = this._tokens[i]
var data = null
switch (type) {
case TYPE_LOOKUP:
data = {subscribe: true, token: token}
break
case TYPE_ANNOUNCE:
data = {subscribe: true, token: token, announce: '' + port}
break
case TYPE_UNANNOUNCE:
data = {token: token, unannounce: '' + port}
break
}
var query = {
index: i,
questions: [{
type: 'TXT',
name: id + '.' + this._domain
}],
additionals: [{
type: 'TXT',
name: id + '.' + this._domain,
ttl: this._ttl,
data: encodeTxt(data)
}]
}
this.socket.query(query, s.port, s.host, cb)
this.emit('traffic', 'out:query', {message: query, peer: s})
}
DNSDiscovery.prototype.lookup = function (id, opts, cb) {
debug('lookup()', id)
this._visit(TYPE_LOOKUP, id, 0, opts, cb)
}
DNSDiscovery.prototype.announce = function (id, port, opts, cb) {
debug('announce()', id)
this._visit(TYPE_ANNOUNCE, id, port, opts, cb)
}
DNSDiscovery.prototype.unannounce = function (id, port, opts, cb) {
debug('unannounce()', id)
this._visit(TYPE_UNANNOUNCE, id, port, opts, cb)
}
DNSDiscovery.prototype._visit = function (type, id, port, opts, cb) {
if (typeof opts === 'function') return this._visit(type, id, port, null, opts)
if (typeof port === 'function') return this._visit(type, id, 0, port)
if (!cb) cb = noop
if (Buffer.isBuffer(id)) id = id.toString('hex')
if (!opts) opts = {}
var self = this
var missing = this.servers.length
var success = false
if (opts.server !== false) {
var publicPort = opts.publicPort || (opts.impliedPort ? 0 : port)
for (var i = 0; i < this.servers.length; i++) {
if (this._tokens[i]) this._send(type, i, id, publicPort, done)
else this._probeAndSend(type, i, id, publicPort, done)
}
}
if (type === TYPE_ANNOUNCE) this._domainStore.add(id, port, '0.0.0.0')
if (type === TYPE_UNANNOUNCE) this._domainStore.remove(id, port, '0.0.0.0')
if (opts.multicast !== false && this.multicast) {
if (type !== TYPE_UNANNOUNCE) {
missing++
var message = {
questions: [{
type: 'TXT',
name: id + '.' + this._domain
}]
}
this.multicast.query(message, done)
this.emit('traffic', 'out:multicastquery', {message: message})
}
}
if (!missing) {
missing++
process.nextTick(done)
}
function done (_, res, q, _port, _host) {
if (res) {
success = true
self.emit('traffic', 'in:response', {message: res, peer: {host: _host, port: _port}})
try {
var data = res.answers.length && decodeTxt(res.answers[0].data)
} catch (err) {
// do nothing
}
if (data) self._parseData(id, data, q.index, _host)
if (type === TYPE_ANNOUNCE) self.emit('announced', id, {port: port})
if (type === TYPE_UNANNOUNCE) self.emit('unannounced', id, {port: port})
}
if (!--missing) cb(success ? null : new Error('Query failed'))
}
}
DNSDiscovery.prototype._parsePeers = function (id, data, host) {
try {
var buf = Buffer.from(data.peers, 'base64')
} catch (err) {
return
}
for (var i = 0; i < buf.length; i += 6) {
var peer = decodePeer(buf, i)
if (!peer) continue
if (peer.host === '0.0.0.0') peer.host = host
this.emit('peer', id, peer)
}
}
DNSDiscovery.prototype._parseData = function (id, data, index, host) {
if (data.token) {
this._tokens[index] = data.token
this._tokensAge[index] = this._tick
}
if (data && data.peers && id) this._parsePeers(id, data, host)
}
DNSDiscovery.prototype.whoami = function (cb) {
var missing = this.servers.length
var prevData = null
var prevHost = null
var called = false
if (this.servers.length) {
for (var i = 0; i < this.servers.length; i++) this._probe(i, 2, done)
} else {
debug('whoami() failed - no servers to ping')
missing = 1
process.nextTick(done)
}
function done (_, data, port, host) {
if (data) {
if (!called && IPv4.test(data.host) && PORT.test(data.port)) {
if (prevHost && prevHost !== host) {
called = true
if (prevData.host === data.host && prevData.port === data.port) {
cb(null, {port: Number(data.port), host: data.host})
} else if (prevData.host === data.host) {
cb(null, {port: 0, host: data.host})
} else {
cb(new Error('Inconsistent remote port/host'))
}
}
prevData = data
prevHost = host
}
}
if (--missing || called) {
if (!called) {
debug('whoami() probe got response; waiting for a confirmation from %d other(s)', missing)
}
return
}
if (data) cb(null, {port: 0, host: data.host})
else cb(new Error('Probe failed'))
}
}
DNSDiscovery.prototype._probe = function (i, retries, cb) {
var self = this
var s = this.servers[i]
var q = {
questions: [{
type: 'TXT',
name: this._domain
}]
}
debug('probing %s:%d', s.host, s.port)
var first = true
var result = null
var id = this.socket.query(q, s.port, s.host, done)
if (retries) this.socket.setRetries(id, retries)
function done (_, res, query, port, host) {
if (res) {
self.emit('traffic', 'in:response', {message: res, peer: {host: host, port: port}})
try {
var data = res.answers.length && decodeTxt(res.answers[0].data)
} catch (err) {
// do nothing
}
if (data && data.token) {
self._parseData(null, data, i, host)
result = data
}
}
if (result) {
if (!first) {
s.port = port
s.secondaryPort = 0
} else {
s.secondaryPort = 0
}
debug('probe of %s:%d succeeded', host, port)
return cb(null, result, port, host)
}
if (!first || !s.secondaryPort) {
debug('probe of %s:%d failed', host, port)
return cb(new Error('Probe failed'))
}
first = false
debug('retrying probe of %s at secondary port %d', host, s.secondaryPort)
id = self.socket.query(q, s.secondaryPort, s.host, done)
if (retries) self.socket.setRetries(id, retries)
}
}
DNSDiscovery.prototype.destroy = function (onclose) {
debug('destroy()')
if (onclose) this.once('close', onclose)
var self = this
var missing = this._sockets.length
clearInterval(this._interval)
if (this.multicast) this.multicast.destroy(onmulticastclose)
else onmulticastclose()
function onmulticastclose () {
for (var i = 0; i < self._sockets.length; i++) {
self._sockets[i].destroy(onsocketclose)
}
}
function onsocketclose () {
if (!--missing) self.emit('close')
}
}
DNSDiscovery.prototype.listen = function (ports, onlistening) {
if (onlistening) this.once('listening', onlistening)
if (this._listening) throw new Error('Server is already listening')
this._listening = true
if (!ports) ports = [53, 5300]
if (!Array.isArray(ports)) ports = [ports]
debug('Listening on port(s)', ports.join(', '))
var self = this
var missing = ports.length
for (var i = 0; i < ports.length; i++) {
var socket = dns()
socket.bind(ports[i], onbind)
this._onsocket(socket)
}
function onbind () {
if (!--missing) self.emit('listening')
}
}
function noop () {}
function parseAddr (addr) {
if (addr.indexOf(':') === -1) addr += ':5300,53'
var match = addr.match(/^([^:]+)(?::(\d{1,5})(?:,(\d{1,5}))?)?$/)
if (!match) throw new Error('Could not parse ' + addr)
return {
port: Number(match[2] || 53),
secondaryPort: Number(match[3] || 0),
host: match[1]
}
}
function hash (secret, host) {
return crypto.createHash('sha256').update(secret).update(host).digest('base64')
}
function parseId (name, domain) {
if (!domain || name.length === domain.length) return null
return name.slice(0, -domain.length - 1)
}
function parseDomain (name) {
var i = name.lastIndexOf('.')
if (i === -1) return null
i = name.lastIndexOf('.', i - 1)
return i === -1 ? name : name.slice(i + 1)
}
function toBuffer (peers) {
var buf = Buffer.alloc(peers.length * 6)
for (var i = 0; i < peers.length; i++) {
if (!peers[i].buffer) peers[i].buffer = encodePeer(peers[i])
peers[i].buffer.copy(buf, i * 6)
}
return buf
}
function encodePeer (peer) {
var buf = Buffer.alloc(6)
var parts = peer.host.split('.')
buf[0] = Number(parts[0] || 0)
buf[1] = Number(parts[1] || 0)
buf[2] = Number(parts[2] || 0)
buf[3] = Number(parts[3] || 0)
buf.writeUInt16BE(peer.port || 0, 4)
return buf
}
function decodePeer (buf, offset) {
if (buf.length - offset < 6) return null
var host = buf[offset++] + '.' + buf[offset++] + '.' + buf[offset++] + '.' + buf[offset++]
var port = buf.readUInt16BE(offset)
offset += 2
return {port: port, host: host}
}
function decodeTxt (bufs) {
var data = {}
for (var i = 0; i < bufs.length; i++) {
var buf = bufs[i]
var j = buf.indexOf(61) // '='
if (j === -1) data[buf.toString()] = true
else data[buf.slice(0, j).toString()] = buf.slice(j + 1).toString()
}
return data
}
function encodeTxt (data) {
var keys = Object.keys(data)
var bufs = []
for (var i = 0; i < keys.length; i++) {
bufs.push(Buffer.from(keys[i] + '=' + data[keys[i]]))
}
return bufs
}
function isMulticaster (m) {
return typeof m === 'object' && m && typeof m.query === 'function'
}
================================================
FILE: package.json
================================================
{
"name": "dns-discovery",
"version": "6.2.3",
"description": "Discovery peers in a distributed system using regular dns and multicast dns.",
"main": "index.js",
"dependencies": {
"circular-append-file": "^1.0.1",
"debug": "^2.6.9",
"dns-socket": "^3.0.0",
"lru": "^2.0.0",
"minimist": "^1.2.0",
"multicast-dns": "^7.1.1",
"network-address": "^1.1.2",
"pump": "^3.0.0",
"speedometer": "^1.0.0",
"unordered-set": "^1.1.0"
},
"devDependencies": {
"standard": "^11.0.0",
"tape": "^4.9.0"
},
"bin": {
"dns-discovery": "./bin.js"
},
"scripts": {
"test": "standard && tape test.js"
},
"repository": {
"type": "git",
"url": "https://github.com/mafintosh/dns-discovery.git"
},
"author": "Mathias Buus (@mafintosh)",
"license": "MIT",
"bugs": {
"url": "https://github.com/mafintosh/dns-discovery/issues"
},
"homepage": "https://github.com/mafintosh/dns-discovery"
}
================================================
FILE: store.js
================================================
var set = require('unordered-set')
var lru = require('lru')
module.exports = Store
function Store (opts) {
if (!(this instanceof Store)) return new Store(opts)
if (!opts) opts = {}
this.maxValues = opts.values || Infinity
this.maxEntries = opts.records || Infinity
this.entries = lru(this.maxEntries)
this.limit = opts.limit || 10000
this.ttl = (opts.ttl || 0) * 1000
this.used = 0
}
Store.prototype.get = function (name, max) {
var entry = this.entries.get(name)
var result = []
if (!entry) return result
if (!max) max = entry.values.length
while (result.length < max) {
var i = result.length
if (i >= entry.values.length) return result
var missing = entry.values.length - i
var next = i + (Math.random() * missing) | 0
var val = entry.values[next]
if (this.ttl && (Date.now() - val._modified) > this.ttl) {
set.remove(entry.values, val)
this.used--
if (!entry.values.length) {
this.entries.remove(name)
return result
}
} else {
set.swap(entry.values, entry.values[i], val)
result.push(val)
}
}
return result
}
Store.prototype.remove = function (name, port, host) {
var address = host + ':' + port
var entry = this.entries.peek(name)
if (!entry) return
var peer = entry.byAddr.remove(address)
if (!peer) return
set.remove(entry.values, peer)
this.used--
if (!entry.values.length) this.entries.remove(name)
}
Store.prototype.add = function (name, port, host) {
var peer = new Peer(port, host)
if (this.used >= this.limit) this.evict()
var entry = this.entries.get(name)
if (!entry) {
entry = this.entries.set(name, new Record(name, this.maxValues))
}
var prev = entry.byAddr.get(peer.address)
var old = !!prev
if (!old) {
prev = peer
set.add(entry.values, peer)
entry.byAddr.set(peer.address, peer)
this.used++
}
if (this.ttl) prev._modified = Date.now()
return !old
}
Store.prototype.evict = function () {
var oldest = this.entries.tail && this.entries.peek(this.entries.tail)
if (!oldest) return
var oldestPeer = oldest.byAddr.tail && oldest.byAddr.remove(oldest.byAddr.tail)
if (!oldestPeer) return
set.remove(oldest.values, oldestPeer)
this.used--
if (!oldest.values.length) {
this.entries.remove(this.entries.tail)
}
}
Store.prototype.toJSON = function () {
var entries = []
var keys = Object.keys(this.entries.cache)
for (var i = 0; i < keys.length; i++) {
entries.push({
name: keys[i],
records: this.entries.peek(keys[i]).values
})
}
return entries
}
Store.prototype.getTopKeyStats = function (n) {
n = n || 10
var entries = []
var keys = Object.keys(this.entries.cache)
for (var i = 0; i < keys.length; i++) {
entries.push({
name: keys[i],
numRecords: this.entries.peek(keys[i]).values.length
})
}
entries.sort(function (a, b) {
return b.numRecords - a.numRecords
})
return entries.slice(0, n)
}
function Peer (port, host) {
this.host = host || '127.0.0.1'
this.port = port
this.address = this.host + ':' + this.port
this.buffer = null
this._modified = 0
this._index = 0
}
function Record (name, limit) {
this.name = name
this.values = []
this.byAddr = lru(limit || Infinity)
}
================================================
FILE: test.js
================================================
var dgram = require('dgram')
var tape = require('tape')
var discovery = require('./')
freePort(function (port) {
tape('discovers', function (t) {
var disc1 = discovery()
var disc2 = discovery()
var ns = Math.random().toString(16) + '-' + process.pid
var appName = 'dns-discovery-' + ns
disc2.on('peer', function (name, peer) {
disc1.destroy()
disc2.destroy()
t.same(name, appName)
t.same(peer.port, 8080)
t.same(typeof peer.host, 'string')
t.end()
})
disc1.announce(appName, 8080)
})
tape('discovers only using server', function (t) {
t.plan(4)
var server = discovery({multicast: false})
var client2 = discovery({multicast: false, server: 'localhost:' + port})
var client1 = discovery({multicast: false, server: 'localhost:' + port})
server.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, 8080)
})
client2.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, 8080)
server.destroy()
client1.destroy()
client2.destroy()
})
server.listen(port, function () {
client1.announce('hello-world', 8080, function () {
client2.lookup('hello-world')
})
})
})
tape('discovers only using server with secondary port', function (t) {
t.plan(4)
var server = discovery({multicast: false})
var client2 = discovery({multicast: false, server: 'localhost:9999,' + port})
var client1 = discovery({multicast: false, server: 'localhost:9998,' + port})
server.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, 8080)
})
client2.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, 8080)
server.destroy()
client1.destroy()
client2.destroy()
})
server.listen(port, function () {
client1.announce('hello-world', 8080, function () {
client2.lookup('hello-world')
})
})
})
tape('discovers only using multiple servers', function (t) {
t.plan(6)
var server = discovery({multicast: false})
var client1 = discovery({multicast: false, server: ['localhost:' + port, 'localhost:' + port]})
var client2 = discovery({multicast: false, server: ['localhost:' + port, 'localhost:' + port]})
server.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, 8080)
})
client2.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, 8080)
server.destroy()
client1.destroy()
client2.destroy()
})
server.listen(port, function () {
client1.announce('hello-world', 8080, function () {
client2.lookup('hello-world')
})
})
})
tape('limit', function (t) {
var server = discovery({multicast: false, limit: 1})
var ns = Math.random().toString(16) + '-' + process.pid
server.announce(ns + 'hello-world', 8080)
server.announce(ns + 'hello-world-2', 8081)
var domains = server.toJSON()
t.same(domains.length, 1)
t.same(domains[0].records.length, 1)
t.end()
})
tape('push', function (t) {
var server = discovery({multicast: false})
var client1 = discovery({multicast: false, server: 'localhost:' + port})
var client2 = discovery({multicast: false, server: 'localhost:' + port})
server.listen(port, function () {
server.once('peer', function () {
client2.announce('hello-world', 8081)
})
client1.lookup('hello-world')
client1.announce('hello-world', 8080)
client1.on('peer', function (id, peer) {
if (peer.port === 8081) {
client1.destroy()
client2.destroy()
server.destroy()
t.pass('got peer')
t.end()
}
})
})
})
tape('unannounce', function (t) {
var server = discovery({multicast: false})
var client1 = discovery({multicast: false, server: 'localhost:' + port})
var client2 = discovery({multicast: false, server: 'localhost:' + port})
client2.on('peer', function () {
t.fail('no peers should be discovered')
})
server.listen(port, function () {
client1.announce('test', 8080, function () {
client1.unannounce('test', 8080, function () {
client2.lookup('test', function () {
setTimeout(function () {
client2.destroy()
client1.destroy()
server.destroy()
t.end()
}, 100)
})
})
})
})
})
tape('custom socket + server', function (t) {
t.plan(5)
var socket = dgram.createSocket('udp4')
socket.once('message', function () {
t.pass('used custom socket')
})
var server = discovery({multicast: false})
var client2 = discovery({multicast: false, server: 'localhost:' + port, socket: socket})
var client1 = discovery({multicast: false, server: 'localhost:' + port})
server.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, 8080)
})
client2.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, 8080)
server.destroy()
client1.destroy()
client2.destroy()
})
server.listen(port, function () {
client1.announce('hello-world', 8080, function () {
client2.lookup('hello-world')
})
})
})
tape('implied port', function (t) {
t.plan(4)
var socket = dgram.createSocket('udp4')
var server = discovery({multicast: false})
var client2 = discovery({multicast: false, server: 'localhost:' + port})
var client1 = discovery({multicast: false, server: 'localhost:' + port, socket: socket})
server.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, socket.address().port)
})
client2.on('peer', function (name, peer) {
t.same(name, 'hello-world')
t.same(peer.port, socket.address().port)
server.destroy()
client1.destroy()
client2.destroy()
})
server.listen(port, function () {
client1.announce('hello-world', 8080, {impliedPort: true}, function () {
client2.lookup('hello-world')
})
})
})
tape('loopback', function (t) {
var client = discovery({loopback: true})
client.on('peer', function () {
client.destroy()
t.end()
})
client.announce('test', 8080)
})
tape('public port', function (t) {
var server = discovery({multicast: false})
var client2 = discovery({server: 'localhost:' + port})
var client1 = discovery({server: 'localhost:' + port})
var ns = Math.random().toString(16) + '-' + process.pid
var appName = 'dns-discovery-' + ns
var missing = 2
server.on('peer', function (name, peer) {
t.same(name, appName)
t.same(peer.port, 9090, 'server port')
})
client2.on('peer', function (name, peer) {
t.same(name, appName)
t.same(peer.port, peer.host === '127.0.0.1' ? 9090 : 8080)
if (--missing) return
server.destroy()
client1.destroy()
client2.destroy()
t.end()
})
server.listen(port, function () {
client1.announce(appName, 8080, {publicPort: 9090}, function () {
client2.lookup(appName)
})
})
})
})
function freePort (cb) {
var socket = dgram.createSocket('udp4')
socket.bind(0, function () {
socket.on('close', cb.bind(null, socket.address().port))
socket.close()
})
}
gitextract_4lt9pael/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin.js ├── diagnostics-server/ │ ├── index.css │ ├── index.html │ └── index.js ├── diagnostics-server.js ├── example.js ├── index.js ├── package.json ├── store.js └── test.js
SYMBOL INDEX (45 symbols across 6 files)
FILE: bin.js
function lookup (line 49) | function lookup () {
function announce (line 53) | function announce () {
function onpeer (line 57) | function onpeer (name, peer) {
function onlisten (line 64) | function onlisten (err) {
function ondiaglisten (line 69) | function ondiaglisten (err) {
FILE: diagnostics-server.js
constant HISTORY_LIMIT (line 9) | const HISTORY_LIMIT = 60
constant HISTORY_INTERVAL (line 10) | const HISTORY_INTERVAL = 10e3
constant LOG_FILE_PATH (line 11) | const LOG_FILE_PATH = './diagnostics-server.log'
constant LOG_SIZE_LIMIT (line 12) | const LOG_SIZE_LIMIT = 1024 /* 1kb */ * 1024 /* 1mb */ * 32
function track (line 22) | function track (evt) {
function renderLogEntry (line 97) | function renderLogEntry (evt, ts, args) {
function renderDNSMsg (line 134) | function renderDNSMsg ({id, questions, answers, additionals}) {
function safen (line 147) | function safen (str) {
FILE: diagnostics-server/index.js
function setView (line 20) | function setView (view) {
function fetchAndRenderPeers (line 34) | async function fetchAndRenderPeers () {
function renderHistoryItem (line 67) | function renderHistoryItem (value, i) {
function fetchAndRenderLog (line 71) | async function fetchAndRenderLog () {
function safen (line 79) | function safen (str) {
FILE: index.js
constant TYPE_LOOKUP (line 13) | const TYPE_LOOKUP = 1
constant TYPE_ANNOUNCE (line 14) | const TYPE_ANNOUNCE = 2
constant TYPE_UNANNOUNCE (line 15) | const TYPE_UNANNOUNCE = 3
function DNSDiscovery (line 19) | function DNSDiscovery (opts) {
function onerror (line 113) | function onerror (err) {
function onquery (line 118) | function onquery (message, port, host) {
function done (line 466) | function done (_, res, q, _port, _host) {
function done (line 521) | function done (_, data, port, host) {
function done (line 567) | function done (_, res, query, port, host) {
function onmulticastclose (line 616) | function onmulticastclose () {
function onsocketclose (line 622) | function onsocketclose () {
function onbind (line 646) | function onbind () {
function noop (line 651) | function noop () {}
function parseAddr (line 653) | function parseAddr (addr) {
function hash (line 665) | function hash (secret, host) {
function parseId (line 669) | function parseId (name, domain) {
function parseDomain (line 674) | function parseDomain (name) {
function toBuffer (line 681) | function toBuffer (peers) {
function encodePeer (line 690) | function encodePeer (peer) {
function decodePeer (line 701) | function decodePeer (buf, offset) {
function decodeTxt (line 709) | function decodeTxt (bufs) {
function encodeTxt (line 722) | function encodeTxt (data) {
function isMulticaster (line 733) | function isMulticaster (m) {
FILE: store.js
function Store (line 6) | function Store (opts) {
function Peer (line 130) | function Peer (port, host) {
function Record (line 140) | function Record (name, limit) {
FILE: test.js
function freePort (line 270) | function freePort (cb) {
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (52K chars).
[
{
"path": ".gitignore",
"chars": 36,
"preview": "node_modules\ndiagnostics-server.log\n"
},
{
"path": ".travis.yml",
"chars": 50,
"preview": "language: node_js\nnode_js:\n - '4.7.3'\n - '5.10'\n"
},
{
"path": "LICENSE",
"chars": 1079,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Mathias Buus\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "README.md",
"chars": 5449,
"preview": "# dns-discovery\n\nDiscovery peers in a distributed system using regular dns and multicast dns.\n\n```\nnpm install dns-disco"
},
{
"path": "bin.js",
"chars": 2009,
"preview": "#!/usr/bin/env node\n\nvar discovery = require('./')\nvar minimist = require('minimist')\n\nvar argv = minimist(process.argv."
},
{
"path": "diagnostics-server/index.css",
"chars": 686,
"preview": "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#n"
},
{
"path": "diagnostics-server/index.html",
"chars": 454,
"preview": "<!doctype html>\n<html>\n <head>\n <title>DNS Discovery Server Diagnostics Panel</title>\n <link rel=\"stylesheet\" hre"
},
{
"path": "diagnostics-server/index.js",
"chars": 2812,
"preview": "/* globals fetch */\n\nconst navAs = Array.from(document.querySelectorAll('#nav > a'))\nconst viewDivs = Array.from(documen"
},
{
"path": "diagnostics-server.js",
"chars": 5502,
"preview": "var os = require('os')\nvar http = require('http')\nvar fs = require('fs')\nvar pump = require('pump')\nvar speedometer = re"
},
{
"path": "example.js",
"chars": 191,
"preview": "var discovery = require('dns-discovery')\n\nvar disc1 = discovery()\nvar disc2 = discovery()\n\ndisc2.on('peer', function (na"
},
{
"path": "index.js",
"chars": 19720,
"preview": "var dns = require('dns-socket')\nvar events = require('events')\nvar util = require('util')\nvar crypto = require('crypto')"
},
{
"path": "package.json",
"chars": 963,
"preview": "{\n \"name\": \"dns-discovery\",\n \"version\": \"6.2.3\",\n \"description\": \"Discovery peers in a distributed system using regul"
},
{
"path": "store.js",
"chars": 3294,
"preview": "var set = require('unordered-set')\nvar lru = require('lru')\n\nmodule.exports = Store\n\nfunction Store (opts) {\n if (!(thi"
},
{
"path": "test.js",
"chars": 7612,
"preview": "var dgram = require('dgram')\nvar tape = require('tape')\nvar discovery = require('./')\n\nfreePort(function (port) {\n tape"
}
]
About this extraction
This page contains the full source code of the mafintosh/dns-discovery GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (48.7 KB), approximately 14.1k tokens, and a symbol index with 45 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.