Skip to content

Instantly share code, notes, and snippets.

@formula1
Forked from nsuan/announce.php
Last active January 25, 2016 05:21
Show Gist options
  • Save formula1/ff1164ed8c311701927b to your computer and use it in GitHub Desktop.
Save formula1/ff1164ed8c311701927b to your computer and use it in GitHub Desktop.
Bitstorm Tracker
var fs = require('fs');
/*
* Yeah, this is the database engine. It's pretty bad, uses files to store peers.
* Should be easy to rewrite to use SQL instead.
*
* Yes, sometimes collisions may occur and screw the DB over. It might or might not
* recover by itself.
*/
var DB;
module.exports = DB = function(location){
this.location = location;
};
DB.prototype.save = function(data){
try{
fs.writeFileSync(this.location, JSON.stringify(data));
return true;
}catch(e){
return false;
}
};
//Load database from file
DB.prototype.save = function(){
try{
return fs.readFileSync(fs.readFileSync(this.location));
}catch(e){
return false;
}
};
//Check if DB file exists, otherwise create it
DB.prototype.exists = function(create_empty){
create_empty = !!create_empty;
if(fs.existsSync(this.location)) return true;
if(!create_empty) return false;
return this.save([]);
};
'use strict';
/*
* Bitstorm - A small and fast Bittorrent tracker
* Copyright 2008 Peter Caprioli
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*************************
** Configuration start **
*************************/
//What version are we at?
const __VERSION = 1.3;
//How long should we wait for a client to re-announce after the last
//announce expires? (Seconds)
const __CLIENT_TIMEOUT = 60;
//Skip sending the peer id if client does not want it?
//Hint: Should be set to true
const __NO_PEER_ID = true;
//Should seeders not see each others?
//Hint: Should be set to true
const __NO_SEED_P2P = true;
//Where should we save the peer database
//On Linux, you should use /dev/shm as it is very fast.
//On Windows, you will need to change this value to some
//other valid path such as C:/Peers.txt
const __LOCATION_PEERS = '/dev/shm/Bittorrent.Peers';
//In case someone tries to access the tracker using a browser,
//redirect to this URL or file
const __REDIR_BROWSER = './ui.php';
//Enable debugging?
//This allows anyone to see the entire peer database by appending ?debug to the announce URL
const __DEBUGGING_ON = false;
/***********************
** Configuration end **
***********************/
var sha1 = require('sha1');
var DB = require('./db');
var db = new DB(__LOCATION_PEERS);
var validateQuery = require('./validate-query');
var track, is_seed;
module.exports = function(req, res){
//Did we get any parameters at all?
//Client is probably a web browser, do a redirect
if(Object.keys(req.query).length === 0){
res.setHeader('Location: ', __REDIR_BROWSER);
return res.end();
}
//Create database if it does not exist
if(!db.exists(true)){
throw new Error(track('Unable to create database'));
}
var d = db.open();
//Did we get a failure from the database?
if(d === false){
return res.end(track('Database failure'));
}
//Do we want to debug? (Should not be used by default)
if('debug' in req.query && __DEBUGGING_ON){
res.write(`Connected peers:${d}\n\n`);
res.write(JSON.stringify(d));
return res.end();
}
var query = req.query;
try{
validateQuery(query);
}catch(e){
return res.end(track(e.message));
}
//Send response as text
res.setHeader('Content-type', 'Text/Plain');
res.setHeader('X-Tracker-Version', `Bitstorm ${__VERSION} by formula1 and ck3r.org`); //Gotta give dredit where its due
//Array key, unique for each client and torrent
var sum = sha1(query.peer_id + query.info_hash);
//Make sure we've got a user agent to avoid errors
//Used for debugging
if(!('HTTP_USER_AGENT' in req.headers)){
req.headers.HTTP_USER_AGENT = ''; //Must always be set
}
//When should we remove the client?
var expire = Date.now() + query.interval;
//Have this client registered itself before? Check that it uses the same key
if(sum in d && d[sum][6] !== query.key){
return setTimeout(function(){
res.statusCode = 403;
res.end(track('Access denied, authentication failed'));
}, 1000);
}
//Add/update the client in our global list of clients, with some information
d[sum] = [
req.remoteAddress, query.peer_id, query.port,
expire, query.info_hash, req.headers.HTTP_USER_AGENT,
query.key, is_seed(query),
];
//No point in saving the user agent, unless we are debugging
if(!__DEBUGGING_ON){
delete d[sum][5];
}else{ //We are debugging, add GET parameters to database
d[sum].get_parm = query;
}
//Did the client stop the torrent?
//We dont care about other events
if('event' in query && query.event === 'stopped'){
delete d[sum];
db.save(d);
return res.end(track([])); //The RFC says its OK to return whatever we want when the client stops downloading,
//however, some clients will complain about the tracker not working, hence we return
//an empty bencoded peer list
}
//Check if any client timed out
for(var k in d){
var data = d[k];
if(Date.now() > data[3] + __CLIENT_TIMEOUT){ //Give the client some extra time before timeout
delete d[k]; //Client has gone away, remove it
}
}
//Save the client list
db.save(d);
//Compare info_hash to the rest of our clients and remove anyone who does not have the correct torrent
for(var id in d){
var info = d[id];
if(info[4] !== query.info_hash){
delete d[id];
}
}
//Remove self from list, no point in having ourselfes in the client dictionary
delete d[sum];
//Add a few more seconds on the timeout to balance the load
query.interval += Math.floor(Math.random() * 10);
//Bencode the dictionary and send it back
res.end(track(d, query.interval, query.interval_min));
};
//Find out if we are seeding or not. Assume not if unknown.
is_seed = function(query){
if(!('left' in query)){
return false;
}
if(query.left == 0){
return true;
}
return false;
};
//If you *really* dont want to, comment this line out
//Bencoding function, returns a bencoded dictionary
//You may go ahead and enter custom keys in the dictionary in
//this function if you'd like.
track = function(req, list, interval, min_ival){
if(!interval) interval = 60;
if(!min_ival) min_ival = 0;
if(typeof list === 'string'){ //Did we get a string? Return an error to the client
return `d14:failure reason${list.length}:${list}e`;
}
var p = ''; //Peer directory
var c, i; //Complete and Incomplete clients
c = i = 0;
for(var k in list){ //Runs for each client
var d = list[k];
if(d[7]){ //Are we seeding?
c++; //Seeding, add to complete list
if(__NO_SEED_P2P && is_seed()){ //Seeds should not see each others
continue;
}
}else{
i++; //Not seeding, add to incomplete list
}
//Do some bencoding
var pid = '';
if(!('no_peer_id' in req.query) && __NO_PEER_ID){ //Shall we include the peer id
pid = `7:peer id${d[1].length}:${d[1]}`;
}
p += `d2:ip${d[0].length}:${d[0]}${pid}4:porti${d[2]}ee`;
}
//Add some other paramters in the dictionary and merge with peer list
return `d8:intervali${interval}e12:min intervali${min_ival}e8:completei${c}e10:incompletei${i}e5:peersl${p}ee`;
};
'use strict';
//How often should clients pull server for new clients? (Seconds)
const __INTERVAL = 1800;
//What's the minimum interval a client may pull the server? (Seconds)
//Some bittorrent clients does not obey this
const __INTERVAL_MIN = 300;
//Should we enable short announces?
//This allows NATed clients to get updates much faster, but it also
//takes more load on the server.
//(This is just an experimental feature which may be turned off)
const __ENABLE_SHORT_ANNOUNCE = true;
var valdata;
module.exports = function(query){
/*
* This is a pretty smart feature not present in other tracker software.
* If you expect to have many NATed clients, add short as a GET parameter,
* and clients will pull much more often.
*
* This can be done automatically, simply try to open a TCP connection to
* the client and assume it is NATed if not successful.
*/
if('short' in query && __ENABLE_SHORT_ANNOUNCE){
query.interval = 120;
query.interval_min = 30;
}else{
//Default announce time
query.interval = __INTERVAL;
//Minimal announce time (does not apply to short announces)
query.interval_min = __INTERVAL_MIN;
}
//Inputs that are needed, do not continue without these
valdata(query, 'peer_id', true);
valdata(query, 'port');
valdata(query, 'info_hash', true);
//Use the tracker key extension. Makes it much harder to steal a session.
if(!('key' in query)) query.key = '';
valdata(query, 'key');
//Do we have a valid client port?
try{
query.port = parseInt(query.port);
}catch(e){
throw new Error('Invalid client port');
}
if(query.port < 1 || query.port > 65535){
throw new Error('Invalid client port');
}
};
//Do some input validation
valdata = function(query, g, must_be_20_chars){
must_be_20_chars = !!must_be_20_chars;
if(!(g in query)){
throw new Error('Missing one or more arguments');
}
if(typeof query[g] !== 'string'){
throw new Error('Invalid types on one or more arguments');
}
if(must_be_20_chars && query[g].length != 20){
throw new Error(`Invalid length on ${g} argument`);
}
if(query[g].length > 128){ //128 chars should really be enough
throw new Error(`Argument ${g} is too large to handle`);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment