diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 7897fa70..00000000 --- a/.npmignore +++ /dev/null @@ -1 +0,0 @@ -img/ diff --git a/.travis.yml b/.travis.yml index ac73e693..2083806b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,4 @@ language: node_js -sudo: false node_js: - - "node" -before_script: - # Add an IPv6 config - see the corresponding Travis issue - # https://github.com/travis-ci/travis-ci/issues/8361 - - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then - sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; - fi - - export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start + - "0.11" + - "0.10" \ No newline at end of file diff --git a/AUTHORS.md b/AUTHORS.md deleted file mode 100644 index 9bd2c542..00000000 --- a/AUTHORS.md +++ /dev/null @@ -1,43 +0,0 @@ -# Authors - -#### Ordered by first contribution. - -- Feross Aboukhadijeh (feross@feross.org) -- Mathias Buus (mathiasbuus@gmail.com) -- thermatk (thermatk@thermatk.com) -- fisch0920 (fisch0920@gmail.com) -- Aliaksei Sapach (aliaksei.dreamsonic@gmail.com) -- John Hiesey (john@hiesey.com) -- hicom150 (necrox666@gmail.com) -- Theadd (pantallazo@gmail.com) -- Astro (astro@spaceboyz.net) -- Anthony MOI (xn1t0x@gmail.com) -- Max Ogden (max@maxogden.com) -- Sidd Sridharan (sidd@sidd.com) -- Nick Rafter (nicholas.rafter@gmail.com) -- zckevin (zckevinzc@gmail.com) -- Michael Williams (dinosaur@riseup.net) -- Garret Buell (gmbuell@gmail.com) -- Linus Unnebäck (linus@folkdatorn.se) -- Aram Drevekenin (aram@onetwotrade.com) -- Gustavo Rodrigues (qgustavor@gmail.com) -- Alex (alxmorais8@msn.com) -- Harsh Vakharia (harshjv@users.noreply.github.com) -- Yoann Ciabaud (yoann@sonora.io) -- Autarc (autarc@gmail.com) -- Diego Rodríguez Baquero (diegorbaquero@gmail.com) -- Kirill Fomichev (fanatid@ya.ru) -- Matt Bell (mappum@gmail.com) -- Diego Rodríguez (diegorbaquero@gmail.com) -- Philipp Henkel (henkel@users.noreply.github.com) -- jakefb (jacobafb@gmail.com) -- Nick Frost (nickfrostatx@gmail.com) -- ZunSThy (zunsthy@gmail.com) -- vijayanand nandam (vijay@cybrilla.com) -- Luigi Pinca (luigipinca@gmail.com) -- Diego R. B (diegorbaquero@gmail.com) -- greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com) -- hrafnkell orri sigurdsson (hrafnkellos@gmail.com) -- Brian Clifton (brian@clifton.me) - -#### Generated by bin/update-authors.sh. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index e5fc5a24..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,75 +0,0 @@ -# Contributing Guidelines - -Contributions welcome! - -**Before spending lots of time on something, ask for feedback on your idea first!** - -Please search issues and pull requests before adding something new to avoid duplicating -efforts and conversations. - -This project welcomes non-code contributions, too! The following types of contributions -are welcome: - -- **Ideas**: participate in an issue thread or start your own to have your voice heard. -- **Writing**: contribute your expertise in an area by helping expand the included docs. -- **Copy editing**: fix typos, clarify language, and improve the quality of the docs. -- **Formatting**: help keep docs easy to read with consistent formatting. - -## Code Style - -[![JavaScript Style Guide][standard-image]][standard-url] - -This repository uses [`standard`][standard-url] to maintain code style and consistency, -and to avoid style arguments. `npm test` runs `standard` automatically, so you don't have -to! - -[standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg -[standard-url]: https://standardjs.com - -## Project Governance - -Individuals making significant and valuable contributions are given commit-access to the -project to contribute as they see fit. This project is more like an open wiki than a -standard guarded open source project. - -### Rules - -There are a few basic ground-rules for contributors: - -1. **No `--force` pushes** or modifying the Git history in any way. -2. **Non-master branches** should be used for ongoing work. -3. **Significant modifications** like API changes should be subject to a **pull request** - to solicit feedback from other contributors. -4. **Pull requests** are *encouraged* for all contributions to solicit feedback, but left to - the discretion of the contributor. - -### Releases - -Declaring formal releases remains the prerogative of the project maintainer. - -### Changes to this arrangement - -This is an experiment and feedback is welcome! This document may also be subject to pull- -requests or changes by contributors where you believe you have something valuable to add -or change. - -## Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -- (a) The contribution was created in whole or in part by me and I have the right to - submit it under the open source license indicated in the file; or - -- (b) The contribution is based upon previous work that, to the best of my knowledge, is - covered under an appropriate open source license and I have the right under that license - to submit that work with modifications, whether created in whole or in part by me, under - the same open source license (unless I am permitted to submit under a different - license), as indicated in the file; or - -- (c) The contribution was provided directly to me by some other person who certified - (a), (b) or (c) and I have not modified it. - -- (d) I understand and agree that this project and the contribution are public and that a - record of the contribution (including all personal information I submit with it, - including my sign-off) is maintained indefinitely and may be redistributed consistent - with this project or the open source license(s) involved. diff --git a/LICENSE b/LICENSE index 70a7089c..c7e68527 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) Feross Aboukhadijeh and WebTorrent, LLC +Copyright (c) Feross Aboukhadijeh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index ef052299..63a657ef 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,24 @@ -# bittorrent-tracker [![travis][travis-image]][travis-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] - -[travis-image]: https://img.shields.io/travis/webtorrent/bittorrent-tracker/master.svg -[travis-url]: https://travis-ci.org/webtorrent/bittorrent-tracker -[npm-image]: https://img.shields.io/npm/v/bittorrent-tracker.svg -[npm-url]: https://npmjs.org/package/bittorrent-tracker -[downloads-image]: https://img.shields.io/npm/dm/bittorrent-tracker.svg -[downloads-url]: https://npmjs.org/package/bittorrent-tracker -[standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg -[standard-url]: https://standardjs.com +# bittorrent-tracker [![build](https://img.shields.io/travis/feross/bittorrent-tracker.svg)](https://travis-ci.org/feross/bittorrent-tracker) [![npm](https://img.shields.io/npm/v/bittorrent-tracker.svg)](https://npmjs.org/package/bittorrent-tracker) [![npm downloads](https://img.shields.io/npm/dm/bittorrent-tracker.svg)](https://npmjs.org/package/bittorrent-tracker) [![gittip](https://img.shields.io/gittip/feross.svg)](https://www.gittip.com/feross/) #### Simple, robust, BitTorrent tracker (client & server) implementation -![tracker visualization](img/img.png) +![tracker](https://raw.githubusercontent.com/feross/bittorrent-tracker/master/img.png) Node.js implementation of a [BitTorrent tracker](https://wiki.theory.org/BitTorrentSpecification#Tracker_HTTP.2FHTTPS_Protocol), client and server. -A **BitTorrent tracker** is a web service which responds to requests from BitTorrent +A **BitTorrent tracker** is an HTTP service which responds to GET requests from BitTorrent clients. The requests include metrics from clients that help the tracker keep overall statistics about the torrent. The response includes a peer list that helps the client -participate in the torrent swarm. +participate in the torrent. -This module is used by [WebTorrent](http://webtorrent.io). +Also see [bittorrent-dht](https://github.com/feross/bittorrent-dht). This module is used +by [WebTorrent](http://webtorrent.io). ## features - Includes client & server implementations -- Supports all mainstream tracker types: - - HTTP trackers - - UDP trackers ([BEP 15](http://www.bittorrent.org/beps/bep_0015.html)) - - WebTorrent trackers ([BEP forthcoming](http://webtorrent.io)) -- Supports ipv4 & ipv6 -- Supports tracker "scrape" extension -- Robust and well-tested - - Comprehensive test suite (runs entirely offline, so it's reliable) - - Used by popular clients: [WebTorrent](http://webtorrent.io), [peerflix](https://www.npmjs.com/package/peerflix), and [playback](https://mafintosh.github.io/playback/) -- Tracker statistics available via web interface at `/stats` or JSON data at `/stats.json` - -Also see [bittorrent-dht](https://www.npmjs.com/package/bittorrent-dht). - -### Tracker stats - -![Screenshot](img/trackerStats.png) +- Supports HTTP & UDP trackers ([BEP 15](http://www.bittorrent.org/beps/bep_0015.html)) +- Supports tracker scrape ## install @@ -56,34 +34,16 @@ To connect to a tracker, just do this: ```js var Client = require('bittorrent-tracker') +var parseTorrent = require('parse-torrent') +var fs = require('fs') -var requiredOpts = { - infoHash: new Buffer('012345678901234567890'), // hex string or Buffer - peerId: new Buffer('01234567890123456789'), // hex string or Buffer - announce: [], // list of tracker server urls - port: 6881 // torrent client port, (in browser, optional) -} - -var optionalOpts = { - getAnnounceOpts: function () { - // Provide a callback that will be called whenever announce() is called - // internally (on timer), or by the user - return { - uploaded: 0, - downloaded: 0, - left: 0, - customParam: 'blah' // custom parameters supported - } - } - // RTCPeerConnection config object (only used in browser) - rtcConfig: {}, - // User-Agent header for http requests - userAgent: '', - // Custom webrtc impl, useful in node to specify [wrtc](https://npmjs.com/package/wrtc) - wrtc: {}, -} - -var client = new Client(requiredOpts) +var torrent = fs.readFileSync(__dirname + '/torrents/bitlove-intro.torrent') +var parsedTorrent = parseTorrent(torrent) // { infoHash: 'xxx', length: xx, announce: ['xx', 'xx'] } + +var peerId = new Buffer('01234567890123456789') +var port = 6881 + +var client = new Client(peerId, port, parsedTorrent) client.on('error', function (err) { // fatal client error! @@ -114,20 +74,9 @@ client.complete() // force a tracker announce. will trigger more 'update' events and maybe more 'peer' events client.update() -// provide parameters to the tracker -client.update({ - uploaded: 0, - downloaded: 0, - left: 0, - customParam: 'blah' // custom parameters supported -}) - // stop getting peers from the tracker, gracefully leave the swarm client.stop() -// ungracefully leave the swarm (without sending final 'stop' message) -client.destroy() - // scrape client.scrape() @@ -135,7 +84,7 @@ client.on('scrape', function (data) { console.log('got a scrape response from tracker: ' + data.announce) console.log('number of seeders in the swarm: ' + data.complete) console.log('number of leechers in the swarm: ' + data.incomplete) - console.log('number of total downloads of this torrent: ' + data.downloaded) + console.log('number of total downloads of this torrent: ' + data.incomplete) }) ``` @@ -148,37 +97,9 @@ var Server = require('bittorrent-tracker').Server var server = new Server({ udp: true, // enable udp server? [default=true] - http: true, // enable http server? [default=true] - ws: true, // enable websocket server? [default=true] - stats: true, // enable web-based statistics? [default=true] - filter: function (infoHash, params, cb) { - // Blacklist/whitelist function for allowing/disallowing torrents. If this option is - // omitted, all torrents are allowed. It is possible to interface with a database or - // external system before deciding to allow/deny, because this function is async. - - // It is possible to block by peer id (whitelisting torrent clients) or by secret - // key (private trackers). Full access to the original HTTP/UDP request parameters - // are available in `params`. - - // This example only allows one torrent. - - var allowed = (infoHash === 'aaa67059ed6bd08362da625b3ae77f6f4a075aaa') - if (allowed) { - // If the callback is passed `null`, the torrent will be allowed. - cb(null) - } else { - // If the callback is passed an `Error` object, the torrent will be disallowed - // and the error's `message` property will be given as the reason. - cb(new Error('disallowed torrent')) - } - } + http: true // enable http server? [default=true] }) -// Internal http, udp, and websocket servers exposed as public properties. -server.http -server.udp -server.ws - server.on('error', function (err) { // fatal server error! console.log(err.message) @@ -189,14 +110,12 @@ server.on('warning', function (err) { console.log(err.message) }) -server.on('listening', function () { - // fired when all requested servers are listening - console.log('listening on http port:' + server.http.address().port) - console.log('listening on udp port:' + server.udp.address().port) +server.on('listening', function (port) { + console.log('tracker server is now listening on ' + port) }) -// start tracker server listening! Use 0 to listen on a random free port. -server.listen(port, hostname, onlistening) +// start tracker server listening! +server.listen(port) // listen for individual tracker messages from peers: @@ -223,63 +142,6 @@ server.torrents[infoHash].peers The http server will handle requests for the following paths: `/announce`, `/scrape`. Requests for other paths will not be handled. -## multi scrape - -Scraping multiple torrent info is possible with a static `Client.scrape` method: - -```js -var Client = require('bittorrent-tracker') -Client.scrape({ announce: announceUrl, infoHash: [ infoHash1, infoHash2 ]}, function (err, results) { - results[infoHash1].announce - results[infoHash1].infoHash - results[infoHash1].complete - results[infoHash1].incomplete - results[infoHash1].downloaded - - // ... -}) -```` - -## command line - -Install `bittorrent-tracker` globally: - -```sh -$ npm install -g bittorrent-tracker -``` - -Easily start a tracker server: - -```sh -$ bittorrent-tracker -http server listening on 8000 -udp server listening on 8000 -ws server listening on 8000 -``` - -Lots of options: - -```sh -$ bittorrent-tracker --help - bittorrent-tracker - Start a bittorrent tracker server - - Usage: - bittorrent-tracker [OPTIONS] - - If no --http, --udp, or --ws option is supplied, all tracker types will be started. - - Options: - -p, --port [number] change the port [default: 8000] - --trust-proxy trust 'x-forwarded-for' header from reverse proxy - --interval client announce interval (ms) [default: 600000] - --http enable http server - --udp enable udp server - --ws enable websocket server - -q, --quiet only show error output - -s, --silent show no output - -v, --version print the current version -``` - ## license -MIT. Copyright (c) [Feross Aboukhadijeh](https://feross.org) and [WebTorrent, LLC](https://webtorrent.io). +MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). diff --git a/bin/cmd.js b/bin/cmd.js deleted file mode 100755 index b5618abd..00000000 --- a/bin/cmd.js +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env node - -var minimist = require('minimist') -var Server = require('../').Server - -var argv = minimist(process.argv.slice(2), { - alias: { - h: 'help', - p: 'port', - q: 'quiet', - s: 'silent', - v: 'version' - }, - boolean: [ - 'help', - 'http', - 'quiet', - 'silent', - 'trust-proxy', - 'udp', - 'version', - 'ws', - 'stats' - ], - string: [ - 'http-hostname', - 'udp-hostname', - 'udp6-hostname' - ], - default: { - port: 8000, - stats: true - } -}) - -if (argv.version) { - console.log(require('../package.json').version) - process.exit(0) -} - -if (argv.help) { - console.log(function () { - /* - bittorrent-tracker - Start a bittorrent tracker server - - Usage: - bittorrent-tracker [OPTIONS] - - If no --http, --udp, or --ws option is supplied, all tracker types will be started. - - Options: - -p, --port [number] change the port [default: 8000] - --http-hostname [string] change the http server hostname [default: '::'] - --udp-hostname [string] change the udp hostname [default: '0.0.0.0'] - --udp6-hostname [string] change the udp6 hostname [default: '::'] - --trust-proxy trust 'x-forwarded-for' header from reverse proxy - --interval client announce interval (ms) [default: 600000] - --http enable http server - --udp enable udp server - --ws enable websocket server - --stats enable web-based statistics (default: true) - -q, --quiet only show error output - -s, --silent show no output - -v, --version print the current version - - */ - }.toString().split(/\n/).slice(2, -2).join('\n')) - process.exit(0) -} - -if (argv.silent) argv.quiet = true - -var allFalsy = !argv.http && !argv.udp && !argv.ws - -argv.http = allFalsy || argv.http -argv.udp = allFalsy || argv.udp -argv.ws = allFalsy || argv.ws - -var server = new Server({ - http: argv.http, - interval: argv.interval, - stats: argv.stats, - trustProxy: argv['trust-proxy'], - udp: argv.udp, - ws: argv.ws -}) - -server.on('error', function (err) { - if (!argv.silent) console.error('ERROR: ' + err.message) -}) -server.on('warning', function (err) { - if (!argv.quiet) console.log('WARNING: ' + err.message) -}) -server.on('update', function (addr) { - if (!argv.quiet) console.log('update: ' + addr) -}) -server.on('complete', function (addr) { - if (!argv.quiet) console.log('complete: ' + addr) -}) -server.on('start', function (addr) { - if (!argv.quiet) console.log('start: ' + addr) -}) -server.on('stop', function (addr) { - if (!argv.quiet) console.log('stop: ' + addr) -}) - -var hostname = { - http: argv['http-hostname'], - udp4: argv['udp-hostname'], - udp6: argv['upd6-hostname'] -} - -server.listen(argv.port, hostname, function () { - if (server.http && argv.http && !argv.quiet) { - var httpAddr = server.http.address() - var httpHost = httpAddr.address !== '::' ? httpAddr.address : 'localhost' - var httpPort = httpAddr.port - console.log('HTTP tracker: http://' + httpHost + ':' + httpPort + '/announce') - } - if (server.udp && !argv.quiet) { - var udpAddr = server.udp.address() - var udpHost = udpAddr.address - var udpPort = udpAddr.port - console.log('UDP tracker: udp://' + udpHost + ':' + udpPort) - } - if (server.udp6 && !argv.quiet) { - var udp6Addr = server.udp6.address() - var udp6Host = udp6Addr.address !== '::' ? udp6Addr.address : 'localhost' - var udp6Port = udp6Addr.port - console.log('UDP6 tracker: udp://' + udp6Host + ':' + udp6Port) - } - if (server.ws && !argv.quiet) { - var wsAddr = server.http.address() - var wsHost = wsAddr.address !== '::' ? wsAddr.address : 'localhost' - var wsPort = wsAddr.port - console.log('WebSocket tracker: ws://' + wsHost + ':' + wsPort) - } - if (server.http && argv.stats && !argv.quiet) { - var statsAddr = server.http.address() - var statsHost = statsAddr.address !== '::' ? statsAddr.address : 'localhost' - var statsPort = statsAddr.port - console.log('Tracker stats: http://' + statsHost + ':' + statsPort + '/stats') - } -}) diff --git a/bin/update-authors.sh b/bin/update-authors.sh deleted file mode 100755 index eba52113..00000000 --- a/bin/update-authors.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# Update AUTHORS.md based on git history. - -git log --reverse --format='%aN (%aE)' | perl -we ' -BEGIN { - %seen = (), @authors = (); -} -while (<>) { - next if $seen{$_}; - next if /(support\@greenkeeper.io)/; - next if /(yoann\@atacma.agency)/; - next if /(yciabaud\@users.noreply.github.com)/; - next if /(DiegoRBaquero\@users.noreply.github.com)/; - next if /(gustavcaplan\@gmail.com)/; - $seen{$_} = push @authors, "- ", $_; -} -END { - print "# Authors\n\n"; - print "#### Ordered by first contribution.\n\n"; - print @authors, "\n"; - print "#### Generated by bin/update-authors.sh.\n"; -} -' > AUTHORS.md diff --git a/client.js b/client.js index dec7fa68..2e044bfb 100644 --- a/client.js +++ b/client.js @@ -1,296 +1,563 @@ module.exports = Client -var Buffer = require('safe-buffer').Buffer -var debug = require('debug')('bittorrent-tracker:client') +var bencode = require('bencode') +var BN = require('bn.js') +var common = require('./lib/common') +var compact2string = require('compact2string') +var concat = require('concat-stream') +var debug = require('debug')('bittorrent-tracker') +var dgram = require('dgram') var EventEmitter = require('events').EventEmitter -var extend = require('xtend') +var extend = require('extend.js') +var hat = require('hat') +var http = require('http') var inherits = require('inherits') var once = require('once') -var parallel = require('run-parallel') -var Peer = require('simple-peer') -var uniq = require('uniq') var url = require('url') -var common = require('./lib/common') -var HTTPTracker = require('./lib/client/http-tracker') // empty object in browser -var UDPTracker = require('./lib/client/udp-tracker') // empty object in browser -var WebSocketTracker = require('./lib/client/websocket-tracker') - inherits(Client, EventEmitter) /** - * BitTorrent tracker client. + * A Client manages tracker connections for a torrent. * - * Find torrent peers, to help a torrent client participate in a torrent swarm. - * - * @param {Object} opts options object - * @param {string|Buffer} opts.infoHash torrent info hash - * @param {string|Buffer} opts.peerId peer id - * @param {string|Array.} opts.announce announce - * @param {number} opts.port torrent client listening port - * @param {function} opts.getAnnounceOpts callback to provide data to tracker - * @param {number} opts.rtcConfig RTCPeerConnection configuration object - * @param {number} opts.userAgent User-Agent header for http requests - * @param {number} opts.wrtc custom webrtc impl (useful in node.js) + * @param {string} peerId this peer's id + * @param {Number} port port number that the client is listening on + * @param {Object} torrent parsed torrent + * @param {Object} opts optional options + * @param {Number} opts.numWant number of peers to request + * @param {Number} opts.interval interval in ms to send announce requests to the tracker */ -function Client (opts) { +function Client (peerId, port, torrent, opts) { var self = this - if (!(self instanceof Client)) return new Client(opts) + if (!(self instanceof Client)) return new Client(peerId, port, torrent, opts) EventEmitter.call(self) - if (!opts) opts = {} - - if (!opts.peerId) throw new Error('Option `peerId` is required') - if (!opts.infoHash) throw new Error('Option `infoHash` is required') - if (!opts.announce) throw new Error('Option `announce` is required') - if (!process.browser && !opts.port) throw new Error('Option `port` is required') - - self.peerId = typeof opts.peerId === 'string' - ? opts.peerId - : opts.peerId.toString('hex') - self._peerIdBuffer = Buffer.from(self.peerId, 'hex') - self._peerIdBinary = self._peerIdBuffer.toString('binary') - - self.infoHash = typeof opts.infoHash === 'string' - ? opts.infoHash - : opts.infoHash.toString('hex') - self._infoHashBuffer = Buffer.from(self.infoHash, 'hex') - self._infoHashBinary = self._infoHashBuffer.toString('binary') - - debug('new client %s', self.infoHash) - - self.destroyed = false - - self._port = opts.port - self._getAnnounceOpts = opts.getAnnounceOpts - self._rtcConfig = opts.rtcConfig - self._userAgent = opts.userAgent - - // Support lazy 'wrtc' module initialization - // See: https://github.com/webtorrent/webtorrent-hybrid/issues/46 - self._wrtc = typeof opts.wrtc === 'function' ? opts.wrtc() : opts.wrtc - - var announce = typeof opts.announce === 'string' - ? [ opts.announce ] - : opts.announce == null ? [] : opts.announce - - // Remove trailing slash from trackers to catch duplicates - announce = announce.map(function (announceUrl) { - announceUrl = announceUrl.toString() - if (announceUrl[announceUrl.length - 1] === '/') { - announceUrl = announceUrl.substring(0, announceUrl.length - 1) - } - return announceUrl - }) - announce = uniq(announce) - - var webrtcSupport = self._wrtc !== false && (!!self._wrtc || Peer.WEBRTC_SUPPORT) - - self._trackers = announce - .map(function (announceUrl) { - var protocol = url.parse(announceUrl).protocol - if ((protocol === 'http:' || protocol === 'https:') && - typeof HTTPTracker === 'function') { - return new HTTPTracker(self, announceUrl) - } else if (protocol === 'udp:' && typeof UDPTracker === 'function') { - return new UDPTracker(self, announceUrl) - } else if ((protocol === 'ws:' || protocol === 'wss:') && webrtcSupport) { - // Skip ws:// trackers on https:// sites because they throw SecurityError - if (protocol === 'ws:' && typeof window !== 'undefined' && - window.location.protocol === 'https:') { - nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl)) - return null - } - return new WebSocketTracker(self, announceUrl) - } else { - nextTickWarn(new Error('Unsupported tracker protocol: ' + announceUrl)) - return null - } + self._opts = opts || {} + + // required + self._peerId = Buffer.isBuffer(peerId) + ? peerId + : new Buffer(peerId, 'utf8') + self._port = port + self._infoHash = Buffer.isBuffer(torrent.infoHash) + ? torrent.infoHash + : new Buffer(torrent.infoHash, 'hex') + self.torrentLength = torrent.length + + // optional + self._numWant = self._opts.numWant || 50 + self._intervalMs = self._opts.interval || (30 * 60 * 1000) // default: 30 minutes + + debug('new client %s', self._infoHash.toString('hex')) + + if (typeof torrent.announce === 'string') torrent.announce = [ torrent.announce ] + self._trackers = (torrent.announce || []) + .filter(function (announceUrl) { + return announceUrl.indexOf('udp://') === 0 || announceUrl.indexOf('http://') === 0 }) - .filter(Boolean) - - function nextTickWarn (err) { - process.nextTick(function () { - self.emit('warning', err) + .map(function (announceUrl) { + return new Tracker(self, announceUrl, self._opts) }) - } } /** - * Simple convenience function to scrape a tracker for an info hash without needing to - * create a Client, pass it a parsed torrent, etc. Support scraping a tracker for multiple - * torrents at the same time. - * @params {Object} opts - * @param {string|Array.} opts.infoHash - * @param {string} opts.announce + * Simple convenience function to scrape a tracker for an infoHash without + * needing to create a Client, pass it a parsed torrent, etc. + * @param {string} announceUrl + * @param {string} infoHash * @param {function} cb */ -Client.scrape = function (opts, cb) { +Client.scrape = function (announceUrl, infoHash, cb) { cb = once(cb) + var dummy = { + peerId: new Buffer('01234567890123456789'), + port: 6881, + torrent: { + infoHash: infoHash, + announce: [ announceUrl ] + } + } + var client = new Client(dummy.peerId, dummy.port, dummy.torrent) + client.once('error', cb) + client.once('scrape', function (data) { + cb(null, data) + }) + client.scrape() +} - if (!opts.infoHash) throw new Error('Option `infoHash` is required') - if (!opts.announce) throw new Error('Option `announce` is required') +Client.prototype.start = function (opts) { + var self = this + self._trackers.forEach(function (tracker) { + tracker.start(opts) + }) +} - var clientOpts = extend(opts, { - infoHash: Array.isArray(opts.infoHash) ? opts.infoHash[0] : opts.infoHash, - peerId: Buffer.from('01234567890123456789'), // dummy value - port: 6881 // dummy value +Client.prototype.stop = function (opts) { + var self = this + self._trackers.forEach(function (tracker) { + tracker.stop(opts) }) +} - var client = new Client(clientOpts) - client.once('error', cb) - client.once('warning', cb) - - var len = Array.isArray(opts.infoHash) ? opts.infoHash.length : 1 - var results = {} - client.on('scrape', function (data) { - len -= 1 - results[data.infoHash] = data - if (len === 0) { - client.destroy() - var keys = Object.keys(results) - if (keys.length === 1) { - cb(null, results[keys[0]]) - } else { - cb(null, results) - } - } +Client.prototype.complete = function (opts) { + var self = this + self._trackers.forEach(function (tracker) { + tracker.complete(opts) }) +} - opts.infoHash = Array.isArray(opts.infoHash) - ? opts.infoHash.map(function (infoHash) { - return Buffer.from(infoHash, 'hex') - }) - : Buffer.from(opts.infoHash, 'hex') - client.scrape({ infoHash: opts.infoHash }) - return client +Client.prototype.update = function (opts) { + var self = this + self._trackers.forEach(function (tracker) { + tracker.update(opts) + }) } -/** - * Send a `start` announce to the trackers. - * @param {Object} opts - * @param {number=} opts.uploaded - * @param {number=} opts.downloaded - * @param {number=} opts.left (if not set, calculated automatically) - */ -Client.prototype.start = function (opts) { +Client.prototype.scrape = function (opts) { var self = this - debug('send `start`') - opts = self._defaultAnnounceOpts(opts) - opts.event = 'started' - self._announce(opts) + self._trackers.forEach(function (tracker) { + tracker.scrape(opts) + }) +} + +Client.prototype.setInterval = function (intervalMs) { + var self = this + self._intervalMs = intervalMs - // start announcing on intervals self._trackers.forEach(function (tracker) { - tracker.setInterval() + tracker.setInterval(intervalMs) }) } +inherits(Tracker, EventEmitter) + /** - * Send a `stop` announce to the trackers. - * @param {Object} opts - * @param {number=} opts.uploaded - * @param {number=} opts.downloaded - * @param {number=} opts.numwant - * @param {number=} opts.left (if not set, calculated automatically) + * An individual torrent tracker (used by Client) + * + * @param {Client} client parent bittorrent tracker client + * @param {string} announceUrl announce url of tracker + * @param {Object} opts optional options */ -Client.prototype.stop = function (opts) { +function Tracker (client, announceUrl, opts) { + var self = this + EventEmitter.call(self) + self._opts = opts || {} + + self.client = client + + self._lastUploaded = 0 + self._lastDownloaded = 0 + self._downloadComplete = false + + debug('new tracker %s', announceUrl) + + self._announceUrl = announceUrl + self._intervalMs = self.client._intervalMs // use client interval initially + self._interval = null + + if (self._announceUrl.indexOf('udp://') === 0) { + self._requestImpl = self._requestUdp + } else if (self._announceUrl.indexOf('http://') === 0) { + self._requestImpl = self._requestHttp + } +} + +Tracker.prototype.start = function (opts) { + var self = this + opts = opts || {} + opts.event = 'started' + + debug('sent `start` %s', self._announceUrl) + self._announce(opts) + self.setInterval(self._intervalMs) // start announcing on intervals +} + +Tracker.prototype.stop = function (opts) { var self = this - debug('send `stop`') - opts = self._defaultAnnounceOpts(opts) + opts = opts || {} opts.event = 'stopped' + + debug('sent `stop` %s', self._announceUrl) self._announce(opts) + self.setInterval(0) // stop announcing on intervals } -/** - * Send a `complete` announce to the trackers. - * @param {Object} opts - * @param {number=} opts.uploaded - * @param {number=} opts.downloaded - * @param {number=} opts.numwant - * @param {number=} opts.left (if not set, calculated automatically) - */ -Client.prototype.complete = function (opts) { +Tracker.prototype.complete = function (opts) { var self = this - debug('send `complete`') - if (!opts) opts = {} - opts = self._defaultAnnounceOpts(opts) + opts = opts || {} opts.event = 'completed' + opts.downloaded = opts.downloaded || self.torrentLength || 0 + + debug('sent `complete` %s', self._announceUrl) + self._announce(opts) +} + +Tracker.prototype.update = function (opts) { + var self = this + opts = opts || {} + + debug('sent `update` %s', self._announceUrl) self._announce(opts) } /** - * Send a `update` announce to the trackers. + * Send an announce request to the tracker. * @param {Object} opts * @param {number=} opts.uploaded * @param {number=} opts.downloaded - * @param {number=} opts.numwant * @param {number=} opts.left (if not set, calculated automatically) */ -Client.prototype.update = function (opts) { +Tracker.prototype._announce = function (opts) { var self = this - debug('send `update`') - opts = self._defaultAnnounceOpts(opts) - if (opts.event) delete opts.event - self._announce(opts) -} + opts = extend({ + uploaded: 0, // default, user should provide real value + downloaded: 0 // default, user should provide real value + }, opts) + + if (!opts.event && self.client.torrentLength <= opts.downloaded) { + if (opts.uploaded == self._lastUploaded) { + return + } else if (!self._downloadComplete) { + opts.downloaded = self._lastDownloaded + } + } -Client.prototype._announce = function (opts) { - var self = this - self._trackers.forEach(function (tracker) { - // tracker should not modify `opts` object, it's passed to all trackers - tracker.announce(opts) - }) + if (self.client.torrentLength != null && opts.left == null) { + if (opts.event == 'completed' || self._downloadComplete) { + // if completed, fix announce to full + self._downloadComplete = true + opts.left = 0 + opts.downloaded = self.client.torrentLength + } else { + opts.left = self.client.torrentLength - (opts.downloaded || 0) + } + } + + self._lastUploaded = opts.uploaded + self._lastDownloaded = opts.downloaded + + self._requestImpl(self._announceUrl, opts) } /** - * Send a scrape request to the trackers. - * @param {Object} opts + * Send a scrape request to the tracker. */ -Client.prototype.scrape = function (opts) { +Tracker.prototype.scrape = function () { var self = this - debug('send `scrape`') - if (!opts) opts = {} - self._trackers.forEach(function (tracker) { - // tracker should not modify `opts` object, it's passed to all trackers - tracker.scrape(opts) - }) + + self._scrapeUrl = self._scrapeUrl || getScrapeUrl(self._announceUrl) + + if (!self._scrapeUrl) { + debug('scrape not supported %s', self._announceUrl) + self.client.emit('error', new Error('scrape not supported for announceUrl ' + self._announceUrl)) + return + } + + debug('sent `scrape` %s', self._announceUrl) + self._requestImpl(self._scrapeUrl, { _scrape: true }) } -Client.prototype.setInterval = function (intervalMs) { +Tracker.prototype.setInterval = function (intervalMs) { var self = this - debug('setInterval %d', intervalMs) - self._trackers.forEach(function (tracker) { - tracker.setInterval(intervalMs) + clearInterval(self._interval) + + self._intervalMs = intervalMs + if (intervalMs) { + self._interval = setInterval(self.update.bind(self), self._intervalMs) + } +} + +Tracker.prototype._requestHttp = function (requestUrl, opts) { + var self = this + + if (opts._scrape) { + opts = extend({ + info_hash: self.client._infoHash.toString('binary') + }, opts) + } else { + opts = extend({ + info_hash: self.client._infoHash.toString('binary'), + peer_id: self.client._peerId.toString('binary'), + port: self.client._port, + compact: 1, + numwant: self.client._numWant + }, opts) + + if (self._trackerId) { + opts.trackerid = self._trackerId + } + } + + var fullUrl = requestUrl + '?' + common.querystringStringify(opts) + + var req = http.get(fullUrl, function (res) { + if (res.statusCode !== 200) { + res.resume() // consume the whole stream + self.client.emit('warning', new Error('Invalid response code ' + res.statusCode + ' from tracker ' + requestUrl)) + return + } + res.pipe(concat(function (data) { + if (data && data.length) self._handleResponse(requestUrl, data) + })) + }) + + req.on('error', function (err) { + self.client.emit('warning', err) }) } -Client.prototype.destroy = function (cb) { +Tracker.prototype._requestUdp = function (requestUrl, opts) { var self = this - if (self.destroyed) return - self.destroyed = true - debug('destroy') + opts = opts || {} + var parsedUrl = url.parse(requestUrl) + var socket = dgram.createSocket('udp4') + var transactionId = new Buffer(hat(32), 'hex') + + var stopped = opts.event === 'stopped' + // if we're sending a stopped message, we don't really care if it arrives, so set + // a short timer and don't call error + var timeout = setTimeout(function () { + timeout = null + cleanup() + if (!stopped) { + error('tracker request timed out') + } + }, stopped ? 1500 : 15000) + + if (timeout && timeout.unref) { + timeout.unref() + } + + send(Buffer.concat([ + common.CONNECTION_ID, + common.toUInt32(common.ACTIONS.CONNECT), + transactionId + ])) + + socket.on('error', error) + + socket.on('message', function (msg) { + if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) { + return error('tracker sent back invalid transaction id') + } + + var action = msg.readUInt32BE(0) + switch (action) { + case 0: // handshake + if (msg.length < 16) { + return error('invalid udp handshake') + } + + if (opts._scrape) { + scrape(msg.slice(8, 16)) + } else { + announce(msg.slice(8, 16), opts) + } + + return + + case 1: // announce + cleanup() + if (msg.length < 20) { + return error('invalid announce message') + } - var tasks = self._trackers.map(function (tracker) { - return function (cb) { - tracker.destroy(cb) + var interval = msg.readUInt32BE(8) + if (interval && !self._opts.interval && self._intervalMs !== 0) { + // use the interval the tracker recommends, UNLESS the user manually specifies an + // interval they want to use + self.setInterval(interval * 1000) + } + + self.client.emit('update', { + announce: self._announceUrl, + complete: msg.readUInt32BE(16), + incomplete: msg.readUInt32BE(12) + }) + + compact2string.multi(msg.slice(20)).forEach(function (addr) { + self.client.emit('peer', addr) + }) + break + + case 2: // scrape + cleanup() + if (msg.length < 20) { + return error('invalid scrape message') + } + self.client.emit('scrape', { + announce: self._announceUrl, + complete: msg.readUInt32BE(8), + downloaded: msg.readUInt32BE(12), + incomplete: msg.readUInt32BE(16) + }) + break + + case 3: // error + cleanup() + if (msg.length < 8) { + return error('invalid error message') + } + self.client.emit('error', new Error(msg.slice(8).toString())) + break } }) - parallel(tasks, cb) + function send (message) { + if (!parsedUrl.port) { + parsedUrl.port = 80 + } + socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname) + } - self._trackers = [] - self._getAnnounceOpts = null + function error (message) { + // errors will often happen if a tracker is offline, so don't treat it as fatal + self.client.emit('warning', new Error(message + ' (' + requestUrl + ')')) + cleanup() + } + + function cleanup () { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + try { socket.close() } catch (err) {} + } + + function genTransactionId () { + transactionId = new Buffer(hat(32), 'hex') + } + + function announce (connectionId, opts) { + opts = opts || {} + genTransactionId() + + send(Buffer.concat([ + connectionId, + common.toUInt32(common.ACTIONS.ANNOUNCE), + transactionId, + self.client._infoHash, + self.client._peerId, + toUInt64(opts.downloaded || 0), + opts.left ? toUInt64(opts.left) : new Buffer('FFFFFFFFFFFFFFFF', 'hex'), + toUInt64(opts.uploaded || 0), + common.toUInt32(common.EVENTS[opts.event] || 0), + common.toUInt32(0), // ip address (optional) + common.toUInt32(0), // key (optional) + common.toUInt32(self.client._numWant), + toUInt16(self.client._port || 0) + ])) + } + + function scrape (connectionId) { + genTransactionId() + + send(Buffer.concat([ + connectionId, + common.toUInt32(common.ACTIONS.SCRAPE), + transactionId, + self.client._infoHash + ])) + } } -Client.prototype._defaultAnnounceOpts = function (opts) { +Tracker.prototype._handleResponse = function (requestUrl, data) { var self = this - if (!opts) opts = {} - if (opts.numwant == null) opts.numwant = common.DEFAULT_ANNOUNCE_PEERS + try { + data = bencode.decode(data) + } catch (err) { + return self.client.emit('warning', new Error('Error decoding tracker response: ' + err.message)) + } + var failure = data['failure reason'] + if (failure) { + return self.client.emit('warning', new Error(failure)) + } + + var warning = data['warning message'] + if (warning) { + self.client.emit('warning', new Error(warning)) + } + + if (requestUrl === self._announceUrl) { + var interval = data.interval || data['min interval'] + if (interval && !self._opts.interval && self._intervalMs !== 0) { + // use the interval the tracker recommends, UNLESS the user manually specifies an + // interval they want to use + self.setInterval(interval * 1000) + } - if (opts.uploaded == null) opts.uploaded = 0 - if (opts.downloaded == null) opts.downloaded = 0 + var trackerId = data['tracker id'] + if (trackerId) { + // If absent, do not discard previous trackerId value + self._trackerId = trackerId + } + + self.client.emit('update', { + announce: self._announceUrl, + complete: data.complete, + incomplete: data.incomplete + }) - if (self._getAnnounceOpts) opts = extend(opts, self._getAnnounceOpts()) - return opts + if (Buffer.isBuffer(data.peers)) { + // tracker returned compact response + compact2string.multi(data.peers).forEach(function (addr) { + self.client.emit('peer', addr) + }) + } else if (Array.isArray(data.peers)) { + // tracker returned normal response + data.peers.forEach(function (peer) { + var ip = peer.ip + var addr = ip[0] + '.' + ip[1] + '.' + ip[2] + '.' + ip[3] + ':' + peer.port + self.client.emit('peer', addr) + }) + } + } else if (requestUrl === self._scrapeUrl) { + // NOTE: the unofficial spec says to use the 'files' key but i've seen 'host' in practice + data = data.files || data.host || {} + data = data[self.client._infoHash.toString('binary')] + + if (!data) { + self.client.emit('warning', new Error('invalid scrape response')) + } else { + // TODO: optionally handle data.flags.min_request_interval (separate from announce interval) + self.client.emit('scrape', { + announce: self._announceUrl, + complete: data.complete, + incomplete: data.incomplete, + downloaded: data.downloaded + }) + } + } +} + +function toUInt16 (n) { + var buf = new Buffer(2) + buf.writeUInt16BE(n, 0) + return buf +} + +var MAX_UINT = 4294967295 + +function toUInt64 (n) { + if (n > MAX_UINT || typeof n === 'string') { + var bytes = new BN(n).toArray() + while (bytes.length < 8) { + bytes.unshift(0) + } + return new Buffer(bytes) + } + return Buffer.concat([common.toUInt32(0), common.toUInt32(n)]) +} + +var UDP_TRACKER = /^udp:\/\// +var HTTP_SCRAPE_SUPPORT = /\/(announce)[^\/]*$/ + +function getScrapeUrl (announceUrl) { + if (announceUrl.match(UDP_TRACKER)) return announceUrl + var match = announceUrl.match(HTTP_SCRAPE_SUPPORT) + if (match) { + var i = match.index + return announceUrl.slice(0, i) + '/scrape' + announceUrl.slice(i + 9) + } + return null } diff --git a/examples/express-embed/package.json b/examples/express-embed/package.json deleted file mode 100644 index 07eceecf..00000000 --- a/examples/express-embed/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "bittorrent-tracker-example-express-embed", - "version": "0.0.0", - "description": "Example for embedding bittorrent-tracker server in express.js", - "scripts": { - "server": "./server.js" - }, - "author": "Astro ", - "license": "MIT", - "dependencies": { - "express": "^4.10.5" - } -} diff --git a/examples/express-embed/server.js b/examples/express-embed/server.js deleted file mode 100755 index bbb85b12..00000000 --- a/examples/express-embed/server.js +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env node - -var Server = require('../..').Server -var express = require('express') -var app = express() - -// https://wiki.theory.org/BitTorrentSpecification#peer_id -var whitelist = { - UT: true // uTorrent -} - -var server = new Server({ - http: false, // we do our own - udp: false, // not interested - ws: false, // not interested - filter: function (params) { - // black/whitelist for disallowing/allowing specific clients [default=allow all] - // this example only allows the uTorrent client - var client = params.peer_id[1] + params.peer_id[2] - return whitelist[client] - } -}) - -var onHttpRequest = server.onHttpRequest.bind(server) -app.get('/announce', onHttpRequest) -app.get('/scrape', onHttpRequest) - -app.listen(8080) diff --git a/examples/tracker-scrape.md b/examples/tracker-scrape.md deleted file mode 100644 index 3ef06a5e..00000000 --- a/examples/tracker-scrape.md +++ /dev/null @@ -1,47 +0,0 @@ -# Count the number of peers using the `scrape` feature of torrent trackers - -Here's a full example with `browserify`. - -``` -npm install browserify parse-torrent bittorrent-tracker -``` - -`scrape.js`: - -```js -var Tracker = require('bittorrent-tracker') -var magnet = require('magnet-uri') - -var magnetURI = "magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&dn=sintel.mp4&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel-1024-surround.mp4" - -var parsedTorrent = magnet(magnetURI) - -var opts = { - infoHash: parsedTorrent.infoHash, - announce: parsedTorrent.announce, - peerId: new Buffer('01234567890123456789'), // hex string or Buffer - port: 6881 // torrent client port -} - -var client = new Tracker(opts) - -client.scrape() - -client.on('scrape', function (data) { - console.log(data) -}) -``` - -Bundle up `scrape.js` and it's dependencies into a single file called `bundle.js`: - -```bash -browserify scrape.js -o bundle.js -``` - -`index.html`: - -```js - -``` - -Open `index.html` in your browser. diff --git a/img/img.png b/img.png similarity index 100% rename from img/img.png rename to img.png diff --git a/img/trackerStats.png b/img/trackerStats.png deleted file mode 100644 index 54ef54bb..00000000 Binary files a/img/trackerStats.png and /dev/null differ diff --git a/lib/client/http-tracker.js b/lib/client/http-tracker.js deleted file mode 100644 index 2cffc2a6..00000000 --- a/lib/client/http-tracker.js +++ /dev/null @@ -1,259 +0,0 @@ -module.exports = HTTPTracker - -var arrayRemove = require('unordered-array-remove') -var bencode = require('bencode') -var compact2string = require('compact2string') -var debug = require('debug')('bittorrent-tracker:http-tracker') -var extend = require('xtend') -var get = require('simple-get') -var inherits = require('inherits') - -var common = require('../common') -var Tracker = require('./tracker') - -var HTTP_SCRAPE_SUPPORT = /\/(announce)[^/]*$/ - -inherits(HTTPTracker, Tracker) - -/** - * HTTP 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 - */ -function HTTPTracker (client, announceUrl, opts) { - var self = this - Tracker.call(self, client, announceUrl) - debug('new http tracker %s', announceUrl) - - // Determine scrape url (if http tracker supports it) - self.scrapeUrl = null - - var match = self.announceUrl.match(HTTP_SCRAPE_SUPPORT) - if (match) { - var pre = self.announceUrl.slice(0, match.index) - var post = self.announceUrl.slice(match.index + 9) - self.scrapeUrl = pre + '/scrape' + post - } - - self.cleanupFns = [] - self.maybeDestroyCleanup = null -} - -HTTPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes - -HTTPTracker.prototype.announce = function (opts) { - var self = this - if (self.destroyed) return - - var params = extend(opts, { - compact: (opts.compact == null) ? 1 : opts.compact, - info_hash: self.client._infoHashBinary, - peer_id: self.client._peerIdBinary, - port: self.client._port - }) - if (self._trackerId) params.trackerid = self._trackerId - - self._request(self.announceUrl, params, function (err, data) { - if (err) return self.client.emit('warning', err) - self._onAnnounceResponse(data) - }) -} - -HTTPTracker.prototype.scrape = function (opts) { - var self = this - if (self.destroyed) return - - if (!self.scrapeUrl) { - self.client.emit('error', new Error('scrape not supported ' + self.announceUrl)) - return - } - - var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(function (infoHash) { - return infoHash.toString('binary') - }) - : (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary - var params = { - info_hash: infoHashes - } - self._request(self.scrapeUrl, params, function (err, data) { - if (err) return self.client.emit('warning', err) - self._onScrapeResponse(data) - }) -} - -HTTPTracker.prototype.destroy = function (cb) { - var self = this - if (self.destroyed) return cb(null) - self.destroyed = true - clearInterval(self.interval) - - // If there are no pending requests, destroy immediately. - if (self.cleanupFns.length === 0) return destroyCleanup() - - // Otherwise, wait a short time for pending requests to complete, then force - // destroy them. - var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) - - // But, if all pending requests complete before the timeout fires, do cleanup - // right away. - self.maybeDestroyCleanup = function () { - if (self.cleanupFns.length === 0) destroyCleanup() - } - - function destroyCleanup () { - if (timeout) { - clearTimeout(timeout) - timeout = null - } - self.maybeDestroyCleanup = null - self.cleanupFns.slice(0).forEach(function (cleanup) { - cleanup() - }) - self.cleanupFns = [] - cb(null) - } -} - -HTTPTracker.prototype._request = function (requestUrl, params, cb) { - var self = this - var u = requestUrl + (requestUrl.indexOf('?') === -1 ? '?' : '&') + - common.querystringStringify(params) - - self.cleanupFns.push(cleanup) - - var request = get.concat({ - url: u, - timeout: common.REQUEST_TIMEOUT, - headers: { - 'user-agent': self.client._userAgent || '' - } - }, onResponse) - - function cleanup () { - if (request) { - arrayRemove(self.cleanupFns, self.cleanupFns.indexOf(cleanup)) - request.abort() - request = null - } - if (self.maybeDestroyCleanup) self.maybeDestroyCleanup() - } - - function onResponse (err, res, data) { - cleanup() - if (self.destroyed) return - - if (err) return cb(err) - if (res.statusCode !== 200) { - return cb(new Error('Non-200 response code ' + - res.statusCode + ' from ' + self.announceUrl)) - } - if (!data || data.length === 0) { - return cb(new Error('Invalid tracker response from' + - self.announceUrl)) - } - - try { - data = bencode.decode(data) - } catch (err) { - return cb(new Error('Error decoding tracker response: ' + err.message)) - } - var failure = data['failure reason'] - if (failure) { - debug('failure from ' + requestUrl + ' (' + failure + ')') - return cb(new Error(failure)) - } - - var warning = data['warning message'] - if (warning) { - debug('warning from ' + requestUrl + ' (' + warning + ')') - self.client.emit('warning', new Error(warning)) - } - - debug('response from ' + requestUrl) - - cb(null, data) - } -} - -HTTPTracker.prototype._onAnnounceResponse = function (data) { - var self = this - - var interval = data.interval || data['min interval'] - if (interval) self.setInterval(interval * 1000) - - var trackerId = data['tracker id'] - if (trackerId) { - // If absent, do not discard previous trackerId value - self._trackerId = trackerId - } - - var response = Object.assign({}, data, { - announce: self.announceUrl, - infoHash: common.binaryToHex(data.info_hash) - }) - self.client.emit('update', response) - - var addrs - if (Buffer.isBuffer(data.peers)) { - // tracker returned compact response - try { - addrs = compact2string.multi(data.peers) - } catch (err) { - return self.client.emit('warning', err) - } - addrs.forEach(function (addr) { - self.client.emit('peer', addr) - }) - } else if (Array.isArray(data.peers)) { - // tracker returned normal response - data.peers.forEach(function (peer) { - self.client.emit('peer', peer.ip + ':' + peer.port) - }) - } - - if (Buffer.isBuffer(data.peers6)) { - // tracker returned compact response - try { - addrs = compact2string.multi6(data.peers6) - } catch (err) { - return self.client.emit('warning', err) - } - addrs.forEach(function (addr) { - self.client.emit('peer', addr) - }) - } else if (Array.isArray(data.peers6)) { - // tracker returned normal response - data.peers6.forEach(function (peer) { - var ip = /^\[/.test(peer.ip) || !/:/.test(peer.ip) - ? peer.ip /* ipv6 w/ brackets or domain name */ - : '[' + peer.ip + ']' /* ipv6 without brackets */ - self.client.emit('peer', ip + ':' + peer.port) - }) - } -} - -HTTPTracker.prototype._onScrapeResponse = function (data) { - var self = this - // NOTE: the unofficial spec says to use the 'files' key, 'host' has been - // seen in practice - data = data.files || data.host || {} - - var keys = Object.keys(data) - if (keys.length === 0) { - self.client.emit('warning', new Error('invalid scrape response')) - return - } - - keys.forEach(function (infoHash) { - // TODO: optionally handle data.flags.min_request_interval - // (separate from announce interval) - var response = Object.assign(data[infoHash], { - announce: self.announceUrl, - infoHash: common.binaryToHex(infoHash) - }) - self.client.emit('scrape', response) - }) -} diff --git a/lib/client/tracker.js b/lib/client/tracker.js deleted file mode 100644 index 73de2cd2..00000000 --- a/lib/client/tracker.js +++ /dev/null @@ -1,30 +0,0 @@ -module.exports = Tracker - -var EventEmitter = require('events').EventEmitter -var inherits = require('inherits') - -inherits(Tracker, EventEmitter) - -function Tracker (client, announceUrl) { - var self = this - EventEmitter.call(self) - self.client = client - self.announceUrl = announceUrl - - self.interval = null - self.destroyed = false -} - -Tracker.prototype.setInterval = function (intervalMs) { - var self = this - if (intervalMs == null) intervalMs = self.DEFAULT_ANNOUNCE_INTERVAL - - clearInterval(self.interval) - - if (intervalMs) { - self.interval = setInterval(function () { - self.announce(self.client._defaultAnnounceOpts()) - }, intervalMs) - if (self.interval.unref) self.interval.unref() - } -} diff --git a/lib/client/udp-tracker.js b/lib/client/udp-tracker.js deleted file mode 100644 index 3198edc5..00000000 --- a/lib/client/udp-tracker.js +++ /dev/null @@ -1,283 +0,0 @@ -module.exports = UDPTracker - -var arrayRemove = require('unordered-array-remove') -var BN = require('bn.js') -var Buffer = require('safe-buffer').Buffer -var compact2string = require('compact2string') -var debug = require('debug')('bittorrent-tracker:udp-tracker') -var dgram = require('dgram') -var inherits = require('inherits') -var randombytes = require('randombytes') -var url = require('url') - -var common = require('../common') -var Tracker = require('./tracker') - -inherits(UDPTracker, 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 - */ -function UDPTracker (client, announceUrl, opts) { - var self = this - Tracker.call(self, client, announceUrl) - debug('new udp tracker %s', announceUrl) - - self.cleanupFns = [] - self.maybeDestroyCleanup = null -} - -UDPTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 60 * 1000 // 30 minutes - -UDPTracker.prototype.announce = function (opts) { - var self = this - if (self.destroyed) return - self._request(opts) -} - -UDPTracker.prototype.scrape = function (opts) { - var self = this - if (self.destroyed) return - opts._scrape = true - self._request(opts) // udp scrape uses same announce url -} - -UDPTracker.prototype.destroy = function (cb) { - var self = this - if (self.destroyed) return cb(null) - self.destroyed = true - clearInterval(self.interval) - - // If there are no pending requests, destroy immediately. - if (self.cleanupFns.length === 0) return destroyCleanup() - - // Otherwise, wait a short time for pending requests to complete, then force - // destroy them. - var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) - - // But, if all pending requests complete before the timeout fires, do cleanup - // right away. - self.maybeDestroyCleanup = function () { - if (self.cleanupFns.length === 0) destroyCleanup() - } - - function destroyCleanup () { - if (timeout) { - clearTimeout(timeout) - timeout = null - } - self.maybeDestroyCleanup = null - self.cleanupFns.slice(0).forEach(function (cleanup) { - cleanup() - }) - self.cleanupFns = [] - cb(null) - } -} - -UDPTracker.prototype._request = function (opts) { - var self = this - if (!opts) opts = {} - var parsedUrl = url.parse(self.announceUrl) - var transactionId = genTransactionId() - var socket = dgram.createSocket('udp4') - - var timeout = setTimeout(function () { - // 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() - - self.cleanupFns.push(cleanup) - - send(Buffer.concat([ - common.CONNECTION_ID, - common.toUInt32(common.ACTIONS.CONNECT), - transactionId - ])) - - socket.once('error', onError) - socket.on('message', onSocketMessage) - - 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 (self.maybeDestroyCleanup) self.maybeDestroyCleanup() - } - - function onError (err) { - cleanup() - if (self.destroyed) return - - if (err.message) err.message += ' (' + self.announceUrl + ')' - // errors will often happen if a tracker is offline, so don't treat it as fatal - self.client.emit('warning', err) - } - - function onSocketMessage (msg) { - if (msg.length < 8 || msg.readUInt32BE(4) !== transactionId.readUInt32BE(0)) { - return onError(new Error('tracker sent invalid transaction id')) - } - - var action = msg.readUInt32BE(0) - debug('UDP response %s, action %s', self.announceUrl, action) - switch (action) { - case 0: // handshake - // Note: no check for `self.destroyed` so that pending messages to the - // tracker can still be sent/received even after destroy() is called - - if (msg.length < 16) return onError(new Error('invalid udp handshake')) - - if (opts._scrape) scrape(msg.slice(8, 16)) - else announce(msg.slice(8, 16), opts) - - break - - case 1: // announce - cleanup() - if (self.destroyed) return - - if (msg.length < 20) return onError(new Error('invalid announce message')) - - var interval = msg.readUInt32BE(8) - if (interval) self.setInterval(interval * 1000) - - self.client.emit('update', { - announce: self.announceUrl, - complete: msg.readUInt32BE(16), - incomplete: msg.readUInt32BE(12) - }) - - var addrs - try { - addrs = compact2string.multi(msg.slice(20)) - } catch (err) { - return self.client.emit('warning', err) - } - addrs.forEach(function (addr) { - self.client.emit('peer', addr) - }) - - break - - case 2: // scrape - cleanup() - if (self.destroyed) return - - if (msg.length < 20 || (msg.length - 8) % 12 !== 0) { - return onError(new Error('invalid scrape message')) - } - var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(function (infoHash) { return infoHash.toString('hex') }) - : [ (opts.infoHash && opts.infoHash.toString('hex')) || self.client.infoHash ] - - for (var 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 - - case 3: // error - 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())) - - break - - default: - onError(new Error('tracker sent invalid action')) - break - } - } - - function send (message) { - if (!parsedUrl.port) { - parsedUrl.port = 80 - } - socket.send(message, 0, message.length, parsedUrl.port, parsedUrl.hostname) - } - - 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) - ])) - } - - function scrape (connectionId) { - transactionId = genTransactionId() - - var infoHash = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? Buffer.concat(opts.infoHash) - : (opts.infoHash || self.client._infoHashBuffer) - - send(Buffer.concat([ - connectionId, - common.toUInt32(common.ACTIONS.SCRAPE), - transactionId, - infoHash - ])) - } -} - -function genTransactionId () { - return randombytes(4) -} - -function toUInt16 (n) { - var buf = Buffer.allocUnsafe(2) - buf.writeUInt16BE(n, 0) - return buf -} - -var MAX_UINT = 4294967295 - -function toUInt64 (n) { - if (n > MAX_UINT || typeof n === 'string') { - var 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 () {} diff --git a/lib/client/websocket-tracker.js b/lib/client/websocket-tracker.js deleted file mode 100644 index e021e929..00000000 --- a/lib/client/websocket-tracker.js +++ /dev/null @@ -1,444 +0,0 @@ -module.exports = WebSocketTracker - -var debug = require('debug')('bittorrent-tracker:websocket-tracker') -var extend = require('xtend') -var inherits = require('inherits') -var Peer = require('simple-peer') -var randombytes = require('randombytes') -var Socket = require('simple-websocket') - -var common = require('../common') -var Tracker = require('./tracker') - -// Use a socket pool, so tracker clients share WebSocket objects for the same server. -// In practice, WebSockets are pretty slow to establish, so this gives a nice performance -// boost, and saves browser resources. -var socketPool = {} - -var RECONNECT_MINIMUM = 15 * 1000 -var RECONNECT_MAXIMUM = 30 * 60 * 1000 -var RECONNECT_VARIANCE = 30 * 1000 -var OFFER_TIMEOUT = 50 * 1000 - -inherits(WebSocketTracker, Tracker) - -function WebSocketTracker (client, announceUrl, opts) { - var self = this - Tracker.call(self, client, announceUrl) - debug('new websocket tracker %s', announceUrl) - - self.peers = {} // peers (offer id -> peer) - self.socket = null - - self.reconnecting = false - self.retries = 0 - self.reconnectTimer = null - - // Simple boolean flag to track whether the socket has received data from - // the websocket server since the last time socket.send() was called. - self.expectingResponse = false - - self._openSocket() -} - -WebSocketTracker.prototype.DEFAULT_ANNOUNCE_INTERVAL = 30 * 1000 // 30 seconds - -WebSocketTracker.prototype.announce = function (opts) { - var self = this - if (self.destroyed || self.reconnecting) return - if (!self.socket.connected) { - self.socket.once('connect', function () { - self.announce(opts) - }) - return - } - - var params = extend(opts, { - action: 'announce', - info_hash: self.client._infoHashBinary, - peer_id: self.client._peerIdBinary - }) - if (self._trackerId) params.trackerid = self._trackerId - - if (opts.event === 'stopped' || opts.event === 'completed') { - // Don't include offers with 'stopped' or 'completed' event - self._send(params) - } else { - // Limit the number of offers that are generated, since it can be slow - var numwant = Math.min(opts.numwant, 10) - - self._generateOffers(numwant, function (offers) { - params.numwant = numwant - params.offers = offers - self._send(params) - }) - } -} - -WebSocketTracker.prototype.scrape = function (opts) { - var self = this - if (self.destroyed || self.reconnecting) return - if (!self.socket.connected) { - self.socket.once('connect', function () { - self.scrape(opts) - }) - return - } - - var infoHashes = (Array.isArray(opts.infoHash) && opts.infoHash.length > 0) - ? opts.infoHash.map(function (infoHash) { - return infoHash.toString('binary') - }) - : (opts.infoHash && opts.infoHash.toString('binary')) || self.client._infoHashBinary - var params = { - action: 'scrape', - info_hash: infoHashes - } - - self._send(params) -} - -WebSocketTracker.prototype.destroy = function (cb) { - var self = this - if (!cb) cb = noop - if (self.destroyed) return cb(null) - - self.destroyed = true - - clearInterval(self.interval) - clearTimeout(self.reconnectTimer) - - // Destroy peers - for (var peerId in self.peers) { - var peer = self.peers[peerId] - clearTimeout(peer.trackerTimeout) - peer.destroy() - } - self.peers = null - - if (self.socket) { - self.socket.removeListener('connect', self._onSocketConnectBound) - self.socket.removeListener('data', self._onSocketDataBound) - self.socket.removeListener('close', self._onSocketCloseBound) - self.socket.removeListener('error', self._onSocketErrorBound) - self.socket = null - } - - self._onSocketConnectBound = null - self._onSocketErrorBound = null - self._onSocketDataBound = null - self._onSocketCloseBound = null - - if (socketPool[self.announceUrl]) { - socketPool[self.announceUrl].consumers -= 1 - } - - // Other instances are using the socket, so there's nothing left to do here - if (socketPool[self.announceUrl].consumers > 0) return cb() - - var socket = socketPool[self.announceUrl] - delete socketPool[self.announceUrl] - socket.on('error', noop) // ignore all future errors - socket.once('close', cb) - - // If there is no data response expected, destroy immediately. - if (!self.expectingResponse) return destroyCleanup() - - // Otherwise, wait a short time for potential responses to come in from the - // server, then force close the socket. - var timeout = setTimeout(destroyCleanup, common.DESTROY_TIMEOUT) - - // But, if a response comes from the server before the timeout fires, do cleanup - // right away. - socket.once('data', destroyCleanup) - - function destroyCleanup () { - if (timeout) { - clearTimeout(timeout) - timeout = null - } - socket.removeListener('data', destroyCleanup) - socket.destroy() - socket = null - } -} - -WebSocketTracker.prototype._openSocket = function () { - var self = this - self.destroyed = false - - if (!self.peers) self.peers = {} - - self._onSocketConnectBound = function () { - self._onSocketConnect() - } - self._onSocketErrorBound = function (err) { - self._onSocketError(err) - } - self._onSocketDataBound = function (data) { - self._onSocketData(data) - } - self._onSocketCloseBound = function () { - self._onSocketClose() - } - - self.socket = socketPool[self.announceUrl] - if (self.socket) { - socketPool[self.announceUrl].consumers += 1 - } else { - self.socket = socketPool[self.announceUrl] = new Socket(self.announceUrl) - self.socket.consumers = 1 - self.socket.once('connect', self._onSocketConnectBound) - } - - self.socket.on('data', self._onSocketDataBound) - self.socket.once('close', self._onSocketCloseBound) - self.socket.once('error', self._onSocketErrorBound) -} - -WebSocketTracker.prototype._onSocketConnect = function () { - var self = this - if (self.destroyed) return - - if (self.reconnecting) { - self.reconnecting = false - self.retries = 0 - self.announce(self.client._defaultAnnounceOpts()) - } -} - -WebSocketTracker.prototype._onSocketData = function (data) { - var self = this - if (self.destroyed) return - - self.expectingResponse = false - - try { - data = JSON.parse(data) - } catch (err) { - self.client.emit('warning', new Error('Invalid tracker response')) - return - } - - if (data.action === 'announce') { - self._onAnnounceResponse(data) - } else if (data.action === 'scrape') { - self._onScrapeResponse(data) - } else { - self._onSocketError(new Error('invalid action in WS response: ' + data.action)) - } -} - -WebSocketTracker.prototype._onAnnounceResponse = function (data) { - var self = this - - if (data.info_hash !== self.client._infoHashBinary) { - debug( - 'ignoring websocket data from %s for %s (looking for %s: reused socket)', - self.announceUrl, common.binaryToHex(data.info_hash), self.client.infoHash - ) - return - } - - if (data.peer_id && data.peer_id === self.client._peerIdBinary) { - // ignore offers/answers from this client - return - } - - debug( - 'received %s from %s for %s', - JSON.stringify(data), self.announceUrl, self.client.infoHash - ) - - var failure = data['failure reason'] - if (failure) return self.client.emit('warning', new Error(failure)) - - var warning = data['warning message'] - if (warning) self.client.emit('warning', new Error(warning)) - - var interval = data.interval || data['min interval'] - if (interval) self.setInterval(interval * 1000) - - var trackerId = data['tracker id'] - if (trackerId) { - // If absent, do not discard previous trackerId value - self._trackerId = trackerId - } - - if (data.complete != null) { - var response = Object.assign({}, data, { - announce: self.announceUrl, - infoHash: common.binaryToHex(data.info_hash) - }) - self.client.emit('update', response) - } - - var peer - if (data.offer && data.peer_id) { - debug('creating peer (from remote offer)') - peer = self._createPeer() - peer.id = common.binaryToHex(data.peer_id) - peer.once('signal', function (answer) { - var params = { - action: 'announce', - info_hash: self.client._infoHashBinary, - peer_id: self.client._peerIdBinary, - to_peer_id: data.peer_id, - answer: answer, - offer_id: data.offer_id - } - if (self._trackerId) params.trackerid = self._trackerId - self._send(params) - }) - peer.signal(data.offer) - self.client.emit('peer', peer) - } - - if (data.answer && data.peer_id) { - var offerId = common.binaryToHex(data.offer_id) - peer = self.peers[offerId] - if (peer) { - peer.id = common.binaryToHex(data.peer_id) - peer.signal(data.answer) - self.client.emit('peer', peer) - - clearTimeout(peer.trackerTimeout) - peer.trackerTimeout = null - delete self.peers[offerId] - } else { - debug('got unexpected answer: ' + JSON.stringify(data.answer)) - } - } -} - -WebSocketTracker.prototype._onScrapeResponse = function (data) { - var self = this - data = data.files || {} - - var keys = Object.keys(data) - if (keys.length === 0) { - self.client.emit('warning', new Error('invalid scrape response')) - return - } - - keys.forEach(function (infoHash) { - // TODO: optionally handle data.flags.min_request_interval - // (separate from announce interval) - var response = Object.assign(data[infoHash], { - announce: self.announceUrl, - infoHash: common.binaryToHex(infoHash) - }) - self.client.emit('scrape', response) - }) -} - -WebSocketTracker.prototype._onSocketClose = function () { - var self = this - if (self.destroyed) return - self.destroy() - self._startReconnectTimer() -} - -WebSocketTracker.prototype._onSocketError = function (err) { - var self = this - if (self.destroyed) return - self.destroy() - // errors will often happen if a tracker is offline, so don't treat it as fatal - self.client.emit('warning', err) - self._startReconnectTimer() -} - -WebSocketTracker.prototype._startReconnectTimer = function () { - var self = this - var ms = Math.floor(Math.random() * RECONNECT_VARIANCE) + Math.min(Math.pow(2, self.retries) * RECONNECT_MINIMUM, RECONNECT_MAXIMUM) - - self.reconnecting = true - clearTimeout(self.reconnectTimer) - self.reconnectTimer = setTimeout(function () { - self.retries++ - self._openSocket() - }, ms) - if (self.reconnectTimer.unref) self.reconnectTimer.unref() - - debug('reconnecting socket in %s ms', ms) -} - -WebSocketTracker.prototype._send = function (params) { - var self = this - if (self.destroyed) return - self.expectingResponse = true - var message = JSON.stringify(params) - debug('send %s', message) - self.socket.send(message) -} - -WebSocketTracker.prototype._generateOffers = function (numwant, cb) { - var self = this - var offers = [] - debug('generating %s offers', numwant) - - for (var i = 0; i < numwant; ++i) { - generateOffer() - } - checkDone() - - function generateOffer () { - var offerId = randombytes(20).toString('hex') - debug('creating peer (from _generateOffers)') - var peer = self.peers[offerId] = self._createPeer({ initiator: true }) - peer.once('signal', function (offer) { - offers.push({ - offer: offer, - offer_id: common.hexToBinary(offerId) - }) - checkDone() - }) - peer.trackerTimeout = setTimeout(function () { - debug('tracker timeout: destroying peer') - peer.trackerTimeout = null - delete self.peers[offerId] - peer.destroy() - }, OFFER_TIMEOUT) - if (peer.trackerTimeout.unref) peer.trackerTimeout.unref() - } - - function checkDone () { - if (offers.length === numwant) { - debug('generated %s offers', numwant) - cb(offers) - } - } -} - -WebSocketTracker.prototype._createPeer = function (opts) { - var self = this - - opts = Object.assign({ - trickle: false, - config: self.client._rtcConfig, - wrtc: self.client._wrtc - }, opts) - - var peer = new Peer(opts) - - peer.once('error', onError) - peer.once('connect', onConnect) - - return peer - - // Handle peer 'error' events that are fired *before* the peer is emitted in - // a 'peer' event. - function onError (err) { - self.client.emit('warning', new Error('Connection error: ' + err.message)) - peer.destroy() - } - - // Once the peer is emitted in a 'peer' event, then it's the consumer's - // responsibility to listen for errors, so the listeners are removed here. - function onConnect () { - peer.removeListener('error', onError) - peer.removeListener('connect', onConnect) - } -} - -function noop () {} diff --git a/lib/common-node.js b/lib/common-node.js deleted file mode 100644 index b06d0004..00000000 --- a/lib/common-node.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Functions/constants needed by both the client and server (but only in node). - * These are separate from common.js so they can be skipped when bundling for the browser. - */ - -var Buffer = require('safe-buffer').Buffer -var querystring = require('querystring') - -exports.IPV4_RE = /^[\d.]+$/ -exports.IPV6_RE = /^[\da-fA-F:]+$/ -exports.REMOVE_IPV4_MAPPED_IPV6_RE = /^::ffff:/ - -exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ]) -exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 } -exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 3 } -exports.EVENT_IDS = { - 0: 'update', - 1: 'completed', - 2: 'started', - 3: 'stopped' -} -exports.EVENT_NAMES = { - update: 'update', - completed: 'complete', - started: 'start', - stopped: 'stop' -} - -/** - * Client request timeout. How long to wait before considering a request to a - * tracker server to have timed out. - */ -exports.REQUEST_TIMEOUT = 15000 - -/** - * Client destroy timeout. How long to wait before forcibly cleaning up all - * pending requests, open sockets, etc. - */ -exports.DESTROY_TIMEOUT = 1000 - -function toUInt32 (n) { - var buf = Buffer.allocUnsafe(4) - buf.writeUInt32BE(n, 0) - return buf -} -exports.toUInt32 = toUInt32 - -/** - * `querystring.parse` using `unescape` instead of decodeURIComponent, since bittorrent - * clients send non-UTF8 querystrings - * @param {string} q - * @return {Object} - */ -exports.querystringParse = function (q) { - return querystring.parse(q, null, null, { decodeURIComponent: unescape }) -} - -/** - * `querystring.stringify` using `escape` instead of encodeURIComponent, since bittorrent - * clients send non-UTF8 querystrings - * @param {Object} obj - * @return {string} - */ -exports.querystringStringify = function (obj) { - var ret = querystring.stringify(obj, null, null, { encodeURIComponent: escape }) - ret = ret.replace(/[@*/+]/g, function (char) { - // `escape` doesn't encode the characters @*/+ so we do it manually - return '%' + char.charCodeAt(0).toString(16).toUpperCase() - }) - return ret -} diff --git a/lib/common.js b/lib/common.js index 493b14a9..ccf1bd41 100644 --- a/lib/common.js +++ b/lib/common.js @@ -2,25 +2,47 @@ * Functions/constants needed by both the client and server. */ -var Buffer = require('safe-buffer').Buffer -var extend = require('xtend/mutable') +var querystring = require('querystring') -exports.DEFAULT_ANNOUNCE_PEERS = 50 -exports.MAX_ANNOUNCE_PEERS = 82 +exports.CONNECTION_ID = Buffer.concat([ toUInt32(0x417), toUInt32(0x27101980) ]) +exports.ACTIONS = { CONNECT: 0, ANNOUNCE: 1, SCRAPE: 2, ERROR: 3 } +exports.EVENTS = { update: 0, completed: 1, started: 2, stopped: 3 } -exports.binaryToHex = function (str) { - if (typeof str !== 'string') { - str = String(str) - } - return Buffer.from(str, 'binary').toString('hex') +function toUInt32 (n) { + var buf = new Buffer(4) + buf.writeUInt32BE(n, 0) + return buf } +exports.toUInt32 = toUInt32 -exports.hexToBinary = function (str) { - if (typeof str !== 'string') { - str = String(str) - } - return Buffer.from(str, 'hex').toString('binary') +exports.binaryToUtf8 = function (str) { + return new Buffer(str, 'binary').toString('utf8') } -var config = require('./common-node') -extend(exports, config) +/** + * `querystring.parse` using `unescape` instead of decodeURIComponent, since bittorrent + * clients send non-UTF8 querystrings + * @param {string} q + * @return {Object} + */ +exports.querystringParse = function (q) { + var saved = querystring.unescape + querystring.unescape = unescape // global + var ret = querystring.parse(q) + querystring.unescape = saved + return ret +} + +/** + * `querystring.stringify` using `escape` instead of encodeURIComponent, since bittorrent + * clients send non-UTF8 querystrings + * @param {Object} obj + * @return {string} + */ +exports.querystringStringify = function (obj) { + var saved = querystring.escape + querystring.escape = escape // global + var ret = querystring.stringify(obj) + querystring.escape = saved + return ret +} diff --git a/lib/server/parse-http.js b/lib/server/parse-http.js deleted file mode 100644 index bf6cb2c4..00000000 --- a/lib/server/parse-http.js +++ /dev/null @@ -1,59 +0,0 @@ -module.exports = parseHttpRequest - -var common = require('../common') - -function parseHttpRequest (req, opts) { - if (!opts) opts = {} - var s = req.url.split('?') - var params = common.querystringParse(s[1]) - params.type = 'http' - - if (opts.action === 'announce' || s[0] === '/announce') { - params.action = common.ACTIONS.ANNOUNCE - - if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) { - throw new Error('invalid info_hash') - } - params.info_hash = common.binaryToHex(params.info_hash) - - if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) { - throw new Error('invalid peer_id') - } - params.peer_id = common.binaryToHex(params.peer_id) - - params.port = Number(params.port) - if (!params.port) throw new Error('invalid port') - - params.left = Number(params.left) - if (Number.isNaN(params.left)) params.left = Infinity - - params.compact = Number(params.compact) || 0 - params.numwant = Math.min( - Number(params.numwant) || common.DEFAULT_ANNOUNCE_PEERS, - common.MAX_ANNOUNCE_PEERS - ) - - params.ip = opts.trustProxy - ? req.headers['x-forwarded-for'] || req.connection.remoteAddress - : req.connection.remoteAddress.replace(common.REMOVE_IPV4_MAPPED_IPV6_RE, '') // force ipv4 - params.addr = (common.IPV6_RE.test(params.ip) ? '[' + params.ip + ']' : params.ip) + ':' + params.port - - params.headers = req.headers - } else if (opts.action === 'scrape' || s[0] === '/scrape') { - params.action = common.ACTIONS.SCRAPE - - if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ] - if (Array.isArray(params.info_hash)) { - params.info_hash = params.info_hash.map(function (binaryInfoHash) { - if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { - throw new Error('invalid info_hash') - } - return common.binaryToHex(binaryInfoHash) - }) - } - } else { - throw new Error('invalid action in HTTP request: ' + req.url) - } - - return params -} diff --git a/lib/server/parse-udp.js b/lib/server/parse-udp.js deleted file mode 100644 index da32bebd..00000000 --- a/lib/server/parse-udp.js +++ /dev/null @@ -1,75 +0,0 @@ -module.exports = parseUdpRequest - -var ipLib = require('ip') -var common = require('../common') - -function parseUdpRequest (msg, rinfo) { - if (msg.length < 16) throw new Error('received packet is too short') - - var params = { - connectionId: msg.slice(0, 8), // 64-bit - action: msg.readUInt32BE(8), - transactionId: msg.readUInt32BE(12), - type: 'udp' - } - - if (!common.CONNECTION_ID.equals(params.connectionId)) { - throw new Error('received packet with invalid connection id') - } - - if (params.action === common.ACTIONS.CONNECT) { - // No further params - } else if (params.action === common.ACTIONS.ANNOUNCE) { - params.info_hash = msg.slice(16, 36).toString('hex') // 20 bytes - params.peer_id = msg.slice(36, 56).toString('hex') // 20 bytes - params.downloaded = fromUInt64(msg.slice(56, 64)) // TODO: track this? - params.left = fromUInt64(msg.slice(64, 72)) - params.uploaded = fromUInt64(msg.slice(72, 80)) // TODO: track this? - - params.event = common.EVENT_IDS[msg.readUInt32BE(80)] - if (!params.event) throw new Error('invalid event') // early return - - var ip = msg.readUInt32BE(84) // optional - params.ip = ip - ? ipLib.toString(ip) - : rinfo.address - - params.key = msg.readUInt32BE(88) // Optional: unique random key from client - - // never send more than MAX_ANNOUNCE_PEERS or else the UDP packet will get bigger than - // 512 bytes which is not safe - params.numwant = Math.min( - msg.readUInt32BE(92) || common.DEFAULT_ANNOUNCE_PEERS, // optional - common.MAX_ANNOUNCE_PEERS - ) - - params.port = msg.readUInt16BE(96) || rinfo.port // optional - params.addr = params.ip + ':' + params.port // TODO: ipv6 brackets - params.compact = 1 // udp is always compact - } else if (params.action === common.ACTIONS.SCRAPE) { // scrape message - if ((msg.length - 16) % 20 !== 0) throw new Error('invalid scrape message') - params.info_hash = [] - for (var i = 0, len = (msg.length - 16) / 20; i < len; i += 1) { - var infoHash = msg.slice(16 + (i * 20), 36 + (i * 20)).toString('hex') // 20 bytes - params.info_hash.push(infoHash) - } - } else { - throw new Error('Invalid action in UDP packet: ' + params.action) - } - - return params -} - -var TWO_PWR_32 = (1 << 16) * 2 - -/** - * Return the closest floating-point representation to the buffer value. Precision will be - * lost for big numbers. - */ -function fromUInt64 (buf) { - var high = buf.readUInt32BE(0) | 0 // force - var low = buf.readUInt32BE(4) | 0 - var lowUnsigned = (low >= 0) ? low : TWO_PWR_32 + low - - return (high * TWO_PWR_32) + lowUnsigned -} diff --git a/lib/server/parse-websocket.js b/lib/server/parse-websocket.js deleted file mode 100644 index ec4e606d..00000000 --- a/lib/server/parse-websocket.js +++ /dev/null @@ -1,78 +0,0 @@ -module.exports = parseWebSocketRequest - -var common = require('../common') - -function parseWebSocketRequest (socket, opts, params) { - if (!opts) opts = {} - params = JSON.parse(params) // may throw - - params.type = 'ws' - params.socket = socket - if (params.action === 'announce') { - params.action = common.ACTIONS.ANNOUNCE - - if (typeof params.info_hash !== 'string' || params.info_hash.length !== 20) { - throw new Error('invalid info_hash') - } - params.info_hash = common.binaryToHex(params.info_hash) - - if (typeof params.peer_id !== 'string' || params.peer_id.length !== 20) { - throw new Error('invalid peer_id') - } - params.peer_id = common.binaryToHex(params.peer_id) - - if (params.answer) { - if (typeof params.to_peer_id !== 'string' || params.to_peer_id.length !== 20) { - throw new Error('invalid `to_peer_id` (required with `answer`)') - } - params.to_peer_id = common.binaryToHex(params.to_peer_id) - } - - params.left = Number(params.left) - if (Number.isNaN(params.left)) params.left = Infinity - - params.numwant = Math.min( - Number(params.offers && params.offers.length) || 0, // no default - explicit only - common.MAX_ANNOUNCE_PEERS - ) - params.compact = -1 // return full peer objects (used for websocket responses) - } else if (params.action === 'scrape') { - params.action = common.ACTIONS.SCRAPE - - if (typeof params.info_hash === 'string') params.info_hash = [ params.info_hash ] - if (Array.isArray(params.info_hash)) { - params.info_hash = params.info_hash.map(function (binaryInfoHash) { - if (typeof binaryInfoHash !== 'string' || binaryInfoHash.length !== 20) { - throw new Error('invalid info_hash') - } - return common.binaryToHex(binaryInfoHash) - }) - } - } else { - throw new Error('invalid action in WS request: ' + params.action) - } - - // On first parse, save important data from `socket.upgradeReq` and delete it - // to reduce memory usage. - if (socket.upgradeReq) { - socket.ip = opts.trustProxy - ? socket.upgradeReq.headers['x-forwarded-for'] || socket.upgradeReq.connection.remoteAddress - : socket.upgradeReq.connection.remoteAddress.replace(common.REMOVE_IPV4_MAPPED_IPV6_RE, '') // force ipv4 - socket.port = socket.upgradeReq.connection.remotePort - if (socket.port) { - socket.addr = (common.IPV6_RE.test(socket.ip) ? '[' + socket.ip + ']' : socket.ip) + ':' + socket.port - } - - socket.headers = socket.upgradeReq.headers - - // Delete `socket.upgradeReq` when it is no longer needed to reduce memory usage - socket.upgradeReq = null - } - - params.ip = socket.ip - params.port = socket.port - params.addr = socket.addr - params.headers = socket.headers - - return params -} diff --git a/lib/server/swarm.js b/lib/server/swarm.js deleted file mode 100644 index 2c6f49a8..00000000 --- a/lib/server/swarm.js +++ /dev/null @@ -1,148 +0,0 @@ -module.exports = Swarm - -var arrayRemove = require('unordered-array-remove') -var debug = require('debug')('bittorrent-tracker:swarm') -var LRU = require('lru') -var randomIterate = require('random-iterate') - -// Regard this as the default implementation of an interface that you -// need to support when overriding Server.createSwarm() and Server.getSwarm() -function Swarm (infoHash, server) { - var self = this - self.infoHash = infoHash - self.complete = 0 - self.incomplete = 0 - - self.peers = new LRU({ - max: server.peersCacheLength || 1000, - maxAge: server.peersCacheTtl || 20 * 60 * 1000 // 20 minutes - }) - - // When a peer is evicted from the LRU store, send a synthetic 'stopped' event - // so the stats get updated correctly. - self.peers.on('evict', function (data) { - var peer = data.value - var params = { - type: peer.type, - event: 'stopped', - numwant: 0, - peer_id: peer.peerId - } - self._onAnnounceStopped(params, peer, peer.peerId) - peer.socket = null - }) -} - -Swarm.prototype.announce = function (params, cb) { - var self = this - var id = params.type === 'ws' ? params.peer_id : params.addr - // Mark the source peer as recently used in cache - var peer = self.peers.get(id) - - if (params.event === 'started') { - self._onAnnounceStarted(params, peer, id) - } else if (params.event === 'stopped') { - self._onAnnounceStopped(params, peer, id) - } else if (params.event === 'completed') { - self._onAnnounceCompleted(params, peer, id) - } else if (params.event === 'update') { - self._onAnnounceUpdate(params, peer, id) - } else { - cb(new Error('invalid event')) - return - } - cb(null, { - complete: self.complete, - incomplete: self.incomplete, - peers: self._getPeers(params.numwant, params.peer_id, !!params.socket) - }) -} - -Swarm.prototype.scrape = function (params, cb) { - cb(null, { - complete: this.complete, - incomplete: this.incomplete - }) -} - -Swarm.prototype._onAnnounceStarted = function (params, peer, id) { - if (peer) { - debug('unexpected `started` event from peer that is already in swarm') - return this._onAnnounceUpdate(params, peer, id) // treat as an update - } - - if (params.left === 0) this.complete += 1 - else this.incomplete += 1 - peer = this.peers.set(id, { - type: params.type, - complete: params.left === 0, - peerId: params.peer_id, // as hex - ip: params.ip, - port: params.port, - socket: params.socket // only websocket - }) -} - -Swarm.prototype._onAnnounceStopped = function (params, peer, id) { - if (!peer) { - debug('unexpected `stopped` event from peer that is not in swarm') - return // do nothing - } - - if (peer.complete) this.complete -= 1 - else this.incomplete -= 1 - - // If it's a websocket, remove this swarm's infohash from the list of active - // swarms that this peer is participating in. - if (peer.socket && !peer.socket.destroyed) { - var index = peer.socket.infoHashes.indexOf(this.infoHash) - arrayRemove(peer.socket.infoHashes, index) - } - - this.peers.remove(id) -} - -Swarm.prototype._onAnnounceCompleted = function (params, peer, id) { - if (!peer) { - debug('unexpected `completed` event from peer that is not in swarm') - return this._onAnnounceStarted(params, peer, id) // treat as a start - } - if (peer.complete) { - debug('unexpected `completed` event from peer that is already completed') - return this._onAnnounceUpdate(params, peer, id) // treat as an update - } - - this.complete += 1 - this.incomplete -= 1 - peer.complete = true - this.peers.set(id, peer) -} - -Swarm.prototype._onAnnounceUpdate = function (params, peer, id) { - if (!peer) { - debug('unexpected `update` event from peer that is not in swarm') - return this._onAnnounceStarted(params, peer, id) // treat as a start - } - - if (!peer.complete && params.left === 0) { - this.complete += 1 - this.incomplete -= 1 - peer.complete = true - } - this.peers.set(id, peer) -} - -Swarm.prototype._getPeers = function (numwant, ownPeerId, isWebRTC) { - var peers = [] - var ite = randomIterate(this.peers.keys) - var peerId - while ((peerId = ite()) && peers.length < numwant) { - // Don't mark the peer as most recently used on announce - var peer = this.peers.peek(peerId) - if (!peer) continue - if (isWebRTC && peer.peerId === ownPeerId) continue // don't send peer to itself - if ((isWebRTC && peer.type !== 'ws') || (!isWebRTC && peer.type === 'ws')) continue // send proper peer type - peers.push(peer) - } - return peers -} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..f1f69d44 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,282 @@ +{ + "name": "bittorrent-tracker", + "version": "2.6.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "addr-to-ip-port": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/addr-to-ip-port/-/addr-to-ip-port-1.4.3.tgz", + "integrity": "sha512-+KHTG8KSAFdKYmLNZp3VnKj94AZ94gDdu2ipAwxNuMmN9vpf5hdsQgk1hNXFqQOXfd+BMHokyDa1GwDAlGAtGQ==" + }, + "bencode": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/bencode/-/bencode-0.6.0.tgz", + "integrity": "sha1-BNYZDh10Z8Vqlp4alNFmgHbqwFA=" + }, + "bn.js": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-0.13.3.tgz", + "integrity": "sha1-NX2DLbWqURcB+snQNpjseS3artA=" + }, + "buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" + }, + "compact2string": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/compact2string/-/compact2string-1.4.0.tgz", + "integrity": "sha1-qZzZbqAAUlaEsmloOuIiLW7qe0k=", + "requires": { + "ipaddr.js": "1.6.0" + } + }, + "concat-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.1.tgz", + "integrity": "sha512-gslSSJx03QKa59cIKqeJO9HQ/WZMotvYJCuaUULrLpjj8oG40kV2Z+gz82pVxlTkOADi4PJxQPPfhl1ELYrrXw==", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.5", + "typedarray": "0.0.6" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.5.tgz", + "integrity": "sha1-9yQSF0MPmd7EwrRz6rkiKOh0wqw=", + "requires": { + "ms": "2.0.0" + } + }, + "deep-equal": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", + "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=", + "dev": true + }, + "defined": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", + "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=", + "dev": true + }, + "extend.js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/extend.js/-/extend.js-0.0.1.tgz", + "integrity": "sha1-gUxFP0EgGhHgXqCApKcfAWmUrQs=" + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "minimatch": "0.3.0" + } + }, + "has": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/has/-/has-0.0.1.tgz", + "integrity": "sha1-ZmOcFOr1WfE52ivg5DiRDvP9Wxs=", + "dev": true + }, + "hat": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", + "integrity": "sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ip": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ip/-/ip-0.3.3.tgz", + "integrity": "sha1-jugwnpLwsEDSh/cu+soaIXAtP7Q=" + }, + "ipaddr.js": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "magnet-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/magnet-uri/-/magnet-uri-2.0.1.tgz", + "integrity": "sha1-0zHT3804NlZa3g/DyjFeOSF7sgk=", + "dev": true, + "requires": { + "thirty-two": "0.0.2" + } + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2.7.3", + "sigmund": "1.0.1" + } + }, + "mkdirp": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.0.7.tgz", + "integrity": "sha1-2JtPDkw+XlylQjWTFnXglP4aUHI=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "object-inspect": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.5.0.tgz", + "integrity": "sha512-UmOFbHbwvv+XHj7BerrhVq+knjceBdkvU5AriwLMvhv2qi+e7DJzxfBeFpILEjVzCp+xA+W/pIf06RGPWlZNfw==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1.0.2" + } + }, + "parse-torrent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/parse-torrent/-/parse-torrent-1.7.0.tgz", + "integrity": "sha1-9k0J0u+gX3qeiTeNBBpj2coeR9I=", + "dev": true, + "requires": { + "bencode": "0.6.0" + } + }, + "portfinder": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-0.2.1.tgz", + "integrity": "sha1-srmwFk+eF/o6nH2yME0KdRQMca0=", + "requires": { + "mkdirp": "0.0.7" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "readable-stream": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", + "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "2.0.0", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "dev": true, + "requires": { + "through": "2.3.8" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, + "string2compact": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/string2compact/-/string2compact-1.2.2.tgz", + "integrity": "sha1-Qgs6nuHEaFSRm0oq6sZcQ/pQWXs=", + "requires": { + "addr-to-ip-port": "1.4.3", + "ipaddr.js": "1.6.0" + } + }, + "tape": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/tape/-/tape-2.14.1.tgz", + "integrity": "sha1-j26y3QO3UKoFao3gGlRF5Z98nM4=", + "dev": true, + "requires": { + "deep-equal": "0.2.2", + "defined": "0.0.0", + "glob": "3.2.11", + "has": "0.0.1", + "inherits": "2.0.3", + "object-inspect": "1.5.0", + "resumer": "0.0.0", + "through": "2.3.8" + } + }, + "thirty-two": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-0.0.2.tgz", + "integrity": "sha1-QlPinYywWPBIAmfFaYwOSSflS2o=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/package.json b/package.json index 731e175e..2d1cf6e8 100644 --- a/package.json +++ b/package.json @@ -1,77 +1,56 @@ { "name": "bittorrent-tracker", "description": "Simple, robust, BitTorrent tracker (client & server) implementation", - "version": "9.7.0", + "version": "2.6.0", "author": { - "name": "WebTorrent, LLC", - "email": "feross@webtorrent.io", - "url": "https://webtorrent.io" - }, - "bin": { - "bittorrent-tracker": "./bin/cmd.js" + "name": "Feross Aboukhadijeh", + "email": "feross@feross.org", + "url": "http://feross.org/" }, "browser": { - "./lib/common-node.js": false, - "./lib/client/http-tracker.js": false, - "./lib/client/udp-tracker.js": false, "./server.js": false }, "bugs": { - "url": "https://github.com/webtorrent/bittorrent-tracker/issues" + "url": "https://github.com/feross/bittorrent-tracker/issues" }, "dependencies": { - "bencode": "^2.0.0", - "bittorrent-peerid": "^1.0.2", - "bn.js": "^4.4.0", + "bencode": "^0.6.0", + "bn.js": "^0.13.2", + "buffer-equal": "0.0.1", "compact2string": "^1.2.0", - "debug": "^3.1.0", + "concat-stream": "^1.4.5", + "debug": "^1.0.0", + "extend.js": "0.0.1", + "hat": "0.0.3", "inherits": "^2.0.1", - "ip": "^1.0.1", - "lru": "^3.0.0", - "minimist": "^1.1.1", + "ip": "^0.3.0", "once": "^1.3.0", - "random-iterate": "^1.0.1", - "randombytes": "^2.0.3", - "run-parallel": "^1.1.2", - "run-series": "^1.0.2", - "safe-buffer": "^5.0.0", - "simple-get": "^2.0.0", - "simple-peer": "^9.0.0", - "simple-websocket": "^7.0.1", - "string2compact": "^1.1.1", - "uniq": "^1.0.1", - "unordered-array-remove": "^1.0.2", - "ws": "^5.0.0", - "xtend": "^4.0.0" + "portfinder": "^0.2.1", + "string2compact": "^1.1.1" }, "devDependencies": { - "electron-webrtc": "^0.3.0", - "magnet-uri": "^5.1.3", - "standard": "*", - "tape": "^4.0.0", - "webtorrent-fixtures": "^1.3.0" - }, - "optionalDependencies": { - "bufferutil": "^3.0.0" + "magnet-uri": "^2.0.1", + "parse-torrent": "^1.1.0", + "tape": "^2.13.3" }, + "homepage": "http://webtorrent.io", "keywords": [ - "bittorrent", - "p2p", - "peer", - "peer-to-peer", - "stream", "torrent", + "bittorrent", "tracker", - "wire" + "stream", + "peer", + "wire", + "p2p", + "peer-to-peer" ], "license": "MIT", "main": "index.js", "repository": { "type": "git", - "url": "git://github.com/webtorrent/bittorrent-tracker.git" + "url": "git://github.com/feross/bittorrent-tracker.git" }, "scripts": { - "update-authors": "./bin/update-authors.sh", - "test": "standard && tape test/*.js" + "test": "tape test/*.js" } } diff --git a/server.js b/server.js index f7a2a62f..7638aead 100644 --- a/server.js +++ b/server.js @@ -1,830 +1,525 @@ module.exports = Server -var Buffer = require('safe-buffer').Buffer var bencode = require('bencode') -var debug = require('debug')('bittorrent-tracker:server') +var bufferEqual = require('buffer-equal') +var common = require('./lib/common') +var debug = require('debug')('bittorrent-tracker') var dgram = require('dgram') var EventEmitter = require('events').EventEmitter var http = require('http') var inherits = require('inherits') -var peerid = require('bittorrent-peerid') -var series = require('run-series') +var ipLib = require('ip') +var portfinder = require('portfinder') var string2compact = require('string2compact') -var WebSocketServer = require('ws').Server -var common = require('./lib/common') -var Swarm = require('./lib/server/swarm') -var parseHttpRequest = require('./lib/server/parse-http') -var parseUdpRequest = require('./lib/server/parse-udp') -var parseWebSocketRequest = require('./lib/server/parse-websocket') +// Use random port above 1024 +portfinder.basePort = Math.floor(Math.random() * 60000) + 1025 + +var NUM_ANNOUNCE_PEERS = 50 +var MAX_ANNOUNCE_PEERS = 82 +var REMOVE_IPV6_RE = /^::ffff:/ inherits(Server, EventEmitter) /** - * BitTorrent tracker server. + * A BitTorrent tracker server. * - * HTTP service which responds to GET requests from torrent clients. Requests include - * metrics from clients that help the tracker keep overall statistics about the torrent. - * Responses include a peer list that helps the client participate in the torrent. + * A "BitTorrent tracker" is an HTTP service which responds to GET requests from + * BitTorrent clients. The requests include metrics from clients that help the tracker + * keep overall statistics about the torrent. The response includes a peer list that + * helps the client participate in the torrent. * - * @param {Object} opts options object - * @param {Number} opts.interval tell clients to announce on this interval (ms) - * @param {Number} opts.trustProxy trust 'x-forwarded-for' header from reverse proxy - * @param {boolean} opts.http start an http server? (default: true) - * @param {boolean} opts.udp start a udp server? (default: true) - * @param {boolean} opts.ws start a websocket server? (default: true) - * @param {boolean} opts.stats enable web-based statistics? (default: true) - * @param {function} opts.filter black/whitelist fn for disallowing/allowing torrents + * @param {Object} opts options + * @param {Number} opts.interval interval in ms that clients should announce on + * @param {Number} opts.trustProxy Trust 'x-forwarded-for' header from reverse proxy + * @param {boolean} opts.http Start an http server? (default: true) + * @param {boolean} opts.udp Start a udp server? (default: true) */ function Server (opts) { var self = this if (!(self instanceof Server)) return new Server(opts) EventEmitter.call(self) - if (!opts) opts = {} + opts = opts || {} - debug('new server %s', JSON.stringify(opts)) - - self.intervalMs = opts.interval - ? opts.interval - : 10 * 60 * 1000 // 10 min + self._intervalMs = opts.interval + ? opts.interval / 1000 + : 10 * 60 // 10 min (in secs) self._trustProxy = !!opts.trustProxy - if (typeof opts.filter === 'function') self._filter = opts.filter - - self.peersCacheLength = opts.peersCacheLength - self.peersCacheTtl = opts.peersCacheTtl - self._listenCalled = false self.listening = false - self.destroyed = false + self.port = null self.torrents = {} - self.http = null - self.udp4 = null - self.udp6 = null - self.ws = null - - // start an http tracker unless the user explictly says no + // default to starting an http server unless the user explictly says no if (opts.http !== false) { - self.http = http.createServer() - self.http.on('error', function (err) { self._onError(err) }) - self.http.on('listening', onListening) - - // Add default http request handler on next tick to give user the chance to add - // their own handler first. Handle requests untouched by user's handler. - process.nextTick(function () { - self.http.on('request', function (req, res) { - if (res.headersSent) return - self.onHttpRequest(req, res) - }) - }) + self._httpServer = http.createServer() + self._httpServer.on('request', self._onHttpRequest.bind(self)) + self._httpServer.on('error', self._onError.bind(self)) + self._httpServer.on('listening', onListening) } - // start a udp tracker unless the user explicitly says no + // default to starting a udp server unless the user explicitly says no if (opts.udp !== false) { - var isNode10 = /^v0.10./.test(process.version) - - self.udp4 = self.udp = dgram.createSocket( - isNode10 ? 'udp4' : { type: 'udp4', reuseAddr: true } - ) - self.udp4.on('message', function (msg, rinfo) { self.onUdpRequest(msg, rinfo) }) - self.udp4.on('error', function (err) { self._onError(err) }) - self.udp4.on('listening', onListening) - - self.udp6 = dgram.createSocket( - isNode10 ? 'udp6' : { type: 'udp6', reuseAddr: true } - ) - self.udp6.on('message', function (msg, rinfo) { self.onUdpRequest(msg, rinfo) }) - self.udp6.on('error', function (err) { self._onError(err) }) - self.udp6.on('listening', onListening) + self._udpServer = dgram.createSocket('udp4') + self._udpServer.on('message', self._onUdpRequest.bind(self)) + self._udpServer.on('error', self._onError.bind(self)) + self._udpServer.on('listening', onListening) } - // start a websocket tracker (for WebTorrent) unless the user explicitly says no - if (opts.ws !== false) { - if (!self.http) { - self.http = http.createServer() - self.http.on('error', function (err) { self._onError(err) }) - self.http.on('listening', onListening) - - // Add default http request handler on next tick to give user the chance to add - // their own handler first. Handle requests untouched by user's handler. - process.nextTick(function () { - self.http.on('request', function (req, res) { - if (res.headersSent) return - // For websocket trackers, we only need to handle the UPGRADE http method. - // Return 404 for all other request types. - res.statusCode = 404 - res.end('404 Not Found') - }) - }) - } - self.ws = new WebSocketServer({ - server: self.http, - perMessageDeflate: false, - clientTracking: false - }) - self.ws.address = function () { - return self.http.address() - } - self.ws.on('error', function (err) { self._onError(err) }) - self.ws.on('connection', function (socket, req) { - // Note: socket.upgradeReq was removed in ws@3.0.0, so re-add it. - // https://github.com/websockets/ws/pull/1099 - socket.upgradeReq = req - self.onWebSocketConnection(socket) - }) - } - - if (opts.stats !== false) { - if (!self.http) { - self.http = http.createServer() - self.http.on('error', function (err) { self._onError(err) }) - self.http.on('listening', onListening) - } - - // Http handler for '/stats' route - self.http.on('request', function (req, res) { - if (res.headersSent) return - - var infoHashes = Object.keys(self.torrents) - var activeTorrents = 0 - var allPeers = {} - - function countPeers (filterFunction) { - var count = 0 - var key - - for (key in allPeers) { - if (allPeers.hasOwnProperty(key) && filterFunction(allPeers[key])) { - count++ - } - } - - return count - } - - function groupByClient () { - var clients = {} - for (var key in allPeers) { - if (allPeers.hasOwnProperty(key)) { - var peer = allPeers[key] - - if (!clients[peer.client.client]) { - clients[peer.client.client] = {} - } - var client = clients[peer.client.client] - // If the client is not known show 8 chars from peerId as version - var version = peer.client.version || new Buffer(peer.peerId, 'hex').toString().substring(0, 8) - if (!client[version]) { - client[version] = 0 - } - client[version]++ - } - } - return clients - } - - function printClients (clients) { - var html = '
    \n' - for (var name in clients) { - if (clients.hasOwnProperty(name)) { - var client = clients[name] - for (var version in client) { - if (client.hasOwnProperty(version)) { - html += '
  • ' + name + ' ' + version + ' : ' + client[version] + '
  • \n' - } - } - } - } - html += '
