forked from WizKid/node-bittorrent-tracker
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Totally rewrite the tracker. Now supports both HTTP and UDP requests
- Loading branch information
Showing
7 changed files
with
539 additions
and
192 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
var tracker = require("./lib/tracker"); | ||
|
||
var t = tracker.Tracker(); | ||
|
||
tracker.udp.createServer(t, 8080); | ||
tracker.http.createServer(t, 8080); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
exports.http = require("./http"); | ||
exports.Tracker = require("./tracker").Tracker; | ||
exports.udp = require("./udp"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Oops, something went wrong.