Navigation Menu

Skip to content

Commit

Permalink
Totally rewrite the tracker. Now supports both HTTP and UDP requests
Browse files Browse the repository at this point in the history
  • Loading branch information
WizKid committed Mar 27, 2011
1 parent cedfc2c commit f777508
Show file tree
Hide file tree
Showing 7 changed files with 539 additions and 192 deletions.
6 changes: 5 additions & 1 deletion README
@@ -1 +1,5 @@
The querystring lib trims values and keys and also convert values to integers. You don't want that so right now you need isaacs fix for that. You can find it here: http://github.com/isaacs/node/commit/d27d47fab6844ff15d0fab509d4419890965d07a
Run node example.js to start the tracker.

Supports both UDP and HTTP requests.

Tested with Node 0.4.4
6 changes: 6 additions & 0 deletions example.js
@@ -0,0 +1,6 @@
var tracker = require("./lib/tracker");

var t = tracker.Tracker();

tracker.udp.createServer(t, 8080);
tracker.http.createServer(t, 8080);
139 changes: 139 additions & 0 deletions lib/tracker/http.js
@@ -0,0 +1,139 @@
var http = require("http"),
querystring = require("querystring"),
tracker = require("./tracker"),
url = require("url"),
util = require("util");

// Until it is possible to tell url.parse that you don't want a string back
// we need to override querystring.unescape so it returns a buffer instead of a
// string
querystring.unescape = function(s, decodeSpaces) {
return querystring.unescapeBuffer(s, decodeSpaces);
};


const FAILURE_REASONS = {
100: "Invalid request type: client request was not a HTTP GET",
101: "Missing info_hash",
102: "Missing peer_id",
103: "Missing port",
150: "Invalid infohash: infohash is not 20 bytes long",
151: "Invalid peerid: peerid is not 20 bytes long",
152: "Invalid numwant. Client requested more peers than allowed by tracker",
200: "info_hash not found in the database. Sent only by trackers that do not automatically include new hashes into the database",
500: "Client sent an eventless request before the specified time",
900: "Generic error"
}


const PARAMS_INTEGER = [
"port", "uploaded", "downloaded", "left", "compact", "numwant"
]

const PARAMS_STRING = [
"event"
]


function Failure(code, reason) {
this.code = code;
this.reason = reason;
if (reason == undefined && typeof FAILURE_REASONS[this.code] != "undefined")
this.reason = FAILURE_REASONS[this.code]
else if (this.code == null)
this.code = 900;
}

Failure.prototype = {
bencode: function() {
return "d14:failure reason"+ this.reason.length +":"+ this.reason +"12:failure codei"+ this.code +"ee"
}
}


function validateRequest(method, query) {
if (method != "GET")
throw new Failure(100);

if (typeof query["info_hash"] == "undefined")
throw new Failure(101);

if (typeof query["peer_id"] == "undefined")
throw new Failure(102);

if (typeof query["port"] == "undefined")
throw new Failure(103);

if (query["info_hash"].length != 20)
throw new Failure(150);

if (query["peer_id"].length != 20)
throw new Failure(151);

for (var i = 0; i < PARAMS_INTEGER.length; i++) {
var p = PARAMS_INTEGER[i];
if (typeof query[p] != "undefined")
query[p] = parseInt(query[p].toString());
}

for (var i = 0; i < PARAMS_STRING.length; i++) {
var p = PARAMS_STRING[i];
if (typeof query[p] != "undefined")
query[p] = query[p].toString();
}

if (typeof query["compact"] == "undefined" || query["compact"] != 1)
throw new Failure(null, "This tracker only supports compact mode")
}

function createServer(trackerInstance, port, host) {
var server = http.createServer(function (request, response) {
request.addListener('end', function() {
var parts = url.parse(request.url, true);
var query = parts["query"];

try {
validateRequest(request.method, query);

var file = trackerInstance.getFile(query["info_hash"]);
var peer = tracker.Peer(request.connection.remoteAddress, query["port"], query["left"]);
file.addPeer(query["peer_id"], peer, tracker.event(query["event"]));

var want = 50;
if (typeof query["numwant"] != "undefined" && query["numwant"] > 0)
want = query["numwant"];

var peerBuffer = new Buffer(want * tracker.PEER_COMPACT_SIZE);
var len = file.writePeers(peerBuffer, want);
peerBuffer = peerBuffer.slice(0, len);

var resp = "d8:intervali"+ tracker.ANNOUNCE_INTERVAL +"e8:completei"+ file.seeders +"e10:incompletei"+ file.leechers +"e10:downloadedi"+ file.downloads +"e5:peers"+ len +":";

response.writeHead(200, {
'Content-Length': resp.length + peerBuffer.length + 1,
'Content-Type': 'text/plain'
});

response.write(resp);
response.write(peerBuffer);
response.end("e");
} catch (failure) {
var resp = failure.bencode();
console.log(resp);
response.writeHead(500, {
'Content-Length': resp.length,
'Content-Type': 'text/plain'
});

response.end(resp);
}
});
});

server.listen(port, host, function() {
var address = server.address();
console.log("HTTP server listening " + address.address + ":" + address.port);
});
}

