├── .gitignore
├── .travis.yml
├── collaborators.md
├── index.js
├── package.json
├── readme.md
├── test-announce.js
├── test-lookup.js
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "5.10.0"
5 |
6 | # Using faster container based build environment.
7 |
8 | sudo: false
9 |
10 | before_script:
11 | - npm install -g npm
12 |
13 | script:
14 | - npm test
15 |
--------------------------------------------------------------------------------
/collaborators.md:
--------------------------------------------------------------------------------
1 | ## Collaborators
2 |
3 | discovery-channel is only possible due to the excellent work of the following collaborators:
4 |
5 |
9 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var dns = require('dns-discovery')
2 | var dht = require('bittorrent-dht')
3 | var thunky = require('thunky')
4 | var crypto = require('crypto')
5 | var events = require('events')
6 | var util = require('util')
7 | var debug = require('debug')('discovery-channel')
8 | var prettyHash = require('pretty-hash')
9 | var bufferFrom = require('buffer-from')
10 |
11 | module.exports = Discovery
12 |
13 | function Discovery (opts) {
14 | if (!(this instanceof Discovery)) return new Discovery(opts)
15 | if (!opts) opts = {}
16 |
17 | var self = this
18 |
19 | this.dht = opts.dht === false ? null : dht(opts.dht)
20 | this.dns = opts.dns === false ? null : dns(opts.dns)
21 | if (this.dns) {
22 | this.dns.on('peer', ondnspeer)
23 | this.dns.on('error', onwarn) // warn for dns errors as they are non critical
24 | this.dns.on('warn', onwarn)
25 | }
26 | if (this.dht) {
27 | this.dht.on('peer', ondhtpeer)
28 | this.dht.on('error', onerror)
29 | this.dht.on('warn', onwarn)
30 | }
31 | this.destroyed = false
32 | this.me = {host: null, port: 0}
33 |
34 | this._hash = opts.hash || (opts.hash === false ? noHash : sha1) // bt dht uses sha1 so we'll default to that
35 | this._dhtInterval = opts.dht && opts.dht.interval
36 | this._dnsInterval = opts.dns && opts.dns.interval
37 | this._announcing = {}
38 | this._unhash = {}
39 | this._whoami = this.dns && this.dns.whoami && thunky(whoami)
40 | if (this._whoami) {
41 | this._whoami()
42 | } else {
43 | debug('not running a whoami() - dns discovery was not enabled')
44 | }
45 |
46 | events.EventEmitter.call(this)
47 |
48 | function whoami (cb) {
49 | debug('whoami() started')
50 | self.dns.whoami(function (_, me) {
51 | if (me) {
52 | debug('whoami() succeeded, I am:', me)
53 | self.me = me
54 | self.emit('whoami', me)
55 | } else {
56 | debug('whoami() failed')
57 | }
58 | cb()
59 | })
60 | }
61 |
62 | function ondhtpeer (peer, infoHash, via) {
63 | if (self.destroyed) return
64 | var id = self._unhash[infoHash.toString('hex')]
65 | if (via) debug('chan=%s dht discovery peer=%s:%s via=%s:%s', prettyHash(id), peer.host, peer.port, via.host || via.address, via.port)
66 | else debug('chan=%s dht discovery peer=%s:%s', prettyHash(id), peer.host, peer.port)
67 | if (id) self.emit('peer', id, peer, 'dht')
68 | }
69 |
70 | function ondnspeer (name, peer) {
71 | if (self.destroyed) return
72 | var id = self._unhash[name]
73 | debug('chan=%s dns discovery peer=%s:%s', prettyHash(id), peer.host, peer.port)
74 | if (id) self.emit('peer', id, peer, 'dns')
75 | }
76 |
77 | function onwarn (err) {
78 | self.emit('warn', err)
79 | }
80 |
81 | function onerror (err) {
82 | self.emit('error', err)
83 | }
84 | }
85 |
86 | util.inherits(Discovery, events.EventEmitter)
87 |
88 | Discovery.prototype.join = function (id, port, opts, cb) {
89 | if (this.destroyed) return
90 | if (typeof id === 'string') id = bufferFrom(id)
91 | if (typeof opts === 'function') {
92 | cb = opts
93 | opts = {}
94 | }
95 | if (!opts) opts = {}
96 | if (!cb) cb = function () {}
97 |
98 | var announcing = typeof port === 'number'
99 | if (!port) port = 0
100 |
101 | var self = this
102 | var name = id.toString('hex')
103 | var key = name + ':' + port
104 | var hash = this._hash(id)
105 | if (hash.length > 20) hash = hash.slice(0, 20) // truncate hash so it fits in the dht
106 | var hashHex = hash.toString('hex')
107 | var dnsTimeout = null
108 | var dhtTimeout = null
109 | var destroyed = false
110 | var publicPort = 0
111 | var skipMulticast = false
112 |
113 | if (this._announcing[key]) return
114 |
115 | debug('chan=%s join()', prettyHash(id))
116 |
117 | this._unhash[hashHex] = id
118 | this._announcing[key] = {
119 | id: id,
120 | port: port,
121 | destroy: destroy
122 | }
123 |
124 | var pending = 0
125 | var firstQueryDone = false
126 | var error = null
127 | var succeded = false
128 |
129 | if (!opts.impliedPort || !this._whoami) return ready()
130 |
131 | // do a multicast only query immediately.
132 | // multicast has no way to know if there will definitively be no replies
133 | // so you can assume if you get no mdns responses by the time the first
134 | // dns/dht responses come back then there are probably no mdns peers online
135 | if (this.dns) {
136 | if (announcing) this.dns.announce(hashHex, port, {server: false})
137 | else this.dns.lookup(hashHex, {server: false})
138 | }
139 |
140 | this._whoami(function () {
141 | if (destroyed) return
142 | if (self.me && self.me.port) publicPort = self.me.port
143 | // since we already did it, skip multicast on the first call
144 | skipMulticast = true
145 | ready()
146 | })
147 |
148 | function queryDone (err) {
149 | if (firstQueryDone) return
150 | if (err) error = err
151 | else succeded = true
152 | if (--pending > 0) return
153 | firstQueryDone = true
154 | self.emit('query-done', true)
155 | cb(succeded ? null : error)
156 | }
157 |
158 | function ready () {
159 | if (self.dns) {
160 | pending++
161 | dns()
162 | }
163 | if (self.dht) {
164 | pending++
165 | dht()
166 | }
167 | }
168 |
169 | function destroy () {
170 | destroyed = true
171 | clearTimeout(dnsTimeout)
172 | clearTimeout(dhtTimeout)
173 | delete self._unhash[hashHex]
174 | if (self.dns) self.dns.unannounce(hashHex, port)
175 | }
176 |
177 | function dns () {
178 | if (announcing) {
179 | debug('chan=%s dns %s', prettyHash(id), 'announce', {port: port, publicPort: publicPort, multicast: !skipMulticast})
180 | self.dns.announce(hashHex, port, {publicPort: publicPort, multicast: !skipMulticast}, queryDone)
181 | } else {
182 | debug('chan=%s dns %s', prettyHash(id), 'lookup')
183 | self.dns.lookup(hashHex, {multicast: !skipMulticast}, queryDone)
184 | }
185 | skipMulticast = false
186 | dnsTimeout = setTimeout(dns, self._dnsInterval || (60 * 1000 + (Math.random() * 10 * 1000) | 0))
187 | }
188 |
189 | function dht () {
190 | debug('chan=%s dht %s', prettyHash(id), announcing ? 'announce' : 'lookup')
191 | if (announcing) self.dht.announce(hash, publicPort || port, queryDone)
192 | else self.dht.lookup(hash, queryDone)
193 | dhtTimeout = setTimeout(dht, self._dhtInterval || (10 * 60 * 1000 + (Math.random() * 5 * 60 * 1000) | 0))
194 | }
195 | }
196 |
197 | Discovery.prototype.leave = function (id, port) {
198 | if (this.destroyed) return
199 | if (!port) port = 0
200 | if (typeof id === 'string') id = bufferFrom(id)
201 | var key = id.toString('hex') + ':' + port
202 | if (!this._announcing[key]) return
203 | debug('chan=%s leave()', prettyHash(id))
204 | this._announcing[key].destroy()
205 | delete this._announcing[key]
206 | }
207 |
208 | Discovery.prototype.update = function () {
209 | var all = this.list()
210 | for (var i = 0; i < all.length; i++) {
211 | all[i].destroy()
212 | this.leave(all[i].id, all[i].port)
213 | this.join(all[i].id, all[i].port)
214 | }
215 | }
216 |
217 | Discovery.prototype.list = function () {
218 | var keys = Object.keys(this._announcing)
219 | var all = new Array(keys.length)
220 | for (var i = 0; i < keys.length; i++) {
221 | var ann = this._announcing[keys[i]]
222 | all[i] = {id: ann.id, port: ann.port}
223 | }
224 | return all
225 | }
226 |
227 | Discovery.prototype.destroy = function (cb) {
228 | if (this.destroyed) {
229 | if (cb) process.nextTick(cb)
230 | return
231 | }
232 | this.destroyed = true
233 | var keys = Object.keys(this._announcing)
234 | for (var i = 0; i < keys.length; i++) this._announcing[keys[i]].destroy()
235 | this._announcing = {}
236 | if (cb) this.once('close', cb)
237 | var self = this
238 |
239 | if (!this.dht) ondhtdestroy()
240 | else this.dht.destroy(ondhtdestroy)
241 |
242 | function ondhtdestroy () {
243 | if (!self.dns) ondnsdestroy()
244 | else self.dns.destroy(ondnsdestroy)
245 | }
246 |
247 | function ondnsdestroy () {
248 | self.emit('close')
249 | }
250 | }
251 |
252 | function sha1 (id) {
253 | return crypto.createHash('sha1').update(id).digest()
254 | }
255 |
256 | function noHash (id) {
257 | if (typeof id === 'string') return bufferFrom(id)
258 | return id
259 | }
260 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "discovery-channel",
3 | "version": "5.5.1",
4 | "description": "discover peers that have hashes using various kewl methods",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "standard && node test.js"
8 | },
9 | "author": "max ogden",
10 | "license": "ISC",
11 | "dependencies": {
12 | "bittorrent-dht": "^7.10.0",
13 | "buffer-from": "^1.0.0",
14 | "debug": "^2.6.9",
15 | "dns-discovery": "^6.0.1",
16 | "pretty-hash": "^1.0.1",
17 | "thunky": "^0.1.0"
18 | },
19 | "devDependencies": {
20 | "standard": "^6.0.5",
21 | "tape": "^4.9.0"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/maxogden/discovery-channel.git"
26 | },
27 | "bugs": {
28 | "url": "https://github.com/maxogden/discovery-channel/issues"
29 | },
30 | "homepage": "https://github.com/maxogden/discovery-channel#readme"
31 | }
32 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/hyperswarm/dht) See [hyperswarm/dht](https://github.com/hyperswarm/dht) for similar functionality.
2 |
3 | More info on active projects and modules at [dat-ecosystem.org](https://dat-ecosystem.org/)
4 |
5 | ---
6 |
7 | # discovery-channel
8 |
9 | Search for a key across multiple discovery networks and find peers who answer.
10 |
11 | Currently searches across and advertises on [the Bittorrent DHT](https://en.wikipedia.org/wiki/Mainline_DHT), centralized DNS servers and [Multicast DNS](https://en.wikipedia.org/wiki/Multicast_DNS) simultaneously.
12 |
13 | Uses the [bittorrent-dht](https://github.com/feross/bittorrent-dht) and [dns-discovery](https://github.com/mafintosh/dns-discovery) modules.
14 |
15 | Also check out [discovery-swarm](https://github.com/mafintosh/discovery-swarm) which adds connection management on top of this module.
16 |
17 | [![travis][travis-image]][travis-url]
18 |
19 | [travis-image]: https://img.shields.io/travis/maxogden/discovery-channel.svg?style=flat
20 | [travis-url]: https://travis-ci.org/maxogden/discovery-channel
21 |
22 | ## Usage
23 |
24 | ### `var DC = require('discovery-channel')`
25 |
26 | Returns a constructor
27 |
28 | ### `var channel = DC()`
29 |
30 | Returns a new instance. `opts` is optional and can have the following properties:
31 |
32 | - `dns` - default `undefined`, if `false` will disable `dns` discovery, any other value type will be passed to the `dns-discovery` constructor
33 | - `dht` - default `undefined`, if `false` will disable `dht` discovery, any other value type will be passed to the `bittorrent-dht` constructor
34 | - `hash` - default `sha1`. provide a custom hash function to hash ids before they are stored in the dht / on dns servers.
35 |
36 | By default hashes are re-announced around every 10 min on the dht and 1 min using dns. Set `dht.interval` or `dns.interval` to change these.
37 |
38 | ### `channel.join(id, [port], [cb])`
39 |
40 | Perform a lookup across all networks for `id`. `id` can be a buffer or a string.
41 | Specify `port` if you want to announce that you share `id` as well.
42 |
43 | If you specify `cb`, it will be called **when the first round** of discovery has completed. But only on the first round.
44 |
45 | ### `channel.leave(id, [port])`
46 |
47 | Stop looking for `id`. `id` can be a buffer or a string.
48 | Specify `port` to stop announcing that you share `id` as well.
49 |
50 | ### `channel.update()`
51 |
52 | Force announce / lookup all joined hashes
53 |
54 | ### `var list = channel.list()`
55 |
56 | List all the channels you have joined. The returned array items look like this
57 |
58 | ``` js
59 | {
60 | id: ,
61 | port:
62 | }
63 | ```
64 |
65 | ### `channel.on('peer', id, peer, type)`
66 |
67 | Emitted when a peer answers your query.
68 |
69 | - `id` is the id (as a buffer) this peer was discovered for
70 | - `peer` is the peer that was discovered `{port: port, host: host}`
71 | - `type` is the network type (one of `['dht', 'dns']`)
72 |
73 | ### `channel.destroy(cb)`
74 |
75 | Stops all lookups and advertisements and call `cb` when done.
76 |
77 | ### `channel.on('close')`
78 |
79 | Emitted when the channel is destroyed
80 |
--------------------------------------------------------------------------------
/test-announce.js:
--------------------------------------------------------------------------------
1 | var DC = require('./index.js')
2 | var bufferFrom = require('buffer-from')
3 |
4 | var channel = DC({
5 | dns: {
6 | servers: [
7 | 'discovery1.publicbits.org',
8 | 'discovery2.publicbits.org'
9 | ]
10 | }
11 | })
12 |
13 | var hash = bufferFrom('deadbeefbeefbeefbeefdeadbeefbeefbeefbeef', 'hex')
14 |
15 | channel.on('whoami', function (me) {
16 | console.log('I am ' + me.host + (me.port ? ':' + me.port : '') + ' on the internet')
17 | })
18 |
19 | channel.join(hash, Number(process.argv[2] || 1337))
20 |
--------------------------------------------------------------------------------
/test-lookup.js:
--------------------------------------------------------------------------------
1 | var DC = require('./index.js')
2 | var bufferFrom = require('buffer-from')
3 |
4 | var channel = DC({
5 | dns: {
6 | servers: [
7 | 'discovery1.publicbits.org',
8 | 'discovery2.publicbits.org'
9 | ]
10 | }
11 | })
12 |
13 | var hash = bufferFrom('deadbeefbeefbeefbeefdeadbeefbeefbeefbeef', 'hex')
14 |
15 | channel.join(hash)
16 | channel.on('peer', function (hash, peer, type) {
17 | console.log('found peer: ' + peer.host + ':' + peer.port + ' using ' + type + (peer.local ? ' (local)' : ''))
18 | })
19 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape')
2 | var DC = require('./index.js')
3 | var crypto = require('crypto')
4 |
5 | test('list', function (t) {
6 | var channel = DC({dht: false, dns: false})
7 | var id = crypto.randomBytes(32)
8 |
9 | channel.join(id)
10 | t.same(channel.list(), [{id: id, port: 0}])
11 |
12 | channel.leave(id)
13 | channel.join(id, 8080)
14 | t.same(channel.list(), [{id: id, port: 8080}])
15 |
16 | channel.leave(id)
17 | t.same(channel.list(), [{id: id, port: 8080}])
18 |
19 | channel.leave(id, 8080)
20 | t.same(channel.list(), [])
21 |
22 | channel.destroy()
23 | t.end()
24 | })
25 |
26 | test('find each other', function (t) {
27 | var id = crypto.randomBytes(32)
28 | var pending = 2
29 | t.plan(2)
30 |
31 | var channel1 = DC()
32 | var channel2 = DC()
33 |
34 | channel1.join(id, 1337)
35 | channel2.join(id, 7331)
36 |
37 | channel1.on('peer', function (hash, peer) {
38 | if (peer.port === 7331) {
39 | t.pass('found second channel')
40 | done()
41 | }
42 | })
43 |
44 | channel2.on('peer', function (hash, peer) {
45 | if (peer.port === 1337) {
46 | t.pass('found first channel')
47 | done()
48 | }
49 | })
50 |
51 | function done () {
52 | if (--pending) return
53 | channel1.destroy()
54 | channel2.destroy()
55 | }
56 | })
57 |
58 | test('join cb gets called', function (t) {
59 | var id = crypto.randomBytes(32)
60 | var channel1 = DC()
61 | channel1.join(id, 1337, function (err) {
62 | t.ifErr(err)
63 | t.ok('called cb')
64 | channel1.destroy()
65 | t.end()
66 | })
67 | })
68 |
--------------------------------------------------------------------------------