UNPKG

6.05 kBJavaScriptView Raw
1/*! torrent-discovery. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
2const debug = require('debug')('torrent-discovery')
3const DHT = require('bittorrent-dht/client') // empty object in browser
4const EventEmitter = require('events').EventEmitter
5const parallel = require('run-parallel')
6const Tracker = require('bittorrent-tracker/client')
7const LSD = require('bittorrent-lsd')
8
9class Discovery extends EventEmitter {
10 constructor (opts) {
11 super()
12
13 if (!opts.peerId) throw new Error('Option `peerId` is required')
14 if (!opts.infoHash) throw new Error('Option `infoHash` is required')
15 if (!process.browser && !opts.port) throw new Error('Option `port` is required')
16
17 this.peerId = typeof opts.peerId === 'string'
18 ? opts.peerId
19 : opts.peerId.toString('hex')
20 this.infoHash = typeof opts.infoHash === 'string'
21 ? opts.infoHash.toLowerCase()
22 : opts.infoHash.toString('hex')
23 this._port = opts.port // torrent port
24 this._userAgent = opts.userAgent // User-Agent header for http requests
25
26 this.destroyed = false
27
28 this._announce = opts.announce || []
29 this._intervalMs = opts.intervalMs || (15 * 60 * 1000)
30 this._trackerOpts = null
31 this._dhtAnnouncing = false
32 this._dhtTimeout = false
33 this._internalDHT = false // is the DHT created internally?
34
35 this._onWarning = err => {
36 this.emit('warning', err)
37 }
38 this._onError = err => {
39 this.emit('error', err)
40 }
41 this._onDHTPeer = (peer, infoHash) => {
42 if (infoHash.toString('hex') !== this.infoHash) return
43 this.emit('peer', `${peer.host}:${peer.port}`, 'dht')
44 }
45 this._onTrackerPeer = peer => {
46 this.emit('peer', peer, 'tracker')
47 }
48 this._onTrackerAnnounce = () => {
49 this.emit('trackerAnnounce')
50 }
51 this._onLSDPeer = (peer, infoHash) => {
52 this.emit('peer', peer, 'lsd')
53 }
54
55 const createDHT = (port, opts) => {
56 const dht = new DHT(opts)
57 dht.on('warning', this._onWarning)
58 dht.on('error', this._onError)
59 dht.listen(port)
60 this._internalDHT = true
61 return dht
62 }
63
64 if (opts.tracker === false) {
65 this.tracker = null
66 } else if (opts.tracker && typeof opts.tracker === 'object') {
67 this._trackerOpts = Object.assign({}, opts.tracker)
68 this.tracker = this._createTracker()
69 } else {
70 this.tracker = this._createTracker()
71 }
72
73 if (opts.dht === false || typeof DHT !== 'function') {
74 this.dht = null
75 } else if (opts.dht && typeof opts.dht.addNode === 'function') {
76 this.dht = opts.dht
77 } else if (opts.dht && typeof opts.dht === 'object') {
78 this.dht = createDHT(opts.dhtPort, opts.dht)
79 } else {
80 this.dht = createDHT(opts.dhtPort)
81 }
82
83 if (this.dht) {
84 this.dht.on('peer', this._onDHTPeer)
85 this._dhtAnnounce()
86 }
87
88 if (opts.lsd === false || typeof LSD !== 'function') {
89 this.lsd = null
90 } else {
91 this.lsd = this._createLSD()
92 }
93 }
94
95 updatePort (port) {
96 if (port === this._port) return
97 this._port = port
98
99 if (this.dht) this._dhtAnnounce()
100
101 if (this.tracker) {
102 this.tracker.stop()
103 this.tracker.destroy(() => {
104 this.tracker = this._createTracker()
105 })
106 }
107 }
108
109 complete (opts) {
110 if (this.tracker) {
111 this.tracker.complete(opts)
112 }
113 }
114
115 destroy (cb) {
116 if (this.destroyed) return
117 this.destroyed = true
118
119 clearTimeout(this._dhtTimeout)
120
121 const tasks = []
122
123 if (this.tracker) {
124 this.tracker.stop()
125 this.tracker.removeListener('warning', this._onWarning)
126 this.tracker.removeListener('error', this._onError)
127 this.tracker.removeListener('peer', this._onTrackerPeer)
128 this.tracker.removeListener('update', this._onTrackerAnnounce)
129 tasks.push(cb => {
130 this.tracker.destroy(cb)
131 })
132 }
133
134 if (this.dht) {
135 this.dht.removeListener('peer', this._onDHTPeer)
136 }
137
138 if (this._internalDHT) {
139 this.dht.removeListener('warning', this._onWarning)
140 this.dht.removeListener('error', this._onError)
141 tasks.push(cb => {
142 this.dht.destroy(cb)
143 })
144 }
145
146 if (this.lsd) {
147 this.lsd.removeListener('warning', this._onWarning)
148 this.lsd.removeListener('error', this._onError)
149 this.lsd.removeListener('peer', this._onLSDPeer)
150 tasks.push(cb => {
151 this.lsd.destroy(cb)
152 })
153 }
154
155 parallel(tasks, cb)
156
157 // cleanup
158 this.dht = null
159 this.tracker = null
160 this.lsd = null
161 this._announce = null
162 }
163
164 _createTracker () {
165 const opts = Object.assign({}, this._trackerOpts, {
166 infoHash: this.infoHash,
167 announce: this._announce,
168 peerId: this.peerId,
169 port: this._port,
170 userAgent: this._userAgent
171 })
172
173 const tracker = new Tracker(opts)
174 tracker.on('warning', this._onWarning)
175 tracker.on('error', this._onError)
176 tracker.on('peer', this._onTrackerPeer)
177 tracker.on('update', this._onTrackerAnnounce)
178 tracker.setInterval(this._intervalMs)
179 tracker.start()
180 return tracker
181 }
182
183 _dhtAnnounce () {
184 if (this._dhtAnnouncing) return
185 debug('dht announce')
186
187 this._dhtAnnouncing = true
188 clearTimeout(this._dhtTimeout)
189
190 this.dht.announce(this.infoHash, this._port, err => {
191 this._dhtAnnouncing = false
192 debug('dht announce complete')
193
194 if (err) this.emit('warning', err)
195 this.emit('dhtAnnounce')
196
197 if (!this.destroyed) {
198 this._dhtTimeout = setTimeout(() => {
199 this._dhtAnnounce()
200 }, this._intervalMs + Math.floor(Math.random() * this._intervalMs / 5))
201 if (this._dhtTimeout.unref) this._dhtTimeout.unref()
202 }
203 })
204 }
205
206 _createLSD () {
207 const opts = Object.assign({}, {
208 infoHash: this.infoHash,
209 peerId: this.peerId,
210 port: this._port
211 })
212
213 const lsd = new LSD(opts)
214 lsd.on('warning', this._onWarning)
215 lsd.on('error', this._onError)
216 lsd.on('peer', this._onLSDPeer)
217 lsd.start()
218 return lsd
219 }
220}
221
222module.exports = Discovery