From 3c67966db3e34829462400d415b2e1ce033886c6 Mon Sep 17 00:00:00 2001 From: Alexandru Branza Date: Tue, 20 Mar 2018 10:44:29 +0200 Subject: [PATCH 1/2] Downgrade + Fix Announce --- .npmignore | 1 - .travis.yml | 11 +- AUTHORS.md | 43 -- CONTRIBUTING.md | 75 -- LICENSE | 2 +- README.md | 186 +---- bin/cmd.js | 144 ---- bin/update-authors.sh | 23 - client.js | 697 +++++++++++------ examples/express-embed/package.json | 13 - examples/express-embed/server.js | 28 - examples/tracker-scrape.md | 47 -- img/img.png => img.png | Bin img/trackerStats.png | Bin 13686 -> 0 bytes lib/client/http-tracker.js | 259 ------- lib/client/tracker.js | 30 - lib/client/udp-tracker.js | 283 ------- lib/client/websocket-tracker.js | 444 ----------- lib/common-node.js | 71 -- lib/common.js | 54 +- lib/server/parse-http.js | 59 -- lib/server/parse-udp.js | 75 -- lib/server/parse-websocket.js | 78 -- lib/server/swarm.js | 148 ---- package-lock.json | 282 +++++++ package.json | 75 +- server.js | 1087 ++++++++++----------------- test/client-large-torrent.js | 85 +-- test/client-magnet.js | 74 +- test/client-ws-socket-pool.js | 57 -- test/client.js | 400 ++-------- test/common.js | 46 +- test/destroy.js | 51 -- test/evict.js | 126 ---- test/filter.js | 166 ---- test/querystring.js | 17 - test/request-handler.js | 74 -- test/scrape.js | 300 +++----- test/server.js | 231 ++---- test/stats.js | 196 ----- test/torrents/bitlove-intro.torrent | Bin 0 -> 597 bytes test/torrents/leaves.torrent | Bin 0 -> 994 bytes test/torrents/sintel-5gb.torrent | Bin 0 -> 26727 bytes 43 files changed, 1621 insertions(+), 4417 deletions(-) delete mode 100644 .npmignore delete mode 100644 AUTHORS.md delete mode 100644 CONTRIBUTING.md delete mode 100755 bin/cmd.js delete mode 100755 bin/update-authors.sh delete mode 100644 examples/express-embed/package.json delete mode 100755 examples/express-embed/server.js delete mode 100644 examples/tracker-scrape.md rename img/img.png => img.png (100%) delete mode 100644 img/trackerStats.png delete mode 100644 lib/client/http-tracker.js delete mode 100644 lib/client/tracker.js delete mode 100644 lib/client/udp-tracker.js delete mode 100644 lib/client/websocket-tracker.js delete mode 100644 lib/common-node.js delete mode 100644 lib/server/parse-http.js delete mode 100644 lib/server/parse-udp.js delete mode 100644 lib/server/parse-websocket.js delete mode 100644 lib/server/swarm.js create mode 100644 package-lock.json delete mode 100644 test/client-ws-socket-pool.js delete mode 100644 test/destroy.js delete mode 100644 test/evict.js delete mode 100644 test/filter.js delete mode 100644 test/querystring.js delete mode 100644 test/request-handler.js delete mode 100644 test/stats.js create mode 100644 test/torrents/bitlove-intro.torrent create mode 100644 test/torrents/leaves.torrent create mode 100644 test/torrents/sintel-5gb.torrent 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..f870ad48 100644 --- a/client.js +++ b/client.js @@ -1,296 +1,561 @@ 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 + + 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 != null && self.client.torrentLength <= opts.downloaded) { + if (opts.uploaded == self._lastUploaded) { + return + } else { + // fix update event if torrentLength larger then opts.downloaded + opts.left = 0 + opts.downloaded = self.client.torrentLength + } + } -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) - }) + self._lastUploaded = opts.uploaded + + if (self.client.torrentLength != null && opts.left == null) { + if (opts.event == 'completed') { + // if completed, fix announce to full + opts.left = 0 + opts.downloaded = self.client.torrentLength + } else { + opts.left = self.client.torrentLength - (opts.downloaded || 0) + } + } + + 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 54ef54bb73ae3628de9f1a3f34b45ca104b7308a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13686 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 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 0000000000000000000000000000000000000000..1c91bf1360446ed6fe793ff0b451437e64b8511a GIT binary patch literal 597 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 utSXgo9GOOSO3+ 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ℜ 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 literal 0 HcmV?d00001 From 442d12189ce6ee6fffefba593a29b3e57185554a Mon Sep 17 00:00:00 2001 From: Alexandru Branza Date: Tue, 20 Mar 2018 20:05:20 +0200 Subject: [PATCH 2/2] Improve Announcements --- client.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/client.js b/client.js index f870ad48..2e044bfb 100644 --- a/client.js +++ b/client.js @@ -145,6 +145,8 @@ function Tracker (client, announceUrl, opts) { self.client = client self._lastUploaded = 0 + self._lastDownloaded = 0 + self._downloadComplete = false debug('new tracker %s', announceUrl) @@ -211,21 +213,18 @@ Tracker.prototype._announce = function (opts) { downloaded: 0 // default, user should provide real value }, opts) - if (!opts.event && self.client.torrentLength != null && self.client.torrentLength <= opts.downloaded) { + if (!opts.event && self.client.torrentLength <= opts.downloaded) { if (opts.uploaded == self._lastUploaded) { return - } else { - // fix update event if torrentLength larger then opts.downloaded - opts.left = 0 - opts.downloaded = self.client.torrentLength + } else if (!self._downloadComplete) { + opts.downloaded = self._lastDownloaded } } - self._lastUploaded = opts.uploaded - if (self.client.torrentLength != null && opts.left == null) { - if (opts.event == 'completed') { + if (opts.event == 'completed' || self._downloadComplete) { // if completed, fix announce to full + self._downloadComplete = true opts.left = 0 opts.downloaded = self.client.torrentLength } else { @@ -233,6 +232,9 @@ Tracker.prototype._announce = function (opts) { } } + self._lastUploaded = opts.uploaded + self._lastDownloaded = opts.downloaded + self._requestImpl(self._announceUrl, opts) }