' - return html - } - - if (req.method === 'GET' && (req.url === '/stats' || req.url === '/stats.json')) { - infoHashes.forEach(function (infoHash) { - var peers = self.torrents[infoHash].peers - var keys = peers.keys - if (keys.length > 0) activeTorrents++ - - keys.forEach(function (peerId) { - // Don't mark the peer as most recently used for stats - var peer = peers.peek(peerId) - if (peer == null) return // peers.peek() can evict the peer - - if (!allPeers.hasOwnProperty(peerId)) { - allPeers[peerId] = { - ipv4: false, - ipv6: false, - seeder: false, - leecher: false - } - } - - if (peer.ip.indexOf(':') >= 0) { - allPeers[peerId].ipv6 = true - } else { - allPeers[peerId].ipv4 = true - } - - if (peer.complete) { - allPeers[peerId].seeder = true - } else { - allPeers[peerId].leecher = true - } - - allPeers[peerId].peerId = peer.peerId - allPeers[peerId].client = peerid(peer.peerId) - }) - }) - - var isSeederOnly = function (peer) { return peer.seeder && peer.leecher === false } - var isLeecherOnly = function (peer) { return peer.leecher && peer.seeder === false } - var isSeederAndLeecher = function (peer) { return peer.seeder && peer.leecher } - var isIPv4 = function (peer) { return peer.ipv4 } - var isIPv6 = function (peer) { return peer.ipv6 } - - var stats = { - torrents: infoHashes.length, - activeTorrents: activeTorrents, - peersAll: Object.keys(allPeers).length, - peersSeederOnly: countPeers(isSeederOnly), - peersLeecherOnly: countPeers(isLeecherOnly), - peersSeederAndLeecher: countPeers(isSeederAndLeecher), - peersIPv4: countPeers(isIPv4), - peersIPv6: countPeers(isIPv6), - clients: groupByClient() - } - - if (req.url === '/stats.json' || req.headers['accept'] === 'application/json') { - res.write(JSON.stringify(stats)) - res.end() - } else if (req.url === '/stats') { - res.end('

