2022-12-05 22:06:54 +00:00
import arrayRemove from 'unordered-array-remove'
import Debug from 'debug'
import dgram from 'dgram'
import Socks from 'socks'
2023-05-26 16:54:30 +00:00
import { concat , hex2arr , randomBytes } from 'uint8-util'
2022-12-05 22:06:54 +00:00
import common from '../common.js'
import Tracker from './tracker.js'
import compact2string from 'compact2string'
const debug = Debug ( 'bittorrent-tracker:udp-tracker' )
2015-03-24 08:01:49 +00:00
2024-07-01 17:23:38 +00:00
// this was done some many years ago to fix "prevent Socks instances concurrency", and used some bloated package, no clue if it's needed, but this is simpler, #356
const clone = obj => JSON . parse ( JSON . stringify ( obj ) )
2015-03-24 08:01:49 +00:00
/ * *
* 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 )
2015-03-24 08:01:49 +00:00
2018-10-03 13:06:38 +00:00
this . cleanupFns = [ ]
this . maybeDestroyCleanup = null
2018-10-03 12:44:11 +00:00
}
2015-07-29 09:12:14 +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
}
2015-03-24 08:01:49 +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
}
2015-03-24 08:01:49 +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 )
2015-05-17 06:25:34 +00:00
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 ( )
2017-01-21 02:41:28 +00:00
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 )
2017-01-21 02:41:28 +00:00
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
}
2017-01-21 02:41:28 +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 )
2017-01-21 02:41:28 +00:00
}
}
2015-05-17 06:25:34 +00:00
2018-10-03 12:44:11 +00:00
_request ( opts ) {
const self = this
if ( ! opts ) opts = { }
2019-08-09 03:04:41 +00:00
2021-05-20 19:38:51 +00:00
let { hostname , port } = common . parseUrl ( this . announceUrl )
2019-08-09 03:04:41 +00:00
if ( port === '' ) port = 80
2021-08-20 21:08:36 +00:00
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 ( )
2021-08-20 21:08:36 +00:00
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 )
}
2017-01-21 02:41:28 +00:00
2018-10-03 13:06:38 +00:00
this . cleanupFns . push ( cleanup )
2017-01-21 02:41:28 +00:00
2021-08-20 21:08:36 +00:00
function onGotConnection ( err , s , info ) {
if ( err ) return onError ( err )
2017-01-21 02:41:28 +00:00
2021-08-20 21:08:36 +00:00
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 ( )
2023-05-26 16:54:30 +00:00
send ( concat ( [
2021-08-20 21:08:36 +00:00
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
2021-08-20 21:08:36 +00:00
if ( proxySocket ) {
try { proxySocket . close ( ) } catch ( err ) { }
proxySocket = null
}
2018-10-03 12:44:11 +00:00
}
if ( self . maybeDestroyCleanup ) self . maybeDestroyCleanup ( )
2015-03-24 08:01:49 +00:00
}
2018-10-03 12:44:11 +00:00
function onError ( err ) {
cleanup ( )
if ( self . destroyed ) return
2015-03-24 08:01:49 +00:00
2020-02-03 23:52:37 +00:00
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 )
}
2015-03-24 08:01:49 +00:00
2018-10-03 12:44:11 +00:00
function onSocketMessage ( msg ) {
2021-08-20 21:08:36 +00:00
if ( proxySocket ) msg = msg . slice ( 10 )
2023-05-26 16:54:30 +00:00
const view = new DataView ( transactionId . buffer )
if ( msg . length < 8 || msg . readUInt32BE ( 4 ) !== view . getUint32 ( 0 ) ) {
2018-10-03 12:44:11 +00:00
return onError ( new Error ( 'tracker sent invalid transaction id' ) )
}
2017-01-21 02:41:28 +00:00
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
2015-03-24 08:01:49 +00:00
2018-10-03 12:44:11 +00:00
if ( msg . length < 16 ) return onError ( new Error ( 'invalid udp handshake' ) )
2015-03-24 08:01:49 +00:00
2018-10-03 12:44:11 +00:00
if ( opts . _scrape ) scrape ( msg . slice ( 8 , 16 ) )
else announce ( msg . slice ( 8 , 16 ) , opts )
2015-03-24 08:01:49 +00:00
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
2015-03-24 08:01:49 +00:00
2018-10-03 12:44:11 +00:00
if ( msg . length < 20 ) return onError ( new Error ( 'invalid announce message' ) )
2017-01-21 02:41:28 +00:00
2018-12-20 19:52:05 +00:00
const interval = msg . readUInt32BE ( 8 )
2018-10-03 12:44:11 +00:00
if ( interval ) self . setInterval ( interval * 1000 )
2015-05-02 00:36:07 +00:00
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 )
2015-05-02 00:36:07 +00:00
} )
2017-01-21 02:41:28 +00:00
2018-12-20 19:52:05 +00:00
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 )
} )
2015-03-24 08:01:49 +00:00
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
2017-01-21 02:41:28 +00:00
2018-10-03 12:44:11 +00:00
if ( msg . length < 20 || ( msg . length - 8 ) % 12 !== 0 ) {
return onError ( new Error ( 'invalid scrape message' ) )
}
2018-12-20 19:52:05 +00:00
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 ]
2015-03-24 08:01:49 +00:00
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 ( ) ) )
2015-03-24 08:01:49 +00:00
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
}
2015-03-24 08:01:49 +00:00
}
2021-08-20 21:08:36 +00:00
function send ( message , proxyInfo ) {
if ( proxyInfo ) {
2021-10-29 14:36:47 +00:00
const pack = Socks . createUDPFrame ( { host : hostname , port } , message )
2021-08-20 21:08:36 +00:00
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
}
2015-03-24 08:01:49 +00:00
2018-10-03 12:44:11 +00:00
function announce ( connectionId , opts ) {
transactionId = genTransactionId ( )
2023-05-26 16:54:30 +00:00
send ( concat ( [
2018-10-03 12:44:11 +00:00
connectionId ,
common . toUInt32 ( common . ACTIONS . ANNOUNCE ) ,
transactionId ,
self . client . _infoHashBuffer ,
self . client . _peerIdBuffer ,
toUInt64 ( opts . downloaded ) ,
2023-05-26 16:54:30 +00:00
opts . left != null ? toUInt64 ( opts . left ) : hex2arr ( 'ffffffffffffffff' ) ,
2018-10-03 12:44:11 +00:00
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 )
2021-08-20 21:08:36 +00:00
] ) , relay )
2018-10-03 12:44:11 +00:00
}
2015-03-24 08:01:49 +00:00
2018-10-03 12:44:11 +00:00
function scrape ( connectionId ) {
transactionId = genTransactionId ( )
2015-03-24 08:01:49 +00:00
2018-10-03 12:44:11 +00:00
const infoHash = ( Array . isArray ( opts . infoHash ) && opts . infoHash . length > 0 )
2023-05-26 16:54:30 +00:00
? concat ( opts . infoHash )
2018-10-03 12:44:11 +00:00
: ( opts . infoHash || self . client . _infoHashBuffer )
2015-05-02 00:36:07 +00:00
2023-05-26 16:54:30 +00:00
send ( concat ( [
2018-10-03 12:44:11 +00:00
connectionId ,
common . toUInt32 ( common . ACTIONS . SCRAPE ) ,
transactionId ,
infoHash
2021-08-20 21:08:36 +00:00
] ) , relay )
2018-10-03 12:44:11 +00:00
}
2015-03-24 08:01:49 +00:00
}
}
2018-10-03 12:44:11 +00:00
UDPTracker . prototype . DEFAULT _ANNOUNCE _INTERVAL = 30 * 60 * 1000 // 30 minutes
2015-03-24 08:01:49 +00:00
function genTransactionId ( ) {
2023-05-26 16:54:30 +00:00
return randomBytes ( 4 )
2015-03-24 08:01:49 +00:00
}
function toUInt16 ( n ) {
2023-05-26 16:54:30 +00:00
const buf = new Uint8Array ( 2 )
const view = new DataView ( buf . buffer )
view . setUint16 ( 0 , n )
2015-03-24 08:01:49 +00:00
return buf
}
2018-10-03 12:44:11 +00:00
const MAX _UINT = 4294967295
2015-03-24 08:01:49 +00:00
function toUInt64 ( n ) {
if ( n > MAX _UINT || typeof n === 'string' ) {
2023-05-26 16:54:30 +00:00
const buf = new Uint8Array ( 8 )
const view = new DataView ( buf . buffer )
2023-06-07 16:44:34 +00:00
view . setBigUint64 ( 0 , BigInt ( n ) )
2023-05-26 16:54:30 +00:00
return buf
2015-03-24 08:01:49 +00:00
}
2023-05-26 16:54:30 +00:00
return concat ( [ new Uint8Array ( 4 ) , common . toUInt32 ( n ) ] )
2015-03-24 08:01:49 +00:00
}
2015-05-17 20:53:36 +00:00
function noop ( ) { }
2018-10-03 12:44:11 +00:00
2022-12-05 22:06:54 +00:00
export default UDPTracker