├── .gitignore ├── lib ├── torrent │ ├── index.js │ ├── createpieces.js │ ├── createfiles.js │ ├── requestmanager.js │ └── torrent.js ├── tracker │ ├── index.js │ ├── protocol.js │ ├── http.js │ ├── tracker.js │ └── udp.js ├── torrentdata │ ├── index.js │ ├── file.js │ ├── torrentdata.js │ ├── magnet.js │ └── http.js ├── util │ ├── processutils.js │ ├── bufferutils.js │ ├── bitfield.js │ └── bencode.js ├── message.js ├── dht.js ├── file.js ├── metadata.js ├── client.js ├── piece.js ├── extension │ └── metadata.js └── peer.js ├── package.json ├── test └── util │ ├── overflowlist-test.js │ └── bitfield-test.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /test.torrent 2 | /test-2.torrent 3 | /ref 4 | .project 5 | -------------------------------------------------------------------------------- /lib/torrent/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = exports = require('./torrent'); 3 | -------------------------------------------------------------------------------- /lib/tracker/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = exports = require('./tracker'); 3 | -------------------------------------------------------------------------------- /lib/torrentdata/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = exports = require('./torrentdata'); 3 | -------------------------------------------------------------------------------- /lib/tracker/protocol.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 'http:': require('./http'), 4 | 'udp:': require('./udp') 5 | }; 6 | -------------------------------------------------------------------------------- /lib/util/processutils.js: -------------------------------------------------------------------------------- 1 | 2 | function nextTick(callback) { 3 | setTimeout(callback, 0); 4 | } 5 | 6 | exports.nextTick = nextTick; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-torrent", 3 | "description": "Bittorrent client for node.js.", 4 | "version": "0.2.1", 5 | "homepage" : "http://github.com/superafroman/node-torrent", 6 | "author": "Max Stewart ", 7 | "main": "lib/client.js", 8 | "repository" : { 9 | "type" : "git", 10 | "url" : "http://github.com/superafroman/node-torrent.git" 11 | }, 12 | "dependencies": { 13 | "base32": ">= 0.0.5", 14 | "log4js": ">= 0.3.0", 15 | "vows": ">= 0.5.6", 16 | "dht.js": ">= 0.2.14" 17 | }, 18 | "directories": { 19 | "lib": "./lib" 20 | }, 21 | "engines": { "node": ">= 0.8.0" } 22 | } 23 | -------------------------------------------------------------------------------- /test/util/overflowlist-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | 4 | var OverflowList = require('../../lib/util/overflowlist'); 5 | 6 | vows.describe('OverflowList').addBatch({ 7 | "A full OverflowList with overflow size 3": { 8 | topic: function() { 9 | var list = new OverflowList(3); 10 | list.push(1); 11 | list.push(2); 12 | list.push(3); 13 | return list; 14 | }, 15 | "after calling `push(4)`": { 16 | topic: function(list) { 17 | list.push(4); 18 | return list; 19 | }, 20 | "should contain `[2, 3, 4]`": function(result) { 21 | assert.equal(result.list[0], 2); 22 | assert.equal(result.list[1], 3); 23 | assert.equal(result.list[2], 4); 24 | }, 25 | "should have length `3`": function(result) { 26 | assert.equal(result.length, 3); 27 | } 28 | } 29 | } 30 | }).export(module); -------------------------------------------------------------------------------- /lib/torrentdata/file.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | bencode = require('../util/bencode'); 3 | 4 | var LOGGER = require('log4js').getLogger('metadata/file.js'); 5 | 6 | /** 7 | * Retrieve torrent metadata from the filesystem. 8 | */ 9 | var FileMetadata = { 10 | 11 | load: function(url, callback) { 12 | 13 | var path; 14 | if (url.match(/^file:/)) { 15 | path = url.substring(7); 16 | } else { 17 | path = url; 18 | } 19 | 20 | LOGGER.debug('Reading file metadata from ' + path); 21 | 22 | fs.readFile(path, 'binary', function(error, data) { 23 | if (error) { 24 | callback(error); 25 | } else { 26 | try { 27 | var metadata = bencode.decode(data.toString('binary')); 28 | callback(null, metadata); 29 | } catch(e) { 30 | callback(e); 31 | } 32 | } 33 | }); 34 | } 35 | }; 36 | 37 | module.exports = exports = FileMetadata; 38 | 39 | /* 40 | 41 | var R = require('./lib/metadata/file') 42 | var r = new R('file:///home/mstewar/Downloads/ubuntu-12.10-desktop-amd64.iso.torrent'); 43 | r.retrieve(function(){console.log(arguments);}); 44 | 45 | */ -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | 2 | var BufferUtils = require('./util/bufferutils'); 3 | 4 | var Message = function(code, payload) { 5 | this.code = code; 6 | this.payload = payload; 7 | }; 8 | 9 | Message.prototype.writeTo = function(stream) { 10 | if (this.code === Message.KEEPALIVE) { 11 | stream.write(BufferUtils.fromInt(0)); 12 | } else { 13 | var length = 1 + (this.payload ? this.payload.length : 0); 14 | stream.write(BufferUtils.fromInt(length)); 15 | 16 | var code = new Buffer(1); 17 | code[0] = this.code; 18 | stream.write(code); 19 | 20 | if (this.payload) { 21 | stream.write(this.payload); 22 | } 23 | } 24 | }; 25 | 26 | Message.KEEPALIVE = -1; 27 | Message.CHOKE = 0; 28 | Message.UNCHOKE = 1; 29 | Message.INTERESTED = 2; 30 | Message.UNINTERESTED = 3; 31 | Message.HAVE = 4; 32 | Message.BITFIELD = 5; 33 | Message.REQUEST = 6; 34 | Message.PIECE = 7; 35 | Message.CANCEL = 8; 36 | Message.PORT = 9; 37 | Message.EXTENDED = 20; 38 | 39 | Message.EXTENDED_HANDSHAKE = 'nt_handshake'; 40 | Message.EXTENDED_METADATA = 'ut_metadata'; 41 | 42 | module.exports = Message; -------------------------------------------------------------------------------- /lib/torrentdata/torrentdata.js: -------------------------------------------------------------------------------- 1 | 2 | var Metadata = require('../metadata') 3 | , Tracker = require('../tracker') 4 | ; 5 | 6 | var LOGGER = require('log4js').getLogger('torrentdata/torrentdata.js'); 7 | 8 | var loaders = { 9 | 'http:': require('./http'), 10 | 'https:': require('./http'), 11 | 'file:': require('./file'), 12 | 'magnet:': require('./magnet') 13 | }; 14 | 15 | TorrentData = { 16 | load: function(url, callback) { 17 | var parsedUrl = require('url').parse(url), 18 | protocol = parsedUrl.protocol || 'file:' 19 | loader = loaders[protocol]; 20 | 21 | if (!loader) { 22 | callback(new Error('No metadata parser for given URL, URL = ' + url)); 23 | } else { 24 | loader.load(url, function(error, torrentData) { 25 | if (error) { 26 | callback(error); 27 | } else { 28 | callback(null, 29 | new Metadata(torrentData.infoHash, torrentData.info), 30 | Tracker.createTrackers(torrentData['announce'], torrentData['announce-list'])); 31 | } 32 | }); 33 | } 34 | } 35 | }; 36 | 37 | module.exports = exports = TorrentData; 38 | -------------------------------------------------------------------------------- /lib/torrent/createpieces.js: -------------------------------------------------------------------------------- 1 | var Piece = require('../piece'); 2 | 3 | var LOGGER = require('log4js').getLogger('createpieces.js'); 4 | 5 | function createPieces(hashes, files, pieceLength, sizeOfDownload, callback) { 6 | var pieces = [], 7 | numberOfPieces = hashes.length / 20, 8 | currentIndex = 0; 9 | 10 | createPiece(pieces, hashes, files, currentIndex, numberOfPieces, pieceLength, 11 | sizeOfDownload, callback); 12 | } 13 | 14 | function createPiece(pieces, hashes, files, currentIndex, numberOfPieces, pieceLength, 15 | sizeOfDownload, callback) { 16 | if (currentIndex === numberOfPieces) { 17 | callback(null, pieces); 18 | } else { 19 | var hash = hashes.substr(currentIndex * 20, 20) 20 | , lengthOfNextPiece = pieceLength; 21 | ; 22 | if (currentIndex === (numberOfPieces - 1)) { 23 | lengthOfNextPiece = sizeOfDownload % pieceLength; 24 | } 25 | var piece = new Piece(currentIndex, currentIndex * pieceLength, lengthOfNextPiece, hash, 26 | files, function() { 27 | createPiece(pieces, hashes, files, currentIndex + 1, numberOfPieces, pieceLength, 28 | sizeOfDownload, callback); 29 | }); 30 | pieces.push(piece); 31 | } 32 | } 33 | 34 | module.exports = exports = createPieces; 35 | -------------------------------------------------------------------------------- /lib/dht.js: -------------------------------------------------------------------------------- 1 | 2 | var dht = require('dht.js'); 3 | 4 | var LOGGER = require('log4js').getLogger('dht.js'); 5 | 6 | var bootstrapNodes = [ 7 | { address: 'router.bittorrent.com', port: 6881 }, 8 | { address: 'router.utorrent.com', port: 6881 } 9 | ]; 10 | 11 | var node = null, 12 | hashes = {}; 13 | 14 | var DHT = { 15 | 16 | init: function(callback) { 17 | 18 | node = dht.node.create(); 19 | 20 | node.on('peer:new', handleNewPeer); 21 | 22 | node.on('error', function(error) { 23 | LOGGER.error('Error recieved from DHT node. error = ' + error); 24 | console.log(error); 25 | }); 26 | 27 | node.once('listening', function() { 28 | LOGGER.debug('Initialised DHT node on port %j', node.port); 29 | bootstrapNodes.forEach(function(bootstrapNode) { 30 | LOGGER.debug('Connecting to node at ' + bootstrapNode.address + ':' + bootstrapNode.port); 31 | node.connect(bootstrapNode); 32 | }); 33 | if (callback) { 34 | callback(); 35 | } 36 | }); 37 | }, 38 | 39 | advertise: function(infohash, callback) { 40 | hashes[infohash] = callback; 41 | node.advertise(infohash); 42 | } 43 | }; 44 | 45 | function handleNewPeer(infohash, peer, isAdvertised) { 46 | LOGGER.debug('Handling peer connection over DHT'); 47 | if (!isAdvertised) { 48 | LOGGER.debug('Incoming peer connection not advertised, ignoring.'); 49 | return; 50 | } 51 | if (hashes[infohash]) { 52 | hashes[infohash](null, peer.address, peer.port); 53 | } 54 | } 55 | 56 | module.exports = exports = DHT; 57 | -------------------------------------------------------------------------------- /test/util/bitfield-test.js: -------------------------------------------------------------------------------- 1 | var vows = require('vows'); 2 | var assert = require('assert'); 3 | 4 | var BitField = require('../../lib/util/bitfield'); 5 | 6 | vows.describe('BitField').addBatch({ 7 | "A BitField set to 10101": { 8 | topic: function() { 9 | var bf = new BitField(5); 10 | bf.set(0); 11 | bf.set(2); 12 | bf.set(4); 13 | return bf; 14 | }, 15 | "when calling `xor(BitField(00111))`": { 16 | topic: function(bitfield) { 17 | var rhs = new BitField(5); 18 | rhs.set(2); 19 | rhs.set(3); 20 | rhs.set(4); 21 | return bitfield.xor(rhs); 22 | }, 23 | "should return `BitField(10010)`": function(result) { 24 | assert.ok(result.isSet(0)); 25 | assert.ok(!result.isSet(1)); 26 | assert.ok(!result.isSet(2)); 27 | assert.ok(result.isSet(3)); 28 | assert.ok(!result.isSet(4)); 29 | } 30 | }, 31 | "when calling `and(BitField(00111))`": { 32 | topic: function(bitfield) { 33 | var rhs = new BitField(5); 34 | rhs.set(2); 35 | rhs.set(3); 36 | rhs.set(4); 37 | return bitfield.and(rhs); 38 | }, 39 | "should return `BitField(00101)`": function(result) { 40 | assert.ok(!result.isSet(0)); 41 | assert.ok(!result.isSet(1)); 42 | assert.ok(result.isSet(2)); 43 | assert.ok(!result.isSet(3)); 44 | assert.ok(result.isSet(4)); 45 | } 46 | }, 47 | "when calling `setIndices()`": { 48 | topic: function(bitfield) { 49 | return bitfield.setIndices(); 50 | }, 51 | "should return `[0, 2, 4]`": function(result) { 52 | assert.equal(result[0], 0); 53 | assert.equal(result[1], 2); 54 | assert.equal(result[2], 4); 55 | } 56 | } 57 | } 58 | }).export(module); -------------------------------------------------------------------------------- /lib/util/bufferutils.js: -------------------------------------------------------------------------------- 1 | 2 | function concat() { 3 | var length = 0; 4 | for (var i = 0; i < arguments.length; i++) { 5 | length += arguments[i].length; 6 | } 7 | var nb = new Buffer(length); 8 | var pos = 0; 9 | for (var i = 0; i < arguments.length; i++) { 10 | var b = arguments[i]; 11 | b.copy(nb, pos, 0); 12 | pos += b.length; 13 | } 14 | return nb; 15 | } 16 | 17 | function equal(b1, b2) { 18 | if (b1.length != b2.length) { 19 | return false; 20 | } 21 | for (var i = 0; i < b1.length; i++) { 22 | if (b1[i] != b2[i]) { 23 | return false; 24 | } 25 | } 26 | return true; 27 | } 28 | 29 | function fromInt(int) { 30 | var b = new Buffer(4); 31 | b[0] = int >> 24 & 0xff; 32 | b[1] = int >> 16 & 0xff; 33 | b[2] = int >> 8 & 0xff; 34 | b[3] = int & 0xff; 35 | return b; 36 | } 37 | 38 | function readInt(buffer, offset) { 39 | offset = offset || 0; 40 | return buffer[offset] << 24 | 41 | buffer[offset + 1] << 16 | 42 | buffer[offset + 2] << 8 | 43 | buffer[offset + 3]; 44 | } 45 | 46 | function fromInt16(int) { 47 | var b = new Buffer(2); 48 | b[2] = int >> 8 & 0xff; 49 | b[3] = int & 0xff; 50 | return b; 51 | } 52 | 53 | function readInt16(buffer, offset) { 54 | offset = offset || 0; 55 | return buffer[offset + 2] << 8 | 56 | buffer[offset + 3]; 57 | } 58 | 59 | function slice(buffer, start, end) { 60 | if (start < 0) start = 0; 61 | if (!end || end > buffer.length) end = buffer.length; 62 | 63 | var b = new Buffer(end - start); 64 | buffer.copy(b, 0, start, end); 65 | return b; 66 | } 67 | 68 | exports.concat = concat; 69 | exports.equal = equal; 70 | exports.fromInt = fromInt; 71 | exports.readInt = readInt; 72 | exports.fromInt16 = fromInt16; 73 | exports.readInt16 = readInt16; 74 | exports.slice = slice; 75 | -------------------------------------------------------------------------------- /lib/torrentdata/magnet.js: -------------------------------------------------------------------------------- 1 | 2 | var base32 = require('base32'); 3 | 4 | var LOGGER = require('log4js').getLogger('metadata/magnet.js'); 5 | 6 | /** 7 | * Retrieve torrent metadata from magnet URL. 8 | */ 9 | var MagnetMetadata = { 10 | load: function(url, callback) { 11 | 12 | if (!url.match(/^magnet:/)) { 13 | callback(new Error('Given URL is not a magnet URL.')); 14 | } 15 | 16 | LOGGER.debug('Reading magnet metadata from ' + url); 17 | 18 | var parsedUrl = require('url').parse(url, true), 19 | hash; 20 | 21 | var urns = parsedUrl.query.xt; 22 | if (!Array.isArray(urns)) { 23 | urns = [urns]; 24 | } 25 | urns.some(function(urn) { 26 | if (urn.match(/^urn:btih:/)) { 27 | hash = urn.substring(9); 28 | return true; 29 | } 30 | }); 31 | 32 | if (!hash) { 33 | callback(new Error('No supported xt URN provided.')); 34 | } else { 35 | var infoHash; 36 | if (hash.length === 40) { 37 | infoHash = new Buffer(hash, 'hex'); 38 | } else { 39 | infoHash = new Buffer(base32.decode(hash), 'binary'); 40 | } 41 | 42 | if (parsedUrl.query.tr) { 43 | var trackers = parsedUrl.query.tr; 44 | if (!Array.isArray(trackers)) { 45 | trackers = [trackers]; 46 | } 47 | } 48 | 49 | callback(null, { 50 | infoHash: infoHash, 51 | info: { 52 | name: parsedUrl.query.dn 53 | }, 54 | 'announce-list': trackers 55 | }); 56 | } 57 | } 58 | }; 59 | 60 | module.exports = exports = MagnetMetadata; 61 | 62 | /* 63 | 64 | var R = require('./lib/metadata/magnet') 65 | var r = new R('magnet:?xt=urn:ed2k:354B15E68FB8F36D7CD88FF94116CDC1&dn=mediawiki-1.15.1.tar.gz'); 66 | r.retrieve(function(){console.log(arguments);}); 67 | 68 | magnet:?xt=urn:ed2k:354B15E68FB8F36D7CD88FF94116CDC1 69 | &xl=10826029&dn=mediawiki-1.15.1.tar.gz 70 | &xt=urn:tree:tiger:7N5OAMRNGMSSEUE3ORHOKWN4WWIQ5X4EBOOTLJY 71 | &xt=urn:btih:QHQXPYWMACKDWKP47RRVIV7VOURXFE5Q 72 | &tr=http%3A%2F%2Ftracker.example.org%2Fannounce.php%3Fuk%3D1111111111%26 73 | &as=http%3A%2F%2Fdownload.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gz 74 | &xs=http%3A%2F%2Fcache.example.org%2FXRX2PEFXOOEJFRVUCX6HMZMKS5TWG4K5 75 | &xs=dchub://example.org 76 | 77 | */ -------------------------------------------------------------------------------- /lib/torrentdata/http.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | https = require('https'), 3 | bencode = require('../util/bencode'); 4 | 5 | var LOGGER = require('log4js').getLogger('metadata/http.js'); 6 | 7 | /** 8 | * Retrieve torrent metadata over http/https. 9 | */ 10 | var HttpMetadata = { 11 | load: function(url, callback) { 12 | 13 | if (!url.match(/^https?:/)) { 14 | callback(new Error('Given URL is not an http URL.')); 15 | } 16 | 17 | LOGGER.debug('Reading http metadata from ' + url); 18 | 19 | var request; 20 | 21 | if (url.match(/^http:/)) { 22 | request = http; 23 | } else { 24 | request = https; 25 | } 26 | 27 | request.get(url, function(response) { 28 | 29 | LOGGER.debug('Response recieved from metadata request. status = ' + response.statusCode); 30 | 31 | var buffers = []; 32 | var length = 0; 33 | 34 | response.on('data', function(chunk) { 35 | buffers.push(chunk); 36 | length += chunk.length; 37 | }); 38 | 39 | response.on('end', function() { 40 | var body = new Buffer(length); 41 | var pos = 0; 42 | for (var i = 0; i < buffers.length; i++) { 43 | body.write(buffers[i].toString('binary'), pos, 'binary'); 44 | pos += buffers[i].length; 45 | } 46 | if (response.statusCode === 200) { 47 | var metadata = bencode.decode(body.toString('binary')); 48 | callback(null, metadata); 49 | } else if (response.statusCode >= 300 && response.statusCode < 400) { 50 | var location = response.headers['location']; 51 | if (location) { 52 | load(location, callback); 53 | } else { 54 | callback(new Error('Received redirect response with no location header. status = ' 55 | + response.statusCode)); 56 | } 57 | } else { 58 | callback(new Error('Unknown response code recieved from metadata request. code = ' 59 | + response.statusCode + ', message = ' + body.toString())); 60 | } 61 | }); 62 | }).on('error', function(e) { 63 | callback(e); 64 | }); 65 | } 66 | }; 67 | 68 | module.exports = exports = HttpMetadata; 69 | 70 | /* 71 | 72 | var R = require('./lib/metadata/http') 73 | var r = new R('http://releases.ubuntu.com/12.10/ubuntu-12.10-desktop-amd64.iso.torrent'); 74 | r.retrieve(function(){console.log(arguments);}); 75 | 76 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-torrent 2 | 3 | A simple bittorrent client for node. 4 | 5 | ## Features 6 | * .torrent support 7 | * Resume 8 | * Seeding 9 | * UDP trackers 10 | * DHT peer discovery 11 | * Magnet links 12 | * Add torrent via URL (file:, https:, http:, magnet:) 13 | 14 | ## In progress 15 | * Expose nice programmatic API for interacting with torrents/peers/trackers 16 | 17 | ## TODO 18 | * Share ratio 19 | * Accurate reporting on download/upload speeds 20 | * Limit download/upload speeds 21 | * Persist? 22 | 23 | ## Usage 24 | 25 | ```javascript 26 | var Client = require('node-torrent'); 27 | var client = new Client({logLevel: 'DEBUG'}); 28 | var torrent = client.addTorrent('a.torrent'); 29 | 30 | // when the torrent completes, move it's files to another area 31 | torrent.on('complete', function() { 32 | console.log('complete!'); 33 | torrent.files.forEach(function(file) { 34 | var newPath = '/new/path/' + file.path; 35 | fs.rename(file.path, newPath); 36 | // while still seeding need to make sure file.path points to the right place 37 | file.path = newPath; 38 | }); 39 | }); 40 | ``` 41 | 42 | ## License 43 | 44 | (The MIT License) 45 | 46 | Copyright (c) 2011 Max Stewart <max.stewart@superafroman.com> 47 | 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of this software and associated documentation files (the 50 | 'Software'), to deal in the Software without restriction, including 51 | without limitation the rights to use, copy, modify, merge, publish, 52 | distribute, sublicense, and/or sell copies of the Software, and to 53 | permit persons to whom the Software is furnished to do so, subject to 54 | the following conditions: 55 | 56 | The above copyright notice and this permission notice shall be 57 | included in all copies or substantial portions of the Software. 58 | 59 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 60 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 61 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 62 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 63 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 64 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 65 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 66 | -------------------------------------------------------------------------------- /lib/file.js: -------------------------------------------------------------------------------- 1 | 2 | var fs = require('fs'); 3 | 4 | var LOGGER = require('log4js').getLogger('file.js'); 5 | 6 | var File = function(filePath, length, offset, cb) { 7 | this.path = filePath; 8 | this.length = length; 9 | this.offset = offset || 0; 10 | 11 | var self = this; 12 | fs.exists(filePath, function(exists) { 13 | if (exists) { 14 | var flag = 'r+'; 15 | } else { 16 | flag = 'w+'; 17 | } 18 | fs.open(filePath, flag, 0666, function(err, fd) { 19 | self.fd = fd; 20 | cb(err); 21 | }); 22 | }); 23 | }; 24 | 25 | File.prototype.contains = function(pieceOffset, length) { 26 | var fileEnd = this.offset + this.length; 27 | var pieceEnd = pieceOffset + length; 28 | 29 | if (pieceOffset >= this.offset && pieceEnd <= fileEnd) { 30 | return File.FULL; 31 | } 32 | if ((this.offset >= pieceOffset && this.offset <= pieceEnd) 33 | || (fileEnd >= pieceOffset && fileEnd <= pieceEnd)) { 34 | return File.PARTIAL; 35 | } 36 | return File.NONE; 37 | }; 38 | 39 | File.prototype.read = function(buffer, bufferOffset, pieceOffset, length, cb) { 40 | var self = this; 41 | var match = self.contains(pieceOffset, length); 42 | if (match === File.PARTIAL || match === File.FULL) { 43 | var bounds = calculateBounds(self, pieceOffset, length); 44 | self.busy = true; 45 | fs.read(self.fd, buffer, bufferOffset, bounds.dataLength, bounds.offset, function(err, bytesRead) { 46 | self.busy = false; 47 | cb(err, bytesRead); 48 | }); 49 | } 50 | else { 51 | cb(null, 0); 52 | } 53 | }; 54 | 55 | File.prototype.write = function(pieceOffset, data, cb) { 56 | var self = this; 57 | var match = self.contains(pieceOffset, data.length); // TODO: undefined 58 | if (match === File.PARTIAL || match === File.FULL) { 59 | var bounds = calculateBounds(self, pieceOffset, data.length); 60 | self.busy = true; 61 | fs.write(self.fd, data, bounds.dataOffset, bounds.dataLength, bounds.offset, function(err, bytesWritten) { 62 | self.busy = false; 63 | cb(err, bytesWritten); 64 | }); 65 | } 66 | else { 67 | cb(null, 0); 68 | } 69 | }; 70 | 71 | function calculateBounds(self, offset, length) { 72 | 73 | var dataStart = Math.max(self.offset, offset); 74 | var dataEnd = Math.min(self.offset+self.length, offset+length); 75 | 76 | return { 77 | dataOffset: dataStart - offset, 78 | dataLength: dataEnd - dataStart, 79 | offset: Math.max(offset-self.offset, 0) 80 | }; 81 | } 82 | 83 | File.PARTIAL = 'partial'; 84 | File.FULL = 'full'; 85 | File.NONE = 'none'; 86 | 87 | module.exports = File; 88 | -------------------------------------------------------------------------------- /lib/torrent/createfiles.js: -------------------------------------------------------------------------------- 1 | 2 | var File = require('../file') 3 | , fs = require('fs') 4 | , path = require('path') 5 | , ProcessUtils = require('../util/processutils') 6 | ; 7 | 8 | var LOGGER = require('log4js').getLogger('createfiles.js'); 9 | 10 | /** 11 | * Create files defined in the given metadata. 12 | */ 13 | function createFiles(downloadPath, metadata, callback) { 14 | 15 | var basePath = path.join(downloadPath, metadata.name); 16 | 17 | if (metadata.length) { 18 | var file = new File(basePath, metadata.length, null, function(error) { 19 | if (error) { 20 | callback(new Error('Error creating file, error = ' + error)); 21 | } else { 22 | callback(null, [file], metadata.length); 23 | } 24 | }); 25 | } else { 26 | makeDirectory(basePath, function(error) { 27 | if (error) { 28 | callback(error); 29 | } else { 30 | nextFile(basePath, metadata.files, [], 0, callback); 31 | } 32 | }); 33 | } 34 | } 35 | 36 | function nextFile(basePath, files, processedFiles, offset, callback) { 37 | if (files.length === 0) { 38 | callback(null, processedFiles, offset); 39 | } else { 40 | var file = files.shift() 41 | , pathArray = file.path.slice(0) 42 | ; 43 | checkPath(basePath, pathArray, function(error) { 44 | if (error) { 45 | callback(error); 46 | } else { 47 | processedFiles.push(new File(path.join(basePath, pathArray[0]), file.length, 48 | offset, function(error) { 49 | if (error) { 50 | callback(new Error('Error creating file, error = ' + error)); 51 | } else { 52 | offset += file.length; 53 | ProcessUtils.nextTick(function() { 54 | nextFile(basePath, files, processedFiles, offset, callback); 55 | }); 56 | } 57 | })); 58 | } 59 | }); 60 | } 61 | } 62 | 63 | function checkPath(basePath, pathArray, callback) { 64 | if (pathArray.length === 1) { 65 | callback(null); 66 | } else { 67 | var currentPath = path.join(basePath, pathArray.shift()); 68 | makeDirectory(currentPath, function(error) { 69 | if (error) { 70 | callback(error); 71 | } else { 72 | checkPath(currentPath, pathArray, callback); 73 | } 74 | }); 75 | } 76 | } 77 | 78 | function makeDirectory(path, callback) { 79 | fs.exists(path, function(pathExists) { 80 | if (!pathExists) { 81 | fs.mkdir(path, 0777, function(error) { 82 | if (error) { 83 | return callback(new Error("Couldn't create directory. error = " + error)); 84 | } 85 | callback(null); 86 | }); 87 | } else { 88 | callback(null); 89 | } 90 | }); 91 | } 92 | 93 | module.exports = exports = createFiles; 94 | -------------------------------------------------------------------------------- /lib/metadata.js: -------------------------------------------------------------------------------- 1 | 2 | var bencode = require('./util/bencode'), 3 | crypto = require("crypto"), 4 | util = require('util'), 5 | BitField = require('./util/bitfield') 6 | EventEmitter = require('events').EventEmitter, 7 | BufferUtils = require('./util/bufferutils'); 8 | 9 | var LOGGER = require('log4js').getLogger('metadata.js'); 10 | 11 | function Metadata(infoHash, metadata) { 12 | EventEmitter.call(this); 13 | this.infoHash = infoHash; 14 | this.bitfield = null; 15 | this._encodedMetadata = null; 16 | this._length = 0; 17 | this.setMetadata(metadata); 18 | } 19 | util.inherits(Metadata, EventEmitter); 20 | 21 | Metadata.prototype.isComplete = function() { 22 | if (!this.bitfield || this.bitfield.length === 0) { 23 | return false; 24 | } 25 | return this.bitfield.cardinality() === this.bitfield.length; 26 | }; 27 | 28 | Metadata.prototype.hasLength = function() { 29 | return this._length > 0; 30 | }; 31 | 32 | Metadata.prototype.setLength = function(length) { 33 | this._length = length; 34 | if (!this._encodedMetadata || this._encodedMetadata.length !== length) { 35 | this.bitfield = new BitField(Math.ceil(length / Metadata.BLOCK_SIZE)); 36 | this._encodedMetadata = new Buffer(length); 37 | } 38 | }; 39 | 40 | Metadata.prototype.setMetadata = function(_metadata) { 41 | 42 | if (!_metadata) return; 43 | 44 | var metadata = this; 45 | metadata._metadata = _metadata; 46 | 47 | Object.keys(_metadata).forEach(function(key) { 48 | metadata[key] = _metadata[key]; 49 | }); 50 | 51 | if (this.files && !this._encodedMetadata) { 52 | LOGGER.debug(this._encodedMetadata.length); 53 | LOGGER.debug(_metadata.pieces.length); 54 | LOGGER.debug(typeof(_metadata.pieces)); 55 | this._encodedMetadata = new Buffer(bencode.encode(_metadata)); 56 | LOGGER.debug(this._encodedMetadata.length); 57 | 58 | this.setLength(_encodedMetadata.length); 59 | this.bitfield.setAll(); 60 | } 61 | 62 | if (!this.infoHash) { 63 | this.infoHash = new Buffer(crypto.createHash('sha1') 64 | .update(bencode.encode(_metadata)) 65 | .digest(), 'binary'); 66 | LOGGER.debug('Metadata complete.'); 67 | this.emit(Metadata.COMPLETE); 68 | } else if (this.isComplete()) { 69 | var infoHash = new Buffer(crypto.createHash('sha1') 70 | .update(this._encodedMetadata) 71 | .digest(), 'binary'); 72 | if (!BufferUtils.equal(this.infoHash, infoHash)) { 73 | LOGGER.warn('Metadata is invalid, reseting.'); 74 | this.bitfield.unsetAll(); 75 | this.emit(Metadata.INVALID); 76 | throw "BOOM"; // TODO: why does re-encoding the metadata cos this to fail? 77 | } else { 78 | LOGGER.debug('Metadata complete.'); 79 | this.emit(Metadata.COMPLETE); 80 | } 81 | } 82 | }; 83 | 84 | Metadata.prototype.setPiece = function(index, data) { 85 | if (this.bitfield.isSet(index)) { 86 | return; 87 | } 88 | LOGGER.debug('Setting piece at index %d with %d bytes', index, data.length); 89 | this.bitfield.set(index); 90 | data.copy(this._encodedMetadata, index * Metadata.BLOCK_SIZE, 0, data.length); 91 | if (this.isComplete()) { 92 | this.setMetadata(bencode.decode(this._encodedMetadata.toString('binary'))); 93 | } 94 | }; 95 | 96 | Metadata.COMPLETE = 'metadata:complete'; 97 | Metadata.INVALID = 'metadata:invalid'; 98 | 99 | Metadata.BLOCK_SIZE = 16384; 100 | 101 | module.exports = exports = Metadata; 102 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 2 | var log4js = require("log4js") 3 | , net = require("net") 4 | , dht = require("./dht") 5 | , Peer = require("./peer") 6 | , Torrent = require("./torrent") 7 | , TorrentData = require("./torrentdata") 8 | ; 9 | 10 | var LOGGER = log4js.getLogger('client.js'); 11 | 12 | /** 13 | * Create a new torrent client. 14 | * 15 | * Options: 16 | * { id: '-NT0000-' || Buffer, 17 | * downloadPath: '.', 18 | * portRange: { start: 6881, end: 6889 }, 19 | * logLevel: 'TRACE' || 'DEBUG' || 'INFO' || ... } 20 | */ 21 | var Client = function(options) { 22 | 23 | options = options || {}; 24 | 25 | log4js.setGlobalLogLevel(log4js.levels[options.logLevel || 'WARN']); 26 | 27 | var id = options.id || '-NT0010-'; 28 | if (id instanceof Buffer) { 29 | if (id.length !== 20) { 30 | throw new Error('Client ID must be 20 bytes'); 31 | } 32 | this.id = id; 33 | } else { 34 | this.id = padId(id); 35 | } 36 | 37 | this.torrents = {}; 38 | this.downloadPath = options.downloadPath || '.'; 39 | this._server = net.createServer(this._handleConnection.bind(this)); 40 | this.port = listen(this._server, options.portRange); 41 | 42 | this._extensions = [ 43 | require('./extension/metadata') 44 | ]; 45 | 46 | dht.init(); 47 | }; 48 | 49 | Client.prototype.addExtension = function(ExtensionClass) { 50 | this._extensions.push(ExtensionClass); 51 | }; 52 | 53 | Client.prototype.addTorrent = function(url) { 54 | var torrent = new Torrent(this.id, this.port, this.downloadPath, url, this._extensions); 55 | var client = this; 56 | 57 | torrent.once(Torrent.INFO_HASH, function(infoHash) { 58 | LOGGER.debug('Received info hash event from torrent, starting.'); 59 | if (!client.torrents[infoHash]) { 60 | client.torrents[infoHash] = torrent; 61 | } 62 | torrent.start(); 63 | }); 64 | return torrent; 65 | }; 66 | 67 | Client.prototype.removeTorrent = function(torrent) { 68 | if (this.torrents[torrent.infoHash]) { 69 | this.torrents[torrent.infoHash].stop(); 70 | delete this.torrents[torrent.infoHash]; 71 | } 72 | }; 73 | 74 | Client.prototype._handleConnection = function(stream) { 75 | var peer = new Peer(stream), 76 | client = this; 77 | peer.once(Peer.CONNECT, function(infoHash) { 78 | var torrent = self.torrents[infoHash]; 79 | if (torrent) { 80 | peer.setTorrent(torrent); 81 | } else { 82 | peer.disconnect('Peer attempting to download unknown torrent.'); 83 | } 84 | }); 85 | }; 86 | 87 | function listen(server, portRange) { 88 | 89 | portRange = portRange || {}; 90 | 91 | var connected = false, 92 | port = portRange.start || 6881, 93 | endPort = portRange.end || port + 8; 94 | 95 | do { 96 | try { 97 | server.listen(port); 98 | connected = true; 99 | LOGGER.info('Listening for connections on %j', server.address()); 100 | } catch(err) { 101 | } 102 | } 103 | while (!connected && port++ != endPort); 104 | 105 | if (!connected) { 106 | throw new Error('Could not listen on any ports in range ' + startPort + ' - ' + endPort); 107 | } 108 | return port; 109 | } 110 | 111 | function padId(id) { 112 | 113 | var newId = new Buffer(20); 114 | newId.write(id, 0, 'ascii'); 115 | 116 | var start = id.length; 117 | for (var i = start; i < 20; i++) { 118 | newId[i] = Math.floor(Math.random() * 255); 119 | } 120 | return newId; 121 | } 122 | 123 | module.exports = exports = Client; -------------------------------------------------------------------------------- /lib/util/bitfield.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Object that represents a series of bits, i.e. 10001101. Bits are stored 4 | * in order, left to right, for example 5 | * 6 | * bits: 10001101 7 | * index: 01234567 8 | */ 9 | var BitField = function(/* length | buffer, length*/) { 10 | if (arguments.length !== 1 && arguments.length !== 2) { 11 | throw new Error('Must create BitField with either a length (Number) or a Buffer and a length.'); 12 | } 13 | if (typeof arguments[0] === 'number') { 14 | this.bits = new Uint8Array(arguments[0]); 15 | } else { 16 | this.bits = fromBuffer(arguments[0], arguments[1]); 17 | } 18 | this.length = this.bits.length; 19 | }; 20 | 21 | BitField.prototype.set = function(index) { 22 | this.bits[index] = 1; 23 | }; 24 | 25 | BitField.prototype.unset = function(index) { 26 | this.bits[index] = 0; 27 | }; 28 | 29 | BitField.prototype.toBuffer = function() { 30 | return toBuffer(this.bits); 31 | }; 32 | 33 | BitField.prototype.isSet = function(index) { 34 | return this.bits[index]; 35 | }; 36 | 37 | BitField.prototype.or = function(rhs) { 38 | var length = Math.min(this.length, rhs.length); 39 | var ret = new BitField(length); 40 | for (var i = 0; i < length; i++) { 41 | ret.bits[i] = this.bits[i] | rhs.bits[i]; 42 | } 43 | return ret; 44 | }; 45 | 46 | BitField.prototype.xor = function(rhs) { 47 | var length = Math.min(this.length, rhs.length); 48 | var ret = new BitField(length); 49 | for (var i = 0; i < length; i++) { 50 | ret.bits[i] = this.bits[i] ^ rhs.bits[i]; 51 | } 52 | return ret; 53 | }; 54 | 55 | BitField.prototype.and = function(rhs) { 56 | var length = Math.min(this.length, rhs.length); 57 | var ret = new BitField(length); 58 | for (var i = 0; i < length; i++) { 59 | ret.bits[i] = this.bits[i] & rhs.bits[i]; 60 | } 61 | return ret; 62 | }; 63 | 64 | BitField.prototype.cardinality = function() { 65 | var count = 0; 66 | for (var i = 0; i < this.bits.length; i++) { 67 | if (this.bits[i]) { 68 | count++; 69 | } 70 | } 71 | return count; 72 | }; 73 | 74 | BitField.prototype.setIndices = function() { 75 | var set = []; 76 | for (var i = 0; i < this.bits.length; i++) { 77 | if (this.bits[i]) { 78 | set.push(i); 79 | } 80 | } 81 | return set; 82 | }; 83 | 84 | BitField.prototype.unsetIndices = function() { 85 | var unset = []; 86 | for (var i = 0; i < this.bits.length; i++) { 87 | if (!this.bits[i]) { 88 | unset.push(i); 89 | } 90 | } 91 | return unset; 92 | }; 93 | 94 | BitField.prototype.setAll = function() { 95 | for (var i = 0; i < this.bits.length; i++) { 96 | this.set(i); 97 | } 98 | }; 99 | 100 | BitField.prototype.unsetAll = function() { 101 | for (var i = 0; i < this.bits.length; i++) { 102 | this.unset(i); 103 | } 104 | }; 105 | 106 | 107 | function toBuffer(array) { 108 | 109 | var buffer = new Buffer(Math.ceil(array.length / 8)); 110 | 111 | for (var i = 0; i < buffer.length; i++) { 112 | buffer[i] = 0; 113 | } 114 | 115 | for (i = 0; i < array.length; i++) { 116 | if (array[i]) { 117 | var bit = 7 - (i % 8) 118 | , byteIndex = ~~(i / 8); 119 | ; 120 | buffer[byteIndex] = buffer[byteIndex] | Math.pow(2, bit); 121 | } 122 | } 123 | return buffer; 124 | } 125 | 126 | function fromBuffer(buffer, length) { 127 | var array = new Uint8Array(length); 128 | for (var i = 0; i < length; i++) { 129 | var bit = 7 - (i % 8) 130 | , byteIndex = ~~(i / 8); 131 | ; 132 | array[i] = buffer[byteIndex] & Math.pow(2, bit) > 0 ? 1 : 0; 133 | } 134 | return array; 135 | } 136 | 137 | module.exports = exports = BitField; 138 | -------------------------------------------------------------------------------- /lib/tracker/http.js: -------------------------------------------------------------------------------- 1 | 2 | var bencode = require('../util/bencode'); 3 | var http = require('http'); 4 | 5 | var LOGGER = require('log4js').getLogger('http.js'); 6 | 7 | function HTTP() { 8 | } 9 | 10 | HTTP.prototype = { 11 | 12 | callback: null, 13 | 14 | data: null, 15 | 16 | event: null, 17 | 18 | tracker: null, 19 | 20 | handle: function(tracker, data, event, callback) { 21 | 22 | this.tracker = tracker; 23 | this.data = data; 24 | this.event = event; 25 | this.callback = callback; 26 | 27 | this._makeRequest(); 28 | }, 29 | 30 | _complete: function(trackerInfo, err) { 31 | this.callback(trackerInfo, err); 32 | }, 33 | 34 | _makeRequest: function() { 35 | var query = '?info_hash=' + escape(this.data['info_hash'].toString('binary')) + 36 | '&peer_id=' + escape(this.data['peer_id'].toString('binary')) + 37 | '&port=' + this.data['port'] + 38 | '&uploaded=' + this.data['uploaded'] + 39 | '&downloaded=' + this.data['downloaded'] + 40 | '&left=' + this.data['left'] + 41 | '&compact=1' + 42 | '&numwant=200' + 43 | '&event=' + this.event || 'empty'; 44 | 45 | if (this.tracker.trackerId) { 46 | query += '&trackerid=' + this.tracker.trackerId; 47 | } 48 | 49 | var options = { 50 | host: this.tracker.url.hostname, 51 | path: this.tracker.url.pathname + query, 52 | port: this.tracker.url.port }; 53 | 54 | var self = this; 55 | 56 | var req = http.get(options, function(res) { 57 | var buffers = []; 58 | var length = 0; 59 | res.on('data', function(chunk) { 60 | buffers.push(chunk); 61 | length += chunk.length; 62 | }); 63 | res.on('end', function() { 64 | var body = new Buffer(length); 65 | var pos = 0; 66 | for (var i = 0; i < buffers.length; i++) { 67 | body.write(buffers[i].toString('binary'), pos, 'binary'); 68 | pos += buffers[i].length; 69 | } 70 | if (res.statusCode === 200) { 71 | var response = bencode.decode(body.toString('binary')); 72 | self._parseResponse(response); 73 | } else { 74 | LOGGER.debug('Unexpected status code: ' + res.statusCode + ', response: ' + body.toString()); 75 | self._complete(null, new Error('Unexpected status code: ' + res.statusCode + ', response: ' + body.toString())); 76 | } 77 | }); 78 | }); 79 | req.on('error', function(e) { 80 | self._complete(null, new Error(e.message)); 81 | }); 82 | }, 83 | 84 | _parseResponse: function(response) { 85 | LOGGER.debug('parsing response from tracker'); 86 | if (response['failure reason']) { 87 | this._complete(null, new Error(response['failure reason'])); 88 | } else { 89 | var trackerInfo = { 90 | trackerId: response['tracker id'], 91 | interval: response['interval'], 92 | seeders: response.complete, 93 | leechers: response.incomplete, 94 | peers: [] }; 95 | 96 | if (response.peers) { 97 | if (typeof(response.peers) === 'string') { 98 | var peers = new Buffer(response.peers, 'binary'); 99 | for (var i = 0; i < peers.length; i += 6) { 100 | var ip = peers[i] + '.' + peers[i + 1] + '.' + peers[i + 2] + '.' + peers[i + 3]; 101 | var port = peers[i + 4] << 8 | peers[i + 5]; 102 | LOGGER.debug('Parsed peer ip:' + ip + ', port: ' + port); 103 | trackerInfo.peers.push({ 104 | ip: ip, 105 | port: port 106 | }); 107 | } 108 | } 109 | else { 110 | trackerInfo.peers = response.peers; 111 | } 112 | } 113 | this._complete(trackerInfo); 114 | } 115 | } 116 | }; 117 | 118 | module.exports = HTTP; 119 | -------------------------------------------------------------------------------- /lib/tracker/tracker.js: -------------------------------------------------------------------------------- 1 | 2 | var bencode = require('../util/bencode'), 3 | protocol = require('./protocol'), 4 | util = require('util'); 5 | 6 | var EventEmitter = require('events').EventEmitter; 7 | 8 | var LOGGER = require('log4js').getLogger('tracker.js'); 9 | 10 | var CONNECTING = 'connecting'; 11 | var ERROR = 'error'; 12 | var STOPPED = 'stopped'; 13 | var WAITING = 'waiting'; 14 | 15 | var ANNOUNCE_START_INTERVAL = 5; 16 | 17 | var Tracker = function(urls) { 18 | EventEmitter.call(this); 19 | if (!Array.isArray(urls)) { 20 | this._urls = [urls]; 21 | } else { 22 | this._urls = urls; 23 | } 24 | // TODO: need to step through URLs as part of announce process 25 | this.url = require('url').parse(this._urls[0]); 26 | this.torrent = null; 27 | this.state = STOPPED; 28 | this.seeders = 0; 29 | this.leechers = 0; 30 | }; 31 | util.inherits(Tracker, EventEmitter); 32 | 33 | Tracker.prototype.setTorrent = function(torrent) { 34 | this.torrent = torrent; 35 | }; 36 | 37 | Tracker.prototype.start = function(callback) { 38 | this.callback = callback; 39 | this._announce('started'); 40 | }; 41 | 42 | Tracker.prototype.stop = function() { 43 | this._announce('stopped'); 44 | }; 45 | 46 | Tracker.prototype._announce = function(event) { 47 | 48 | LOGGER.debug('Announce' + (event ? ' ' + event : '')); 49 | 50 | var handlerClass = protocol[this.url.protocol], 51 | tracker = this; 52 | 53 | if (handlerClass) { 54 | var handler = new handlerClass(); 55 | var data = { 56 | peer_id: this.torrent.clientId, 57 | info_hash: this.torrent.infoHash, 58 | port: this.torrent.clientPort 59 | }; 60 | this.state = CONNECTING; 61 | handler.handle(this, data, event, function(info, error) { 62 | if (error) { 63 | LOGGER.warn('announce error from ' + tracker.url.href + ': ' + error.message); 64 | tracker.state = ERROR; 65 | tracker.errorMessage = error.message; 66 | if (event === 'started') { 67 | LOGGER.warn('retry announce \'started\' in ' + ANNOUNCE_START_INTERVAL + 's'); 68 | setTimeout(function() { 69 | tracker._announce('started'); 70 | }, ANNOUNCE_START_INTERVAL * 1000); 71 | } 72 | } else { 73 | if (info.trackerId) { 74 | tracker.trackerId = info.trackerId; 75 | } 76 | tracker.state = WAITING; 77 | if (event === 'started') { 78 | var interval = info.interval; 79 | if (tracker.timeoutId) { 80 | clearInterval(tracker.timeoutId); 81 | } 82 | if (interval) { 83 | tracker.timeoutId = setInterval(function() { 84 | tracker._announce(null); 85 | }, interval * 1000); 86 | } 87 | } else if (event === 'stopped') { 88 | clearInterval(tracker.timeoutId); 89 | delete tracker.timeoutId; 90 | tracker.state = STOPPED; 91 | } 92 | } 93 | tracker._updateInfo(info); 94 | }); 95 | } 96 | }; 97 | 98 | Tracker.prototype._updateInfo = function(data) { 99 | LOGGER.debug('Updating details from tracker. ' + (data && data.peers ? data.peers.length : 0) + ' new peers'); 100 | if (data) { 101 | this.seeders = data.seeders || 0; 102 | this.leechers = data.leechers || 0; 103 | if (data.peers) { 104 | for (var i = 0; i < data.peers.length; i++) { 105 | var peer = data.peers[i]; 106 | this.callback(peer.peer_id, peer.ip, peer.port); 107 | } 108 | } 109 | this.emit('updated'); 110 | } 111 | }; 112 | 113 | Tracker.createTrackers = function(announce, announceList) { 114 | var trackers = []; 115 | if (announceList) { 116 | announceList.forEach(function(announce) { 117 | trackers.push(new Tracker(announce)); 118 | }); 119 | } else { 120 | trackers.push(new Tracker(announce)); 121 | } 122 | return trackers; 123 | }; 124 | 125 | module.exports = Tracker; 126 | -------------------------------------------------------------------------------- /lib/torrent/requestmanager.js: -------------------------------------------------------------------------------- 1 | var BitField = require('../util/bitfield') 2 | , Peer = require('../peer') 3 | , Piece = require('../piece') 4 | , Torrent = null 5 | ; 6 | 7 | var LOGGER = require('log4js').getLogger('torrent/requestmanager.js'); 8 | 9 | function RequestManager(torrent) { 10 | this._activePeers = {}; 11 | this._activePieces = null; 12 | this._bitfield = null; 13 | this._peers = []; 14 | this._pieces = null; 15 | this._torrent = torrent; 16 | 17 | // lazily require Torrent so it's initialised.. shouldn't really need to do this. 18 | // TODO: think of another way... 19 | Torrent = require('./torrent'); 20 | 21 | torrent.once(Torrent.READY, this._torrentReady.bind(this)); 22 | 23 | this.__addPeer_event = this._addPeer.bind(this); 24 | torrent.on(Torrent.PEER, this.__addPeer_event); 25 | } 26 | 27 | RequestManager.prototype._addPeer = function(peer) { 28 | LOGGER.debug('adding peer %s', peer.getIdentifier()); 29 | this._peers.push(peer); 30 | this.__peerDisconnect_event = this._peerDisconnect.bind(this); 31 | this.__peerReady_event = this._peerReady.bind(this); 32 | peer.on(Peer.DISCONNECT, this.__peerDisconnect_event); 33 | peer.on(Peer.READY, this.__peerReady_event); 34 | }; 35 | 36 | RequestManager.prototype._peerDisconnect = function(peer) { 37 | LOGGER.debug('_peerDisconnect: ' + peer.getIdentifier()); 38 | 39 | // TODO: review... 40 | 41 | var activePieces = this._activePieces; 42 | 43 | Object.keys(peer.pieces).forEach(function(key) { 44 | activePieces.unset(peer.pieces[key]); 45 | }); 46 | peer.pieces = {}; 47 | peer.removeListener(Peer.DISCONNECT, this.__peerDisconnect_event); 48 | peer.removeListener(Peer.READY, this.__peerReady_event); 49 | }; 50 | 51 | RequestManager.prototype._peerReady = function(peer) { 52 | LOGGER.debug('_peerReady: ' + peer.getIdentifier()); 53 | 54 | if (!this._torrent.hasMetadata()) { 55 | LOGGER.debug('Peer [%s] has no metadata, ignoring for now.', peer.getIdentifier()); 56 | return; 57 | } 58 | if (!this._bitfield) { 59 | LOGGER.debug('RequestManager not initialised, ignoring peer for now.'); 60 | return; 61 | } 62 | 63 | var activePieces = this._activePieces.setIndices() 64 | , nextPiece = null 65 | , requestManager = this 66 | ; 67 | 68 | // find an active piece for the peer 69 | activePieces.some(function(pieceIndex) { 70 | var piece = requestManager._pieces[pieceIndex]; 71 | if (!piece.hasRequestedAllChunks() && peer.bitfield.isSet(piece.index)) { 72 | nextPiece = piece; 73 | return piece; 74 | } 75 | }); 76 | 77 | if (!nextPiece) { 78 | // if no active piece found, pick a new piece and activate it 79 | 80 | // available = peerhas ^ (peerhas & (active | completed)) 81 | var available = peer.bitfield.xor( 82 | peer.bitfield.and(this._activePieces.or(this._bitfield))); 83 | 84 | // pick a random piece out of the available ones 85 | var set = available.setIndices(); 86 | var index = set[Math.round(Math.random() * (set.length - 1))]; 87 | if (index !== undefined) { 88 | nextPiece = this._pieces[index]; 89 | this._activePieces.set(index); 90 | } 91 | } 92 | if (nextPiece) { 93 | LOGGER.debug('Peer [%s] ready, requesting piece %d', peer.getIdentifier(), nextPiece.index); 94 | peer.requestPiece(nextPiece); 95 | } else if (peer.numRequests === 0) { 96 | LOGGER.debug('No available pieces for peer %s', peer.getIdentifier()); 97 | peer.setAmInterested(false); 98 | } 99 | }; 100 | 101 | RequestManager.prototype._pieceComplete = function(piece) { 102 | LOGGER.debug('pieceComplete: ' + piece.index); 103 | this._bitfield.set(piece.index); 104 | }; 105 | 106 | RequestManager.prototype._torrentReady = function() { 107 | LOGGER.debug('_torrentReady'); 108 | this._bitfield = torrent.bitfield; 109 | this._activePieces = new BitField(this._bitfield.length); 110 | this._pieces = torrent._pieces; 111 | 112 | var requestManager = this; 113 | this._pieces.forEach(function(piece) { 114 | piece.once(Piece.COMPLETE, requestManager._pieceComplete.bind(requestManager, piece)); 115 | }); 116 | 117 | this._peers.forEach(function(peer) { 118 | if (peer.isReady()) { 119 | requestManager._peerReady(peer); 120 | } 121 | }); 122 | }; 123 | 124 | module.exports = exports = RequestManager; 125 | -------------------------------------------------------------------------------- /lib/util/bencode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Original source: 3 | * https://github.com/WizKid/node-bittorrent/blob/master/lib/bencode.js 4 | */ 5 | 6 | function Decoder(content) { 7 | this.pos = 0; 8 | this.content = content; 9 | } 10 | 11 | Decoder.prototype = { 12 | decode: function(ignoreRemainder) { 13 | var ret = this._decode(); 14 | if (ignoreRemainder) { 15 | return [ret, this.pos]; 16 | } 17 | if (this.pos != this.content.length) { 18 | throw "Wrongly formatted bencoding string. Tried to parse something it didn't understood "+ this.pos +", "+ this.content.length; 19 | } 20 | return ret; 21 | }, 22 | 23 | _decode: function () { 24 | if (this.pos >= this.content.length) 25 | throw "Wrongly formatted bencoding string. Pos have passed the length of the string." 26 | 27 | var ret; 28 | var c = this.content.charAt(this.pos); 29 | switch (c) { 30 | // Integer 31 | case 'i': 32 | var s = this.pos + 1; 33 | while (this.pos < this.content.length && this.content.charAt(this.pos) != 'e') 34 | this.pos++; 35 | 36 | this.pos++; 37 | ret = parseInt(this.content.substring(s, this.pos)); 38 | break; 39 | 40 | // Dict 41 | case 'd': 42 | ret = {}; 43 | this.pos++; 44 | while (this.pos < this.content.length && this.content.charAt(this.pos) != 'e') { 45 | var key = this._decode(); 46 | if (key.constructor != String) 47 | throw "Keys in dict must be strings" 48 | ret[key] = this._decode(); 49 | } 50 | 51 | this.pos++; 52 | break; 53 | 54 | // List 55 | case 'l': 56 | ret = []; 57 | this.pos++; 58 | while (this.pos < this.content.length && this.content.charAt(this.pos) != 'e') 59 | ret.push(this._decode()); 60 | 61 | this.pos++; 62 | break; 63 | 64 | // String 65 | case '0': 66 | case '1': 67 | case '2': 68 | case '3': 69 | case '4': 70 | case '5': 71 | case '6': 72 | case '7': 73 | case '8': 74 | case '9': 75 | var s = this.pos; 76 | while (this.pos < this.content.length && this.content.charAt(this.pos) != ':') 77 | this.pos++; 78 | 79 | var len = parseInt(this.content.substring(s, this.pos)); 80 | s = this.pos + 1; 81 | this.pos = s + len; 82 | ret = this.content.substring(s, this.pos); 83 | break; 84 | 85 | default: 86 | throw "Can't decode. No type starts with: " + c + ", at position " + this.pos; 87 | break; 88 | } 89 | 90 | return ret; 91 | } 92 | } 93 | 94 | function encode(obj) { 95 | var ret; 96 | switch(obj.constructor) { 97 | case Number: 98 | if (Math.round(obj) !== obj) 99 | throw "Numbers can only contain integers and not floats"; 100 | 101 | ret = "i"+ obj.toString() +"e"; 102 | break; 103 | case String: 104 | ret = obj.length +":"+ obj; 105 | break; 106 | case Array: 107 | ret = "l"; 108 | for (var k in obj) 109 | ret += encode(obj[k]); 110 | ret += "e"; 111 | break; 112 | case Object: 113 | ret = "d"; 114 | for (var k in obj) 115 | ret += encode(k) + encode(obj[k]); 116 | ret += "e"; 117 | break; 118 | default: 119 | throw "Bencode can only encode integers, strings, lists and dicts"; 120 | break; 121 | } 122 | return ret; 123 | } 124 | 125 | exports.decode = function(content, ignoreRemainder) { 126 | var p = new Decoder(content); 127 | return p.decode(ignoreRemainder); 128 | } 129 | 130 | exports.encode = function(obj) { 131 | return encode(obj); 132 | } 133 | -------------------------------------------------------------------------------- /lib/tracker/udp.js: -------------------------------------------------------------------------------- 1 | 2 | var dgram = require('dgram'); 3 | 4 | var BufferUtils = require('../util/bufferutils'); 5 | 6 | var CONNECTION_ID = BufferUtils.concat( 7 | BufferUtils.fromInt(0x417), 8 | BufferUtils.fromInt(0x27101980)); 9 | 10 | var LOGGER = require('log4js').getLogger('udp.js'); 11 | 12 | // Actions 13 | var Action = { 14 | CONNECT: 0, 15 | ANNOUNCE: 1, 16 | SCRAPE: 2, 17 | ERROR: 3 18 | }; 19 | 20 | function UDP() { 21 | } 22 | 23 | UDP.prototype = { 24 | 25 | callback: null, 26 | 27 | connectionId: null, 28 | 29 | data: null, 30 | 31 | event: null, 32 | 33 | socket: null, 34 | 35 | tracker: null, 36 | 37 | transactionId: null, 38 | 39 | handle: function(tracker, data, event, callback) { 40 | 41 | this.tracker = tracker; 42 | this.data = data; 43 | this.event = event; 44 | this.callback = callback; 45 | 46 | var self = this; 47 | this.socket = dgram.createSocket('udp4', function(msg, rinfo) { 48 | self._handleMessage(msg); 49 | }).on('error', function(e) { 50 | self._complete(null, new Error(e.message)); 51 | }); 52 | this._connect(); 53 | }, 54 | 55 | _announce: function() { 56 | LOGGER.debug('Sending announce request to UDP tracker at ' + this.tracker.url.hostname + ':' + this.tracker.url.port); 57 | this._generateTransactionId(); 58 | var packet = BufferUtils.concat(this.connectionId, 59 | BufferUtils.fromInt(Action.ANNOUNCE), 60 | this.transactionId, this.data['info_hash'], this.data['peer_id'], 61 | BufferUtils.fromInt(0), BufferUtils.fromInt(this.data['downloaded'] || 0), // int64, TODO: split data into two parts etc 62 | BufferUtils.fromInt(0), BufferUtils.fromInt(this.data['left'] || 0), // 64 63 | BufferUtils.fromInt(0), BufferUtils.fromInt(this.data['uploaded'] || 0), //64 64 | BufferUtils.fromInt(this.event), 65 | BufferUtils.fromInt(0), 66 | BufferUtils.fromInt(Math.random() * 255), 67 | BufferUtils.fromInt(200), 68 | BufferUtils.fromInt16(this.data['port']) 69 | ); 70 | this._send(packet); 71 | }, 72 | 73 | _announceResponse: function(msg) { 74 | 75 | var trackerInfo = { 76 | interval: BufferUtils.readInt(msg, 8), 77 | leechers: BufferUtils.readInt(msg, 12), 78 | seeders: BufferUtils.readInt(msg, 16), 79 | peers: [] 80 | }; 81 | 82 | for (var i = 20; i < msg.length; i += 6) { 83 | var ip = msg[i] + '.' + msg[i + 1] + '.' + msg[i + 2] + '.' + msg[i + 3]; 84 | var port = msg[i + 4] << 8 | msg[i + 5]; 85 | LOGGER.debug('Parsed peer with details: ' + ip + ':' + port); 86 | trackerInfo.peers.push({ip: ip, port: port}); 87 | } 88 | 89 | this._complete(trackerInfo); 90 | }, 91 | 92 | _complete: function(trackerInfo, err) { 93 | try { 94 | this.socket.close(); 95 | } catch(e) {} 96 | this.callback(trackerInfo, err); 97 | }, 98 | 99 | _connect: function() { 100 | LOGGER.debug('sending connect request to UDP tracker at ' + this.tracker.url.hostname + ':' + this.tracker.url.port); 101 | this._generateTransactionId(); 102 | var packet = BufferUtils.concat(CONNECTION_ID, BufferUtils.fromInt(Action.CONNECT), this.transactionId); 103 | this._send(packet); 104 | }, 105 | 106 | _generateTransactionId: function() { 107 | LOGGER.debug('generating transaction id'); 108 | var id = new Buffer(4); 109 | id[0] = Math.random() * 255; 110 | id[1] = Math.random() * 255; 111 | id[2] = Math.random() * 255; 112 | id[3] = Math.random() * 255; 113 | this.transactionId = id; 114 | }, 115 | 116 | _handleMessage: function(msg) { 117 | LOGGER.debug('handling message from tracker'); 118 | var action = BufferUtils.readInt(msg); 119 | var responseTransactionId = BufferUtils.slice(msg, 4, 8); 120 | console.log(responseTransactionId, this.transactionId); 121 | if (BufferUtils.equal(responseTransactionId, this.transactionId)) { 122 | LOGGER.debug('transactionIds equals, action = ' + action); 123 | switch (action) { 124 | case Action.CONNECT: 125 | this.connectionId = BufferUtils.slice(msg, 8, 16); 126 | LOGGER.debug('Received connectionId from server, id = ' + this.connectionId); 127 | this._announce(); 128 | break; 129 | case Action.ANNOUNCE: 130 | LOGGER.debug('Received announce response.'); 131 | this._announceResponse(msg); 132 | break; 133 | case Action.SCRAPE: 134 | break; 135 | case Action.ERROR: 136 | LOGGER.debug('Received error from server.'); 137 | var message = BufferUtils.slice(msg, 8, msg.length); 138 | this._complete(null, new Error(message.toString('utf8'))); 139 | break; 140 | default: 141 | LOGGER.warn('Unknown action received from server. Action = ' + action); 142 | } 143 | } else { 144 | this._complete(null, new Error('Received invalid transactionId from server.')); 145 | } 146 | }, 147 | 148 | _send: function(packet) { 149 | var self = this; 150 | this.socket.send(packet, 0, packet.length, this.tracker.url.port, this.tracker.url.hostname, function(err) { 151 | LOGGER.debug('packet sent, err = ', err); 152 | if (err) { 153 | self._complete(null, err); 154 | } 155 | }); 156 | } 157 | }; 158 | 159 | module.exports = UDP; 160 | -------------------------------------------------------------------------------- /lib/piece.js: -------------------------------------------------------------------------------- 1 | 2 | var crypto = require("crypto"); 3 | var util = require('util'); 4 | 5 | var ProcessUtils = require('./util/processutils'); 6 | var BitField = require('./util/bitfield'); 7 | var BufferUtils = require('./util/bufferutils'); 8 | var EventEmitter = require('events').EventEmitter; 9 | var File = require('./file'); 10 | 11 | var LOGGER = require('log4js').getLogger('piece.js'); 12 | 13 | var Piece = function(index, offset, length, hash, files, callback) { 14 | EventEmitter.call(this); 15 | 16 | this.complete = new BitField(Math.ceil(length / Piece.CHUNK_LENGTH)); 17 | this.files = []; 18 | this.hash = hash; 19 | this.index = index; 20 | this.length = length; 21 | this.offset = offset; 22 | this.requested = new BitField(this.complete.length); 23 | this.setMaxListeners(this.requested.length); 24 | 25 | var lastMatch = File.NONE; 26 | for (var i = 0; i < files.length; i++) { 27 | var file = files[i]; 28 | var match = file.contains(this.offset, this.length); 29 | if (match === File.FULL 30 | || (match === File.PARTIAL 31 | && lastMatch === File.PARTIAL)) { 32 | this.files.push(file); 33 | } else if (match === File.PARTIAL) { 34 | this.files.push(file); 35 | } 36 | lastMatch = match; 37 | } 38 | 39 | var self = this; 40 | this.isValid(function(valid) { 41 | if (valid) { 42 | setState(self, Piece.COMPLETE); 43 | } else { 44 | setState(self, Piece.INCOMPLETE); 45 | } 46 | callback(); 47 | }); 48 | }; 49 | util.inherits(Piece, EventEmitter); 50 | 51 | Piece.prototype.cancelRequest = function(begin) { 52 | var index = begin / Piece.CHUNK_LENGTH; 53 | this.requested.unset(index); 54 | }; 55 | 56 | Piece.prototype.getData = function(begin, length, cb) { 57 | var data = new Buffer(length) 58 | , dataOffset = 0 59 | , files = this.files.slice(0) 60 | , self = this 61 | ; 62 | (function next() { 63 | if (files.length === 0 || dataOffset >= length) { 64 | cb(null, data); 65 | } else { 66 | var file = files.shift(); 67 | file.read(data, dataOffset, self.offset + begin, length, function(error, bytesRead) { 68 | if (error) { 69 | cb(error); 70 | } else { 71 | dataOffset += bytesRead; 72 | ProcessUtils.nextTick(next); 73 | } 74 | }); 75 | } 76 | })(); 77 | }; 78 | 79 | Piece.prototype.hasRequestedAllChunks = function() { 80 | return this.requested.cardinality() === this.requested.length; 81 | }; 82 | 83 | Piece.prototype.isComplete = function() { 84 | return this.state === Piece.COMPLETE; 85 | }; 86 | 87 | Piece.prototype.isValid = function(cb) { 88 | var self = this; 89 | this.getData(0, this.length, function(error, data) { 90 | if (error) { 91 | cb(error); 92 | } else { 93 | var dataHash = crypto.createHash('sha1').update(data).digest(); 94 | cb(self.hash === dataHash); 95 | } 96 | }); 97 | }; 98 | 99 | Piece.prototype.nextChunk = function() { 100 | 101 | if (this.state === Piece.COMPLETE) { 102 | return null; 103 | } 104 | 105 | var indices = this.requested.or(this.complete).unsetIndices(); 106 | if (indices.length === 0) { 107 | return null; 108 | } 109 | this.requested.set(indices[0]); 110 | 111 | if (indices[0] === this.complete.length - 1 112 | && this.length % Piece.CHUNK_LENGTH > 0) { 113 | var length = this.length % Piece.CHUNK_LENGTH; 114 | } else { 115 | length = Piece.CHUNK_LENGTH; 116 | } 117 | return { 118 | begin: indices[0] * Piece.CHUNK_LENGTH, 119 | length: length 120 | }; 121 | }; 122 | 123 | Piece.prototype.setData = function(data, begin, cb) { 124 | var index = begin / Piece.CHUNK_LENGTH 125 | , self = this 126 | , cb = cb || function() {} // TODO: refactor below.. 127 | ; 128 | 129 | if (!this.complete.isSet(index)) { 130 | this.complete.set(index); 131 | 132 | var files = this.files.slice(0); 133 | 134 | function complete(err) { 135 | if (err) { 136 | self.complete.unset(index); 137 | self.requested.unset(index); 138 | cb(err); 139 | } else if (self.complete.cardinality() === self.complete.length) { 140 | self.isValid(function(valid) { 141 | if (valid) { 142 | setState(self, Piece.COMPLETE); 143 | } else { 144 | LOGGER.debug('invalid piece, clearing.'); 145 | self.complete = new BitField(self,complete.length); 146 | self.requested = new BitField(self,complete.length); 147 | } 148 | cb(); 149 | }); 150 | } else { 151 | cb(); 152 | } 153 | } 154 | 155 | (function next() { 156 | if (files.length === 0) { 157 | complete(); 158 | } else { 159 | var file = files.shift(); 160 | file.write(self.offset + begin, data, function(match) { 161 | if (match instanceof Error) { 162 | complete(match) 163 | } else { 164 | ProcessUtils.nextTick(next); 165 | } 166 | }); 167 | } 168 | })(); 169 | } else { 170 | LOGGER.warn('Attempt to overwrite data at ' + self.offset + '.'); 171 | cb(); 172 | } 173 | }; 174 | 175 | function setState(self, state) { 176 | self.state = state; 177 | self.emit(state, self); 178 | } 179 | 180 | Piece.CHUNK_LENGTH = 16384; 181 | 182 | Piece.COMPLETE = 'complete'; 183 | Piece.INCOMPLETE = 'incomplete'; 184 | 185 | module.exports = Piece; -------------------------------------------------------------------------------- /lib/extension/metadata.js: -------------------------------------------------------------------------------- 1 | 2 | var bencode = require('../util/bencode') 3 | , BitField = require('../util/bitfield') 4 | , Message = require('../message') 5 | , Metadata = require('../metadata') 6 | , Peer = require('../peer') 7 | , Torrent = require('../torrent') 8 | ; 9 | 10 | var LOGGER = require('log4js').getLogger('extension/metadata.js'); 11 | 12 | var EXTENSION_KEY = 'ut_metadata'; 13 | 14 | var MessageCode = { 15 | REQUEST: 0, 16 | DATA: 1, 17 | REJECT: 2 18 | }; 19 | 20 | function MetadataExtension(torrent) { 21 | this._metadata = new Metadata(torrent.infoHash); 22 | this._torrent = torrent; 23 | this._activePeers = {}; 24 | this._activePieces = null; 25 | this._peers = []; 26 | 27 | this.__addPeer_event = this._addPeer.bind(this); 28 | torrent.on(Torrent.PEER, this.__addPeer_event); 29 | } 30 | 31 | MetadataExtension.prototype.handleMessage = function(peer, payload) { 32 | 33 | LOGGER.debug('Peer [%s] notified of metadata message.', peer.getIdentifier()); 34 | 35 | var decodedPayload = bencode.decode(payload.toString('binary'), true) 36 | messageDetail = decodedPayload[0], 37 | messageType = messageDetail['msg_type'], 38 | activePieces = this._activePieces, 39 | requestedPieces = this._activePeers[peer.getIdentifier()]; 40 | 41 | switch (messageType) { 42 | case MessageCode.REQUEST: 43 | LOGGER.debug('Peer [%s] ignoring REQUEST message.', peer.getIdentifier()); 44 | break; 45 | 46 | case MessageCode.DATA: 47 | LOGGER.debug('Peer [%s] recieved DATA message.', peer.getIdentifier()); 48 | 49 | if (this._metadata.isComplete()) { 50 | LOGGER.debug('Metadata already complete, ignoring data.'); 51 | return; 52 | } 53 | 54 | var piece = messageDetail['piece']; 55 | this._cleanupPieceRequest(peer, piece); 56 | this._activePieces.set(piece); 57 | this._metadata.setPiece(piece, payload.slice(decodedPayload[1])); 58 | 59 | if (this._metadata.isComplete()) { 60 | this._torrent.setMetadata(this._metadata); 61 | torrent.removeListener(Torrent.PEER, this.__addPeer_event); 62 | 63 | var peer; 64 | while (peer = this._peers.shift()) { 65 | peer.removeListener(Peer.DISCONNECT, this.__peerDisconnect_event); 66 | peer.removeListener(Peer.READY, this.__peerReady_event); 67 | } 68 | } 69 | break; 70 | 71 | case MessageCode.REJECT: 72 | LOGGER.debug('Peer [%s] recieved REJECT message.', peer.getIdentifier()); 73 | var piece = messageDetail['piece']; 74 | this._cleanupPieceRequest(peer, piece); 75 | activePieces.unset(piece); 76 | break; 77 | 78 | default: 79 | LOGGER.warn('Peer [%s] sent unknown metadata message. messageType = %j', 80 | peer.getIdentifier(), messageType); 81 | } 82 | }; 83 | 84 | MetadataExtension.prototype._addPeer = function(peer) { 85 | LOGGER.debug('addPeer, hasMetadata: %j, supportsExtension: %j', this._torrent.hasMetadata(), 86 | peer.supportsExtension(EXTENSION_KEY)); 87 | if (!this._torrent.hasMetadata()) { 88 | if (peer.supportsExtension(EXTENSION_KEY)) { 89 | this._peers.push(peer); 90 | this.__peerDisconnect_event = this._peerDisconnect.bind(this); 91 | this.__peerReady_event = this._peerReady.bind(this); 92 | peer.on(Peer.DISCONNECT, this.__peerDisconnect_event); 93 | peer.on(Peer.READY, this.__peerReady_event); 94 | if (peer.isReady()) { 95 | this._peerReady(peer); 96 | } 97 | } else { 98 | var self = this; 99 | peer.once(Peer.EXTENSIONS_UPDATED, function() { 100 | self._addPeer(peer); 101 | }); 102 | } 103 | } 104 | }; 105 | 106 | MetadataExtension.prototype._cleanupPieceRequest = function(peer, piece) { 107 | var requestedPieces = this._activePeers[peer.getIdentifier()]; 108 | var pieceIndex = requestedPieces.indexOf(piece); 109 | if (pieceIndex > -1) { 110 | requestedPieces = requestedPieces.slice(0, pieceIndex).concat(requestedPieces.slice(pieceIndex + 1)); 111 | this._activePeers[peer.getIdentifier()] = requestedPieces; 112 | } 113 | }; 114 | 115 | MetadataExtension.prototype._peerDisconnect = function(peer) { 116 | 117 | // TODO: not cleaning up peer from _peers 118 | 119 | var requestedPieces = this._activePeers[peer.getIdentifier()], 120 | activePieces = this._activePieces; 121 | 122 | if (requestedPieces) { 123 | requestedPieces.forEach(function(index) { 124 | activePieces.unset(index); 125 | }); 126 | } 127 | peer.removeListener(Peer.DISCONNECT, this.__peerDisconnect_event); 128 | peer.removeListener(Peer.READY, this.__peerReady_event); 129 | }; 130 | 131 | MetadataExtension.prototype._peerReady = function(peer) { 132 | 133 | LOGGER.debug('Peer [%s] ready. metadata complete: %j ', peer.getIdentifier(), 134 | this._metadata.isComplete()); 135 | 136 | if (!this._metadata.isComplete()) { 137 | 138 | var metadata = this._metadata, 139 | activePeers = this._activePeers, 140 | activePieces = this._activePieces, 141 | availableBlocks = activePieces && activePieces.unsetIndices(), 142 | pieceToRequest = -1; 143 | 144 | if (!metadata.hasLength()) { 145 | metadata.setLength(peer._extensionData['metadata_size']); 146 | this._activePieces = activePieces = new BitField(metadata.bitfield.length); 147 | availableBlocks = activePieces.unsetIndices(); 148 | } 149 | 150 | pieceToRequest = availableBlocks[Math.round(Math.random() * (availableBlocks.length - 1))]; 151 | 152 | LOGGER.debug('Peer [%s] requesting piece %j', peer.getIdentifier(), pieceToRequest); 153 | 154 | if (!activePeers[peer.getIdentifier()]) { 155 | activePeers[peer.getIdentifier()] = []; 156 | } 157 | activePeers[peer.getIdentifier()].push(pieceToRequest); 158 | 159 | peer.sendExtendedMessage(EXTENSION_KEY, { 160 | msg_type: MessageCode.REQUEST, 161 | piece: pieceToRequest 162 | }); 163 | } 164 | }; 165 | 166 | MetadataExtension.EXTENSION_KEY = EXTENSION_KEY; 167 | 168 | module.exports = exports = MetadataExtension; 169 | -------------------------------------------------------------------------------- /lib/torrent/torrent.js: -------------------------------------------------------------------------------- 1 | 2 | var bencode = require('../util/bencode'), 3 | util = require('util'); 4 | 5 | var BitField = require('../util/bitfield'), 6 | DHT = require('../dht'), 7 | RequestManager = require('./requestmanager'), 8 | EventEmitter = require('events').EventEmitter, 9 | Message = require('../message'), 10 | Metadata = require('../metadata'), 11 | Peer = require('../peer'), 12 | Piece = require('../piece'), 13 | ProcessUtils = require('../util/processutils'), 14 | TorrentData = require('../torrentdata'), 15 | Tracker = require('../tracker'), 16 | createFiles = require('./createfiles'), 17 | createPieces = require('./createpieces') 18 | ; 19 | 20 | var LOGGER = require('log4js').getLogger('torrent.js'); 21 | 22 | function Torrent(clientId, clientPort, downloadPath, dataUrl, extensions) { 23 | EventEmitter.call(this); 24 | 25 | this.clientId = clientId; 26 | this.clientPort = clientPort; 27 | 28 | this.infoHash = null; 29 | this.name = null; 30 | 31 | this.stats = { 32 | downloaded: 0, 33 | downloadRate: 0, 34 | uploaded: 0, 35 | uploadRate: 0 36 | }; 37 | 38 | this.peers = {}; 39 | this.trackers = []; 40 | this.bitfield = null; 41 | this.status = null; 42 | 43 | this._setStatus(Torrent.LOADING); 44 | this._metadata = null; 45 | this._requestManager = new RequestManager(this); 46 | this._files = []; 47 | this._pieces = []; 48 | this._downloadPath = downloadPath; 49 | this._extensions = extensions; 50 | this._extensionMap = null; 51 | 52 | var torrent = this; 53 | // load torrent data 54 | TorrentData.load(dataUrl, function(error, metadata, trackers) { 55 | if (error) { 56 | LOGGER.warn('Error loading torrent data. error = %j', error); 57 | torrent._setStatus(Torrent.ERROR, error); 58 | } else { 59 | LOGGER.debug('Torrent data loaded.'); 60 | torrent._metadata = metadata; 61 | trackers.forEach(function(tracker) { 62 | torrent.addTracker(tracker); 63 | }); 64 | torrent._initialise(); 65 | } 66 | }); 67 | } 68 | util.inherits(Torrent, EventEmitter); 69 | 70 | Torrent.prototype.start = function() { 71 | LOGGER.debug('Starting torrent.'); 72 | var callback = this.addPeer.bind(this); 73 | // TODO: treat as tracker 74 | DHT.advertise(this.infoHash, callback); 75 | 76 | this.trackers.forEach(function(tracker) { 77 | tracker.start(callback); 78 | }); 79 | }; 80 | 81 | Torrent.prototype.stop = function() { 82 | if (this.status === Torrent.READY) { 83 | for (var i = 0; i < this.trackers.length; i++) { 84 | this.trackers[i].stop(); 85 | } 86 | for (var id in this.peers) { 87 | var peer = this.peers[id]; 88 | peer.disconnect('Torrent stopped.'); 89 | } 90 | } 91 | }; 92 | 93 | Torrent.prototype.addPeer = function(/* peer | id, address, port */) { 94 | 95 | var peer = null, 96 | torrent = this; 97 | 98 | if (arguments.length === 1) { 99 | peer = arguments[0]; 100 | } else { 101 | var id = arguments[0], 102 | address = arguments[1], 103 | port = arguments[2]; 104 | 105 | peer = new Peer(id, address, port, this); 106 | } 107 | 108 | LOGGER.debug('Adding peer, id = ' + peer.getIdentifier()); 109 | 110 | if (!(peer.getIdentifier() in this.peers)) { 111 | this.peers[peer.getIdentifier()] = peer; 112 | 113 | function onConnect() { 114 | LOGGER.debug('CONNECT from %s', peer.getIdentifier()); 115 | if (peer.supportsExtension()) { 116 | LOGGER.debug('Sending extended handshake. extension map = %j', torrent._extensionMap); 117 | peer.sendExtendedMessage(Message.EXTENDED_HANDSHAKE, { 118 | m: torrent._extensionMap, 119 | port: torrent.clientPort 120 | }); 121 | } 122 | if (torrent.bitfield) { 123 | peer.sendMessage(new Message(Message.BITFIELD, torrent.bitfield.toBuffer())); 124 | } 125 | } 126 | peer.on(Peer.RATE_UPDATE, function(rate) { 127 | if (rate.type === 'upload') { 128 | torrent.stats.uploadRate -= rate.previous; 129 | torrent.stats.uploadRate += rate.current; 130 | } else { 131 | torrent.stats.downloadRate -= rate.previous; 132 | torrent.stats.downloadRate += rate.current; 133 | } 134 | }); 135 | peer.once(Peer.DISCONNECT, function() { 136 | LOGGER.debug('DISCONNECT from %s', peer.getIdentifier()); 137 | peer.removeAllListeners(Peer.CONNECT); 138 | peer.removeAllListeners(Peer.DISCONNECT); 139 | peer.removeAllListeners(Peer.EXTENDED); 140 | peer.removeAllListeners(Peer.UPDATED); 141 | delete torrent.peers[peer.getIdentifier()]; 142 | }); 143 | peer.on(Peer.EXTENDED, function(peer, code, message) { 144 | LOGGER.debug('EXTENDED from %s, code = %d', peer.getIdentifier(), code); 145 | var extensionKey; 146 | Object.keys(torrent._extensionMap).some(function(key) { 147 | if (torrent._extensionMap[key] === code) { 148 | extensionKey = key; 149 | return true; 150 | } 151 | }); 152 | if (torrent._extensionMap[extensionKey]) { 153 | torrent._extensions[torrent._extensionMap[extensionKey] - 1].handleMessage(peer, message); 154 | } 155 | }); 156 | peer.on(Peer.UPDATED, function() { 157 | var interested = !torrent.bitfield || (peer.bitfield.xor(peer.bitfield.and(torrent.bitfield)).setIndices().length > 0); 158 | LOGGER.debug('UPDATED: ' + (interested ? 'interested' : 'not interested') + ' in ' + peer.getIdentifier()); 159 | peer.setAmInterested(interested); 160 | }); 161 | this.emit(Torrent.PEER, peer); 162 | 163 | if (peer.connected) { 164 | onConnect() 165 | } else { 166 | peer.once(Peer.CONNECT, onConnect); 167 | } 168 | } 169 | }; 170 | 171 | Torrent.prototype.addTracker = function(tracker) { 172 | this.trackers.push(tracker); 173 | tracker.setTorrent(this); 174 | // tracker.on(Tracker.PEER, this.addPeer.bind(this)); 175 | }; 176 | 177 | Torrent.prototype.hasMetadata = function() { 178 | return this._metadata.isComplete(); 179 | }; 180 | 181 | Torrent.prototype.isComplete = function() { 182 | return this.bitfield.cardinality() === this.bitfield.length; 183 | }; 184 | 185 | Torrent.prototype.setMetadata = function(metadata) { 186 | this._metadata = metadata; 187 | this._initialise(); 188 | }; 189 | 190 | Torrent.prototype._initialise = function() { 191 | 192 | LOGGER.debug('Initialising torrent.'); 193 | if (this.status === Torrent.READY) { 194 | LOGGER.debug('Already initialised, skipping.'); 195 | return; 196 | } 197 | 198 | var torrent = this; 199 | 200 | this.name = this._metadata.name; 201 | 202 | if (!this._extensionMap) { 203 | this._extensionMap = {}; 204 | for (var i = 0; i < this._extensions.length; i++) { 205 | var ExtensionClass = this._extensions[i] 206 | , extension = new ExtensionClass(this) 207 | , extensionCode = i + 1 208 | ; 209 | this._extensions[i] = extension; 210 | this._extensionMap[ExtensionClass.EXTENSION_KEY] = extensionCode; 211 | } 212 | } 213 | 214 | if (!this.infoHash) { 215 | this.infoHash = this._metadata.infoHash; 216 | ProcessUtils.nextTick(function() { 217 | torrent.emit(Torrent.INFO_HASH, torrent.infoHash); 218 | }); 219 | } 220 | 221 | if (this.hasMetadata()) { 222 | LOGGER.debug('Metadata is complete, initialising files and pieces.'); 223 | 224 | createFiles(this._downloadPath, this._metadata, function(error, _files, _size) { 225 | if (error) { 226 | torrent._setStatus(Torrent.ERROR, error); 227 | } else { 228 | torrent.files = _files; 229 | torrent.size = _size; 230 | 231 | createPieces(torrent._metadata.pieces, _files, torrent._metadata['piece length'], 232 | _size, function(error, _pieces) { 233 | if (error) { 234 | torrent._setStatus(Torrent.ERROR, error); 235 | } else { 236 | torrent._pieces = _pieces; 237 | torrent.bitfield = new BitField(_pieces.length); 238 | var completeHandler = torrent._pieceComplete.bind(torrent); 239 | _pieces.forEach(function(piece) { 240 | if (piece.isComplete()) { 241 | torrent.bitfield.set(piece.index); 242 | } else { 243 | piece.once(Piece.COMPLETE, completeHandler); 244 | } 245 | }); 246 | ProcessUtils.nextTick(function() { 247 | torrent._setStatus(Torrent.READY); 248 | }); 249 | } 250 | }); 251 | } 252 | }); 253 | } 254 | }; 255 | 256 | Torrent.prototype._pieceComplete = function(piece) { 257 | LOGGER.debug('Piece complete, piece index = ' + piece.index); 258 | this.stats.downloaded += piece.length; 259 | 260 | this.emit(Torrent.PROGRESS, this.stats.downloaded / this.size); 261 | 262 | if (this.isComplete()) { 263 | LOGGER.info('torrent download complete'); 264 | this._setStatus(Torrent.COMPLETE); 265 | } 266 | 267 | var have = new Message(Message.HAVE, BufferUtils.fromInt(piece.index)); 268 | for (var i in this.peers) { 269 | var peer = this.peers[i]; 270 | if (peer.initialised) { 271 | peer.sendMessage(have); 272 | } 273 | } 274 | }; 275 | 276 | Torrent.prototype._setStatus = function(status, data) { 277 | LOGGER.debug('Status updated to %s', status); 278 | this.emit(status, data); 279 | this.status = status; 280 | if (status === Torrent.ERROR) { 281 | this.stop(); 282 | } 283 | }; 284 | 285 | Torrent.COMPLETE = 'torrent:complete'; 286 | Torrent.ERROR = 'torrent:error'; 287 | Torrent.INFO_HASH = 'torrent:info_hash'; 288 | Torrent.LOADING = 'torrent:loading'; 289 | Torrent.PEER = 'torrent:peer'; 290 | Torrent.PROGRESS = 'torrent:progress'; 291 | Torrent.READY = 'torrent:ready'; 292 | 293 | module.exports = exports = Torrent; 294 | -------------------------------------------------------------------------------- /lib/peer.js: -------------------------------------------------------------------------------- 1 | 2 | var net = require('net'); 3 | var util = require('util'); 4 | var bencode = require('./util/bencode'); 5 | 6 | var ProcessUtils = require('./util/processutils'); 7 | var BitField = require('./util/bitfield'); 8 | var BufferUtils = require('./util/bufferutils'); 9 | var EventEmitter = require('events').EventEmitter; 10 | var Message = require('./message'); 11 | var Piece = require('./piece'); 12 | 13 | var BITTORRENT_HEADER = new Buffer("\x13BitTorrent protocol\x00\x00\x00\x00\x00\x10\x00\x00", "binary"); 14 | var KEEPALIVE_PERIOD = 120000; 15 | var MAX_REQUESTS = 10; 16 | 17 | var LOGGER = require('log4js').getLogger('peer.js'); 18 | 19 | var Peer = function(/* stream */ /* or */ /* peer_id, address, port, torrent */) { 20 | EventEmitter.call(this); 21 | 22 | this.choked = true; 23 | this.data = new Buffer(0); 24 | this.drained = true; 25 | this.initialised = false; 26 | this.interested = false; 27 | this.messages = []; 28 | this.toSend = []; 29 | this.pieces = {}; 30 | this.numRequests = 0; 31 | this.requests = {}; 32 | this.requestsCount = {}; 33 | this.stream = null; 34 | this.handshake = false; 35 | 36 | this.downloaded = 0; 37 | this.uploaded = 0; 38 | this.downloadedHistory = []; 39 | this.downloadRates = []; 40 | this.currentDownloadRate = 0; 41 | this.uploadedHistory = []; 42 | this.uploadRates = []; 43 | this.currentUploadRate = 0; 44 | 45 | this.running = false; 46 | this.processing = false; 47 | 48 | this.debugStatus = ''; 49 | 50 | if (arguments.length === 1) { 51 | this.debugStatus += 'incoming:'; 52 | this.stream = arguments[0]; 53 | this.address = this.stream.remoteAddress; 54 | this.port = this.stream.remotePort; 55 | } else { 56 | this.debugStatus += 'outgoing:'; 57 | this.peerId = arguments[0]; 58 | this.address = arguments[1]; 59 | this.port = arguments[2]; 60 | this.setTorrent(arguments[3]); 61 | } 62 | 63 | this.connect(); 64 | 65 | var self = this; 66 | setTimeout(function() { forceUpdateRates(self) }, 1000); 67 | }; 68 | util.inherits(Peer, EventEmitter); 69 | 70 | Peer.prototype.connect = function() { 71 | 72 | var self = this; 73 | 74 | if (this.stream === null) { 75 | LOGGER.debug('Connecting to peer at ' + this.address + ' on ' + this.port); 76 | this.stream = net.createConnection(this.port, this.address); 77 | this.stream.on('connect', function() {onConnect(self);}); 78 | } 79 | 80 | this.stream.on('data', function(data) {onData(self, data);}); 81 | this.stream.on('drain', function() {onDrain(self);}); 82 | this.stream.on('end', function() {onEnd(self);}); 83 | this.stream.on('error', function(e) {onError(self, e);}); 84 | }; 85 | 86 | function forceUpdateRates(self) { 87 | 88 | updateRates(self, 'down'); 89 | updateRates(self, 'up'); 90 | 91 | if (!self.disconnected) { 92 | setTimeout(function() { forceUpdateRates(self) }, 1000); 93 | } 94 | } 95 | 96 | Peer.prototype.disconnect = function(message, reconnectTimeout) { 97 | LOGGER.debug('Peer.disconnect [' + this.getIdentifier() + '] message =', message); 98 | this.disconnected = true; 99 | this.connected = false; 100 | if (this.stream) { 101 | this.stream.removeAllListeners(); 102 | this.stream = null; 103 | } 104 | if (this.keepAliveId) { 105 | clearInterval(this.keepAliveId); 106 | delete this.keepAliveId; 107 | } 108 | for (var index in this.pieces) { 109 | var piece = this.pieces[index]; 110 | var requests = this.requests[index]; 111 | if (requests) { 112 | for (var reqIndex in requests) { 113 | piece.cancelRequest(requests[reqIndex]); 114 | } 115 | } 116 | } 117 | this.requests = {}; 118 | this.requestsCount = {}; 119 | 120 | this.messages = []; 121 | this.toSend = []; 122 | 123 | this.emit(Peer.DISCONNECT, this); 124 | 125 | if (reconnectTimeout) { 126 | var self = this; 127 | setTimeout(function() {self.connect();}, reconnectTimeout); 128 | } 129 | }; 130 | 131 | Peer.prototype.getIdentifier = function() { 132 | return Peer.getIdentifier(this); 133 | }; 134 | 135 | Peer.prototype.requestPiece = function(piece) { 136 | var self = this; 137 | 138 | if (piece && !piece.isComplete()) { 139 | if (!self.pieces[piece.index]) { 140 | self.pieces[piece.index] = piece; 141 | self.requests[piece.index] = {}; 142 | piece.once(Piece.COMPLETE, function() { 143 | delete self.pieces[piece.index]; 144 | }); 145 | } 146 | 147 | var nextChunk; 148 | while (self.numRequests < MAX_REQUESTS && (nextChunk = piece.nextChunk())) { 149 | self.requests[piece.index][nextChunk.begin] = new Date(); 150 | var msgBuffer = new Buffer(12); 151 | msgBuffer.writeInt32BE(piece.index, 0, true); 152 | msgBuffer.writeInt32BE(nextChunk.begin, 4, true); 153 | msgBuffer.writeInt32BE(nextChunk.length, 8, true); 154 | var message = new Message(Message.REQUEST, msgBuffer); 155 | self.sendMessage(message); 156 | self.requestsCount[piece.index] = (self.requestsCount[piece.index] || 0) + 1; 157 | self.numRequests++; 158 | } 159 | } 160 | 161 | if (self.isReady()) { 162 | ProcessUtils.nextTick(function() { 163 | self.emit(Peer.READY, self); 164 | }); 165 | } 166 | }; 167 | 168 | Peer.prototype.sendMessage = function(message) { 169 | var self = this; 170 | self.messages.push(message); 171 | if (!self.running) { 172 | self.running = true; 173 | ProcessUtils.nextTick(function(){nextMessage(self)}); 174 | } 175 | }; 176 | 177 | Peer.prototype.sendExtendedMessage = function(type, data) { 178 | 179 | LOGGER.debug('Peer [%s] sending extended message of type %j', this.getIdentifier(), type); 180 | 181 | var code = Message.EXTENDED_HANDSHAKE === type 182 | ? 0 183 | : this._extensionData && this._extensionData.m[type]; 184 | 185 | if (code !== undefined) { 186 | 187 | var codeAsBuffer = new Buffer(1); 188 | codeAsBuffer[0] = code; 189 | 190 | LOGGER.debug('Peer [%s] extended request code = %j', this.getIdentifier(), codeAsBuffer[0]); 191 | 192 | var payload = new Buffer(bencode.encode(data)); 193 | 194 | var message = new Message(Message.EXTENDED, BufferUtils.concat(codeAsBuffer, payload)); 195 | 196 | this.sendMessage(message); 197 | } else { 198 | throw new Error("Peer doesn't support extended request of type " + type); 199 | } 200 | }; 201 | 202 | Peer.prototype.setAmInterested = function(interested) { 203 | var self = this; 204 | if (interested && !self.amInterested) { 205 | self.sendMessage(new Message(Message.INTERESTED)); 206 | LOGGER.debug('Sent INTERESTED to ' + self.getIdentifier()); 207 | self.amInterested = true; 208 | if (self.isReady()) { 209 | self.emit(Peer.READY, self); 210 | } 211 | } else if (!interested && self.amInterested) { 212 | self.sendMessage(new Message(Message.UNINTERESTED)); 213 | LOGGER.debug('Sent UNINTERESTED to ' + self.getIdentifier()); 214 | self.amInterested = false; 215 | } 216 | }; 217 | 218 | Peer.prototype.setTorrent = function(torrent) { 219 | var self = this; 220 | var stream = self.stream; 221 | this.torrent = torrent; 222 | this.bitfield = new BitField(torrent.bitfield ? torrent.bitfield.length : 0); 223 | if (this.stream) { 224 | if (this.initialised) { 225 | throw "Already initialised"; 226 | } 227 | doHandshake(this); 228 | this.initialised = true; 229 | } 230 | this.torrent.addPeer(this); 231 | }; 232 | 233 | Peer.prototype.isReady = function() { 234 | return this.amInterested && !this.choked && this.numRequests < MAX_REQUESTS; 235 | }; 236 | 237 | Peer.prototype.supportsExtension = function(key) { 238 | if (key) { 239 | return this._extensionData && this._extensionData.m[key]; 240 | } 241 | return this._supportsExtension; 242 | }; 243 | 244 | function doHandshake(self) { 245 | self.debugStatus += 'handshake:'; 246 | var stream = self.stream; 247 | stream.write(BITTORRENT_HEADER); 248 | stream.write(self.torrent.infoHash); 249 | stream.write(self.torrent.clientId); 250 | self.handshake = true; 251 | LOGGER.debug('Sent HANDSHAKE to ' + self.getIdentifier()); 252 | } 253 | 254 | function handleHandshake(self) { 255 | var data = self.data; 256 | if (data.length < 68) { 257 | // Not enough data. 258 | return; 259 | } 260 | if (!BufferUtils.equal(BITTORRENT_HEADER.slice(0, 20), data.slice(0, 20))) { 261 | self.disconnect('Invalid handshake. data = ' + data.toString('binary')); 262 | } else { 263 | self.debugStatus += 'incoming_handshake:'; 264 | 265 | var infoHash = data.slice(28, 48); 266 | self.peerId = data.toString('binary', 48, 68); 267 | LOGGER.debug('Received HANDSHAKE from ' + self.getIdentifier()); 268 | 269 | self.data = BufferUtils.slice(data, 68); 270 | 271 | self._supportsExtension = (data[25] & 0x10) > 0; 272 | 273 | self.connected = true; 274 | if (self.torrent) { 275 | self.initialised = true; 276 | self.running = true; 277 | nextMessage(self); 278 | processData(self); 279 | self.emit(Peer.CONNECT); 280 | } else { 281 | self.emit(Peer.CONNECT, infoHash); 282 | } 283 | } 284 | } 285 | 286 | function nextMessage(self) { 287 | if (!self.disconnected && self.initialised) { 288 | (function next() { 289 | if (self.messages.length === 0) { 290 | self.running = false; 291 | setKeepAlive(self); 292 | } else { 293 | if (!self.stream) { 294 | self.connect(); 295 | } else { 296 | if (self.keepAliveId) { 297 | clearInterval(self.keepAliveId); 298 | delete self.keepAliveId; 299 | } 300 | while (self.messages.length > 0) { 301 | var message = self.messages.shift(); 302 | message.writeTo(self.stream); 303 | } 304 | next(); 305 | } 306 | } 307 | })(); 308 | } 309 | } 310 | 311 | function onConnect(self) { 312 | self.debugStatus += 'onConnect:'; 313 | self.disconnected = false; 314 | if (self.torrent) { 315 | if (!self.handshake) { 316 | doHandshake(self); 317 | } else { 318 | self.running = true; 319 | nextMessage(self); 320 | } 321 | } 322 | } 323 | 324 | function onData(self, data) { 325 | self.data = BufferUtils.concat(self.data, data); 326 | if (!self.initialised) { 327 | handleHandshake(self); 328 | } else { 329 | if (!self.processing) { 330 | processData(self); 331 | } 332 | } 333 | } 334 | 335 | function onDrain(self) { 336 | self.drained = true; 337 | } 338 | 339 | function onEnd(self) { 340 | LOGGER.debug('Peer [' + self.getIdentifier() + '] received end'); 341 | self.stream = null; 342 | if (self.amInterested) { 343 | self.disconnect('after end, reconnect', 5000); 344 | } else { 345 | self.disconnect('stream ended and no interest'); 346 | } 347 | } 348 | 349 | function onError(self, e) { 350 | self.disconnect(e.message); 351 | } 352 | 353 | function sendData(self) { 354 | var retry = false; 355 | (function next() { 356 | if (self.toSend.length > 0) { 357 | var message = self.toSend.shift(); 358 | var index = message.payload.readInt32BE(0, true); 359 | var begin = message.payload.readInt32BE(4, true); 360 | var length = message.payload.readInt32BE(8, true); 361 | 362 | self.torrent.requestChunk(index, begin, length, function(err, data) { 363 | if (err) { 364 | if (err.code===Piece.ERR_FILEBUSY) { 365 | LOGGER.warn('Peer [' + self.getIdentifier() + '] sendData file busy'); 366 | retry = true; 367 | self.toSend.push(message); 368 | } 369 | else { 370 | LOGGER.error('Failed to read file chunk: ' + err); 371 | throw err; 372 | } 373 | } 374 | else { 375 | if (data) { 376 | var msgBuffer = new Buffer(8+data.length); 377 | msgBuffer.writeInt32BE(index, 0, true); 378 | msgBuffer.writeInt32BE(begin, 4, true); 379 | data.copy(msgBuffer, 8); 380 | self.sendMessage(new Message(Message.PIECE, msgBuffer)); 381 | self.uploaded += data.length; 382 | updateRates(self, 'up'); 383 | } else { 384 | LOGGER.debug('No data found for request, index = ' + index + ', begin = ' + begin); 385 | } 386 | ProcessUtils.nextTick(next); 387 | } 388 | }); 389 | } 390 | else { 391 | self.sending = false; 392 | if (retry) { 393 | setTimeout(function() { 394 | if (!self.sending) { 395 | self.sending = true; 396 | sendData(self); 397 | } 398 | }, 10); 399 | } 400 | } 401 | })(); 402 | } 403 | 404 | function processData(self) { 405 | 406 | var offset = 0; 407 | self.processing = true; 408 | 409 | function done() { 410 | if (offset > 0) { 411 | self.data = self.data.slice(offset); 412 | } 413 | self.processing = false; 414 | } 415 | 416 | do { 417 | if (self.data.length - offset >= 4) { 418 | var messageLength = self.data.readInt32BE(offset, true); 419 | offset += 4; 420 | if (messageLength === 0) { 421 | LOGGER.debug('Peer [%s] sent keep alive', self.getIdentifier()); 422 | } else if (self.data.length - offset >= messageLength) { 423 | // Have everything we need to process a message 424 | var code = self.data[offset]; 425 | var payload = messageLength > 1 ? self.data.slice(offset+1, offset+messageLength) : null; 426 | offset += messageLength; 427 | 428 | var message = new Message(code, payload); 429 | switch (message.code) { 430 | 431 | case Message.CHOKE: 432 | LOGGER.debug('Peer [%s] sent CHOKE', self.getIdentifier()); 433 | self.debugStatus += 'choke:' 434 | self.choked = true; 435 | self.emit(Peer.CHOKED); 436 | break; 437 | 438 | case Message.UNCHOKE: 439 | LOGGER.debug('Peer [%s] sent UNCHOKE, interested = %j', self.getIdentifier(), self.amInterested); 440 | self.debugStatus += 'unchoke:' 441 | self.choked = false; 442 | if (self.isReady()) { 443 | self.emit(Peer.READY, self); 444 | } 445 | break; 446 | 447 | case Message.INTERESTED: 448 | LOGGER.debug('Peer [%s] sent INTERESTED', self.getIdentifier()); 449 | self.interested = true; 450 | // TODO: choke/unchoke handling 451 | // self.sendMessage(new Message(Message.UNCHOKE)); 452 | // LOGGER.info('Sent UNCHOKE to ' + self.getIdentifier()); 453 | break; 454 | 455 | case Message.UNINTERESTED: 456 | LOGGER.debug('Peer [%s] sent UNINTERESTED', self.getIdentifier()); 457 | self.interested = false; 458 | break; 459 | 460 | case Message.HAVE: 461 | LOGGER.debug('Peer [%s] sent HAVE', self.getIdentifier()); 462 | var piece = message.payload.readInt32BE(0, true); 463 | self.bitfield.set(piece); 464 | self.emit(Peer.UPDATED); 465 | break; 466 | 467 | case Message.BITFIELD: 468 | LOGGER.debug('Peer [%s] sent BITFIELD', self.getIdentifier()); 469 | self.bitfield = new BitField(message.payload, message.payload.length); // TODO: figure out nicer way of handling bitfield lengths 470 | self.emit(Peer.UPDATED); 471 | break; 472 | 473 | case Message.REQUEST: 474 | LOGGER.debug('Peer [%s] sent REQUEST', self.getIdentifier()); 475 | self.toSend.push(message); 476 | if (!self.sending) { 477 | self.sending = true; 478 | setTimeout(function() {sendData(self)}, 10); 479 | } 480 | break; 481 | 482 | case Message.PIECE: 483 | LOGGER.debug('Peer [%s] sent PIECE', self.getIdentifier()); 484 | 485 | var index = message.payload.readInt32BE(0, true); 486 | var begin = message.payload.readInt32BE(4, true); 487 | var data = message.payload.slice(8); 488 | 489 | var piece = self.pieces[index]; 490 | if (piece) { 491 | piece.setData(data, begin); 492 | 493 | var requestTime = new Date() - self.requests[index][begin]; 494 | self.downloaded += data.length; 495 | 496 | delete self.requests[index][begin]; 497 | self.requestsCount[index]--; 498 | self.numRequests--; 499 | 500 | updateRates(self, 'down'); 501 | self.requestPiece(piece); 502 | } else { 503 | LOGGER.debug('Peer [%s] received chunk for inactive piece', self.getIdentifier()); 504 | } 505 | 506 | break; 507 | 508 | case Message.CANCEL: 509 | LOGGER.debug('Ignoring CANCEL'); 510 | break; 511 | 512 | case Message.PORT: 513 | LOGGER.debug('Ignoring PORT'); 514 | break; 515 | 516 | case Message.EXTENDED: 517 | LOGGER.debug('Received EXTENDED from ' + Peer.getIdentifier(self)); 518 | 519 | var extendedCode = message.payload[0] 520 | , data = message.payload.slice(1) 521 | , payload = null 522 | ; 523 | 524 | if (extendedCode === 0) { 525 | payload = bencode.decode(data.toString('binary')); 526 | self._extensionData = payload; 527 | LOGGER.debug('Peer [%s] supports extensions %j', self.getIdentifier(), payload); 528 | self.emit(Peer.EXTENSIONS_UPDATED); 529 | } else { 530 | LOGGER.debug('Peer [%s] received extended code %d', self.getIdentifier(), extendedCode); 531 | self.emit(Peer.EXTENDED, self, extendedCode, data); 532 | } 533 | break; 534 | 535 | default: 536 | LOGGER.warn('Peer [' + self.getIdentifier() + '] received unknown message, disconnecting. '); 537 | self.disconnect('Unknown message received.'); 538 | // stop processing 539 | done(); 540 | return; 541 | } 542 | } 543 | else { 544 | // not enough data, stop processing until more data arrives 545 | offset -= 4; 546 | done(); 547 | return; 548 | } 549 | } 550 | else { 551 | // not enough data to read the message length, stop processing until more data arrives 552 | done(); 553 | if (!self.running) { 554 | self.running = true; 555 | ProcessUtils.nextTick(function(){nextMessage(self)}); 556 | } 557 | return; 558 | } 559 | } while (true); 560 | } 561 | 562 | function setKeepAlive(self) { 563 | if (!self.keepAliveId) { 564 | self.keepAliveId = setInterval(function() { 565 | LOGGER.debug('keepAlive tick'); 566 | if (self.stream && self.stream.writable) { 567 | var message = new Message(Message.KEEPALIVE); 568 | message.writeTo(self.stream); 569 | } else { 570 | clearInterval(self.keepAliveId); 571 | } 572 | }, KEEPALIVE_PERIOD); 573 | } 574 | } 575 | 576 | // calculate weighted average upload/download rate 577 | function calculateRate(self, kind) { 578 | var isUpload = (kind=='up'); 579 | 580 | var rates = isUpload ? self.uploadRates : self.downloadRates; 581 | 582 | // take the last recorded rate 583 | // var rate = (rates.length > 0) ? rates[rates.length-1].value : 0 584 | 585 | // calculate weighted average rate 586 | //var decayFactor = 0.13863; 587 | var rateSum = 0, weightSum = 0; 588 | for (var idx=0; idx0) ? (rateSum/weightSum) : 0; 595 | 596 | if (rate > 0) { 597 | LOGGER.debug('Peer [' + self.getIdentifier() + '] ' + kind + 'loading at ' + rate); 598 | } 599 | 600 | if (isUpload) { 601 | self.emit(Peer.RATE_UPDATE, { 602 | type: 'upload', 603 | previous: self.currentUploadRate, 604 | current: rate 605 | }); 606 | self.currentUploadRate = rate; 607 | } 608 | else { 609 | self.emit(Peer.RATE_UPDATE, { 610 | type: 'download', 611 | previous: self.currentDownloadRate, 612 | current: rate 613 | }); 614 | self.currentDownloadRate = rate; 615 | } 616 | } 617 | 618 | function updateRates(self, kind) { 619 | var isUpload = (kind=='up'); 620 | 621 | var history = isUpload ? self.uploadedHistory : self.downloadedHistory; 622 | var rates = isUpload ? self.uploadRates : self.downloadRates; 623 | 624 | var now = Date.now(); 625 | var bytes = isUpload ? self.uploaded : self.downloaded; 626 | history.push({ts: now, value: bytes}); 627 | 628 | if (history.length > 1) { 629 | var start = history[0].ts; 630 | if (now-start > 1*1000) { 631 | // calculate a new rate and remove first entry from history 632 | var rate = (bytes-history.shift().value)/(now-start)*1000; 633 | rates.push({ts: now, value: rate}); 634 | // throw out any rates that are too old to be of interest 635 | while((rates.length>1) && (now-rates[0].ts>3*1000)) { 636 | rates.shift(); 637 | } 638 | // re-calculate current upload/download rate 639 | calculateRate(self, kind); 640 | } 641 | else { 642 | // just want to keep the first and the last entry in history 643 | history.splice(1,1); 644 | } 645 | } 646 | } 647 | 648 | Peer.CHOKED = 'choked'; 649 | Peer.CONNECT = 'connect'; 650 | Peer.DISCONNECT = 'disconnect'; 651 | Peer.READY = 'ready'; 652 | Peer.UPDATED = 'updated'; 653 | Peer.EXTENDED = 'extended'; 654 | Peer.EXTENSIONS_UPDATED = 'peer:extensions_updated'; 655 | Peer.RATE_UPDATE = 'peer:rate_update' 656 | 657 | Peer.getIdentifier = function(peer) { 658 | return (peer.address || peer.ip) + ':' + peer.port; 659 | } 660 | 661 | module.exports = Peer; 662 | --------------------------------------------------------------------------------