├── .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 | 6 | 7 | 8 |
maxogdenGitHub/maxogden
mafintoshGitHub/mafintosh
karissaGitHub/karissa
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 | [![deprecated](http://badges.github.io/stability-badges/dist/deprecated.svg)](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 | --------------------------------------------------------------------------------