diff --git a/.gitignore b/.gitignore index e099484..c3ae6b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ tests/a.torrent tests/data +.project +.settings +*.torrent diff --git a/README b/README index eceef49..09724d6 100644 --- a/README +++ b/README @@ -11,13 +11,13 @@ which is currently more of a bitTorrent seeder than a full bitTorrent client. Implemented features: + Parse .torrent files. + Scan disk to compute good/bad pieces for data files. -+ Request peer list from tracker to request. ++ Request peer list from tracker. ++ Parse tracker response. TODO: - UPNP open a port for listening for peers. - Handle redirects from trackers. - Create directories for multi-file torrents. - Listen for peers. -- Parse tracker response. - Connect to peers. - Exchange data with peers. diff --git a/listener.js b/listener.js new file mode 100644 index 0000000..4773229 --- /dev/null +++ b/listener.js @@ -0,0 +1,18 @@ +var net = require('net'), + peer = require('./peer'), + sys = require('sys'); + + +function create(port, hashes) { + var state = 0, server = net.createServer(function(stream) { + sys.log('createServer callback'); + stream.setEncoding('binary'); + stream.addListener('connect', function() { + sys.log('New connection'); + // TODO: Tell tracker about this connection. + }); + }); + server.listen(port); +} + +exports.create = create; diff --git a/main.js b/main.js index fada018..264bae6 100644 --- a/main.js +++ b/main.js @@ -2,41 +2,42 @@ var sys = require('sys'), torrent = require('./torrent'); function parseArgs(args) { - var result = {destDir:'.'}, - torrentFiles = [], - i, argLen, arg; - for (i = 0, argLen = args.length; i < argLen; i += 1) { - arg = args[i]; - if (arg.length == 0) { - throw "Empty argument"; - } - if (arg.charAt(0) == '-') { - if (arg === '--destDir') { - result.destDir = args[i+1]; - i += 1; - } else { - throw "Unknown flag " + arg; - } - } else { - torrentFiles.push(arg); - } - } + var result = {destDir:'.'}, + torrentFiles = [], + i, argLen, arg; + for (i = 0, argLen = args.length; i < argLen; i += 1) { + arg = args[i]; + if (arg.length == 0) { + throw "Empty argument"; + } + if (arg.charAt(0) == '-') { + if (arg === '--destDir') { + result.destDir = args[i+1]; + i += 1; + } else { + throw "Unknown flag " + arg; + } + } else { + torrentFiles.push(arg); + } + } result.files = torrentFiles; return result; } function main() { - var args = parseArgs(process.argv.slice(2)), - torrentFiles = args.files, - i, iLen, file; - if (torrentFiles.length == 0) { - throw "No torrent files specified."; - } else { - for (i = 0, iLen = torrentFiles.length; i < iLen; i += 1) { - file = torrentFiles[i]; - torrent.startTorrent(file, args.destDir); - } - } + var args = parseArgs(process.argv.slice(2)), + torrentFiles = args.files, + i, iLen, file, aTorrent; + if (torrentFiles.length == 0) { + throw "No torrent files specified."; + } else { + for (i = 0, iLen = torrentFiles.length; i < iLen; i += 1) { + file = torrentFiles[i]; + aTorrent = torrent.create(file, args.destDir); + aTorrent.start(); + } + } } -main(); \ No newline at end of file +main(); diff --git a/peer.js b/peer.js new file mode 100644 index 0000000..bba4bd9 --- /dev/null +++ b/peer.js @@ -0,0 +1,30 @@ +var net = require('net'), + sys = require('sys'); + +function create(host, port) { + sys.log('peer.create ' + host + ':' + port); + var peer = { + host: host, + port: port, + exchangedHeader: false, + stream: net.createConnection(port, host), + connect: function() { + sys.log("Connection established to " + this.host + ':' + this.port); + this.stream.write('\x13BitTorrent protocol\0\0\0\0\0\0\0\0'); + } + }; + + peer.stream.setEncoding('binary'); + sys.log('peer.create 3 ' + host + ':' + port); + peer.stream.addListener('connect', function() { + sys.log('peer.stream connect ' + host + ':' + port); + peer.connect(); + }); + peer.stream.addListener('error', function(e) { + sys.log('peer.stream error ' + host + ':' + port + ' ' + e); + }) + sys.log('peer.create 4 ' + host + ':' + port); + return peer; +} + +exports.create = create; diff --git a/torrent.js b/torrent.js index f24ce02..008ed8f 100644 --- a/torrent.js +++ b/torrent.js @@ -1,63 +1,100 @@ -var sys = require('sys'), - fs = require('fs'), - bencode = require('./bencode'), - filestore = require('./filestore'), - tracker = require("./tracker"), +var bencode = require('./bencode'), crypto = require('crypto'); + filestore = require('./filestore'), + fs = require('fs'), + listener = require('./listener'), + peer = require('./peer'), + sys = require('sys'), + tracker = require("./tracker"); -function log(msg) { - sys.puts(msg); -} +function create(torrentPath, destDir) { + return { + torrentPath: torrentPath, + destDir: destDir, + listenerPort: 6881, + peers: {}, + pingTracker: function () { + var that = this, + params = { + info_hash: this.metaInfo.info_hash, + peer_id: '01234567890123456789', + port: this.listenerPort, + uploaded: 0, + downloaded: 0, + left: this.store.left, + compact:1, + event:'started' + }; + tracker.ping(this.trackerClient, params, function (error, response) { + var newPeers, numPeers, i, interval = 3600; + if (!error) { + interval = Math.max(interval, response.interval); + newPeers = response.peers; + numPeers = Math.floor(newPeers.length / 6); + sys.log('Tracker gave us ' + numPeers + ' peers.'); + for (i = 0; i < numPeers; i++ ) { + that.addPeer(newPeers.substring(i*6,(i+1)*6)); + } + } + that.pingTimer = setTimeout(function () { + that.pingTracker(); + }, interval * 1000); + }); + }, -function pingTracker(metaInfo, store, trackerClient) { - var params = { - info_hash: metaInfo.info_hash, - peer_id: '01234567890123456789', - port: 6881, - uploaded: 0, - downloaded: 0, - left: store.left, - compact:1, - event:'started' - }; - tracker.ping(trackerClient, params, function (error, response) { - system.log('pingTracker callback ' + error + ' ' + JSON.stringify(response)); - }); -} + addPeer : function (peerAddress) { + if ( ! (peerAddress in this.peers) ) { + this.peers[peerAddress] = peer.create( + this.decodeHost(peerAddress), + this.decodePort(peerAddress)); + } + }, -function computeHash(info) { - var encoded = bencode.encode(info), - hash = crypto.createHash('sha1'); - hash.update(encoded); - return hash.digest('binary'); -} + computeHash: function (info) { + var encoded = bencode.encode(info), + hash = crypto.createHash('sha1'); + hash.update(encoded); + return hash.digest('binary'); + }, + + decodeHost: function (address) { + return address.charCodeAt(0) + '.' + address.charCodeAt(1) + '.' + address.charCodeAt(2) + '.' + address.charCodeAt(3); + }, + + decodePort: function (address) { + return (address.charCodeAt(4) << 8) + address.charCodeAt(5); + }, -function startTorrent(torrentPath, destDir) { - var metaInfo, store, error2, trackerClient; - sys.puts('Starting torrent ' + torrentPath); - fs.readFile(torrentPath, 'binary', function startTorrentCallback(error, contents) { - var aFileStore; - if (error) { - log('Could not open torrent file ' + torrentPath + ': ' + error); - } else { - metaInfo = bencode.decode(contents); - if ('comment' in metaInfo) { - sys.puts('Torrent \'' + metaInfo.comment + '\''); - } - metaInfo.info_hash = computeHash(metaInfo.info); - store = filestore.create(metaInfo, destDir); - sys.log('inspecting files'); - filestore.inspect(store, - function inspectCallback(error) { - if (error) { - log('Could not inspect torrent files ' + error); - } else { - trackerClient = tracker.create(metaInfo); - pingTracker(metaInfo, store, trackerClient); - } - }); - } - }); + start : function() { + var that = this; + sys.puts('Starting torrent ' + this.torrentPath); + fs.readFile(this.torrentPath, 'binary', + function startTorrentCallback(error, contents) { + if (error) { + sys.log('Could not open torrent file ' + that.torrentPath + ': ' + error); + } else { + that.metaInfo = bencode.decode(contents); + if ('comment' in that.metaInfo) { + sys.log('Torrent \'' + that.metaInfo.comment + '\''); + } + that.metaInfo.info_hash = that.computeHash(that.metaInfo.info); + that.store = filestore.create(that.metaInfo, that.destDir); + sys.log('inspecting files'); + filestore.inspect(that.store, + function inspectCallback(error) { + if (error) { + sys.log('Could not inspect torrent files ' + error); + } else { + sys.log('finished inspecting files.'); + listener.create(that.listenerPort, [that.metaInfo.info_hash]); + that.trackerClient = tracker.create(that.metaInfo); + that.pingTracker(); + } + }); + } + }); + } + }; } -exports.startTorrent = startTorrent; \ No newline at end of file +exports.create = create; diff --git a/tracker.js b/tracker.js index 0432201..b3260c5 100644 --- a/tracker.js +++ b/tracker.js @@ -1,6 +1,7 @@ var sys = require('sys'), - http = require('http'), - url = require('url'); + http = require('http'), + bencode = require('bencode'), + url = require('url'); function toHexDigit(n) { return '0123456789abcdef'[n]; @@ -15,8 +16,8 @@ function escapeBinary(s) { for (i = 0, len = s.length; i < len; i += 1) { c = s.charAt(i); if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') - || (c == '.' || c == '-' || c == '_' || c == '~')) { - result += c; + || (c == '.' || c == '-' || c == '_' || c == '~')) { + result += c; } else { cc = s.charCodeAt(i); result += '%' + toHexDigit(0xf & (cc >> 4)) + toHexDigit(0xf & cc); @@ -26,46 +27,83 @@ function escapeBinary(s) { } function queryStringify(params) { - var result = '', key, first = true; - for (key in params) { - if (params.hasOwnProperty(key)) { - if (first) { - first = false; - } else { - result += '&'; - } - result += key + '=' + escapeBinary(params[key]); - } - } - return result; + var result = '', key, first = true; + for (key in params) { + if (params.hasOwnProperty(key)) { + if (first) { + first = false; + } else { + result += '&'; + } + result += key + '=' + escapeBinary(params[key]); + } + } + return result; } function create(metaInfo) { - var announce = metaInfo.announce, - parsedUrl = url.parse(announce), - port = parsedUrl.port ? parsedUrl.port : 80; - return {metaInfo: metaInfo, - tracker: http.createClient(port, parsedUrl.hostname), - trackerRelativeUri: parsedUrl.pathname, - host: parsedUrl.hostname, - peers: {}}; + var announce = metaInfo.announce, + parsedUrl = url.parse(announce), + port = parsedUrl.port ? parsedUrl.port : 80; + return {metaInfo: metaInfo, + port: port, + trackerRelativeUri: parsedUrl.pathname, + host: parsedUrl.hostname, + peers: {}}; +} + +// callback(exception, response, body) +// Handles redirects, coalescing response. + +function httpRequestHelper(verb, host, port, path, headers, redirectLimit, callback) { + headers.host = host; + var client = http.createClient(port, host), + request = client.request(verb, path, headers); + request.addListener('response', function (response) { + var statusCode = response.statusCode, + body = ''; + if (statusCode == 200) { + response.setEncoding('binary'); + response.addListener('error', function (error) { + callback(error, response, body); + }); + response.addListener('end', function () { + callback(null, response, body); + }); + response.addListener('data', function (chunk) { + body += chunk; + }); + } else if (statusCode >= 300 && statusCode <= 399) { + if (redirectLimit <= 0) { + callback('Too many redirects', response); + } else { + sys.log('redirect ' + statusCode + ' ' + JSON.stringify(body)); + httpRequestHelper(verb, host, port, path, headers, redirectLimit-1, callback); + } + } else { + callback('error', response, body); + } + }); + request.end(); } +// callback(error, {response}) function ping(trackerClient, params, callback) { - var path = trackerClient.trackerRelativeUri + '?' + - queryStringify(params), - request = trackerClient.tracker.request('GET', path, - {'host': trackerClient.host}); - sys.log('path:' + path); - request.addListener('response', function (response) { - sys.puts('STATUS: ' + response.statusCode); - sys.puts('HEADERS: ' + JSON.stringify(response.headers)); - response.setEncoding('binary'); - response.addListener('data', function (chunk) { - sys.puts('BODY: ' + chunk); - }); - }); - request.end(); + var path = trackerClient.trackerRelativeUri + '?' + + queryStringify(params); + sys.log('pinging tracker'); + httpRequestHelper('GET', trackerClient.host, trackerClient.port, path, {}, 10, + function (error, response, body) { + var result = {}; + if (!error) { + try { + result = bencode.decode(body); + } catch (e) { + error = e; + } + } + callback(error, result); + }); } exports.create = create;