bittorrent-tracker/lib/client/udp-tracker.js

335 lines
9.6 KiB
JavaScript
Raw Normal View History

import arrayRemove from 'unordered-array-remove'
import BN from 'bn.js'
import clone from 'clone'
import Debug from 'debug'
import dgram from 'dgram'
import randombytes from 'randombytes'
import Socks from 'socks'
import common from '../common.js'
import Tracker from './tracker.js'
import compact2string from 'compact2string'
const debug = Debug('bittorrent-tracker:udp-tracker')
/**
* UDP torrent tracker client (for an individual tracker)
*
* @param {Client} client parent bittorrent tracker client
* @param {string} announceUrl announce url of tracker
* @param {Object} opts options object
*/
2018-10-03 12:44:11 +00:00
class UDPTracker extends Tracker {
2021-06-15 01:54:41 +00:00
constructor (client, announceUrl) {
2018-10-03 12:44:11 +00:00
super(client, announceUrl)
debug('new udp tracker %s', announceUrl)
2018-10-03 13:06:38 +00:00
this.cleanupFns = []
this.maybeDestroyCleanup = null
2018-10-03 12:44:11 +00:00
}
2018-10-03 12:44:11 +00:00
announce (opts) {
2018-10-03 13:06:38 +00:00
if (this.destroyed) return
this._request(opts)
2018-10-03 12:44:11 +00:00
}
2018-10-03 12:44:11 +00:00
scrape (opts) {
2018-10-03 13:06:38 +00:00
if (this.destroyed) return
2018-10-03 12:44:11 +00:00
opts._scrape = true
2018-10-03 13:06:38 +00:00
this._request(opts) // udp scrape uses same announce url
2018-10-03 12:44:11 +00:00
}
2018-10-03 12:44:11 +00:00
destroy (cb) {
const self = this
2018-10-03 13:06:38 +00:00
if (this.destroyed) return cb(null)
this.destroyed = true
clearInterval(this.interval)
2020-10-29 20:25:57 +00:00
let timeout
2018-10-03 12:44:11 +00:00
// If there are no pending requests, destroy immediately.
2018-10-03 13:06:38 +00:00
if (this.cleanupFns.length === 0) return destroyCleanup()
2018-10-03 12:44:11 +00:00
// Otherwise, wait a short time for pending requests to complete, then force
// destroy them.
2020-10-29 20:25:57 +00:00
timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT)
2018-10-03 12:44:11 +00:00
// But, if all pending requests complete before the timeout fires, do cleanup
// right away.
2018-10-03 13:06:38 +00:00
this.maybeDestroyCleanup = () => {
if (this.cleanupFns.length === 0) destroyCleanup()
2018-10-03 12:44:11 +00:00
}
2018-10-03 12:44:11 +00:00
function destroyCleanup () {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
self.maybeDestroyCleanup = null
self.cleanupFns.slice(0).forEach(cleanup => {
cleanup()
})
self.cleanupFns = []
cb(null)
}
}
2018-10-03 12:44:11 +00:00
_request (opts) {
const self = this
if (!opts) opts = {}
2021-05-20 19:38:51 +00:00
let { hostname, port } = common.parseUrl(this.announceUrl)
if (port === '') port = 80
let timeout
// Socket used to connect to the socks server to create a relay, null if socks is disabled
let proxySocket
// Socket used to connect to the tracker or to the socks relay if socks is enabled
let socket
// Contains the host/port of the socks relay
let relay
2018-10-03 12:44:11 +00:00
let transactionId = genTransactionId()
const proxyOpts = this.client._proxyOpts && clone(this.client._proxyOpts.socksProxy)
if (proxyOpts) {
if (!proxyOpts.proxy) proxyOpts.proxy = {}
// UDP requests uses the associate command
proxyOpts.proxy.command = 'associate'
if (!proxyOpts.target) {
// This should contain client IP and port but can be set to 0 if we don't have this information
proxyOpts.target = {
host: '0.0.0.0',
port: 0
}
}
if (proxyOpts.proxy.type === 5) {
Socks.createConnection(proxyOpts, onGotConnection)
} else {
debug('Ignoring Socks proxy for UDP request because type 5 is required')
onGotConnection(null)
}
} else {
onGotConnection(null)
}
2018-10-03 13:06:38 +00:00
this.cleanupFns.push(cleanup)
function onGotConnection (err, s, info) {
if (err) return onError(err)
proxySocket = s
socket = dgram.createSocket('udp4')
relay = info
timeout = setTimeout(() => {
// does not matter if `stopped` event arrives, so supress errors
if (opts.event === 'stopped') cleanup()
else onError(new Error(`tracker request timed out (${opts.event})`))
timeout = null
}, common.REQUEST_TIMEOUT)
if (timeout.unref) timeout.unref()
send(Buffer.concat([
common.CONNECTION_ID,
common.toUInt32(common.ACTIONS.CONNECT),
transactionId
]), relay)
socket.once('error', onError)
socket.on('message', onSocketMessage)
}
2018-10-03 12:44:11 +00:00
function cleanup () {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
if (socket) {
arrayRemove(self.cleanupFns, self.cleanupFns.indexOf(cleanup))
socket.removeListener('error', onError)
socket.removeListener('message', onSocketMessage)
socket.on('error', noop) // ignore all future errors
try { socket.close() } catch (err) {}
socket = null
if (proxySocket) {
try { proxySocket.close() } catch (err) {}
proxySocket = null
}
2018-10-03 12:44:11 +00:00
}
if (self.maybeDestroyCleanup) self.maybeDestroyCleanup()
}
2018-10-03 12:44:11 +00:00
function onError (err) {
cleanup()
if (self.destroyed) return
try {
// Error.message is readonly on some platforms.
if (err.message) err.message += ` (${self.announceUrl})`
2020-03-29 17:22:44 +00:00
} catch (ignoredErr) {}
2018-10-03 12:44:11 +00:00
// errors will often happen if a tracker is offline, so don't treat it as fatal
self.client.emit('warning', err)
}
2018-10-03 12:44:11 +00:00
function onSocketMessage (msg) {
if (proxySocket) msg = msg.slice(10)
2018-10-03 12:44:11 +00:00
if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) {
return onError(new Error('tracker sent invalid transaction id'))
}
2018-10-03 12:44:11 +00:00
const action = msg.readUInt32BE(0)
debug('UDP response %s, action %s', self.announceUrl, action)
switch (action) {
2018-12-20 19:59:53 +00:00
case 0: { // handshake
2018-10-03 12:44:11 +00:00
// Note: no check for `self.destroyed` so that pending messages to the
// tracker can still be sent/received even after destroy() is called
2018-10-03 12:44:11 +00:00
if (msg.length < 16) return onError(new Error('invalid udp handshake'))
2018-10-03 12:44:11 +00:00
if (opts._scrape) scrape(msg.slice(8, 16))
else announce(msg.slice(8, 16), opts)
2018-10-03 12:44:11 +00:00
break
2018-12-20 19:59:53 +00:00
}
case 1: { // announce
2018-10-03 12:44:11 +00:00
cleanup()
if (self.destroyed) return
2018-10-03 12:44:11 +00:00
if (msg.length < 20) return onError(new Error('invalid announce message'))
const interval = msg.readUInt32BE(8)
2018-10-03 12:44:11 +00:00
if (interval) self.setInterval(interval * 1000)
2018-10-03 12:44:11 +00:00
self.client.emit('update', {
2015-07-29 08:47:09 +00:00
announce: self.announceUrl,
2018-10-03 12:44:11 +00:00
complete: msg.readUInt32BE(16),
incomplete: msg.readUInt32BE(12)
})
let addrs
2018-10-03 12:44:11 +00:00
try {
addrs = compact2string.multi(msg.slice(20))
} catch (err) {
return self.client.emit('warning', err)
}
addrs.forEach(addr => {
self.client.emit('peer', addr)
})
2018-10-03 12:44:11 +00:00
break
2018-12-20 19:59:53 +00:00
}
case 2: { // scrape
2018-10-03 12:44:11 +00:00
cleanup()
if (self.destroyed) return
2018-10-03 12:44:11 +00:00
if (msg.length < 20 || (msg.length - 8) % 12 !== 0) {
return onError(new Error('invalid scrape message'))
}
const infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
2021-06-15 01:54:41 +00:00
? opts.infoHash.map(infoHash => infoHash.toString('hex'))
2019-07-05 21:36:14 +00:00
: [(opts.infoHash && opts.infoHash.toString('hex')) || self.client.infoHash]
2018-10-03 12:44:11 +00:00
for (let i = 0, len = (msg.length - 8) / 12; i < len; i += 1) {
self.client.emit('scrape', {
announce: self.announceUrl,
infoHash: infoHashes[i],
complete: msg.readUInt32BE(8 + (i * 12)),
downloaded: msg.readUInt32BE(12 + (i * 12)),
incomplete: msg.readUInt32BE(16 + (i * 12))
})
}
break
2018-12-20 19:59:53 +00:00
}
case 3: { // error
2018-10-03 12:44:11 +00:00
cleanup()
if (self.destroyed) return
if (msg.length < 8) return onError(new Error('invalid error message'))
self.client.emit('warning', new Error(msg.slice(8).toString()))
2018-10-03 12:44:11 +00:00
break
2018-12-20 19:59:53 +00:00
}
2018-12-20 20:03:15 +00:00
default:
2018-10-03 12:44:11 +00:00
onError(new Error('tracker sent invalid action'))
break
}
}
function send (message, proxyInfo) {
if (proxyInfo) {
const pack = Socks.createUDPFrame({ host: hostname, port }, message)
socket.send(pack, 0, pack.length, proxyInfo.port, proxyInfo.host)
} else {
socket.send(message, 0, message.length, port, hostname)
}
2018-10-03 12:44:11 +00:00
}
2018-10-03 12:44:11 +00:00
function announce (connectionId, opts) {
transactionId = genTransactionId()
send(Buffer.concat([
connectionId,
common.toUInt32(common.ACTIONS.ANNOUNCE),
transactionId,
self.client._infoHashBuffer,
self.client._peerIdBuffer,
toUInt64(opts.downloaded),
opts.left != null ? toUInt64(opts.left) : Buffer.from('FFFFFFFFFFFFFFFF', 'hex'),
toUInt64(opts.uploaded),
common.toUInt32(common.EVENTS[opts.event] || 0),
common.toUInt32(0), // ip address (optional)
common.toUInt32(0), // key (optional)
common.toUInt32(opts.numwant),
toUInt16(self.client._port)
]), relay)
2018-10-03 12:44:11 +00:00
}
2018-10-03 12:44:11 +00:00
function scrape (connectionId) {
transactionId = genTransactionId()
2018-10-03 12:44:11 +00:00
const infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0)
? Buffer.concat(opts.infoHash)
: (opts.infoHash || self.client._infoHashBuffer)
2018-10-03 12:44:11 +00:00
send(Buffer.concat([
connectionId,
common.toUInt32(common.ACTIONS.SCRAPE),
transactionId,
infoHash
]), relay)
2018-10-03 12:44:11 +00:00
}
}
}
2018-10-03 12:44:11 +00:00
UDPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes
function genTransactionId () {
2016-08-21 02:37:36 +00:00
return randombytes(4)
}
function toUInt16 (n) {
2018-10-03 12:44:11 +00:00
const buf = Buffer.allocUnsafe(2)
buf.writeUInt16BE(n, 0)
return buf
}
2018-10-03 12:44:11 +00:00
const MAX_UINT = 4294967295
function toUInt64 (n) {
if (n > MAX_UINT || typeof n === 'string') {
2018-10-03 12:44:11 +00:00
const bytes = new BN(n).toArray()
while (bytes.length < 8) {
bytes.unshift(0)
}
return Buffer.from(bytes)
}
return Buffer.concat([common.toUInt32(0), common.toUInt32(n)])
}
function noop () {}
2018-10-03 12:44:11 +00:00
export default UDPTracker