exports.createServer = createServer
3 changes: 3 additions & 0 deletions lib/tracker/index.js
@@ -0,0 +1,3 @@
exports.http = require("./http");
exports.Tracker = require("./tracker").Tracker;
exports.udp = require("./udp");
205 changes: 205 additions & 0 deletions lib/tracker/tracker.js
@@ -0,0 +1,205 @@

function now() {
return Math.floor(new Date().getTime()/1000);
}

const EVENT_NONE = 0;
const EVENT_COMPLETED = 1;
const EVENT_STARTED = 2;
const EVENT_STOPPED = 3;

function event(e) {
switch (e) {
case "completed":
return EVENT_COMPLETED;
case "started":
return EVENT_STARTED;
case "stopped":
return EVENT_STOPPED;
}

return EVENT_NONE;
}


const PEERSTATE_SEEDER = 0;
const PEERSTATE_LEECHER = 1;

const PEER_COMPACT_SIZE = 6;

const ANNOUNCE_INTERVAL = 60;

function Peer(ip, port, left) {
if (!(this instanceof Peer))
return new Peer(ip, port, left);

this.compact = this._compact(ip, port)

this.state = (left > 0) ? PEERSTATE_LEECHER : PEERSTATE_SEEDER;

this.touch();
}

Peer.prototype = {
touch: function() {
this.lastAction = now();
},
timedOut: function(n) {
return n - this.lastAction > ANNOUNCE_INTERVAL * 2;
},
_compact: function(ip, port) {
var b = new Buffer(PEER_COMPACT_SIZE);

var parts = ip.split(".");
if (parts.length != 4)
throw 1

for (var i = 0; i < 4; i++)
b[i] = parseInt(parts[i]);

b[4] = (port >> 8) & 0xff;
b[5] = port & 0xff;

return b;
}
}

function File() {
if (!(this instanceof File))
return new File();

this.peerList = [];
this.peerDict = {};

this.downloads = 0;
this.seeders = 0;
this.leechers = 0;

this.lastCompact = now();
}

File.prototype = {
addPeer: function(peerId, peer, event) {
// Check if it is time to compact the peer list
var n = now();
if (this.seeders + this.leechers < this.peerList.length / 2 && this.peerList.length > 10 || (n - this.lastCompact) > ANNOUNCE_INTERVAL * 2) {
newPeerList = [];
var i = 0;
for (var p in this.peerDict) {
if (!this.peerDict.hasOwnProperty(p))
continue;

var tmpPeer = this.peerList[this.peerDict[p]];

// Check if the peer is still alive
if (tmpPeer.timedOut(n)) {
if (tmpPeer.state == PEERSTATE_LEECHER)
this.leechers--;
else
this.seeders--;

delete this.peerDict[p];
continue;
}

newPeerList.push(tmpPeer);
this.peerDict[p] = i++;
}

this.peerList = newPeerList;

this.lastCompact = n;
}

if (event == EVENT_COMPLETED && peer.state == PEERSTATE_SEEDER)
this.downloads++;

// Check if the peer already exists
if (this.peerDict.hasOwnProperty(peerId)) {
var index = this.peerDict[peerId];
var oldPeer = this.peerList[index];

if (event == EVENT_STOPPED) {
if (oldPeer.state === PEERSTATE_LEECHER)
this.leechers--;
else
this.seeders--;

delete this.peerList[index];
delete this.peerDict[peerId];
} else {
// TODO: Should probably update compact in the old peer. So we
// handle the case if the user switched IP or Port. But we
// probably only want to do it if they differ
// oldPeer.compact = peer.compact;

if (oldPeer.state != peer.state) {
if (peer.state === PEERSTATE_LEECHER) {
this.leechers++;
this.seeders--;
} else {
this.leechers--;
this.seeders++;
}

oldPeer.state = peer.state;
}
}

} else if (event != EVENT_STOPPED) {
this.peerDict[peerId] = this.peerList.length;
this.peerList.push(peer);

if (peer.state === PEERSTATE_LEECHER)
this.leechers++;
else
this.seeders++;
}
},
writePeers: function(b, count) {
var c = 0;
if (count > this.seeders + this.leechers) {
for (var i = this.peerList.length - 1; i >= 0; i--) {
var p = this.peerList[i];
if (p != undefined)
p.compact.copy(b, c++ * PEER_COMPACT_SIZE);
}
} else {
var m = Math.min(this.peerList.length, count);
for (var i = 0; i < m; i++) {
var index = Math.floor(Math.random() * this.peerList.length);
var p = this.peerList[index];
if (p != undefined)
p.compact.copy(b, c++ * PEER_COMPACT_SIZE);
}
}

return c * PEER_COMPACT_SIZE;
}
}

function Tracker() {
if (!(this instanceof Tracker))
return new Tracker();

this.files = {};
}

Tracker.prototype = {
getFile: function(infoHash) {
if (this.files.hasOwnProperty(infoHash))
return this.files[infoHash];

return this.addFile(infoHash);
},
addFile: function(infoHash) {
return (this.files[infoHash] = new File());
}
}

exports.PEER_COMPACT_SIZE = PEER_COMPACT_SIZE
exports.ANNOUNCE_INTERVAL = ANNOUNCE_INTERVAL;

exports.event = event;
exports.Peer = Peer;
exports.Tracker = Tracker;

0 comments on commit f777508

Please sign in to comment.