' + stats.torrents + ' torrents (' + stats.activeTorrents + ' active)

\n' + - '

Connected Peers: ' + stats.peersAll + '

\n' + - '

Peers Seeding Only: ' + stats.peersSeederOnly + '

\n' + - '

Peers Leeching Only: ' + stats.peersLeecherOnly + '

\n' + - '

Peers Seeding & Leeching: ' + stats.peersSeederAndLeecher + '

\n' + - '

IPv4 Peers: ' + stats.peersIPv4 + '

\n' + - '

IPv6 Peers: ' + stats.peersIPv6 + '

\n' + - '

Clients:

\n' + - printClients(stats.clients) - ) - } - } - }) - } - - var num = !!self.http + !!self.udp4 + !!self.udp6 + var num = !!self._httpServer + !!self._udpServer function onListening () { num -= 1 if (num === 0) { self.listening = true - debug('listening') - self.emit('listening') + self.emit('listening', self.port) } } } -Server.Swarm = Swarm - Server.prototype._onError = function (err) { var self = this self.emit('error', err) } -Server.prototype.listen = function (/* port, hostname, onlistening */) { +Server.prototype.listen = function (port, onlistening) { var self = this - - if (self._listenCalled || self.listening) throw new Error('server already listening') - self._listenCalled = true - - var lastArg = arguments[arguments.length - 1] - if (typeof lastArg === 'function') self.once('listening', lastArg) - - var port = toNumber(arguments[0]) || arguments[0] || 0 - var hostname = typeof arguments[1] !== 'function' ? arguments[1] : undefined - - debug('listen (port: %o hostname: %o)', port, hostname) - - function isObject (obj) { - return typeof obj === 'object' && obj !== null + if (typeof port === 'function') { + onlistening = port + port = undefined + } + if (self.listening) throw new Error('server already listening') + if (onlistening) self.once('listening', onlistening) + + function onPort (err, port) { + if (err) return self.emit('error', err) + self.port = port + self._httpServer && self._httpServer.listen(port.http || port) + self._udpServer && self._udpServer.bind(port.udp || port) } - var httpPort = isObject(port) ? (port.http || 0) : port - var udpPort = isObject(port) ? (port.udp || 0) : port - - // binding to :: only receives IPv4 connections if the bindv6only sysctl is set 0, - // which is the default on many operating systems - var httpHostname = isObject(hostname) ? hostname.http : hostname - var udp4Hostname = isObject(hostname) ? hostname.udp : hostname - var udp6Hostname = isObject(hostname) ? hostname.udp6 : hostname - - if (self.http) self.http.listen(httpPort, httpHostname) - if (self.udp4) self.udp4.bind(udpPort, udp4Hostname) - if (self.udp6) self.udp6.bind(udpPort, udp6Hostname) + if (port) onPort(null, port) + else portfinder.getPort(onPort) } Server.prototype.close = function (cb) { var self = this - if (!cb) cb = noop - debug('close') - - self.listening = false - self.destroyed = true - - if (self.udp4) { - try { - self.udp4.close() - } catch (err) {} + cb = cb || function () {} + if (self._udpServer) { + self._udpServer.close() } - - if (self.udp6) { - try { - self.udp6.close() - } catch (err) {} - } - - if (self.ws) { - try { - self.ws.close() - } catch (err) {} + if (self._httpServer) { + self._httpServer.close(cb) + } else { + cb(null) } - - if (self.http) self.http.close(cb) - else cb(null) } -Server.prototype.createSwarm = function (infoHash, cb) { +Server.prototype.getSwarm = function (infoHash) { var self = this - if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') - - process.nextTick(function () { - var swarm = self.torrents[infoHash] = new Server.Swarm(infoHash, self) - cb(null, swarm) - }) + var binaryInfoHash = Buffer.isBuffer(infoHash) + ? infoHash.toString('binary') + : new Buffer(infoHash, 'hex').toString('binary') + return self._getSwarm(binaryInfoHash) } -Server.prototype.getSwarm = function (infoHash, cb) { +Server.prototype._getSwarm = function (binaryInfoHash) { var self = this - if (Buffer.isBuffer(infoHash)) infoHash = infoHash.toString('hex') - - process.nextTick(function () { - cb(null, self.torrents[infoHash]) - }) + var swarm = self.torrents[binaryInfoHash] + if (!swarm) { + swarm = self.torrents[binaryInfoHash] = { + complete: 0, + incomplete: 0, + peers: {} + } + } + return swarm } -Server.prototype.onHttpRequest = function (req, res, opts) { +Server.prototype._onHttpRequest = function (req, res) { var self = this - if (!opts) opts = {} - opts.trustProxy = opts.trustProxy || self._trustProxy - - var params - try { - params = parseHttpRequest(req, opts) - params.httpReq = req - params.httpRes = res - } catch (err) { - res.end(bencode.encode({ - 'failure reason': err.message - })) + var warning + var s = req.url.split('?') + var params = common.querystringParse(s[1]) + var response + if (s[0] === '/announce') { + var infoHash = typeof params.info_hash === 'string' && params.info_hash + var peerId = typeof params.peer_id === 'string' && common.binaryToUtf8(params.peer_id) + var port = Number(params.port) + + if (!infoHash) return error('invalid info_hash') + if (infoHash.length !== 20) return error('invalid info_hash') + if (!peerId) return error('invalid peer_id') + if (peerId.length !== 20) return error('invalid peer_id') + if (!port) return error('invalid port') + + var ip = self._trustProxy + ? req.headers['x-forwarded-for'] || req.connection.remoteAddress + : req.connection.remoteAddress.replace(REMOVE_IPV6_RE, '') // force ipv4 + var addr = ip + ':' + port + var swarm = self._getSwarm(infoHash) + var peer = swarm.peers[addr] + + var numWant = Math.min( + Number(params.numwant) || NUM_ANNOUNCE_PEERS, + MAX_ANNOUNCE_PEERS + ) - // even though it's an error for the client, it's just a warning for the server. - // don't crash the server because a client sent bad data :) - self.emit('warning', err) - return - } + switch (params.event) { + case 'started': + if (peer) { + warning = 'unexpected `started` event from peer that is already in swarm' + break + } - self._onRequest(params, function (err, response) { - if (err) { - self.emit('warning', err) - response = { - 'failure reason': err.message - } - } - if (self.destroyed) return res.end() + if (Number(params.left) === 0) { + swarm.complete += 1 + } else { + swarm.incomplete += 1 + } - delete response.action // only needed for UDP encoding - res.end(bencode.encode(response)) + swarm.peers[addr] = { + ip: ip, + port: port, + peerId: peerId + } + self.emit('start', addr) + break - if (params.action === common.ACTIONS.ANNOUNCE) { - self.emit(common.EVENT_NAMES[params.event], params.addr, params) - } - }) -} + case 'stopped': + if (!peer) { + warning = 'unexpected `stopped` event from peer that is not in swarm' + break + } -Server.prototype.onUdpRequest = function (msg, rinfo) { - var self = this + if (peer.complete) { + swarm.complete -= 1 + } else { + swarm.incomplete -= 1 + } - var params - try { - params = parseUdpRequest(msg, rinfo) - } catch (err) { - self.emit('warning', err) - // Do not reply for parsing errors - return - } + swarm.peers[addr] = null + self.emit('stop', addr) + break - self._onRequest(params, function (err, response) { - if (err) { - self.emit('warning', err) - response = { - action: common.ACTIONS.ERROR, - 'failure reason': err.message - } + case 'completed': + if (!peer) { + warning = 'unexpected `completed` event from peer that is not in swarm' + break + } + if (peer.complete) { + warning = 'unexpected `completed` event from peer that is already marked as completed' + break + } + + swarm.complete += 1 + swarm.incomplete -= 1 + + peer.complete = true + self.emit('complete', addr) + break + + case '': // update + case undefined: + if (!peer) { + warning = 'unexpected `update` event from peer that is not in swarm' + break + } + + self.emit('update', addr) + break + + default: + return error('invalid event') // early return } - if (self.destroyed) return - response.transactionId = params.transactionId - response.connectionId = params.connectionId + // send peers + var peers = Number(params.compact) === 1 + ? self._getPeersCompact(swarm, numWant) + : self._getPeers(swarm, numWant) - var buf = makeUdpPacket(response) + response = { + complete: swarm.complete, + incomplete: swarm.incomplete, + peers: peers, + interval: self._intervalMs + } - try { - var udp = (rinfo.family === 'IPv4') ? self.udp4 : self.udp6 - udp.send(buf, 0, buf.length, rinfo.port, rinfo.address) - } catch (err) { - self.emit('warning', err) + if (warning) { + response['warning message'] = warning } + res.end(bencode.encode(response)) + debug('sent response %s', response) + + } else if (s[0] === '/scrape') { // unofficial scrape message + if (typeof params.info_hash === 'string') { + params.info_hash = [ params.info_hash ] + } else if (params.info_hash == null) { + // if info_hash param is omitted, stats for all torrents are returned + params.info_hash = Object.keys(self.torrents) + } + + if (!Array.isArray(params.info_hash)) return error('invalid info_hash') - if (params.action === common.ACTIONS.ANNOUNCE) { - self.emit(common.EVENT_NAMES[params.event], params.addr, params) + response = { + files: {}, + flags: { + min_request_interval: self._intervalMs + } } - }) -} -Server.prototype.onWebSocketConnection = function (socket, opts) { - var self = this - if (!opts) opts = {} - opts.trustProxy = opts.trustProxy || self._trustProxy + params.info_hash.some(function (infoHash) { + if (infoHash.length !== 20) { + error('invalid info_hash') + return true // early return + } - socket.peerId = null // as hex - socket.infoHashes = [] // swarms that this socket is participating in - socket.onSend = function (err) { - self._onWebSocketSend(socket, err) - } + var swarm = self._getSwarm(infoHash) - socket.onMessageBound = function (params) { - self._onWebSocketRequest(socket, opts, params) - } - socket.on('message', socket.onMessageBound) + response.files[infoHash] = { + complete: swarm.complete, + incomplete: swarm.incomplete, + downloaded: swarm.complete // TODO: this only provides a lower-bound + } + }) - socket.onErrorBound = function (err) { - self._onWebSocketError(socket, err) + res.end(bencode.encode(response)) + debug('sent response %s', response) + + } else { + error('only /announce and /scrape are valid endpoints') } - socket.on('error', socket.onErrorBound) - socket.onCloseBound = function () { - self._onWebSocketClose(socket) + function error (message) { + debug('sent error %s', message) + res.end(bencode.encode({ + 'failure reason': message + })) + + // even though it's an error for the client, it's just a warning for the server. + // don't crash the server because a client sent bad data :) + self.emit('warning', new Error(message)) } - socket.on('close', socket.onCloseBound) } -Server.prototype._onWebSocketRequest = function (socket, opts, params) { +Server.prototype._onUdpRequest = function (msg, rinfo) { var self = this - try { - params = parseWebSocketRequest(socket, opts, params) - } catch (err) { - socket.send(JSON.stringify({ - 'failure reason': err.message - }), socket.onSend) + if (msg.length < 16) { + return error('received packet is too short') + } - // even though it's an error for the client, it's just a warning for the server. - // don't crash the server because a client sent bad data :) - self.emit('warning', err) - return + if (rinfo.family !== 'IPv4') { + return error('udp tracker does not support IPv6') } - if (!socket.peerId) socket.peerId = params.peer_id // as hex + var connectionId = msg.slice(0, 8) // 64-bit + var action = msg.readUInt32BE(8) + var transactionId = msg.readUInt32BE(12) - self._onRequest(params, function (err, response) { - if (self.destroyed || socket.destroyed) return - if (err) { - socket.send(JSON.stringify({ - action: params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape', - 'failure reason': err.message, - info_hash: common.hexToBinary(params.info_hash) - }), socket.onSend) + if (!bufferEqual(connectionId, common.CONNECTION_ID)) { + return error('received packet with invalid connection id') + } - self.emit('warning', err) - return + var socket = dgram.createSocket('udp4') + + var infoHash, swarm + if (action === common.ACTIONS.CONNECT) { + send(Buffer.concat([ + common.toUInt32(common.ACTIONS.CONNECT), + common.toUInt32(transactionId), + connectionId + ])) + } else if (action === common.ACTIONS.ANNOUNCE) { + infoHash = msg.slice(16, 36).toString('binary') // 20 bytes + var peerId = msg.slice(36, 56).toString('utf8') // 20 bytes + var downloaded = fromUInt64(msg.slice(56, 64)) // TODO: track this? + var left = fromUInt64(msg.slice(64, 72)) + var uploaded = fromUInt64(msg.slice(72, 80)) // TODO: track this? + var event = msg.readUInt32BE(80) + var ip = msg.readUInt32BE(84) // optional + var key = msg.readUInt32BE(88) // TODO: what is this for? + var numWant = msg.readUInt32BE(92) // optional + var port = msg.readUInt16BE(96) // optional + + if (ip) { + ip = ipLib.toString(ip) + } else { + ip = rinfo.address } - response.action = params.action === common.ACTIONS.ANNOUNCE ? 'announce' : 'scrape' + if (!port) { + port = rinfo.port + } - var peers - if (response.action === 'announce') { - peers = response.peers - delete response.peers + var addr = ip + ':' + port - if (socket.infoHashes.indexOf(params.info_hash) === -1) { - socket.infoHashes.push(params.info_hash) - } + swarm = self._getSwarm(infoHash) + var peer = swarm.peers[addr] - response.info_hash = common.hexToBinary(params.info_hash) + // never send more than MAX_ANNOUNCE_PEERS or else the UDP packet will get bigger than + // 512 bytes which is not safe + numWant = Math.min(numWant || NUM_ANNOUNCE_PEERS, MAX_ANNOUNCE_PEERS) - // WebSocket tracker should have a shorter interval – default: 2 minutes - response.interval = Math.ceil(self.intervalMs / 1000 / 5) - } + var warning + switch (event) { + case common.EVENTS.started: + if (peer) { + warning = 'unexpected `started` event from peer that is already in swarm' + break + } - // Skip sending update back for 'answer' announce messages – not needed - if (!params.answer) { - socket.send(JSON.stringify(response), socket.onSend) - debug('sent response %s to %s', JSON.stringify(response), params.peer_id) - } + if (left === 0) { + swarm.complete += 1 + } else { + swarm.incomplete += 1 + } - if (Array.isArray(params.offers)) { - debug('got %s offers from %s', params.offers.length, params.peer_id) - debug('got %s peers from swarm %s', peers.length, params.info_hash) - peers.forEach(function (peer, i) { - peer.socket.send(JSON.stringify({ - action: 'announce', - offer: params.offers[i].offer, - offer_id: params.offers[i].offer_id, - peer_id: common.hexToBinary(params.peer_id), - info_hash: common.hexToBinary(params.info_hash) - }), peer.socket.onSend) - debug('sent offer to %s from %s', peer.peerId, params.peer_id) - }) - } + swarm.peers[addr] = { + ip: ip, + port: port, + peerId: peerId + } + self.emit('start', addr) + break - if (params.answer) { - debug('got answer %s from %s', JSON.stringify(params.answer), params.peer_id) + case common.EVENTS.stopped: + if (!peer) { + warning = 'unexpected `stopped` event from peer that is not in swarm' + break + } + + if (peer.complete) { + swarm.complete -= 1 + } else { + swarm.incomplete -= 1 + } + + swarm.peers[addr] = null + self.emit('stop', addr) + break - self.getSwarm(params.info_hash, function (err, swarm) { - if (self.destroyed) return - if (err) return self.emit('warning', err) - if (!swarm) { - return self.emit('warning', new Error('no swarm with that `info_hash`')) + case common.EVENTS.completed: + if (!peer) { + warning = 'unexpected `completed` event from peer that is not in swarm' + break } - // Mark the destination peer as recently used in cache - var toPeer = swarm.peers.get(params.to_peer_id) - if (!toPeer) { - return self.emit('warning', new Error('no peer with that `to_peer_id`')) + if (peer.complete) { + warning = 'unexpected `completed` event from peer that is already marked as completed' + break } - toPeer.socket.send(JSON.stringify({ - action: 'announce', - answer: params.answer, - offer_id: params.offer_id, - peer_id: common.hexToBinary(params.peer_id), - info_hash: common.hexToBinary(params.info_hash) - }), toPeer.socket.onSend) - debug('sent answer to %s from %s', toPeer.peerId, params.peer_id) - - done() - }) - } else { - done() - } + swarm.complete += 1 + swarm.incomplete -= 1 - function done () { - // emit event once the announce is fully "processed" - if (params.action === common.ACTIONS.ANNOUNCE) { - self.emit(common.EVENT_NAMES[params.event], params.peer_id, params) - } - } - }) -} + peer.complete = true + self.emit('complete', addr) + break -Server.prototype._onWebSocketSend = function (socket, err) { - var self = this - if (err) self._onWebSocketError(socket, err) -} + case common.EVENTS.update: // update + if (!peer) { + warning = 'unexpected `update` event from peer that is not in swarm' + break + } -Server.prototype._onWebSocketClose = function (socket) { - var self = this - debug('websocket close %s', socket.peerId) - socket.destroyed = true - - if (socket.peerId) { - socket.infoHashes.slice(0).forEach(function (infoHash) { - var swarm = self.torrents[infoHash] - if (swarm) { - swarm.announce({ - type: 'ws', - event: 'stopped', - numwant: 0, - peer_id: socket.peerId - }, noop) - } - }) - } + self.emit('update', addr) + break - // ignore all future errors - socket.onSend = noop - socket.on('error', noop) + default: + return error('invalid event') // early return + } - socket.peerId = null - socket.infoHashes = null + // send peers + var peers = self._getPeersCompact(swarm, numWant) - if (typeof socket.onMessageBound === 'function') { - socket.removeListener('message', socket.onMessageBound) - } - socket.onMessageBound = null + send(Buffer.concat([ + common.toUInt32(common.ACTIONS.ANNOUNCE), + common.toUInt32(transactionId), + common.toUInt32(self._intervalMs), + common.toUInt32(swarm.incomplete), + common.toUInt32(swarm.complete), + peers + ])) - if (typeof socket.onErrorBound === 'function') { - socket.removeListener('error', socket.onErrorBound) - } - socket.onErrorBound = null + } else if (action === common.ACTIONS.SCRAPE) { // scrape message + infoHash = msg.slice(16, 36).toString('binary') // 20 bytes - if (typeof socket.onCloseBound === 'function') { - socket.removeListener('close', socket.onCloseBound) - } - socket.onCloseBound = null -} + // TODO: support multiple info_hash scrape + if (msg.length > 36) { + error('multiple info_hash scrape not supported') + } -Server.prototype._onWebSocketError = function (socket, err) { - var self = this - debug('websocket error %s', err.message || err) - self.emit('warning', err) - self._onWebSocketClose(socket) -} + swarm = self._getSwarm(infoHash) -Server.prototype._onRequest = function (params, cb) { - var self = this - if (params && params.action === common.ACTIONS.CONNECT) { - cb(null, { action: common.ACTIONS.CONNECT }) - } else if (params && params.action === common.ACTIONS.ANNOUNCE) { - self._onAnnounce(params, cb) - } else if (params && params.action === common.ACTIONS.SCRAPE) { - self._onScrape(params, cb) - } else { - cb(new Error('Invalid action')) + send(Buffer.concat([ + common.toUInt32(common.ACTIONS.SCRAPE), + common.toUInt32(transactionId), + common.toUInt32(swarm.complete), + common.toUInt32(swarm.complete), // TODO: this only provides a lower-bound + common.toUInt32(swarm.incomplete) + ])) } -} -Server.prototype._onAnnounce = function (params, cb) { - var self = this + function send (buf) { + debug('sent response %s', buf.toString('hex')) + socket.send(buf, 0, buf.length, rinfo.port, rinfo.address, function () { + try { + socket.close() + } catch (err) {} + }) + } - self.getSwarm(params.info_hash, function (err, swarm) { - if (err) return cb(err) - if (swarm) { - announce(swarm) - } else { - createSwarm() - } - }) - - function createSwarm () { - if (self._filter) { - self._filter(params.info_hash, params, function (err) { - // Precense of err means that this info_hash is disallowed - if (err) { - cb(err) - } else { - self.createSwarm(params.info_hash, function (err, swarm) { - if (err) return cb(err) - announce(swarm) - }) - } - }) - } else { - self.createSwarm(params.info_hash, function (err, swarm) { - if (err) return cb(err) - announce(swarm) - }) - } + function error (message) { + debug('sent error %s', message) + send(Buffer.concat([ + common.toUInt32(common.ACTIONS.ERROR), + common.toUInt32(transactionId || 0), + new Buffer(message, 'utf8') + ])) + self.emit('warning', new Error(message)) } +} - function announce (swarm) { - if (!params.event || params.event === 'empty') params.event = 'update' - swarm.announce(params, function (err, response) { - if (err) return cb(err) - - if (!response.action) response.action = common.ACTIONS.ANNOUNCE - if (!response.interval) response.interval = Math.ceil(self.intervalMs / 1000) - - if (params.compact === 1) { - var peers = response.peers - - // Find IPv4 peers - response.peers = string2compact(peers.filter(function (peer) { - return common.IPV4_RE.test(peer.ip) - }).map(function (peer) { - return peer.ip + ':' + peer.port - })) - // Find IPv6 peers - response.peers6 = string2compact(peers.filter(function (peer) { - return common.IPV6_RE.test(peer.ip) - }).map(function (peer) { - return '[' + peer.ip + ']:' + peer.port - })) - } else if (params.compact === 0) { - // IPv6 peers are not separate for non-compact responses - response.peers = response.peers.map(function (peer) { - return { - 'peer id': common.hexToBinary(peer.peerId), - ip: peer.ip, - port: peer.port - } - }) - } // else, return full peer objects (used for websocket responses) - - cb(null, response) +Server.prototype._getPeers = function (swarm, numWant) { + var peers = [] + for (var peerId in swarm.peers) { + if (peers.length >= numWant) break + var peer = swarm.peers[peerId] + if (!peer) continue // ignore null values + peers.push({ + 'peer id': peer.peerId, + ip: peer.ip, + port: peer.port }) } + return peers } -Server.prototype._onScrape = function (params, cb) { - var self = this +Server.prototype._getPeersCompact = function (swarm, numWant) { + var peers = [] - if (params.info_hash == null) { - // if info_hash param is omitted, stats for all torrents are returned - // TODO: make this configurable! - params.info_hash = Object.keys(self.torrents) + for (var peerId in swarm.peers) { + if (peers.length >= numWant) break + var peer = swarm.peers[peerId] + if (!peer) continue // ignore null values + peers.push(peer.ip + ':' + peer.port) } - series(params.info_hash.map(function (infoHash) { - return function (cb) { - self.getSwarm(infoHash, function (err, swarm) { - if (err) return cb(err) - if (swarm) { - swarm.scrape(params, function (err, scrapeInfo) { - if (err) return cb(err) - cb(null, { - infoHash: infoHash, - complete: (scrapeInfo && scrapeInfo.complete) || 0, - incomplete: (scrapeInfo && scrapeInfo.incomplete) || 0 - }) - }) - } else { - cb(null, { infoHash: infoHash, complete: 0, incomplete: 0 }) - } - }) - } - }), function (err, results) { - if (err) return cb(err) - - var response = { - action: common.ACTIONS.SCRAPE, - files: {}, - flags: { min_request_interval: Math.ceil(self.intervalMs / 1000) } - } + return string2compact(peers) +} - results.forEach(function (result) { - response.files[common.hexToBinary(result.infoHash)] = { - complete: result.complete || 0, - incomplete: result.incomplete || 0, - downloaded: result.complete || 0 // TODO: this only provides a lower-bound - } - }) +// HELPER FUNCTIONS - cb(null, response) - }) -} +var TWO_PWR_32 = (1 << 16) * 2 -function makeUdpPacket (params) { - var packet - switch (params.action) { - case common.ACTIONS.CONNECT: - packet = Buffer.concat([ - common.toUInt32(common.ACTIONS.CONNECT), - common.toUInt32(params.transactionId), - params.connectionId - ]) - break - case common.ACTIONS.ANNOUNCE: - packet = Buffer.concat([ - common.toUInt32(common.ACTIONS.ANNOUNCE), - common.toUInt32(params.transactionId), - common.toUInt32(params.interval), - common.toUInt32(params.incomplete), - common.toUInt32(params.complete), - params.peers - ]) - break - case common.ACTIONS.SCRAPE: - var scrapeResponse = [ - common.toUInt32(common.ACTIONS.SCRAPE), - common.toUInt32(params.transactionId) - ] - for (var infoHash in params.files) { - var file = params.files[infoHash] - scrapeResponse.push( - common.toUInt32(file.complete), - common.toUInt32(file.downloaded), // TODO: this only provides a lower-bound - common.toUInt32(file.incomplete) - ) - } - packet = Buffer.concat(scrapeResponse) - break - case common.ACTIONS.ERROR: - packet = Buffer.concat([ - common.toUInt32(common.ACTIONS.ERROR), - common.toUInt32(params.transactionId || 0), - Buffer.from(String(params['failure reason'])) - ]) - break - default: - throw new Error('Action not implemented: ' + params.action) - } - return packet -} +/** + * Return the closest floating-point representation to the buffer value. Precision will be + * lost for big numbers. + */ +function fromUInt64 (buf) { + var high = buf.readUInt32BE(0) | 0 // force + var low = buf.readUInt32BE(4) | 0 + var lowUnsigned = (low >= 0) ? low : TWO_PWR_32 + low -function toNumber (x) { - x = Number(x) - return x >= 0 ? x : false + return high * TWO_PWR_32 + lowUnsigned } - -function noop () {} diff --git a/test/client-large-torrent.js b/test/client-large-torrent.js index 7f9f2497..f5556c27 100644 --- a/test/client-large-torrent.js +++ b/test/client-large-torrent.js @@ -1,64 +1,59 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') -var common = require('./common') -var fixtures = require('webtorrent-fixtures') +var fs = require('fs') +var parseTorrent = require('parse-torrent') +var portfinder = require('portfinder') +var Server = require('../').Server var test = require('tape') -var peerId = Buffer.from('01234567890123456789') +var torrent = fs.readFileSync(__dirname + '/torrents/sintel-5gb.torrent') +var parsedTorrent = parseTorrent(torrent) +var peerId = new Buffer('01234567890123456789') -function testLargeTorrent (t, serverType) { - t.plan(9) +test('large torrent: client.start()', function (t) { + t.plan(6) - common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.sintel.parsedTorrent.infoHash, - peerId: peerId, - port: 6881, - announce: announceUrl, - wrtc: {} - }) + var server = new Server({ http: false }) - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) + server.on('error', function (err) { + t.fail(err.message) + }) - client.once('update', function (data) { - t.equal(data.announce, announceUrl) - t.equal(typeof data.complete, 'number') - t.equal(typeof data.incomplete, 'number') + server.on('warning', function (err) { + t.fail(err.message) + }) - client.update() + portfinder.getPort(function (err, port) { + t.error(err, 'found free port') + server.listen(port) - client.once('update', function (data) { - t.equal(data.announce, announceUrl) - t.equal(typeof data.complete, 'number') - t.equal(typeof data.incomplete, 'number') + // remove all tracker servers except a single UDP one, for now + parsedTorrent.announce = [ 'udp://127.0.0.1:' + port ] - client.stop() + var client = new Client(peerId, 6881, parsedTorrent) - client.once('update', function (data) { - t.equal(data.announce, announceUrl) - t.equal(typeof data.complete, 'number') - t.equal(typeof data.incomplete, 'number') + client.on('error', function (err) { + t.error(err) + }) - server.close() - client.destroy() - }) - }) + client.once('update', function (data) { + t.equal(data.announce, 'udp://127.0.0.1:' + port) + t.equal(typeof data.complete, 'number') + t.equal(typeof data.incomplete, 'number') }) client.start() - }) -} -test('http: large torrent: client.start()', function (t) { - testLargeTorrent(t, 'http') -}) + client.once('peer', function (addr) { + t.pass('there is at least one peer') // TODO: this shouldn't rely on an external server! -test('udp: large torrent: client.start()', function (t) { - testLargeTorrent(t, 'udp') -}) + client.stop() -test('ws: large torrent: client.start()', function (t) { - testLargeTorrent(t, 'ws') + client.once('update', function () { + server.close(function () { + t.pass('server close') + }) + }) + + }) + }) }) diff --git a/test/client-magnet.js b/test/client-magnet.js index ba1949c9..aa4d25b8 100644 --- a/test/client-magnet.js +++ b/test/client-magnet.js @@ -1,38 +1,56 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') -var common = require('./common') -var fixtures = require('webtorrent-fixtures') +var fs = require('fs') var magnet = require('magnet-uri') +var portfinder = require('portfinder') +var Server = require('../').Server var test = require('tape') -var peerId = Buffer.from('01234567890123456789') +var uri = 'magnet:?xt=urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36&dn=Leaves+of+Grass+by+Walt+Whitman.epub&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80' +var parsedTorrent = magnet(uri) +var peerId = new Buffer('01234567890123456789') -function testMagnet (t, serverType) { - t.plan(9) +test('magnet + udp: client.start/update/stop()', function (t) { + t.plan(12) - var parsedTorrent = magnet(fixtures.leaves.magnetURI) + var server = new Server({ http: false }) - common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881, - wrtc: {} - }) + server.on('error', function (err) { + t.fail(err.message) + }) + + server.on('warning', function (err) { + t.fail(err.message) + }) + + portfinder.getPort(function (err, port) { + t.error(err, 'found free port') + server.listen(port) + var announceUrl = 'udp://127.0.0.1:' + port - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) + // remove all tracker servers except a single UDP one, for now + parsedTorrent.announce = [ announceUrl ] + + var client = new Client(peerId, 6881, parsedTorrent) + + client.on('error', function (err) { + t.error(err) + }) client.once('update', function (data) { t.equal(data.announce, announceUrl) t.equal(typeof data.complete, 'number') t.equal(typeof data.incomplete, 'number') + }) + + client.start() + + client.once('peer', function (addr) { + t.pass('there is at least one peer') // TODO: this shouldn't rely on an external server! client.update() client.once('update', function (data) { + // receive one final update after calling stop t.equal(data.announce, announceUrl) t.equal(typeof data.complete, 'number') t.equal(typeof data.incomplete, 'number') @@ -40,28 +58,16 @@ function testMagnet (t, serverType) { client.stop() client.once('update', function (data) { + // received an update! t.equal(data.announce, announceUrl) t.equal(typeof data.complete, 'number') t.equal(typeof data.incomplete, 'number') - server.close() - client.destroy() + server.close(function () { + t.pass('server closed') + }) }) }) }) - - client.start() }) -} - -test('http: magnet: client.start/update/stop()', function (t) { - testMagnet(t, 'http') -}) - -test('udp: magnet: client.start/update/stop()', function (t) { - testMagnet(t, 'udp') -}) - -test('ws: magnet: client.start/update/stop()', function (t) { - testMagnet(t, 'ws') }) diff --git a/test/client-ws-socket-pool.js b/test/client-ws-socket-pool.js deleted file mode 100644 index 94f6b2dd..00000000 --- a/test/client-ws-socket-pool.js +++ /dev/null @@ -1,57 +0,0 @@ -var Buffer = require('safe-buffer').Buffer -var Client = require('../') -var common = require('./common') -var fixtures = require('webtorrent-fixtures') -var test = require('tape') - -var peerId = Buffer.from('01234567890123456789') -var port = 6681 - -test('ensure client.destroy() callback is called with re-used websockets in socketPool', function (t) { - t.plan(4) - - common.createServer(t, 'ws', function (server, announceUrl) { - var client1 = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: port, - wrtc: {} - }) - - common.mockWebsocketTracker(client1) - client1.on('error', function (err) { t.error(err) }) - client1.on('warning', function (err) { t.error(err) }) - - client1.start() - - client1.once('update', function () { - t.pass('got client1 update') - // second ws client using same announce url will re-use the same websocket - var client2 = new Client({ - infoHash: fixtures.alice.parsedTorrent.infoHash, // different info hash - announce: announceUrl, - peerId: peerId, - port: port, - wrtc: {} - }) - - common.mockWebsocketTracker(client2) - client2.on('error', function (err) { t.error(err) }) - client2.on('warning', function (err) { t.error(err) }) - - client2.start() - - client2.once('update', function () { - t.pass('got client2 update') - client1.destroy(function (err) { - t.error(err, 'got client1 destroy callback') - client2.destroy(function (err) { - t.error(err, 'got client2 destroy callback') - server.close() - }) - }) - }) - }) - }) -}) diff --git a/test/client.js b/test/client.js index 9858bf55..ea022bf9 100644 --- a/test/client.js +++ b/test/client.js @@ -1,41 +1,44 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') var common = require('./common') -var fixtures = require('webtorrent-fixtures') +var fs = require('fs') +var parseTorrent = require('parse-torrent') var test = require('tape') -var peerId1 = Buffer.from('01234567890123456789') -var peerId2 = Buffer.from('12345678901234567890') -var peerId3 = Buffer.from('23456789012345678901') +var torrent = fs.readFileSync(__dirname + '/torrents/bitlove-intro.torrent') +var parsedTorrent = parseTorrent(torrent) +var peerId1 = new Buffer('01234567890123456789') +var peerId2 = new Buffer('12345678901234567890') +var peerId3 = new Buffer('23456789012345678901') var port = 6881 function testClientStart (t, serverType) { - t.plan(4) - + t.plan(5) common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId1, - port: port, - wrtc: {} + parsedTorrent.announce = [ announceUrl ] + var client = new Client(peerId1, port, parsedTorrent) + + client.on('error', function (err) { + t.error(err) }) - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) + client.on('warning', function (err) { + t.error(err) + }) client.once('update', function (data) { t.equal(data.announce, announceUrl) t.equal(typeof data.complete, 'number') t.equal(typeof data.incomplete, 'number') + }) + client.once('peer', function (addr) { + t.pass('there is at least one peer') client.stop() client.once('update', function () { - t.pass('got response to stop') - server.close() - client.destroy() + server.close(function () { + t.pass('server close') + }) }) }) @@ -51,31 +54,23 @@ test('udp: client.start()', function (t) { testClientStart(t, 'udp') }) -test('ws: client.start()', function (t) { - testClientStart(t, 'ws') -}) - function testClientStop (t, serverType) { t.plan(4) - common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId1, - port: port, - wrtc: {} + parsedTorrent.announce = [ announceUrl ] + var client = new Client(peerId1, port, parsedTorrent) + + client.on('error', function (err) { + t.error(err) }) - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) + client.on('warning', function (err) { + t.error(err) + }) client.start() - client.once('update', function () { - t.pass('client received response to "start" message') - + setTimeout(function () { client.stop() client.once('update', function (data) { @@ -84,10 +79,12 @@ function testClientStop (t, serverType) { t.equal(typeof data.complete, 'number') t.equal(typeof data.incomplete, 'number') - server.close() - client.destroy() + server.close(function () { + t.pass('server close') + }) }) - }) + + }, 1000) }) } @@ -99,88 +96,24 @@ test('udp: client.stop()', function (t) { testClientStop(t, 'udp') }) -test('ws: client.stop()', function (t) { - testClientStop(t, 'ws') -}) - -function testClientStopDestroy (t, serverType) { - t.plan(2) - - common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId1, - port: port, - wrtc: {} - }) - - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) - - client.start() - - client.once('update', function () { - t.pass('client received response to "start" message') - - client.stop() - - client.on('update', function () { t.fail('client should not receive update after destroy is called') }) - - // Call destroy() in the same tick as stop(), but the message should still - // be received by the server, though obviously the client won't receive the - // response. - client.destroy() - - server.once('stop', function (peer, params) { - t.pass('server received "stop" message') - setTimeout(function () { - // give the websocket server time to finish in progress (stream) messages - // to peers - server.close() - }, 100) - }) - }) - }) -} - -test('http: client.stop(); client.destroy()', function (t) { - testClientStopDestroy(t, 'http') -}) - -test('udp: client.stop(); client.destroy()', function (t) { - testClientStopDestroy(t, 'udp') -}) - -test('ws: client.stop(); client.destroy()', function (t) { - testClientStopDestroy(t, 'ws') -}) - function testClientUpdate (t, serverType) { t.plan(4) - common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId1, - port: port, - wrtc: {} - }) + parsedTorrent.announce = [ announceUrl ] + var client = new Client(peerId1, port, parsedTorrent, { interval: 5000 }) - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) + client.on('error', function (err) { + t.error(err) + }) - client.setInterval(500) + client.on('warning', function (err) { + t.error(err) + }) client.start() client.once('update', function () { - client.setInterval(500) - // after interval, we should get another update client.once('update', function (data) { // received an update! t.equal(data.announce, announceUrl) @@ -189,9 +122,9 @@ function testClientUpdate (t, serverType) { client.stop() client.once('update', function () { - t.pass('got response to stop') - server.close() - client.destroy() + server.close(function () { + t.pass('server close') + }) }) }) }) @@ -206,25 +139,19 @@ test('udp: client.update()', function (t) { testClientUpdate(t, 'udp') }) -test('ws: client.update()', function (t) { - testClientUpdate(t, 'ws') -}) - function testClientScrape (t, serverType) { - t.plan(4) - + t.plan(5) common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId1, - port: port, - wrtc: {} + parsedTorrent.announce = [ announceUrl ] + var client = new Client(peerId1, port, parsedTorrent) + + client.on('error', function (err) { + t.error(err) }) - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) + client.on('warning', function (err) { + t.error(err) + }) client.once('scrape', function (data) { t.equal(data.announce, announceUrl) @@ -232,8 +159,9 @@ function testClientScrape (t, serverType) { t.equal(typeof data.incomplete, 'number') t.equal(typeof data.downloaded, 'number') - server.close() - client.destroy() + server.close(function () { + t.pass('server close') + }) }) client.scrape() @@ -248,223 +176,45 @@ test('udp: client.scrape()', function (t) { testClientScrape(t, 'udp') }) -test('ws: client.scrape()', function (t) { - testClientScrape(t, 'ws') -}) - -function testClientAnnounceWithParams (t, serverType) { - t.plan(5) - - common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId1, - port: port, - wrtc: {} - }) - - server.on('start', function (peer, params) { - t.equal(params.testParam, 'this is a test') - }) - - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) - - client.once('update', function (data) { - t.equal(data.announce, announceUrl) - t.equal(typeof data.complete, 'number') - t.equal(typeof data.incomplete, 'number') - - client.stop() - - client.once('update', function () { - t.pass('got response to stop') - server.close() - client.destroy() - }) - }) - - client.start({ - testParam: 'this is a test' - }) - }) -} - -test('http: client.announce() with params', function (t) { - testClientAnnounceWithParams(t, 'http') -}) - -test('ws: client.announce() with params', function (t) { - testClientAnnounceWithParams(t, 'ws') -}) - -function testClientGetAnnounceOpts (t, serverType) { - t.plan(5) - - common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId1, - port: port, - getAnnounceOpts: function () { - return { - testParam: 'this is a test' - } - }, - wrtc: {} - }) - - server.on('start', function (peer, params) { - t.equal(params.testParam, 'this is a test') - }) - - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) - - client.once('update', function (data) { - t.equal(data.announce, announceUrl) - t.equal(typeof data.complete, 'number') - t.equal(typeof data.incomplete, 'number') - - client.stop() - - client.once('update', function () { - t.pass('got response to stop') - server.close() - client.destroy() - }) - }) - - client.start() - }) -} - -test('http: client `opts.getAnnounceOpts`', function (t) { - testClientGetAnnounceOpts(t, 'http') -}) - -test('ws: client `opts.getAnnounceOpts`', function (t) { - testClientGetAnnounceOpts(t, 'ws') -}) - function testClientAnnounceWithNumWant (t, serverType) { - t.plan(4) - + t.plan(1) common.createServer(t, serverType, function (server, announceUrl) { - var client1 = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: [ announceUrl ], - peerId: peerId1, - port: port, - wrtc: {} + parsedTorrent.announce = [ announceUrl ] + var client1 = new Client(peerId1, port, parsedTorrent) + client1.on('error', function (err) { + t.error(err) }) - if (serverType === 'ws') common.mockWebsocketTracker(client1) - client1.on('error', function (err) { t.error(err) }) - client1.on('warning', function (err) { t.error(err) }) - client1.start() - client1.once('update', function () { - var client2 = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId2, - port: port + 1, - wrtc: {} + client1.once('update', function (data) { + var client2 = new Client(peerId2, port + 1, parsedTorrent) + client2.on('error', function (err) { + t.error(err) }) - - if (serverType === 'ws') common.mockWebsocketTracker(client2) - client2.on('error', function (err) { t.error(err) }) - client2.on('warning', function (err) { t.error(err) }) - client2.start() client2.once('update', function () { - var client3 = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId3, - port: port + 2, - wrtc: {} + var client3 = new Client(peerId3, port + 2, parsedTorrent, { numWant: 1 }) + client3.on('error', function (err) { + t.error(err) }) - - if (serverType === 'ws') common.mockWebsocketTracker(client3) - client3.on('error', function (err) { t.error(err) }) - client3.on('warning', function (err) { t.error(err) }) - - client3.start({ numwant: 1 }) + client3.start() client3.on('peer', function () { t.pass('got one peer (this should only fire once)') - var num = 3 - function tryCloseServer () { - num -= 1 - if (num === 0) server.close() - } - client1.stop() - client1.once('update', function () { - t.pass('got response to stop (client1)') - client1.destroy() - tryCloseServer() - }) client2.stop() - client2.once('update', function () { - t.pass('got response to stop (client2)') - client2.destroy() - tryCloseServer() - }) client3.stop() - client3.once('update', function () { - t.pass('got response to stop (client3)') - client3.destroy() - tryCloseServer() - }) + server.close() }) }) }) }) } -test('http: client announce with numwant', function (t) { +test('http: client announce with numWant', function (t) { testClientAnnounceWithNumWant(t, 'http') }) -test('udp: client announce with numwant', function (t) { +test('udp: client announce with numWant', function (t) { testClientAnnounceWithNumWant(t, 'udp') }) - -test('http: userAgent', function (t) { - t.plan(2) - - common.createServer(t, 'http', function (server, announceUrl) { - // Confirm that user-agent header is set - server.http.on('request', function (req, res) { - t.ok(req.headers['user-agent'].indexOf('WebTorrent') !== -1) - }) - - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId1, - port: port, - userAgent: 'WebTorrent/0.98.0 (https://webtorrent.io)', - wrtc: {} - }) - - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) - - client.once('update', function (data) { - t.equal(data.announce, announceUrl) - - server.close() - client.destroy() - }) - - client.start() - }) -}) diff --git a/test/common.js b/test/common.js index 0f872e39..bfbb9441 100644 --- a/test/common.js +++ b/test/common.js @@ -1,40 +1,26 @@ +var portfinder = require('portfinder') var Server = require('../').Server -exports.createServer = function (t, opts, cb) { - if (typeof opts === 'string') opts = { serverType: opts } +exports.createServer = function (t, serverType, cb) { + var opts = serverType === 'http' ? { udp: false } : { http: false } + var server = new Server(opts) - opts.http = (opts.serverType === 'http') - opts.udp = (opts.serverType === 'udp') - opts.ws = (opts.serverType === 'ws') + server.on('error', function (err) { + t.error(err) + }) - var server = new Server(opts) + server.on('warning', function (err) { + t.error(err) + }) - server.on('error', function (err) { t.error(err) }) - server.on('warning', function (err) { t.error(err) }) + portfinder.getPort(function (err, port) { + if (err) return t.error(err) - server.listen(0, function () { - var port = server[opts.serverType].address().port - var announceUrl - if (opts.serverType === 'http') { - announceUrl = 'http://127.0.0.1:' + port + '/announce' - } else if (opts.serverType === 'udp') { - announceUrl = 'udp://127.0.0.1:' + port - } else if (opts.serverType === 'ws') { - announceUrl = 'ws://127.0.0.1:' + port - } + var announceUrl = serverType === 'http' + ? 'http://127.0.0.1:' + port + '/announce' + : 'udp://127.0.0.1:' + port + server.listen(port) cb(server, announceUrl) }) } - -exports.mockWebsocketTracker = function (client) { - client._trackers[0]._generateOffers = function (numwant, cb) { - var offers = [] - for (var i = 0; i < numwant; i++) { - offers.push({ fake_offer: 'fake_offer_' + i }) - } - process.nextTick(function () { - cb(offers) - }) - } -} diff --git a/test/destroy.js b/test/destroy.js deleted file mode 100644 index f6eafb64..00000000 --- a/test/destroy.js +++ /dev/null @@ -1,51 +0,0 @@ -var Buffer = require('safe-buffer').Buffer -var Client = require('../') -var common = require('./common') -var fixtures = require('webtorrent-fixtures') -var test = require('tape') - -var peerId = Buffer.from('01234567890123456789') -var port = 6881 - -function testNoEventsAfterDestroy (t, serverType) { - t.plan(1) - - common.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: port, - wrtc: {} - }) - - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) - - client.once('update', function () { - t.fail('no "update" event should fire, since client is destroyed') - }) - - // announce, then immediately destroy - client.update() - client.destroy() - - setTimeout(function () { - t.pass('wait to see if any events are fired') - server.close() - }, 1000) - }) -} - -test('http: no "update" events after destroy()', function (t) { - testNoEventsAfterDestroy(t, 'http') -}) - -test('udp: no "update" events after destroy()', function (t) { - testNoEventsAfterDestroy(t, 'udp') -}) - -test('ws: no "update" events after destroy()', function (t) { - testNoEventsAfterDestroy(t, 'ws') -}) diff --git a/test/evict.js b/test/evict.js deleted file mode 100644 index 1ff60bbb..00000000 --- a/test/evict.js +++ /dev/null @@ -1,126 +0,0 @@ -var Buffer = require('safe-buffer').Buffer -var Client = require('../') -var common = require('./common') -var test = require('tape') -var electronWebrtc = require('electron-webrtc') - -var wrtc - -var infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' -var peerId = Buffer.from('01234567890123456789') -var peerId2 = Buffer.from('12345678901234567890') -var peerId3 = Buffer.from('23456789012345678901') - -function serverTest (t, serverType, serverFamily) { - t.plan(10) - - var hostname = serverFamily === 'inet6' - ? '[::1]' - : '127.0.0.1' - - var opts = { - serverType: serverType, - peersCacheLength: 2 // LRU cache can only contain a max of 2 peers - } - - common.createServer(t, opts, function (server) { - // Not using announceUrl param from `common.createServer()` since we - // want to control IPv4 vs IPv6. - var port = server[serverType].address().port - var announceUrl = serverType + '://' + hostname + ':' + port + '/announce' - - var client1 = new Client({ - infoHash: infoHash, - announce: [ announceUrl ], - peerId: peerId, - port: 6881, - wrtc: wrtc - }) - if (serverType === 'ws') common.mockWebsocketTracker(client1) - - client1.start() - - client1.once('update', function (data) { - var client2 = new Client({ - infoHash: infoHash, - announce: [ announceUrl ], - peerId: peerId2, - port: 6882, - wrtc: wrtc - }) - if (serverType === 'ws') common.mockWebsocketTracker(client2) - - client2.start() - - client2.once('update', function (data) { - server.getSwarm(infoHash, function (err, swarm) { - t.error(err) - - t.equal(swarm.complete + swarm.incomplete, 2) - - // Ensure that first peer is evicted when a third one is added - var evicted = false - swarm.peers.once('evict', function (evictedPeer) { - t.equal(evictedPeer.value.peerId, peerId.toString('hex')) - t.equal(swarm.complete + swarm.incomplete, 2) - evicted = true - }) - - var client3 = new Client({ - infoHash: infoHash, - announce: [ announceUrl ], - peerId: peerId3, - port: 6880, - wrtc: wrtc - }) - if (serverType === 'ws') common.mockWebsocketTracker(client3) - - client3.start() - - client3.once('update', function (data) { - t.ok(evicted, 'client1 was evicted from server before client3 gets response') - t.equal(swarm.complete + swarm.incomplete, 2) - - client1.destroy(function () { - t.pass('client1 destroyed') - }) - - client2.destroy(function () { - t.pass('client3 destroyed') - }) - - client3.destroy(function () { - t.pass('client3 destroyed') - }) - - server.close(function () { - t.pass('server destroyed') - }) - }) - }) - }) - }) - }) -} - -test('evict: ipv4 server', function (t) { - serverTest(t, 'http', 'inet') -}) - -test('evict: http ipv6 server', function (t) { - serverTest(t, 'http', 'inet6') -}) - -test('evict: udp server', function (t) { - serverTest(t, 'udp', 'inet') -}) - -test('evict: ws server', function (t) { - wrtc = electronWebrtc() - wrtc.electronDaemon.once('ready', function () { - serverTest(t, 'ws', 'inet') - }) - t.once('end', function () { - wrtc.close() - }) -}) diff --git a/test/filter.js b/test/filter.js deleted file mode 100644 index 8eb4dd80..00000000 --- a/test/filter.js +++ /dev/null @@ -1,166 +0,0 @@ -var Buffer = require('safe-buffer').Buffer -var Client = require('../') -var common = require('./common') -var fixtures = require('webtorrent-fixtures') -var test = require('tape') - -var peerId = Buffer.from('01234567890123456789') - -function testFilterOption (t, serverType) { - t.plan(8) - - var opts = { serverType: serverType } // this is test-suite-only option - opts.filter = function (infoHash, params, cb) { - process.nextTick(function () { - if (infoHash === fixtures.alice.parsedTorrent.infoHash) { - cb(new Error('disallowed info_hash (Alice)')) - } else { - cb(null) - } - }) - } - - common.createServer(t, opts, function (server, announceUrl) { - var client1 = new Client({ - infoHash: fixtures.alice.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881, - wrtc: {} - }) - - client1.on('error', function (err) { t.error(err) }) - if (serverType === 'ws') common.mockWebsocketTracker(client1) - - client1.once('warning', function (err) { - t.ok(err.message.includes('disallowed info_hash (Alice)'), 'got client warning') - - client1.destroy(function () { - t.pass('client1 destroyed') - - var client2 = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881, - wrtc: {} - }) - if (serverType === 'ws') common.mockWebsocketTracker(client2) - - client2.on('error', function (err) { t.error(err) }) - client2.on('warning', function (err) { t.error(err) }) - - client2.on('update', function () { - t.pass('got announce') - client2.destroy(function () { t.pass('client2 destroyed') }) - server.close(function () { t.pass('server closed') }) - }) - - server.on('start', function () { - t.equal(Object.keys(server.torrents).length, 1) - }) - - client2.start() - }) - }) - - server.removeAllListeners('warning') - server.once('warning', function (err) { - t.ok(err.message.includes('disallowed info_hash (Alice)'), 'got server warning') - t.equal(Object.keys(server.torrents).length, 0) - }) - - client1.start() - }) -} - -test('http: filter option blocks tracker from tracking torrent', function (t) { - testFilterOption(t, 'http') -}) - -test('udp: filter option blocks tracker from tracking torrent', function (t) { - testFilterOption(t, 'udp') -}) - -test('ws: filter option blocks tracker from tracking torrent', function (t) { - testFilterOption(t, 'ws') -}) - -function testFilterCustomError (t, serverType) { - t.plan(8) - - var opts = { serverType: serverType } // this is test-suite-only option - opts.filter = function (infoHash, params, cb) { - process.nextTick(function () { - if (infoHash === fixtures.alice.parsedTorrent.infoHash) { - cb(new Error('alice blocked')) - } else { - cb(null) - } - }) - } - - common.createServer(t, opts, function (server, announceUrl) { - var client1 = new Client({ - infoHash: fixtures.alice.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881, - wrtc: {} - }) - - client1.on('error', function (err) { t.error(err) }) - if (serverType === 'ws') common.mockWebsocketTracker(client1) - - client1.once('warning', function (err) { - t.ok(/alice blocked/.test(err.message), 'got client warning') - - client1.destroy(function () { - t.pass('client1 destroyed') - var client2 = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881, - wrtc: {} - }) - if (serverType === 'ws') common.mockWebsocketTracker(client2) - - client2.on('error', function (err) { t.error(err) }) - client2.on('warning', function (err) { t.error(err) }) - - client2.on('update', function () { - t.pass('got announce') - client2.destroy(function () { t.pass('client2 destroyed') }) - server.close(function () { t.pass('server closed') }) - }) - - server.on('start', function () { - t.equal(Object.keys(server.torrents).length, 1) - }) - - client2.start() - }) - }) - - server.removeAllListeners('warning') - server.once('warning', function (err) { - t.ok(/alice blocked/.test(err.message), 'got server warning') - t.equal(Object.keys(server.torrents).length, 0) - }) - - client1.start() - }) -} - -test('http: filter option with custom error', function (t) { - testFilterCustomError(t, 'http') -}) - -test('udp: filter option filter option with custom error', function (t) { - testFilterCustomError(t, 'udp') -}) - -test('ws: filter option filter option with custom error', function (t) { - testFilterCustomError(t, 'ws') -}) diff --git a/test/querystring.js b/test/querystring.js deleted file mode 100644 index e20ddfad..00000000 --- a/test/querystring.js +++ /dev/null @@ -1,17 +0,0 @@ -var Buffer = require('safe-buffer').Buffer -var common = require('../lib/common') -var test = require('tape') - -// https://github.com/webtorrent/webtorrent/issues/196 -test('encode special chars +* in http tracker urls', function (t) { - var q = { - info_hash: Buffer.from('a2a15537542b22925ad10486bf7a8b2a9c42f0d1', 'hex').toString('binary') - } - var encoded = 'info_hash=%A2%A1U7T%2B%22%92Z%D1%04%86%BFz%8B%2A%9CB%F0%D1' - t.equal(common.querystringStringify(q), encoded) - - // sanity check that encode-decode matches up - t.deepEqual(common.querystringParse(common.querystringStringify(q)), q) - - t.end() -}) diff --git a/test/request-handler.js b/test/request-handler.js deleted file mode 100644 index e04e6651..00000000 --- a/test/request-handler.js +++ /dev/null @@ -1,74 +0,0 @@ -var Buffer = require('safe-buffer').Buffer -var Client = require('../') -var common = require('./common') -var fixtures = require('webtorrent-fixtures') -var test = require('tape') -var Server = require('../server') - -var peerId = Buffer.from('01234567890123456789') - -function testRequestHandler (t, serverType) { - t.plan(5) - - var opts = { serverType: serverType } // this is test-suite-only option - - class Swarm extends Server.Swarm { - announce (params, cb) { - super.announce(params, function (err, response) { - if (err) return cb(response) - response.complete = 246 - response.extraData = 'hi' - cb(null, response) - }) - } - } - - // Use a custom Swarm implementation for this test only - var OldSwarm = Server.Swarm - Server.Swarm = Swarm - t.on('end', function () { - Server.Swarm = OldSwarm - }) - - common.createServer(t, opts, function (server, announceUrl) { - var client1 = new Client({ - infoHash: fixtures.alice.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881, - wrtc: {} - }) - - client1.on('error', function (err) { t.error(err) }) - if (serverType === 'ws') common.mockWebsocketTracker(client1) - - server.once('start', function () { - t.pass('got start message from client1') - }) - - client1.once('update', function (data) { - t.equal(data.complete, 246) - t.equal(data.extraData.toString(), 'hi') - - client1.destroy(function () { - t.pass('client1 destroyed') - }) - - server.close(function () { - t.pass('server destroyed') - }) - }) - - client1.start() - }) -} - -test('http: request handler option intercepts announce requests and responses', function (t) { - testRequestHandler(t, 'http') -}) - -test('ws: request handler option intercepts announce requests and responses', function (t) { - testRequestHandler(t, 'ws') -}) - -// NOTE: it's not possible to include extra data in a UDP response, because it's compact and accepts only params that are in the spec! diff --git a/test/scrape.js b/test/scrape.js index 6391449b..29b0f526 100644 --- a/test/scrape.js +++ b/test/scrape.js @@ -1,40 +1,44 @@ var bencode = require('bencode') -var Buffer = require('safe-buffer').Buffer var Client = require('../') -var common = require('./common') var commonLib = require('../lib/common') var commonTest = require('./common') -var fixtures = require('webtorrent-fixtures') -var get = require('simple-get') +var concat = require('concat-stream') +var fs = require('fs') +var http = require('http') +var parseTorrent = require('parse-torrent') +var portfinder = require('portfinder') +var querystring = require('querystring') +var Server = require('../').Server var test = require('tape') -var peerId = Buffer.from('01234567890123456789') +function hexToBinary (str) { + return new Buffer(str, 'hex').toString('binary') +} + +var infoHash1 = 'aaa67059ed6bd08362da625b3ae77f6f4a075aaa' +var binaryInfoHash1 = hexToBinary(infoHash1) +var infoHash2 = 'bbb67059ed6bd08362da625b3ae77f6f4a075bbb' +var binaryInfoHash2 = hexToBinary(infoHash2) + +var bitlove = fs.readFileSync(__dirname + '/torrents/bitlove-intro.torrent') +var parsedBitlove = parseTorrent(bitlove) +var binaryBitlove = hexToBinary(parsedBitlove.infoHash) + +var peerId = new Buffer('01234567890123456789') function testSingle (t, serverType) { commonTest.createServer(t, serverType, function (server, announceUrl) { - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881, - wrtc: {} - }) - - if (serverType === 'ws') common.mockWebsocketTracker(client) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) - - client.scrape() - - client.on('scrape', function (data) { - t.equal(data.announce, announceUrl) - t.equal(data.infoHash, fixtures.leaves.parsedTorrent.infoHash) - t.equal(typeof data.complete, 'number') - t.equal(typeof data.incomplete, 'number') - t.equal(typeof data.downloaded, 'number') - client.destroy() - server.close(function () { - t.end() + var scrapeUrl = announceUrl.replace('announce', 'scrape') + server.once('listening', function () { + Client.scrape(announceUrl, infoHash1, function (err, data) { + t.error(err) + t.equal(data.announce, announceUrl) + t.equal(typeof data.complete, 'number') + t.equal(typeof data.incomplete, 'number') + t.equal(typeof data.downloaded, 'number') + server.close(function () { + t.end() + }) }) }) }) @@ -48,28 +52,20 @@ test('udp: single info_hash scrape', function (t) { testSingle(t, 'udp') }) -test('ws: single info_hash scrape', function (t) { - testSingle(t, 'ws') -}) - function clientScrapeStatic (t, serverType) { commonTest.createServer(t, serverType, function (server, announceUrl) { - var client = Client.scrape({ - announce: announceUrl, - infoHash: fixtures.leaves.parsedTorrent.infoHash, - wrtc: {} - }, function (err, data) { - t.error(err) - t.equal(data.announce, announceUrl) - t.equal(data.infoHash, fixtures.leaves.parsedTorrent.infoHash) - t.equal(typeof data.complete, 'number') - t.equal(typeof data.incomplete, 'number') - t.equal(typeof data.downloaded, 'number') - server.close(function () { - t.end() + server.once('listening', function () { + Client.scrape(announceUrl, infoHash1, function (err, data) { + t.error(err) + t.equal(data.announce, announceUrl) + t.equal(typeof data.complete, 'number') + t.equal(typeof data.incomplete, 'number') + t.equal(typeof data.downloaded, 'number') + server.close(function () { + t.end() + }) }) }) - if (serverType === 'ws') common.mockWebsocketTracker(client) }) } @@ -81,150 +77,104 @@ test('udp: scrape using Client.scrape static method', function (t) { clientScrapeStatic(t, 'udp') }) -test('ws: scrape using Client.scrape static method', function (t) { - clientScrapeStatic(t, 'ws') -}) +// TODO: test client for multiple scrape for UDP trackers -// Ensure the callback function gets called when an invalid url is passed -function clientScrapeStaticInvalid (t, serverType) { - var announceUrl = serverType + '://invalid.lol' - if (serverType === 'http') announceUrl += '/announce' - - var client = Client.scrape({ - announce: announceUrl, - infoHash: fixtures.leaves.parsedTorrent.infoHash, - wrtc: {} - }, function (err, data) { - t.ok(err instanceof Error) - t.end() +test('server: multiple info_hash scrape', function (t) { + var server = new Server({ udp: false }) + server.on('error', function (err) { + t.error(err) + }) + server.on('warning', function (err) { + t.error(err) }) - if (serverType === 'ws') common.mockWebsocketTracker(client) -} - -test('http: scrape using Client.scrape static method (invalid url)', function (t) { - clientScrapeStaticInvalid(t, 'http') -}) - -test('udp: scrape using Client.scrape static method (invalid url)', function (t) { - clientScrapeStaticInvalid(t, 'udp') -}) - -test('ws: scrape using Client.scrape static method (invalid url)', function (t) { - clientScrapeStaticInvalid(t, 'ws') -}) -function clientScrapeMulti (t, serverType) { - var infoHash1 = fixtures.leaves.parsedTorrent.infoHash - var infoHash2 = fixtures.alice.parsedTorrent.infoHash + portfinder.getPort(function (err, port) { + t.error(err) + server.listen(port) + var scrapeUrl = 'http://127.0.0.1:' + port + '/scrape' - commonTest.createServer(t, serverType, function (server, announceUrl) { - Client.scrape({ - infoHash: [ infoHash1, infoHash2 ], - announce: announceUrl - }, function (err, results) { - t.error(err) - - t.equal(results[infoHash1].announce, announceUrl) - t.equal(results[infoHash1].infoHash, infoHash1) - t.equal(typeof results[infoHash1].complete, 'number') - t.equal(typeof results[infoHash1].incomplete, 'number') - t.equal(typeof results[infoHash1].downloaded, 'number') - - t.equal(results[infoHash2].announce, announceUrl) - t.equal(results[infoHash2].infoHash, infoHash2) - t.equal(typeof results[infoHash2].complete, 'number') - t.equal(typeof results[infoHash2].incomplete, 'number') - t.equal(typeof results[infoHash2].downloaded, 'number') - - server.close(function () { - t.end() + server.once('listening', function () { + var url = scrapeUrl + '?' + commonLib.querystringStringify({ + info_hash: [ binaryInfoHash1, binaryInfoHash2 ] + }) + http.get(url, function (res) { + t.equal(res.statusCode, 200) + res.pipe(concat(function (data) { + data = bencode.decode(data) + t.ok(data.files) + t.equal(Object.keys(data.files).length, 2) + + t.ok(data.files[binaryInfoHash1]) + t.equal(typeof data.files[binaryInfoHash1].complete, 'number') + t.equal(typeof data.files[binaryInfoHash1].incomplete, 'number') + t.equal(typeof data.files[binaryInfoHash1].downloaded, 'number') + + t.ok(data.files[binaryInfoHash2]) + t.equal(typeof data.files[binaryInfoHash2].complete, 'number') + t.equal(typeof data.files[binaryInfoHash2].incomplete, 'number') + t.equal(typeof data.files[binaryInfoHash2].downloaded, 'number') + + server.close(function () { + t.end() + }) + })) + }).on('error', function (e) { + t.error(err) }) }) }) -} - -test('http: MULTI scrape using Client.scrape static method', function (t) { - clientScrapeMulti(t, 'http') -}) - -test('udp: MULTI scrape using Client.scrape static method', function (t) { - clientScrapeMulti(t, 'udp') }) -test('server: multiple info_hash scrape (manual http request)', function (t) { - t.plan(13) - - var binaryInfoHash1 = commonLib.hexToBinary(fixtures.leaves.parsedTorrent.infoHash) - var binaryInfoHash2 = commonLib.hexToBinary(fixtures.alice.parsedTorrent.infoHash) - - commonTest.createServer(t, 'http', function (server, announceUrl) { - var scrapeUrl = announceUrl.replace('/announce', '/scrape') - - var url = scrapeUrl + '?' + commonLib.querystringStringify({ - info_hash: [ binaryInfoHash1, binaryInfoHash2 ] - }) - - get.concat(url, function (err, res, data) { - t.error(err) - - t.equal(res.statusCode, 200) - - data = bencode.decode(data) - t.ok(data.files) - t.equal(Object.keys(data.files).length, 2) - - t.ok(data.files[binaryInfoHash1]) - t.equal(typeof data.files[binaryInfoHash1].complete, 'number') - t.equal(typeof data.files[binaryInfoHash1].incomplete, 'number') - t.equal(typeof data.files[binaryInfoHash1].downloaded, 'number') - - t.ok(data.files[binaryInfoHash2]) - t.equal(typeof data.files[binaryInfoHash2].complete, 'number') - t.equal(typeof data.files[binaryInfoHash2].incomplete, 'number') - t.equal(typeof data.files[binaryInfoHash2].downloaded, 'number') - - server.close(function () { t.pass('server closed') }) - }) +test('server: all info_hash scrape', function (t) { + var server = new Server({ udp: false }) + server.on('error', function (err) { + t.error(err) + }) + server.on('warning', function (err) { + t.error(err) }) -}) - -test('server: all info_hash scrape (manual http request)', function (t) { - t.plan(10) - - var binaryInfoHash = commonLib.hexToBinary(fixtures.leaves.parsedTorrent.infoHash) - commonTest.createServer(t, 'http', function (server, announceUrl) { - var scrapeUrl = announceUrl.replace('/announce', '/scrape') + portfinder.getPort(function (err, port) { + t.error(err) + server.listen(port) + var announceUrl = 'http://127.0.0.1:' + port + '/announce' + var scrapeUrl = 'http://127.0.0.1:' + port + '/scrape' - // announce a torrent to the tracker - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881 - }) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) + parsedBitlove.announce = [ announceUrl ] - client.start() + server.once('listening', function () { - server.once('start', function () { - // now do a scrape of everything by omitting the info_hash param - get.concat(scrapeUrl, function (err, res, data) { + // announce a torrent to the tracker + var client = new Client(peerId, port, parsedBitlove) + client.on('error', function (err) { t.error(err) - - t.equal(res.statusCode, 200) - data = bencode.decode(data) - t.ok(data.files) - t.equal(Object.keys(data.files).length, 1) - - t.ok(data.files[binaryInfoHash]) - t.equal(typeof data.files[binaryInfoHash].complete, 'number') - t.equal(typeof data.files[binaryInfoHash].incomplete, 'number') - t.equal(typeof data.files[binaryInfoHash].downloaded, 'number') - - client.destroy(function () { t.pass('client destroyed') }) - server.close(function () { t.pass('server closed') }) + }) + client.start() + + server.once('start', function (data) { + + // now do a scrape of everything by omitting the info_hash param + http.get(scrapeUrl, function (res) { + + t.equal(res.statusCode, 200) + res.pipe(concat(function (data) { + data = bencode.decode(data) + t.ok(data.files) + t.equal(Object.keys(data.files).length, 1) + + t.ok(data.files[binaryBitlove]) + t.equal(typeof data.files[binaryBitlove].complete, 'number') + t.equal(typeof data.files[binaryBitlove].incomplete, 'number') + t.equal(typeof data.files[binaryBitlove].downloaded, 'number') + + client.stop() + server.close(function () { + t.end() + }) + })) + }).on('error', function (e) { + t.error(err) + }) }) }) }) diff --git a/test/server.js b/test/server.js index 73e9fe66..bcd02693 100644 --- a/test/server.js +++ b/test/server.js @@ -1,163 +1,104 @@ -var Buffer = require('safe-buffer').Buffer var Client = require('../') -var common = require('./common') +var Server = require('../').Server var test = require('tape') -var electronWebrtc = require('electron-webrtc') - -var wrtc var infoHash = '4cb67059ed6bd08362da625b3ae77f6f4a075705' -var peerId = Buffer.from('01234567890123456789') -var peerId2 = Buffer.from('12345678901234567890') -var peerId3 = Buffer.from('23456789012345678901') - -function serverTest (t, serverType, serverFamily) { - t.plan(40) - - var hostname = serverFamily === 'inet6' - ? '[::1]' - : '127.0.0.1' - var clientIp = serverFamily === 'inet6' - ? '::1' - : '127.0.0.1' - - var opts = { - serverType: serverType - } - - common.createServer(t, opts, function (server) { - // Not using announceUrl param from `common.createServer()` since we - // want to control IPv4 vs IPv6. - var port = server[serverType].address().port - var announceUrl = serverType + '://' + hostname + ':' + port + '/announce' - - var client1 = new Client({ +var peerId = '01234567890123456789' +var peerId2 = '12345678901234567890' +var torrentLength = 50000 + +function serverTest (t, serverType) { + t.plan(26) + + var opts = serverType === 'http' ? { udp: false } : { http: false } + var server = new Server(opts) + + server.on('error', function (err) { + t.fail(err.message) + }) + + server.on('warning', function (err) { + t.fail(err.message) + }) + + server.on('listening', function () { + t.pass('server listening') + }) + + server.listen(function (port) { + var announceUrl = 'http://127.0.0.1:' + port + '/announce' + + var client = new Client(peerId, 6881, { infoHash: infoHash, - announce: [ announceUrl ], - peerId: peerId, - port: 6881, - wrtc: wrtc + length: torrentLength, + announce: [ announceUrl ] }) - if (serverType === 'ws') common.mockWebsocketTracker(client1) - client1.start() + client.start() server.once('start', function () { t.pass('got start message from client1') }) - client1.once('update', function (data) { + client.once('update', function (data) { t.equal(data.announce, announceUrl) t.equal(data.complete, 0) t.equal(data.incomplete, 1) - server.getSwarm(infoHash, function (err, swarm) { - t.error(err) - - t.equal(Object.keys(server.torrents).length, 1) - t.equal(swarm.complete, 0) - t.equal(swarm.incomplete, 1) - t.equal(swarm.peers.length, 1) - - var id = serverType === 'ws' - ? peerId.toString('hex') - : hostname + ':6881' - - var peer = swarm.peers.peek(id) - t.equal(peer.type, serverType) - t.equal(peer.ip, clientIp) - t.equal(peer.peerId, peerId.toString('hex')) - t.equal(peer.complete, false) - if (serverType === 'ws') { - t.equal(typeof peer.port, 'number') - t.ok(peer.socket) - } else { - t.equal(peer.port, 6881) - t.notOk(peer.socket) - } - - client1.complete() - - client1.once('update', function (data) { + t.equal(Object.keys(server.torrents).length, 1) + t.equal(server.getSwarm(infoHash).complete, 0) + t.equal(server.getSwarm(infoHash).incomplete, 1) + t.equal(Object.keys(server.getSwarm(infoHash).peers).length, 1) + t.deepEqual(server.getSwarm(infoHash).peers['127.0.0.1:6881'], { + ip: '127.0.0.1', + port: 6881, + peerId: peerId + }) + + client.complete() + + client.once('update', function (data) { + t.equal(data.announce, announceUrl) + t.equal(data.complete, 1) + t.equal(data.incomplete, 0) + + client.scrape() + + client.once('scrape', function (data) { t.equal(data.announce, announceUrl) - t.equal(data.complete, 1) - t.equal(data.incomplete, 0) - - client1.scrape() - - client1.once('scrape', function (data) { - t.equal(data.announce, announceUrl) - t.equal(data.complete, 1) - t.equal(data.incomplete, 0) - t.equal(typeof data.downloaded, 'number') - - var client2 = new Client({ - infoHash: infoHash, - announce: [ announceUrl ], - peerId: peerId2, - port: 6882, - wrtc: wrtc - }) - if (serverType === 'ws') common.mockWebsocketTracker(client2) + t.equal(typeof data.complete, 'number') + t.equal(typeof data.incomplete, 'number') + t.equal(typeof data.downloaded, 'number') + + var client2 = new Client(peerId2, 6882, { + infoHash: infoHash, + length: torrentLength, + announce: [ announceUrl ] + }) - client2.start() + client2.start() - server.once('start', function () { - t.pass('got start message from client2') - }) + server.once('start', function () { + t.pass('got start message from client2') + }) + client2.once('peer', function (addr) { + t.equal(addr, '127.0.0.1:6881') + + client2.stop() client2.once('update', function (data) { t.equal(data.announce, announceUrl) t.equal(data.complete, 1) - t.equal(data.incomplete, 1) - - var client3 = new Client({ - infoHash: infoHash, - announce: [ announceUrl ], - peerId: peerId3, - port: 6880, - wrtc: wrtc - }) - if (serverType === 'ws') common.mockWebsocketTracker(client3) - - client3.start() - - server.once('start', function () { - t.pass('got start message from client3') - }) + t.equal(data.incomplete, 0) - client3.once('update', function (data) { + client.stop() + client.once('update', function (data) { t.equal(data.announce, announceUrl) - t.equal(data.complete, 1) - t.equal(data.incomplete, 2) - - client2.stop() - client2.once('update', function (data) { - t.equal(data.announce, announceUrl) - t.equal(data.complete, 1) - t.equal(data.incomplete, 1) - - client2.destroy(function () { - t.pass('client2 destroyed') - client3.stop() - client3.once('update', function (data) { - t.equal(data.announce, announceUrl) - t.equal(data.complete, 1) - t.equal(data.incomplete, 0) - - client1.destroy(function () { - t.pass('client1 destroyed') - }) - - client3.destroy(function () { - t.pass('client3 destroyed') - }) - - server.close(function () { - t.pass('server destroyed') - }) - }) - }) + t.equal(data.complete, 0) + t.equal(data.incomplete, 0) + + server.close(function () { + t.pass('server closed') }) }) }) @@ -168,24 +109,10 @@ function serverTest (t, serverType, serverFamily) { }) } -test('http ipv4 server', function (t) { - serverTest(t, 'http', 'inet') -}) - -test('http ipv6 server', function (t) { - serverTest(t, 'http', 'inet6') +test('http server', function (t) { + serverTest(t, 'http') }) test('udp server', function (t) { - serverTest(t, 'udp', 'inet') -}) - -test('ws server', function (t) { - wrtc = electronWebrtc() - wrtc.electronDaemon.once('ready', function () { - serverTest(t, 'ws', 'inet') - }) - t.once('end', function () { - wrtc.close() - }) + serverTest(t, 'http') }) diff --git a/test/stats.js b/test/stats.js deleted file mode 100644 index 15d0c0f5..00000000 --- a/test/stats.js +++ /dev/null @@ -1,196 +0,0 @@ -var Buffer = require('safe-buffer').Buffer -var Client = require('../') -var commonTest = require('./common') -var fixtures = require('webtorrent-fixtures') -var get = require('simple-get') -var test = require('tape') - -var peerId = Buffer.from('-WW0091-4ea5886ce160') -var unknownPeerId = Buffer.from('01234567890123456789') - -function parseHtml (html) { - var extractValue = new RegExp('[^v^h](\\d+)') - var array = html.replace('torrents', '\n').split('\n').filter(function (line) { - return line && line.trim().length > 0 - }).map(function (line) { - var a = extractValue.exec(line) - if (a) { - return parseInt(a[1]) - } - }) - var i = 0 - return { - torrents: array[i++], - activeTorrents: array[i++], - peersAll: array[i++], - peersSeederOnly: array[i++], - peersLeecherOnly: array[i++], - peersSeederAndLeecher: array[i++], - peersIPv4: array[i++], - peersIPv6: array[i] - } -} - -test('server: get empty stats', function (t) { - t.plan(11) - - commonTest.createServer(t, 'http', function (server, announceUrl) { - var url = announceUrl.replace('/announce', '/stats') - - get.concat(url, function (err, res, data) { - t.error(err) - - var stats = parseHtml(data.toString()) - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 0) - t.equal(stats.activeTorrents, 0) - t.equal(stats.peersAll, 0) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 0) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.peersIPv4, 0) - t.equal(stats.peersIPv6, 0) - - server.close(function () { t.pass('server closed') }) - }) - }) -}) - -test('server: get empty stats with json header', function (t) { - t.plan(11) - - commonTest.createServer(t, 'http', function (server, announceUrl) { - var opts = { - url: announceUrl.replace('/announce', '/stats'), - headers: { - accept: 'application/json' - }, - json: true - } - - get.concat(opts, function (err, res, stats) { - t.error(err) - - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 0) - t.equal(stats.activeTorrents, 0) - t.equal(stats.peersAll, 0) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 0) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.peersIPv4, 0) - t.equal(stats.peersIPv6, 0) - - server.close(function () { t.pass('server closed') }) - }) - }) -}) - -test('server: get empty stats on stats.json', function (t) { - t.plan(11) - - commonTest.createServer(t, 'http', function (server, announceUrl) { - var opts = { - url: announceUrl.replace('/announce', '/stats.json'), - json: true - } - - get.concat(opts, function (err, res, stats) { - t.error(err) - - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 0) - t.equal(stats.activeTorrents, 0) - t.equal(stats.peersAll, 0) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 0) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.peersIPv4, 0) - t.equal(stats.peersIPv6, 0) - - server.close(function () { t.pass('server closed') }) - }) - }) -}) - -test('server: get leecher stats.json', function (t) { - t.plan(11) - - commonTest.createServer(t, 'http', function (server, announceUrl) { - // announce a torrent to the tracker - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: peerId, - port: 6881 - }) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) - - client.start() - - server.once('start', function () { - var opts = { - url: announceUrl.replace('/announce', '/stats.json'), - json: true - } - - get.concat(opts, function (err, res, stats) { - t.error(err) - - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 1) - t.equal(stats.activeTorrents, 1) - t.equal(stats.peersAll, 1) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 1) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.clients['WebTorrent']['0.91'], 1) - - client.destroy(function () { t.pass('client destroyed') }) - server.close(function () { t.pass('server closed') }) - }) - }) - }) -}) - -test('server: get leecher stats.json (unknown peerId)', function (t) { - t.plan(11) - - commonTest.createServer(t, 'http', function (server, announceUrl) { - // announce a torrent to the tracker - var client = new Client({ - infoHash: fixtures.leaves.parsedTorrent.infoHash, - announce: announceUrl, - peerId: unknownPeerId, - port: 6881 - }) - client.on('error', function (err) { t.error(err) }) - client.on('warning', function (err) { t.error(err) }) - - client.start() - - server.once('start', function () { - var opts = { - url: announceUrl.replace('/announce', '/stats.json'), - json: true - } - - get.concat(opts, function (err, res, stats) { - t.error(err) - - t.equal(res.statusCode, 200) - t.equal(stats.torrents, 1) - t.equal(stats.activeTorrents, 1) - t.equal(stats.peersAll, 1) - t.equal(stats.peersSeederOnly, 0) - t.equal(stats.peersLeecherOnly, 1) - t.equal(stats.peersSeederAndLeecher, 0) - t.equal(stats.clients['unknown']['01234567'], 1) - - client.destroy(function () { t.pass('client destroyed') }) - server.close(function () { t.pass('server closed') }) - }) - }) - }) -}) diff --git a/test/torrents/bitlove-intro.torrent b/test/torrents/bitlove-intro.torrent new file mode 100644 index 00000000..1c91bf13 Binary files /dev/null and b/test/torrents/bitlove-intro.torrent differ diff --git a/test/torrents/leaves.torrent b/test/torrents/leaves.torrent new file mode 100644 index 00000000..f18e7c11 Binary files /dev/null and b/test/torrents/leaves.torrent differ diff --git a/test/torrents/sintel-5gb.torrent b/test/torrents/sintel-5gb.torrent new file mode 100644 index 00000000..65f43c4d Binary files /dev/null and b/test/torrents/sintel-5gb.torrent differ