/ server.js
server.js
1 import bencode from 'bencode' 2 import Debug from 'debug' 3 import dgram from 'dgram' 4 import EventEmitter from 'events' 5 import http from 'http' 6 import peerid from 'bittorrent-peerid' 7 import series from 'run-series' 8 import string2compact from 'string2compact' 9 import { WebSocketServer } from 'ws' 10 import { hex2bin } from 'uint8-util' 11 12 import common from './lib/common.js' 13 import Swarm from './lib/server/swarm.js' 14 import parseHttpRequest from './lib/server/parse-http.js' 15 import parseUdpRequest from './lib/server/parse-udp.js' 16 import parseWebSocketRequest from './lib/server/parse-websocket.js' 17 18 const debug = Debug('bittorrent-tracker:server') 19 const hasOwnProperty = Object.prototype.hasOwnProperty 20 21 /** 22 * BitTorrent tracker server. 23 * 24 * HTTP service which responds to GET requests from torrent clients. Requests include 25 * metrics from clients that help the tracker keep overall statistics about the torrent. 26 * Responses include a peer list that helps the client participate in the torrent. 27 * 28 * @param {Object} opts options object 29 * @param {Number} opts.interval tell clients to announce on this interval (ms) 30 * @param {Number} opts.trustProxy trust 'x-forwarded-for' header from reverse proxy 31 * @param {boolean|Object} opts.http start an http server?, or options for http.createServer (default: true) 32 * @param {boolean|Object} opts.udp start a udp server?, or extra options for dgram.createSocket (default: true) 33 * @param {boolean|Object} opts.ws start a websocket server?, or extra options for new WebSocketServer (default: true) 34 * @param {boolean} opts.stats enable web-based statistics? (default: true) 35 * @param {function} opts.filter black/whitelist fn for disallowing/allowing torrents 36 */ 37 class Server extends EventEmitter { 38 constructor (opts = {}) { 39 super() 40 debug('new server %s', JSON.stringify(opts)) 41 42 this.intervalMs = opts.interval 43 ? opts.interval 44 : 10 * 60 * 1000 // 10 min 45 46 this._trustProxy = !!opts.trustProxy 47 if (typeof opts.filter === 'function') this._filter = opts.filter 48 49 this.peersCacheLength = opts.peersCacheLength 50 this.peersCacheTtl = opts.peersCacheTtl 51 52 this._listenCalled = false 53 this.listening = false 54 this.destroyed = false 55 this.torrents = {} 56 57 this.http = null 58 this.udp4 = null 59 this.udp6 = null 60 this.ws = null 61 62 // start an http tracker unless the user explictly says no 63 if (opts.http !== false) { 64 this.http = http.createServer(isObject(opts.http) ? opts.http : undefined) 65 this.http.on('error', err => { this._onError(err) }) 66 this.http.on('listening', onListening) 67 68 // Add default http request handler on next tick to give user the chance to add 69 // their own handler first. Handle requests untouched by user's handler. 70 process.nextTick(() => { 71 this.http.on('request', (req, res) => { 72 if (res.headersSent) return 73 this.onHttpRequest(req, res) 74 }) 75 }) 76 } 77 78 // start a udp tracker unless the user explicitly says no 79 if (opts.udp !== false) { 80 this.udp4 = this.udp = dgram.createSocket({ 81 type: 'udp4', 82 reuseAddr: true, 83 ...(isObject(opts.udp) ? opts.udp : undefined) 84 }) 85 this.udp4.on('message', (msg, rinfo) => { this.onUdpRequest(msg, rinfo) }) 86 this.udp4.on('error', err => { this._onError(err) }) 87 this.udp4.on('listening', onListening) 88 89 this.udp6 = dgram.createSocket({ 90 type: 'udp6', 91 reuseAddr: true, 92 ...(isObject(opts.udp) ? opts.udp : undefined) 93 }) 94 this.udp6.on('message', (msg, rinfo) => { this.onUdpRequest(msg, rinfo) }) 95 this.udp6.on('error', err => { this._onError(err) }) 96 this.udp6.on('listening', onListening) 97 } 98 99 // start a websocket tracker (for WebTorrent) unless the user explicitly says no 100 if (opts.ws !== false) { 101 const noServer = isObject(opts.ws) && opts.ws.noServer 102 if (!this.http && !noServer) { 103 this.http = http.createServer() 104 this.http.on('error', err => { this._onError(err) }) 105 this.http.on('listening', onListening) 106 107 // Add default http request handler on next tick to give user the chance to add 108 // their own handler first. Handle requests untouched by user's handler. 109 process.nextTick(() => { 110 this.http.on('request', (req, res) => { 111 if (res.headersSent) return 112 // For websocket trackers, we only need to handle the UPGRADE http method. 113 // Return 404 for all other request types. 114 res.statusCode = 404 115 res.end('404 Not Found') 116 }) 117 }) 118 } 119 this.ws = new WebSocketServer({ 120 server: noServer ? undefined : this.http, 121 perMessageDeflate: false, 122 clientTracking: false, 123 ...(isObject(opts.ws) ? opts.ws : undefined) 124 }) 125 126 this.ws.address = () => { 127 if (noServer) { 128 throw new Error('address() unavailable with { noServer: true }') 129 } 130 return this.http.address() 131 } 132 133 this.ws.on('error', err => { this._onError(err) }) 134 this.ws.on('connection', (socket, req) => { 135 // Note: socket.upgradeReq was removed in ws@3.0.0, so re-add it. 136 // https://github.com/websockets/ws/pull/1099 137 socket.upgradeReq = req 138 this.onWebSocketConnection(socket) 139 }) 140 } 141 142 if (opts.stats !== false) { 143 if (!this.http) { 144 this.http = http.createServer() 145 this.http.on('error', err => { this._onError(err) }) 146 this.http.on('listening', onListening) 147 } 148 149 // Http handler for '/stats' route 150 this.http.on('request', (req, res) => { 151 if (res.headersSent) return 152 153 const infoHashes = Object.keys(this.torrents) 154 let activeTorrents = 0 155 const allPeers = {} 156 157 function countPeers (filterFunction) { 158 let count = 0 159 let key 160 161 for (key in allPeers) { 162 if (hasOwnProperty.call(allPeers, key) && filterFunction(allPeers[key])) { 163 count++ 164 } 165 } 166 167 return count 168 } 169 170 function groupByClient () { 171 const clients = {} 172 for (const key in allPeers) { 173 if (hasOwnProperty.call(allPeers, key)) { 174 const peer = allPeers[key] 175 176 if (!clients[peer.client.client]) { 177 clients[peer.client.client] = {} 178 } 179 const client = clients[peer.client.client] 180 // If the client is not known show 8 chars from peerId as version 181 const version = peer.client.version || Buffer.from(peer.peerId, 'hex').toString().substring(0, 8) 182 if (!client[version]) { 183 client[version] = 0 184 } 185 client[version]++ 186 } 187 } 188 return clients 189 } 190 191 function printClients (clients) { 192 let html = '<ul>\n' 193 for (const name in clients) { 194 if (hasOwnProperty.call(clients, name)) { 195 const client = clients[name] 196 for (const version in client) { 197 if (hasOwnProperty.call(client, version)) { 198 html += `<li><strong>${name}</strong> ${version} : ${client[version]}</li>\n` 199 } 200 } 201 } 202 } 203 html += '</ul>' 204 return html 205 } 206 207 if (req.method === 'GET' && (req.url === '/stats' || req.url === '/stats.json')) { 208 infoHashes.forEach(infoHash => { 209 const peers = this.torrents[infoHash].peers 210 const keys = peers.keys 211 if (keys.length > 0) activeTorrents++ 212 213 keys.forEach(peerId => { 214 // Don't mark the peer as most recently used for stats 215 const peer = peers.peek(peerId) 216 if (peer == null) return // peers.peek() can evict the peer 217 218 if (!hasOwnProperty.call(allPeers, peerId)) { 219 allPeers[peerId] = { 220 ipv4: false, 221 ipv6: false, 222 seeder: false, 223 leecher: false 224 } 225 } 226 227 if (peer.ip.includes(':')) { 228 allPeers[peerId].ipv6 = true 229 } else { 230 allPeers[peerId].ipv4 = true 231 } 232 233 if (peer.complete) { 234 allPeers[peerId].seeder = true 235 } else { 236 allPeers[peerId].leecher = true 237 } 238 239 allPeers[peerId].peerId = peer.peerId 240 allPeers[peerId].client = peerid(peer.peerId) 241 }) 242 }) 243 244 const isSeederOnly = peer => peer.seeder && peer.leecher === false 245 const isLeecherOnly = peer => peer.leecher && peer.seeder === false 246 const isSeederAndLeecher = peer => peer.seeder && peer.leecher 247 const isIPv4 = peer => peer.ipv4 248 const isIPv6 = peer => peer.ipv6 249 250 const stats = { 251 torrents: infoHashes.length, 252 activeTorrents, 253 peersAll: Object.keys(allPeers).length, 254 peersSeederOnly: countPeers(isSeederOnly), 255 peersLeecherOnly: countPeers(isLeecherOnly), 256 peersSeederAndLeecher: countPeers(isSeederAndLeecher), 257 peersIPv4: countPeers(isIPv4), 258 peersIPv6: countPeers(isIPv6), 259 clients: groupByClient() 260 } 261 262 if (req.url === '/stats.json' || req.headers.accept === 'application/json') { 263 res.setHeader('Content-Type', 'application/json') 264 res.end(JSON.stringify(stats)) 265 } else if (req.url === '/stats') { 266 res.setHeader('Content-Type', 'text/html') 267 res.end(` 268 <h1>${stats.torrents} torrents (${stats.activeTorrents} active)</h1> 269 <h2>Connected Peers: ${stats.peersAll}</h2> 270 <h3>Peers Seeding Only: ${stats.peersSeederOnly}</h3> 271 <h3>Peers Leeching Only: ${stats.peersLeecherOnly}</h3> 272 <h3>Peers Seeding & Leeching: ${stats.peersSeederAndLeecher}</h3> 273 <h3>IPv4 Peers: ${stats.peersIPv4}</h3> 274 <h3>IPv6 Peers: ${stats.peersIPv6}</h3> 275 <h3>Clients:</h3> 276 ${printClients(stats.clients)} 277 `.replace(/^\s+/gm, '')) // trim left 278 } 279 } 280 }) 281 } 282 283 let num = !!this.http + !!this.udp4 + !!this.udp6 284 const self = this 285 function onListening () { 286 num -= 1 287 if (num === 0) { 288 self.listening = true 289 debug('listening') 290 self.emit('listening') 291 } 292 } 293 } 294 295 _onError (err) { 296 this.emit('error', err) 297 } 298 299 listen (...args) /* port, hostname, onlistening */{ 300 if (this._listenCalled || this.listening) throw new Error('server already listening') 301 this._listenCalled = true 302 303 const lastArg = args[args.length - 1] 304 if (typeof lastArg === 'function') this.once('listening', lastArg) 305 306 const port = toNumber(args[0]) || args[0] || 0 307 const hostname = typeof args[1] !== 'function' ? args[1] : undefined 308 309 debug('listen (port: %o hostname: %o)', port, hostname) 310 311 const httpPort = isObject(port) ? (port.http || 0) : port 312 const udpPort = isObject(port) ? (port.udp || 0) : port 313 314 // binding to :: only receives IPv4 connections if the bindv6only sysctl is set 0, 315 // which is the default on many operating systems 316 const httpHostname = isObject(hostname) ? hostname.http : hostname 317 const udp4Hostname = isObject(hostname) ? hostname.udp : hostname 318 const udp6Hostname = isObject(hostname) ? hostname.udp6 : hostname 319 320 if (this.http) this.http.listen(httpPort, httpHostname) 321 if (this.udp4) this.udp4.bind(udpPort, udp4Hostname) 322 if (this.udp6) this.udp6.bind(udpPort, udp6Hostname) 323 } 324 325 close (cb = noop) { 326 debug('close') 327 328 this.listening = false 329 this.destroyed = true 330 331 if (this.udp4) { 332 try { 333 this.udp4.close() 334 } catch (err) {} 335 } 336 337 if (this.udp6) { 338 try { 339 this.udp6.close() 340 } catch (err) {} 341 } 342 343 if (this.ws) { 344 try { 345 this.ws.close() 346 } catch (err) {} 347 } 348 349 if (this.http) this.http.close(cb) 350 else cb(null) 351 } 352 353 createSwarm (infoHash, cb) { 354 if (ArrayBuffer.isView(infoHash)) infoHash = infoHash.toString('hex') 355 356 process.nextTick(() => { 357 const swarm = this.torrents[infoHash] = new Server.Swarm(infoHash, this) 358 cb(null, swarm) 359 }) 360 } 361 362 getSwarm (infoHash, cb) { 363 if (ArrayBuffer.isView(infoHash)) infoHash = infoHash.toString('hex') 364 365 process.nextTick(() => { 366 cb(null, this.torrents[infoHash]) 367 }) 368 } 369 370 onHttpRequest (req, res, opts = {}) { 371 opts.trustProxy = opts.trustProxy || this._trustProxy 372 373 let params 374 try { 375 params = parseHttpRequest(req, opts) 376 params.httpReq = req 377 params.httpRes = res 378 } catch (err) { 379 res.end(bencode.encode({ 380 'failure reason': err.message 381 })) 382 383 // even though it's an error for the client, it's just a warning for the server. 384 // don't crash the server because a client sent bad data :) 385 this.emit('warning', err) 386 return 387 } 388 389 this._onRequest(params, (err, response) => { 390 if (err) { 391 this.emit('warning', err) 392 response = { 393 'failure reason': err.message 394 } 395 } 396 if (this.destroyed) return res.end() 397 398 delete response.action // only needed for UDP encoding 399 res.end(bencode.encode(response)) 400 401 if (params.action === common.ACTIONS.ANNOUNCE) { 402 this.emit(common.EVENT_NAMES[params.event], params.addr, params) 403 } 404 }) 405 } 406 407 onUdpRequest (msg, rinfo) { 408 let params 409 try { 410 params = parseUdpRequest(msg, rinfo) 411 } catch (err) { 412 this.emit('warning', err) 413 // Do not reply for parsing errors 414 return 415 } 416 417 this._onRequest(params, (err, response) => { 418 if (err) { 419 this.emit('warning', err) 420 response = { 421 action: common.ACTIONS.ERROR, 422 'failure reason': err.message 423 } 424 } 425 if (this.destroyed) return 426 427 response.transactionId = params.transactionId 428 response.connectionId = params.connectionId 429 430 const buf = makeUdpPacket(response) 431 432 try { 433 const udp = (rinfo.family === 'IPv4') ? this.udp4 : this.udp6 434 udp.send(buf, 0, buf.length, rinfo.port, rinfo.address) 435 } catch (err) { 436 this.emit('warning', err) 437 } 438 439 if (params.action === common.ACTIONS.ANNOUNCE) { 440 this.emit(common.EVENT_NAMES[params.event], params.addr, params) 441 } 442 }) 443 } 444 445 onWebSocketConnection (socket, opts = {}) { 446 opts.trustProxy = opts.trustProxy || this._trustProxy 447 448 socket.peerId = null // as hex 449 socket.infoHashes = [] // swarms that this socket is participating in 450 socket.onSend = err => { 451 this._onWebSocketSend(socket, err) 452 } 453 454 socket.onMessageBound = params => { 455 this._onWebSocketRequest(socket, opts, params) 456 } 457 socket.on('message', socket.onMessageBound) 458 459 socket.onErrorBound = err => { 460 this._onWebSocketError(socket, err) 461 } 462 socket.on('error', socket.onErrorBound) 463 464 socket.onCloseBound = () => { 465 this._onWebSocketClose(socket) 466 } 467 socket.on('close', socket.onCloseBound) 468 } 469 470 _onWebSocketRequest (socket, opts, params) { 471 try { 472 params = parseWebSocketRequest(socket, opts, params) 473 } catch (err) { 474 socket.send(JSON.stringify({ 475 'failure reason': err.message 476 }), socket.onSend) 477 478 // even though it's an error for the client, it's just a warning for the server. 479 // don't crash the server because a client sent bad data :) 480 this.emit('warning', err) 481 return 482 } 483 484 if (!socket.peerId) socket.peerId = params.peer_id // as hex 485 486 this._onRequest(params, (err, response) => { 487 if (this.destroyed || socket.destroyed) return 488 if (err) { 489 socket.send(JSON.stringify({ 490 action: params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape', 491 'failure reason': err.message, 492 info_hash: hex2bin(params.info_hash) 493 }), socket.onSend) 494 495 this.emit('warning', err) 496 return 497 } 498 499 response.action = params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape' 500 501 let peers 502 if (response.action === 'announce') { 503 peers = response.peers 504 delete response.peers 505 506 if (!socket.infoHashes.includes(params.info_hash)) { 507 socket.infoHashes.push(params.info_hash) 508 } 509 510 response.info_hash = hex2bin(params.info_hash) 511 512 // WebSocket tracker should have a shorter interval – default: 2 minutes 513 response.interval = Math.ceil(this.intervalMs / 1000 / 5) 514 } 515 516 // Skip sending update back for 'answer' announce messages – not needed 517 if (!params.answer) { 518 socket.send(JSON.stringify(response), socket.onSend) 519 debug('sent response %s to %s', JSON.stringify(response), params.peer_id) 520 } 521 522 if (Array.isArray(params.offers)) { 523 debug('got %s offers from %s', params.offers.length, params.peer_id) 524 debug('got %s peers from swarm %s', peers.length, params.info_hash) 525 peers.forEach((peer, i) => { 526 peer.socket.send(JSON.stringify({ 527 action: 'announce', 528 offer: params.offers[i].offer, 529 offer_id: params.offers[i].offer_id, 530 peer_id: hex2bin(params.peer_id), 531 info_hash: hex2bin(params.info_hash) 532 }), peer.socket.onSend) 533 debug('sent offer to %s from %s', peer.peerId, params.peer_id) 534 }) 535 } 536 537 const done = () => { 538 // emit event once the announce is fully "processed" 539 if (params.action === common.ACTIONS.ANNOUNCE) { 540 this.emit(common.EVENT_NAMES[params.event], params.peer_id, params) 541 } 542 } 543 544 if (params.answer) { 545 debug('got answer %s from %s', JSON.stringify(params.answer), params.peer_id) 546 547 this.getSwarm(params.info_hash, (err, swarm) => { 548 if (this.destroyed) return 549 if (err) return this.emit('warning', err) 550 if (!swarm) { 551 return this.emit('warning', new Error('no swarm with that `info_hash`')) 552 } 553 // Mark the destination peer as recently used in cache 554 const toPeer = swarm.peers.get(params.to_peer_id) 555 if (!toPeer) { 556 return this.emit('warning', new Error('no peer with that `to_peer_id`')) 557 } 558 559 toPeer.socket.send(JSON.stringify({ 560 action: 'announce', 561 answer: params.answer, 562 offer_id: params.offer_id, 563 peer_id: hex2bin(params.peer_id), 564 info_hash: hex2bin(params.info_hash) 565 }), toPeer.socket.onSend) 566 debug('sent answer to %s from %s', toPeer.peerId, params.peer_id) 567 568 done() 569 }) 570 } else { 571 done() 572 } 573 }) 574 } 575 576 _onWebSocketSend (socket, err) { 577 if (err) this._onWebSocketError(socket, err) 578 } 579 580 _onWebSocketClose (socket) { 581 debug('websocket close %s', socket.peerId) 582 socket.destroyed = true 583 584 if (socket.peerId) { 585 socket.infoHashes.slice(0).forEach(infoHash => { 586 const swarm = this.torrents[infoHash] 587 if (swarm) { 588 swarm.announce({ 589 type: 'ws', 590 event: 'stopped', 591 numwant: 0, 592 peer_id: socket.peerId 593 }) 594 } 595 }) 596 } 597 598 // ignore all future errors 599 socket.onSend = noop 600 socket.on('error', noop) 601 602 socket.peerId = null 603 socket.infoHashes = null 604 605 if (typeof socket.onMessageBound === 'function') { 606 socket.removeListener('message', socket.onMessageBound) 607 } 608 socket.onMessageBound = null 609 610 if (typeof socket.onErrorBound === 'function') { 611 socket.removeListener('error', socket.onErrorBound) 612 } 613 socket.onErrorBound = null 614 615 if (typeof socket.onCloseBound === 'function') { 616 socket.removeListener('close', socket.onCloseBound) 617 } 618 socket.onCloseBound = null 619 } 620 621 _onWebSocketError (socket, err) { 622 debug('websocket error %s', err.message || err) 623 this.emit('warning', err) 624 this._onWebSocketClose(socket) 625 } 626 627 _onRequest (params, cb) { 628 if (params && params.action === common.ACTIONS.CONNECT) { 629 cb(null, { action: common.ACTIONS.CONNECT }) 630 } else if (params && params.action === common.ACTIONS.ANNOUNCE) { 631 this._onAnnounce(params, cb) 632 } else if (params && params.action === common.ACTIONS.SCRAPE) { 633 this._onScrape(params, cb) 634 } else { 635 cb(new Error('Invalid action')) 636 } 637 } 638 639 _onAnnounce (params, cb) { 640 const self = this 641 642 if (this._filter) { 643 this._filter(params.info_hash, params, err => { 644 // Presence of `err` means that this announce request is disallowed 645 if (err) return cb(err) 646 647 getOrCreateSwarm((err, swarm) => { 648 if (err) return cb(err) 649 announce(swarm) 650 }) 651 }) 652 } else { 653 getOrCreateSwarm((err, swarm) => { 654 if (err) return cb(err) 655 announce(swarm) 656 }) 657 } 658 659 // Get existing swarm, or create one if one does not exist 660 function getOrCreateSwarm (cb) { 661 self.getSwarm(params.info_hash, (err, swarm) => { 662 if (err) return cb(err) 663 if (swarm) return cb(null, swarm) 664 self.createSwarm(params.info_hash, (err, swarm) => { 665 if (err) return cb(err) 666 cb(null, swarm) 667 }) 668 }) 669 } 670 671 function announce (swarm) { 672 if (!params.event || params.event === 'empty') params.event = 'update' 673 swarm.announce(params, (err, response) => { 674 if (err) return cb(err) 675 676 if (!response.action) response.action = common.ACTIONS.ANNOUNCE 677 if (!response.interval) response.interval = Math.ceil(self.intervalMs / 1000) 678 679 if (params.compact === 1) { 680 const peers = response.peers 681 682 // Find IPv4 peers 683 response.peers = string2compact(peers.filter(peer => common.IPV4_RE.test(peer.ip)).map(peer => `${peer.ip}:${peer.port}`)) 684 // Find IPv6 peers 685 response.peers6 = string2compact(peers.filter(peer => common.IPV6_RE.test(peer.ip)).map(peer => `[${peer.ip}]:${peer.port}`)) 686 } else if (params.compact === 0) { 687 // IPv6 peers are not separate for non-compact responses 688 response.peers = response.peers.map(peer => ({ 689 'peer id': hex2bin(peer.peerId), 690 ip: peer.ip, 691 port: peer.port 692 })) 693 } // else, return full peer objects (used for websocket responses) 694 695 cb(null, response) 696 }) 697 } 698 } 699 700 _onScrape (params, cb) { 701 if (params.info_hash == null) { 702 // if info_hash param is omitted, stats for all torrents are returned 703 // TODO: make this configurable! 704 params.info_hash = Object.keys(this.torrents) 705 } 706 707 series(params.info_hash.map(infoHash => cb => { 708 this.getSwarm(infoHash, (err, swarm) => { 709 if (err) return cb(err) 710 if (swarm) { 711 swarm.scrape(params, (err, scrapeInfo) => { 712 if (err) return cb(err) 713 cb(null, { 714 infoHash, 715 complete: (scrapeInfo && scrapeInfo.complete) || 0, 716 incomplete: (scrapeInfo && scrapeInfo.incomplete) || 0 717 }) 718 }) 719 } else { 720 cb(null, { infoHash, complete: 0, incomplete: 0 }) 721 } 722 }) 723 }), (err, results) => { 724 if (err) return cb(err) 725 726 const response = { 727 action: common.ACTIONS.SCRAPE, 728 files: {}, 729 flags: { min_request_interval: Math.ceil(this.intervalMs / 1000) } 730 } 731 732 results.forEach(result => { 733 response.files[hex2bin(result.infoHash)] = { 734 complete: result.complete || 0, 735 incomplete: result.incomplete || 0, 736 downloaded: result.complete || 0 // TODO: this only provides a lower-bound 737 } 738 }) 739 740 cb(null, response) 741 }) 742 } 743 } 744 745 Server.Swarm = Swarm 746 747 function makeUdpPacket (params) { 748 let packet 749 switch (params.action) { 750 case common.ACTIONS.CONNECT: { 751 packet = Buffer.concat([ 752 common.toUInt32(common.ACTIONS.CONNECT), 753 common.toUInt32(params.transactionId), 754 params.connectionId 755 ]) 756 break 757 } 758 case common.ACTIONS.ANNOUNCE: { 759 packet = Buffer.concat([ 760 common.toUInt32(common.ACTIONS.ANNOUNCE), 761 common.toUInt32(params.transactionId), 762 common.toUInt32(params.interval), 763 common.toUInt32(params.incomplete), 764 common.toUInt32(params.complete), 765 params.peers 766 ]) 767 break 768 } 769 case common.ACTIONS.SCRAPE: { 770 const scrapeResponse = [ 771 common.toUInt32(common.ACTIONS.SCRAPE), 772 common.toUInt32(params.transactionId) 773 ] 774 for (const infoHash in params.files) { 775 const file = params.files[infoHash] 776 scrapeResponse.push( 777 common.toUInt32(file.complete), 778 common.toUInt32(file.downloaded), // TODO: this only provides a lower-bound 779 common.toUInt32(file.incomplete) 780 ) 781 } 782 packet = Buffer.concat(scrapeResponse) 783 break 784 } 785 case common.ACTIONS.ERROR: { 786 packet = Buffer.concat([ 787 common.toUInt32(common.ACTIONS.ERROR), 788 common.toUInt32(params.transactionId || 0), 789 Buffer.from(String(params['failure reason'])) 790 ]) 791 break 792 } 793 default: 794 throw new Error(`Action not implemented: ${params.action}`) 795 } 796 return packet 797 } 798 799 function isObject (obj) { 800 return typeof obj === 'object' && obj !== null 801 } 802 803 function toNumber (x) { 804 x = Number(x) 805 return x >= 0 ? x : false 806 } 807 808 function noop () {} 809 810 export default Server