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;
}

?>