tracker.phps
#!/usr/bin/php5
<?
// TODO
//
// Fixa cleanup() timer
// Fixa äckliga count_peers()
//
error_reporting(E_ALL);
ini_set('memory_limit', '256M');
define('NEWLINE', "\n");
define('VERSION', '0.1');
$docroot = './public_html/';
$maxconns = 500;
$sleepdelay = 50;
$sleepmax = 5000;
$sleeptime = 0;
$announce_interval = 2100;
$announce_timeout = $announce_interval * 1.6;
$stats_hits = 0;
$stats_fdevents = 0;
$stats_fdtimer = time();
//$scale = array();
$peers = 0;
$deadpeers = 0;
$peerinfo = array();
$db = array(
'bytes' => 0,
'bytes_tmp' => 0,
'starttime' => time(),
'proctime' => 0,
'proctime_tmp' => 0,
'hit_announce' => 0,
'hit_scrape' => 0,
'hits' => 0,
'hits_tmp' => 0,
'timeout' => 0,
'timeout_old' => 0,
'sockets' => 0,
'torrents' => 0,
'leechers' => 0,
'seeders' => 0,
'torrents' => array(/*
'01234567890123456789' => array(
'hex' => '01234567890123456789',
'leechers' => 0,
'seeders' => 0,
'times_completed' => 0,
'peers' => array(
)
)*/
)
);
// bind the socket
$socket = stream_socket_server("tcp://0.0.0.0:2710", $errno, $errstr);
if(!is_resource($socket))
{
die('Error: '.$errno.' ('.$errstr.')'.NEWLINE);
}
$conns['main'] = $socket;
while( TRUE )
{ // main server loop
if($stats_fdtimer < time()-15)
{ // do periodic stuff every 15 seconds - cleanup and stat-calculations
$deadpeers = cleanup($db['torrents']);
foreach($conns as $conn)
{
if($conn === $socket)
continue;
if(function_timer($peerinfo[$conn]['time'], gettimeofday(), 1000000, 0) > 60)
{
$ckey = array_search($conn, $conns, true);
//var_dump($ckey, $pkey, $conn);
unset($peerinfo[$conn]);
fclose($conn);
unset($conns[$ckey]);
$db['timeout']++;
//echo 'Timed out connection: '.$conn.' '.(function_timer($peerinfo[$conn]['time'], gettimeofday(), 1000000, 0)-60).' seconds ago!'.NEWLINE;
}
}
$elapsed = time() - $stats_fdtimer;
$stats_fdtimer=time();
$peers = count_total_peers($db['torrents']);
echo date('[H:i:s]').' fds['.number_format($stats_fdevents).'] hits['.number_format($db['hits']).' '.round($db['hits_tmp']/15).'/s] sleeptime['.number_format($sleeptime/1000).' ms] sock['.number_format(count($conns)).'] timeouts['.number_format($db['timeout']).'] peers[L:'.number_format($peers['leechers']).' S:'.number_format($peers['seeders']).' T:'.number_format($peers['leechers']+$peers['seeders']).'] torrents['.number_format(count($db['torrents'])).'] deadpeers['.number_format($deadpeers).']'.NEWLINE;
#echo 'DB: '.count($db, COUNT_RECURSIVE).' peerinfo: '.count($peerinfo).' conns: '.count($conns).' torrents: '.count($db['torrents']).NEWLINE.NEWLINE;
$sleeptime = 0;
$db['bytes'] = $db['bytes'] + $db['bytes_tmp'];
$db['bytes_tmp'] = 0;
$db['hits'] = $db['hits'] + $db['hits_tmp'];
$db['hits_tmp'] = 0;
$db['timeout_old'] = $db['timeout_old'] + $db['timeout'];
$db['timeout'] = 0;
$db['proctime'] = $db['proctime'] + $db['proctime_tmp'];
$db['proctime_tmp'] = 0;
$db['sockets'] = count($conns)-1;
$stats_fdevents=0;
}
$read = $conns;
// find new events on the socket(s)
$fds = stream_select($read, $write = NULL, $except = NULL, 0);
if($fds <= 0 || $fds === false)
{ // no news today, short pause to prevent high load
//usleep(100);
//continue;
$sleep = true;
} else {
$sleep = false;
$sleepdelay = 50;
}
$stats_fdevents=$stats_fdevents+$fds;
for ($i = 0; $i < $fds; ++$i)
{
if ($read[$i] === $socket)
{ // listener socket - new connection probably
if(count($conns) < $maxconns)
{
$conn = stream_socket_accept($socket);
$peerinfo[$conn]['time'] = gettimeofday();
stream_set_timeout($conn, 30);
// no buffer - only writing once before shutting down anyawys
stream_set_write_buffer($socket, 0);
// add client to main connection array
$conns[] = $conn;
} else
{ // fixme? (exceeding maximum allowed sockets)
$sleep = true;
continue;
//usleep(1);
}
} else
{ // new incoming data
#$proctime = gettimeofday();
$data = fread($read[$i], 2048);
if(strlen($data) === 0)
{ // closed connection - remove
$key_to_del = array_search($read[$i], $conns, true);
fclose($read[$i]);
unset($conns[$key_to_del], $peerinfo[$read[$i]]);
#$db['proctime_tmp'] = $db['proctime_tmp'] + function_timer($proctime, gettimeofday(), 1, 0);
} elseif($data === false)
{ // fread error! - close connection
echo date('[H:i:s]').' fread() error!'.NEWLINE;
$key_to_del = array_search($read[$i], $conns, true);
fclose($read[$i]);
unset($conns[$key_to_del], $peerinfo[$read[$i]]);
#$db['proctime_tmp'] = $db['proctime_tmp'] + function_timer($proctime, gettimeofday(), 1, 0);
} else
{ // new data
$db['hits_tmp']++;
handler_data($read[$i], $data);
//handler_send($read[$i], 'blahbleh');
$key_to_del = array_search($read[$i], $conns, true);
fclose($read[$i]);
unset($conns[$key_to_del], $peerinfo[$read[$i]]);
#$proc = function_timer($proctime, gettimeofday(), 1, 0);
#$db['proctime_tmp'] = $db['proctime_tmp'] + $proc;
//if($peers != 0)
//{
// @$scale[$peers]['hits']++;
// @$scale[$peers]['time'] += $proc;
//}
$peers = 0;
}
}
}
if(isset($sleep)) {
usleep($sleepdelay);
$sleeptime += $sleepdelay;
if($sleepdelay < $sleepmax) {
$sleepdelay += 50;
}
}
/*
if(isset($sleep))
{ // maximum connections exceeded - sleep for 1 us to prevent infinite loop if no new data
unset($sleep);
usleep(1);
}
*/
}
function function_timer($start, $end, $div = 1, $format = 1) // $start gettimeofday(); $end gettimeofday(); $div = number to divide by
{ // thanks ethernal
$end["usec"] = ($end["usec"] + ( ($end["sec"] - $start["sec"]) * 1000000));
if($format)
{
return number_format(( ($end["usec"] - $start["usec"]) / $div), 0);
}
else
{
return round(( ($end["usec"] - $start["usec"]) / $div), 0);
}
}
////////////////// calculate and format elapsed time ////////////////////
function duration($ts)
{
$mins = floor((time() - $ts) / 60);
$hours = floor($mins / 60);
$mins -= $hours * 60;
$days = floor($hours / 24);
$hours -= $days * 24;
$weeks = floor($days / 7);
$days -= $weeks * 7;
$t = "";
if ($weeks > 0)
return $weeks. " week" . ($weeks > 1 ? "s" : "");
if ($days > 0)
return $days." day" . ($days > 1 ? "s" : "");
if ($hours > 0)
return $hours." hour" . ($hours > 1 ? "s" : "");
if ($mins > 0)
return $mins." min" . ($mins > 1 ? "s" : "");
return "< 1 min";
}
//////////////////////// format and round bytes ////////////////////////
function mksize($bytes)
{
$suffix = array("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "NB", "DB");
$pos = 0;
while ($bytes >= 1024) {
if ($pos == 4) { break; } // 4 = GB
$bytes /= 1024;
$pos++;
}
$result=number_format($bytes,2).''.$suffix[$pos];
return $result;
}
//////////////// check incoming data and get variables //////////////////
function handler_data($socket, $data)
{
$clientaddr = stream_socket_get_name($socket, true);
if(strpos($clientaddr, ':')) {
list($ip, $port) = explode(':', $clientaddr);
} else { // Lost in trans..?
return;
}
$header = explode("\r\n", $data);
if(preg_match("'([^ ]+) ([^ ]+) (HTTP/[^ ]+)'", $header[0], $matches))
{
//var_dump($matches);
$req['method'] = $matches[1];
$req['uri'] = $matches[2];
$req['protocol'] = $matches[3];
@$tmp = explode('?', $req['uri']);
@$req['target'] = $tmp[0];
@$req['params'] = $tmp[1];
//list(, $req['method'], $req['uri'], ) = $matches;
//@list($req['target'], $req['params']) = explode('?', $req['uri']);
if(isset($req['params']))
{ // variables sent to target from client
foreach(explode('&', $req['params']) as $param)
{
if(strpos($param, '=')) {
list($key, $val) = explode('=', $param);
$keys[$key] = $val;
}
}
}
//handler_send($socket, 'blahbleh');
@handler_http($socket, $req['method'], $req['target'], $keys, $ip, $agent, null);
} else
{ // invalid http-request
handler_send($socket, null, 400);
}
//handler_send($socket, 'blahbleh');
return;
}
////////////////////// do basic httpd-function //////////////////////////
function handler_http($socket, $method, $target, $keys, $ip, $agent, $http_opts)
{
if($method != 'GET')
{
handler_send($socket, null, 501);
return;
}
switch ($target)
{
case '/':
case '/index.html':
global $docroot, $db, $proctime;
$template = file_get_contents($docroot.'tracker.html');
$pattern[] = '/%%HITS%%/';
$replace[] = $db['hits']+$db['hits_tmp'];
$pattern[] = '/%%HITSANNOUNCE%%/';
$replace[] = $db['hit_announce'];
$pattern[] = '/%%HITSSCRAPE%%/';
$replace[] = $db['hit_scrape'];
$pattern[] = '/%%UPTIME%%/';
$replace[] = duration($db['starttime']);
$pattern[] = '/%%DBSIZE%%/';
$replace[] = mksize(count($db, COUNT_RECURSIVE));
$pattern[] = '/%%TORRENTS%%/';
$replace[] = count($db['torrents']);
$pattern[] = '/%%TIMEOUTS%%/';
$replace[] = $db['timeout']+$db['timeout_old'];
$pattern[] = '/%%SOCKETS%%/';
$replace[] = $db['sockets'];
$pattern[] = '/%%GENTIME%%/';
$replace[] = round(function_timer($proctime, gettimeofday(), 1, 0)/1000,2);
$pattern[] = '/%%VERSION%%/';
$replace[] = VERSION;
handler_send($socket, preg_replace($pattern, $replace, $template), 200, 'text/html');
break;
case '/scale':
global $scale;
$x = array_keys($scale);
foreach($x as $key)
{
$y[] = (int)($scale[$key]['time']/$scale[$key]['hits']);
}
handler_send($socket, serialize($x).NEWLINE.serialize($y), 200, 'text/plain');
break;
case '/debug':
global $db;
handler_send($socket, '<html><head><title>BitTorrent Tracker Debug</title></head><body><pre>'.print_r($db, true).'</pre></body></html>', 200, 'text/html');
break;
case '/scrape':
case '/scrape.php':
case '/tracker.php/747ea56569aed2b61c127941471f8ef3/scrape':
case '/tracker.php/f428b0287dc925af1c8efab6762a79a0/scrape':
case '/tracker.php/a8726297ba172fbd223c65e2f99e219f/scrape':
$tmptime = gettimeofday();
handler_send($socket, handler_scrape($keys, $tmptime));
break;
case '/announce':
case '/announce.php':
case '/tracker.php/747ea56569aed2b61c127941471f8ef3/announce':
case '/tracker.php/f428b0287dc925af1c8efab6762a79a0/announce':
case '/tracker.php/a8726297ba172fbd223c65e2f99e219f/announce':
$tmptime = gettimeofday();
handler_send($socket, handler_announce($ip, $keys, $tmptime));
break;
default:
echo date('[H:i:s]').' Got 404 for: '.$target.' with variables: '.join('.',$keys).NEWLINE;
handler_send($socket, null, 404, 'text/html');
}
//handler_send($socket, 'blahbleh', 200);
return;
}
///////////////////// client requested scrape ///////////////////////////
function handler_scrape($keys, $tmptime)
{
global $db;
$db['hit_scrape']++;
if(isset($keys['info_hash']))
{
$info_hash = urldecode($keys['info_hash']);
if(strlen($info_hash) != 20)
{ // invalid hash - ignore request
echo 'Debug: scrape with invalid hash.'.NEWLINE;
return 'd5:filesdee';
}
}
if($info_hash)
{
if(isset($db['torrents'][$info_hash]))
{ // send hash for single torrent
//echo 'Scrape time: '.function_timer($tmptime, gettimeofday()).' microsec. (hash)'.NEWLINE;
return 'd5:filesd20:'.$info_hash.'d8:completei'.$db['torrents'][$info_hash]['seeders'].'e10:incompletei'.$db['torrents'][$info_hash]['leechers'].'eeee';
} else
{ // hash not found - send empty
//echo 'Scrape time: '.function_timer($tmptime, gettimeofday()).' microsec. (no hash)'.NEWLINE;
return 'd5:filesdee';
}
}
$out = 'd5:filesd';
foreach(array_keys($db['torrents']) as $hash)
{
$out .= '20:'.$hash.
'd'.
'8:completei' . $db['torrents'][$hash]['seeders'] . 'e' .
'10:incompletei' . $db['torrents'][$hash]['leechers'] . 'e' .
'e';
}
$out .= 'ee';
//echo 'Scrape time: '.function_timer($tmptime, gettimeofday()).' microsec. (empty)'.NEWLINE;
return $out;
//return 'd5:filesdee';
}
//////////////////////// client requested announce //////////////////////
function handler_announce($ip, $keys, $tmptime)
{ // $info_hash, $peer_id, $ip, $port, $compact, $no_peer_id, $seeder
global $db, $announce_interval;
$db['hit_announce']++;
if(isset($keys['info_hash']))
{ // decode and check info_hash variable
$info_hash = urldecode($keys['info_hash']);
if(strlen($info_hash) != 20)
{
return 'info_hash error!';
}
} else
{
return 'info_hash error!';
}
if(isset($keys['peer_id']))
{ // decode and check peer_id variable
$peer_id = urldecode($keys['peer_id']);
if(strlen($peer_id) != 20)
{
return 'peer_id error!';
}
} else
{
return 'peer_id error!';
}
if(isset($keys['port']))
{ // verify that port is a number
if(ctype_digit($keys['port']) === false)
{
return 'port error!';
}
} else
{
return 'port error!';
}
if(isset($keys['event']))
{ // verify that we have a valid event
if(ctype_alpha($keys['event']) === false)
{
return 'invalid event';
}
}
if(!isset($keys['compact']) || $keys['compact'] != '1')
{
$keys['compact'] = false;
if(isset($keys['no_peer_id']) && $keys['no_peer_id'] == '1')
{
$keys['no_peer_id'] = true;
} else
{
$keys['no_peer_id'] = false;
}
} else
{
$keys['compact'] = true;
}
if(isset($keys['left']) && ctype_digit($keys['left']))
{
if($keys['left'] == 0)
{ // is a seeder
$keys['left'] = false;
} else
{ // is not a seeder
$keys['left'] = true;
}
} else
{
return 'left key error!';
}
if($keys['compact'] !== true)
{ // tracker only supports compact, and apparently you don't - bye bye
return 'Your client is outdated! (no compact)';
}
if(isset($db['torrents'][$info_hash]))
{ // found hash - send peers to client
//echo 'Announce time: '.function_timer($tmptime, gettimeofday()).' microsec. (found)'.NEWLINE;
//return 'd8:intervali30e5:peers0:e';
} else
{ // hash was not found - add it
$db['torrents'][$info_hash] = array('leechers' => 0, 'seeders' => 0, 'times_completed' => 0, 'hex' => bin2hex($info_hash),
'peers' => array(
)
);
//echo 'Announce time: '.function_timer($tmptime, gettimeofday()).' microsec. (didnt find hash)'.NEWLINE;
//return 'd8:intervali30e5:peers0:e';
}
$client = pack('Nn', ip2long($ip), $keys['port']);
//echo 'Peer hash: 0x'.bin2hex($client).NEWLINE;
if($keys['event'] == 'stopped')
{ // stopped - don't send any stuff back
unset($db['torrents'][$info_hash]['peers'][$client]);
count_peers(&$db['torrents'][$info_hash]);
return;
}
if(isset($db['torrents'][$info_hash]['peers'][$client]))
{ // peer exists in database
$seed = ($keys['left'] === true) ? '0':'1';
if($seed !== $db['torrents'][$info_hash]['peers'][$client]['seed'])
{
$db['torrents'][$info_hash]['peers'][$client]['seed'] = $seed;
}
$db['torrents'][$info_hash]['peers'][$client]['last'] = time();
} else
{ // peer does not exist in database
$db['torrents'][$info_hash]['peers'][$client] = array('start' => time(), 'last' => time(), 'seed' => ($keys['left'] === true) ? '0':'1');
}
if(count($db['torrents'][$info_hash]['peers']) > 50)
{
$clients = join('',array_rand($db['torrents'][$info_hash]['peers'], 50));
} else
{
$clients = join('',array_keys($db['torrents'][$info_hash]['peers']));
}
//global $proctime;
//echo 'Announce time: '.function_timer($proctime, gettimeofday()).' microsec. (finished, '.count($db['torrents'][$info_hash]['peers']).' peers, giving '.(strlen($clients)/6).')'.NEWLINE;
count_peers(&$db['torrents'][$info_hash]);
global $peers;
$peers = $db['torrents'][$info_hash]['leechers'] + $db['torrents'][$info_hash]['seeders'];
//echo 'Announce time: '.function_timer($tmptime, gettimeofday()).' microsec. (finished, '.count($db['torrents'][$info_hash]['peers']).' peers)'.NEWLINE;
return 'd8:intervali'.$announce_interval.'e5:peers'.strlen($clients).':'.$clients.'e';
//return 'd8:intervali30e5:peers0:e';
}
//////////////////////// send data to client ////////////////////////////
function handler_send($socket, $data, $code = 200, $content = 'text/plain')
{
global $db;
switch ($code)
{
case 200:
$code = '200 OK';
break;
case 400: // malformed syntax
$code = '400 Bad Request';
$data = 'Bad Request';
case 404:
$code = '404 Not Found';
//$data = 'Not Found';
$data = '<html><head><title>HTTP error 404</title></head><body><h1 align="left">HTTP error 404: Not Found</h1></body></html>';
break;
case 501: // does not support (method?)
$code = '501 Not Implemented';
$data = 'Not Implemented';
break;
default:
$code = '500 Internal Server Error';
$data = 'Internal Server Error';
}
$head = "HTTP/1.1 ".$code."\nServer: PHP BitTorrent Tracker v".VERSION."\nContent-type: ".$content."\nConnection: close\n\n";
$data = $head . $data;
$len = strlen($data);
if(($bytes = fwrite($socket, $data, $len)) === false)
{ // data was not sent
echo date('[H:i:s]').'Failed to send data to client!'.NEWLINE;
} else
{ // data was sent
if($bytes != $len)
{ // data sent does not match the amount of data given!
$db['bytes_tmp'] = $db['bytes_tmp'] + ($len-$bytes);
echo date('[H:i:s]').'Gave '.$len.' but '.$bytes.' was sent!'.NEWLINE;
} else
{ // data sent - all good
$db['bytes_tmp'] = $db['bytes_tmp'] + $len;
}
}
return;
}
function count_peers(&$arr)
{ // i want cookies!
$seed = $leech = 0;
foreach(array_keys($arr['peers']) as $key)
{
if(!is_array($key))
{
if($arr['peers'][$key]['seed'] == '1')
{
++$seed;
} else
{
++$leech;
}
}
}
//echo 'seed: '.$seed.', leech: '.$leech.NEWLINE;
$arr['leechers'] = $leech;
$arr['seeders'] = $seed;
return;
}
function count_total_peers($arr)
{
$leechers = $seeders = 0;
foreach(array_keys($arr) as $key)
{
$leechers += $arr[$key]['leechers'];
$seeders += $arr[$key]['seeders'];
}
return array('leechers' => $leechers, 'seeders' => $seeders);
}
function cleanup(&$arr)
{
global $announce_timeout;
$time = time();
$deleted = 0;
foreach(array_keys($arr) as $torrent)
{ // torrent
if(!is_array($arr[$torrent]['peers']))
{
continue;
}
foreach(array_keys($arr[$torrent]['peers']) as $peer)
{ // peer
if($time-$arr[$torrent]['peers'][$peer]['last'] > $announce_timeout)
{
//echo 'Dead peer: '.duration($arr[$torrent]['peers'][$peer]['last']).' old.'.NEWLINE;
if($arr[$torrent]['peers'][$peer]['seed'] == '1')
{
$arr[$torrent]['seeders']--;
} else
{
$arr[$torrent]['leechers']--;
}
unset($arr[$torrent]['peers'][$peer]);
$deleted++;
}
}
if(count($arr[$torrent]['peers']) <= 0)
{
#echo 'Dead torrent: '.bin2hex($torrent).NEWLINE;
unset($arr[$torrent]);
}
}
return $deleted;
}
?>