1 |
|
2 | const debug = require('debug')('torrent-discovery')
|
3 | const DHT = require('bittorrent-dht/client')
|
4 | const EventEmitter = require('events').EventEmitter
|
5 | const parallel = require('run-parallel')
|
6 | const Tracker = require('bittorrent-tracker/client')
|
7 | const LSD = require('bittorrent-lsd')
|
8 |
|
9 | class 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
|
24 | this._userAgent = opts.userAgent
|
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
|
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 |
|
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 |
|
222 | module.exports = Discovery
|