tracker-dev.phps

<?php
/*
experimental tracker with passkey for tbsource by hellfairy
access method: /tracker-dev.php/$passkey/announce

  TODO:
    write code for waittime? (yuck!)
    check if ip is banned? (account is checked if banned anyways, point be?)
    detect if client is connectable or not
    something faster then group by rand() for the mysql-query that gets the peers from database
    logging of "cheat-detection" to a table instead for long-term stats
    fix broken numwant
    times_completed not updating


  FEATURES:
    * passkey support (not optional)
    * limitations for how many different ips each user may have active on the tracker
    * supports original, no_peer_id and compact protocols, with option to disable original and no_peer_id
    * gzip compression option for scrape, original and no_peer_id protocols
    * option to enable or disable scrape statistics for all torrents on the tracker, saves bandwidth on large trackers
    * works without need for the functions in the tbsource code
    * keeps track of peer even if the ip would change, as long as the client has not restarted or it has been deleted from the database
    * optional support for saving upload/download statistics for users
    * basic anti-cheat ability thru calculating the clients average speed and logging it


    Database alterations:
      USERS
        passkey - varchar(16)
      PEERS
        compact - varchar(6/binary)

*/
/*
// debugging stuff
$_SERVER['REQUEST_URI'] = '/tracker-dev.php/d074731f0c3c5424/announce';
$_GET['info_hash'] = pack('H*', '4128e1fb4b4ede8065a093f104450b32b524c214');
$_GET['peer_id'] = pack("H*", sha1(rand(0, 1)));
$_GET['port'] = (string)rand(20, 120);
$_GET['left'] = (string)rand(0, 3);
$_GET['uploaded'] = (string)rand(0, 3);
$_GET['downloaded'] = (string)rand(0, 3);
$_GET['compact'] = (string)1;
$_GET['numwant'] = (string)50;
// debugging stuff
*/

// SETTINGS
$time_me = true; // calculate execution times (requires log_debug)
$log_debug = true; // log debugging information using error_log()
$log_errors = false; // log all errors sent using err()
$gzip = true; // gzip the data sent to the clients
$allow_old_protocols = true; // allow no_peer_id and original protocols for compatibility
$allow_global_scrape = false; // enable scrape-statistics for all torrents if no info_hash specified - wastes bandwidth on big trackers
$default_give_peers = 50; // how many peers to give to client by default
$max_give_peers = 150; // maximum peers client may request
$announce_interval = rand(1500,2100); // 28-33 min - spread load a bit on the webserver
$max_unique_ips = false; // how many different ips for each user to allow simultaneously, change passkey if exceeds, use false to disable [1-3 extra mysql-queries]
$max_unique_ips_pertorrent = false; // should this be checked for each torrent instead of for all users peers?
$max_unique_ips_autoreset = false; // reset the users passkey if exceeding
$rate_limitation = true; // calculate the clients average download-speed
$rate_limitation_exceptions = array(5, 4); // ignore these classes
$rate_limitation_warn_up = 4; // log a warning if exceeding this amount of MB/s
$rate_limitation_warn_down = 5;
$rate_limitation_err_up = 5; // log a error and don't save stats for user if exceeding this amount of MB/s
$rate_limitation_err_down = 10;
$register_stats = true; // save transfer statistics for the users? [0-1 extra mysql-queries]
$ignore_stats = array(5, 4); // ignore registering transfer stats for these classes
// SETTINGS


if($time_me || $log_debug)
{
  $start = gettimeofday();
}

// $keys['1'] = this scriptname
// $keys['2'] = passkey - alphanumeric string(16)
// $keys['3'] = scrape/announce - string(6/8)
$keys = explode('/',$_SERVER['REQUEST_URI']);

if(count($keys) != 4) // check number of arguments passed to the script
{
  err('Invalid request.');
}

if(strlen($keys['2']) !== 16 || !ctype_alnum($keys['2'])) // check passkey-format
{
  err('Invalid passkey length or format. Length: ' . strlen($keys['2']) . '.');
}
$passkey = $keys['2'];

if(strpos($keys['3'], 'announce') !== false) // jump into appropriate section for announce or scrape mode
{ // do-announce code

  // validate important values
  $info_hash = hasheval($_GET['info_hash'], '20', 'info_hash');
  $peer_id = hasheval($_GET['peer_id'], '20', 'peer_id');
  $seeder = ($_GET['left'] == 0) ? 'yes' : 'no';
  // required values - we want numbers only
  $intvars = array('port', 'uploaded', 'downloaded', 'left');
  foreach($intvars as $var)
  {
    if(!isset($_GET[$var]) || ctype_digit($_GET[$var]) === false)
    {
      err('Invalid key: ' . $var . '.');
    }
  }
  if ($_GET['port'] > 0xffff || $_GET['port'] < 1)
  {
    err('Invalid port number.');
  }
  $ip = getip();
  // optional values - we want numbers only
  $intoptvars = array('numwant', 'compact', 'no_peer_id');
  foreach($intoptvars as $var)
  {
    if(isset($_GET[$var]) && ctype_digit($_GET[$var] === false))
    {
      err('Invalid opt key: ' . $var . '.');
    }
  }
  if(isset($_GET['event']))
  {
    if(ctype_alpha($_GET['event']) === false)
    {
      // event was sent, but it contains invalid information
      err('Invalid event.');
    }
    $events = array('started','stopped','completed');
    if(!in_array($_GET['event'], $events))
    {
      err('Invalid event.');
    }
    $event = $_GET['event'];
  }
  if(!$allow_old_protocols)
  {
    if(!isset($_GET['compact']) && ($event != 'stopped' && $event != 'completed'))
    { // client has not stopped or completed - should say it supports compact if doing so
      err('Old tracker protocols disabled, please upgrade or change client.');
    }
  }
  // all values should now have checked out ok

  mysqlconn();

  $res = mysql_query('SELECT id, class FROM users WHERE passkey = "' . $passkey . '" AND enabled = "yes"') or err('Could not query user info!');
  if(mysql_num_rows($res) !== 1)
  { // a valid passkey was not found or the account was disabled
    err('Permission denied.');
  }
  list($userid, $userclass) = mysql_fetch_row($res) or err('Could not get user info!');

  $res = mysql_query('SELECT id, leechers, seeders, visible FROM torrents WHERE info_hash = "' . mysql_real_escape_string($info_hash) . '"') or err('Could not query torrent info!');
  if(mysql_num_rows($res) == 0)
  { // could not find the requested torrent in the database
    err('Torrent does not exist on this tracker.');
  }
  elseif(mysql_num_rows($res) != 1)
  { // It's Epidemical-proof now!
    err('Found multiple torrents, database error!');
  }
  list($torrentid, $leechers, $seeders, $visible) = mysql_fetch_row($res) or err('Could not get torrent info!');

  if($log_debug)
  {
    error_log('announce: user ' . $userid . ' just announced on torrent ' . $torrentid . '. (class: ' . $userclass . ', peers: ' . ($leechers+$seeders) . ', ip: ' . $ip .')');
  }

  // try to find peer and get it's stats from database
  $res = mysql_query('SELECT id, downloaded, uploaded, to_go, seeder, ip, UNIX_TIMESTAMP(last_action) FROM peers WHERE torrent = "' . $torrentid . '" AND port = "' . $_GET['port'] . '" AND peer_id = "' . mysql_real_escape_string($peer_id) . '"') or err('Could not query peer!');

  if(mysql_num_rows($res) == 0)
  { // peer not found - insert into database, but only if not event=stopped
    if($log_debug)
    {
      error_log('announce: peer not found!');
    }
    if($_GET['event'] == 'stopped')
    {
      err('Client sent stop, but peer not found!');
    }

    // count how many unique ips for this users peers
    if($max_unique_ips)
    {
      $res = mysql_query('SELECT COUNT(DISTINCT(ip)) FROM peers WHERE userid = "'.$userid.'"'.($max_unique_ips_pertorrent == true ? ' AND torrent = "' . $torrentid . '"' : '')) or error_log(mysql_error());
      $ip_count = mysql_fetch_row($res);
      //error_log('unique ips for user: '.$ip_count[0]);
      if($ip_count[0] > $max_unique_ips)
      { // number of ips exceeded limit - take actions
        if($max_unique_ips_autoreset === true)
        { // reset the passkey stuffs
          mysql_query('UPDATE users SET passkey = "" WHERE id = "'.$userid.'"');
          mysql_query('DELETE FROM peers WHERE userid = "'.$userid.'"');
        }
        err('IP limit triggered. (You have '.$ip_count[0].' different currently active)');
      }
    }

    // insert waittime / etc here..?

    // everything seems ok, insert new peer into the database
    $query = 'INSERT INTO peers '.
      '(torrent, userid, peer_id, ip, compact, port, uploaded, uploadoffset, downloaded, downloadoffset, to_go, seeder, started, last_action, agent, connectable) VALUES ' .
      '("'.$torrentid.'", "'.$userid.'", "'.mysql_real_escape_string($peer_id).'", "'.$ip.'", "'.mysql_real_escape_String(pack('Nn', ip2long($ip), $_GET['port'])).'", "'.$_GET['port'].'", "'.$_GET['uploaded'].'", "'.$_GET['uploaded'].'", "'.$_GET['downloaded'].'", "'.$_GET['downloaded'].'", "'.$_GET['left'].'", "'.$seeder.'", FROM_UNIXTIME("'.time().'"), FROM_UNIXTIME("'.time().'"), "'.mysql_real_escape_string($_SERVER['HTTP_USER_AGENT']).'","yes")';
    //error_log($query);
    mysql_query($query) or err('Could not insert peer into database! ('.mysql_error().')');

    if($seeder == 'yes')
    {
      mysql_query('UPDATE torrents SET visible = "yes", last_action = NOW() WHERE id = "'.$torrentid.'"');
    }

    give_peers();

  } elseif (mysql_num_rows($res) == 1) {
    // peer found - update stats, check if peer is stopping, else send peer list

    list($peerid, $downloaded, $uploaded, $left, $seeder_db, $ip_db, $last_access) = mysql_fetch_row($res) or err('Could not get peer info!');

    // calculate download and upload speed based on difference in amounts since last time reported in
    if($rate_limitation === true && in_array($userclass, $rate_limitation_exceptions) === false)
    {
      $duration = time() - $last_access;
      if( $duration > 0 )
      {
        $downspeed = round( ( $_GET['downloaded'] - $downloaded ) / $duration );
        $upspeed = round( ( $_GET['uploaded'] - $uploaded ) / $duration );
        if($downspeed > (1024000 * $rate_limitation_err_down) || $upspeed > (1024000 * $rate_limitation_err_up))
        { // check for excessive speeds
          $register_stats = false;
          error_log('announce: warning - user '.$userid.' has exceeded 5 MB/s speed (up: '.number_format($upspeed / 1024, 3).' kB/s, down: '.number_format($downspeed / 1024, 3).' kB/s)');
        } elseif($downspeed > (1024000 * $rate_limitation_warn_down) || $upspeed > (1024000 * $rate_limitation_warn_up)) {
          error_log('announce: warning - user '.$userid.' has exceeded 2 MB/s speed (up: '.number_format($upspeed / 1024, 3).' kB/s, down: '.number_format($downspeed / 1024, 3).' kB/s)');
        } else {
          if($log_debug)
          {
            error_log('announce: speed - in '.$duration.' sec, up: '.number_format($upspeed / 1024, 3).' kB/s, down: '.number_format($downspeed / 1024, 3).' kB/s');
          }
        }
      } else {
        // less then a second or negative since last contacted tracker - suspicious, log?
        $down = ( $_GET['downloaded'] - $downloaded );
        $up = ( $_GET['uploaded'] - $uploaded );
        $register_stats = false;
        error_log('announce: user '.$userid.' client hammering - up: '.number_format($up).', down: '.number_format($down));
      }
    }

    // update the user-stats
    if( $register_stats === true && ( ($_GET['downloaded'] > $downloaded) || ($_GET['uploaded'] > $uploaded) ) && in_array($userclass, $ignore_stats) === false )
    { // only update if there has been a change, and it is a increase :)
      if($log_debug)
      {
        error_log('announce: updating user stats - up/down: '.($_GET['uploaded'] - $uploaded).'/'.($_GET['downloaded'] - $downloaded));
      }
      mysql_query('UPDATE users SET uploaded = uploaded + "' . ($_GET['uploaded'] - $uploaded) . '", downloaded = downloaded + "' . ($_GET['downloaded'] - $downloaded) . '" WHERE id="' . $userid . '"') or err('Could not update your transfer stats!');
    }

    // if download just completed - add to the number on the torrent table - but only if a seeder
    if($event == 'completed' && $left == 0)
    {
      mysql_query('UPDATE torrents SET times_completed = times_completed + "1" WHERE id = "' . $torrentid . '"') or error_log(mysql_error());
    }

    // peer has closed - remove the peer and exit, no updates to do or peers to send to client
    if($_GET['event'] == 'stopped')
    {
      $res = mysql_query('DELETE FROM peers WHERE id="'.$peerid.'"') or err('Peer deletion query failed!');
      exit;
    }

    // update stats for the peer
    mysql_query('UPDATE peers SET uploaded = "'.$_GET['uploaded'].'", downloaded = "'.$_GET['downloaded'].'", to_go = "'.$_GET['left'].'", seeder = "'.$seeder.'", last_action = FROM_UNIXTIME("'.time().'")' . ($event == 'completed' && $seeder == 'yes' && $seeder_db == 'no' ? ', finishedat = "' . time() . '"':'') . ($ip != ip_db ? ', ip = "' . $ip . '"':'') . ' WHERE id = "' . $peerid . '"') or err('Could not update peer stats!');

    if($seeder == 'yes')
    {
      mysql_query('UPDATE torrents SET visible = "yes", last_action = NOW() WHERE id = "'.$torrentid.'"');
    }

    // give the client some peers to play with
    give_peers();

  } else {
    // we hit multiple? but that's UNPOSSIBLE! ;)
    if($log_debug)
    {
      error_log('announce: got multiple targets in peer table!');
    }
    err('Got multiple targets in peer table!');
  }

  // give clients something to be happy while debugging :)
  //echo 'd8:intervali'.$announce_interval.'e5:peers0:e';

  if($time_me && $log_debug)
  {
    error_log('announce: ' . function_timer($start, gettimeofday(), 1) . ' us)');
  }

  exit;
}
elseif(strpos($keys['3'], 'scrape') !== false)
{ // do-scrape code

  //$info_hash = hasheval($_GET['info_hash'],'20' , 'info_hash');
  $info_hash = $_GET['info_hash'];
  if(strlen($info_hash) != 20)
  {
    $info_hash = stripcslashes($_GET['info_hash']);
  }

  // compression - saves few bytes?
  if($gzip)
  {
    ini_set('zlib.output_compression_level', 1);
    ob_start('ob_gzhandler');
  }

  if(strlen($info_hash) != 20 && $allow_global_scrape == false) // if a valid info_hash was not specified, send empty - save bandwidth
  {
    if($time_me && $log_debug)
    {
      error_log('scrape - empty: ' . function_timer($start, gettimeofday(), 1) . ' us)');
    }
    die('d5:filesdee');
  }

  mysqlconn();

  //$res = mysql_query('SELECT info_hash, times_completed, seeders, leechers FROM torrents WHERE ' . hash_where('info_hash', $info_hash));
  $res = mysql_query('SELECT info_hash, times_completed, seeders, leechers FROM torrents' . (strlen($info_hash) == 20 ? ' WHERE info_hash = "' . mysql_real_escape_string($info_hash) . '"':''));
  $resp = 'd5:filesd';
  while ($torrent = mysql_fetch_assoc($res))
  { // yes, no bencoding functions here
    $resp .= '20:' . $torrent['info_hash'] .
    'd'.
      '8:completei' . (int)$torrent['seeders'] . 'e' .
      '10:incompletei' . (int)$torrent['leechers'] . 'e' .
      '10:downloadedi' . (int)$torrent['times_completed'] . 'e' .
    'e';
  }

  $resp .= 'ee';
  echo($resp);
  if($time_me && $log_debug)
  {
    error_log('scrape: ' . function_timer($start, gettimeofday(), 1) . ' us)');
  }
  exit;
}
else
{
  err('Unknown action.');
}

// functions used in the tracker starts here


function give_peers()
{
  global $torrentid, $gzip, $max_give_peers, $leechers, $seeders, $announce_interval, $log_debug;
  // give the client some peers to play with

  $peers = $seeders + $leechers;

  if( $_GET['numwant'] > 0 || ( $peers > $max_give_peers || $peers > $_GET['numwant'] ) )
  { // i hate you! (i think this is ok, well, that's far from knowing tho...)
    if($_GET['numwant'] > $max_give_peers)
    {
      $_GET['numwant'] = $max_give_peers;
    }
    $limit = 'ORDER BY RAND() LIMIT ' . $_GET['numwant'];
  }

  if($_GET['compact'] == 1)
  {
    $what = 'compact, port';
  } elseif($_GET['no_peer_id'] == 1)
  {
    $what = 'ip, port';
  } else
  {
    $what = 'ip, port, peer_id';
  }

  $res = mysql_query('SELECT ' . $what . ' FROM peers WHERE torrent = "' . $torrentid . '"' . $limit) or err('Could not fetch peers from the database!');

  $resp = "d8:intervali" . $announce_interval . "e5:peers";//"e7:privatei0e5:peers";
  if($_GET['compact'] == 1)
  { // compact mode - we like (gzip not gaining anything - don't use)
    while ($peer = mysql_fetch_assoc($res))
    {
      $clients .= $peer['compact'];
    }
    echo $resp . strlen($clients) . ':' . $clients . 'ee';
    if($log_debug)
    {
      error_log('announce: gave '.mysql_num_rows($res).' using compact protocol');
    }
  } elseif($_GET['no_peer_id'] == 1)
  { // no_peer_id protocol - better then nothing
    if($gzip)
    {
      ini_set('zlib.output_compression_level', 1);
      ob_start("ob_gzhandler");
    }
    $resp .= 'l';
    while ($peer = mysql_fetch_assoc($res))
    {
      $resp .= 'd2:ip' . strlen($peer['ip']) . ':' . $peer['ip'] . '4:porti' . $peer['port'] . 'ee';
    }
    echo $resp . 'ee';
    if($log_debug)
    {
      error_log('announce: gave '.mysql_num_rows($res).' using no_peer_id protocol');
    }
  } else
  { // horrible! gzip to the rescue!
    if($gzip)
    {
      ini_set('zlib.output_compression_level', 1);
      ob_start("ob_gzhandler");
    }
    $resp .= 'l';
    while ($peer = mysql_fetch_assoc($res))
    {
      $resp .= 'd2:ip' . strlen($peer['ip']) . ':' . $peer['ip'] . '7:peer id20:' . $peer['peer_id'] . '4:porti' . $peer['port'] . 'ee';
    }
    echo $resp . 'ee';
    if($log_debug)
    {
      error_log('announce: gave '.mysql_num_rows($res).' using original protocol');
    }
  }
}

function hasheval($str, $len, $name=false)
{ // try to get a $len-byte string, err out if not possible, give $name if possible
  if(strlen($str) != $len)
  {
    $str = stripcslashes($str);
    if(strlen($str) != $len)
    {
      if($name)
      {
        err('Invalid '.$name.' ('.strlen($str).')');
      } else {
        err('Invalid string ('.strlen($str).')');
      }
    }
  }
  return $str;
}

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

function err($txt)
{
  global $start, $time_me, $log_errors;
  if($time_me)
  {
    $txt .= ' (' . function_timer($start, gettimeofday(), 1) . ' us)';
  }
  echo('d14:failure reason' . strlen($txt) . ':' . $txt . 'e');
  if($log_errors)
  {
    $fd = fopen('./tmp.log', 'a');
    fwrite($fd, date('[Y-m-d H:i:s]').' '.$txt.' (' . function_timer($start, gettimeofday(), 1) . " us)\n");
    fclose($fd);
  }
  exit;
}

function mysqlconn()
{
  require('./include/secrets.php'); //or die(err('Database error (could not connect)'));
  /*
  if($_SERVER['SERVER_ADDR'] == '83.250.17.46')
  {
    $mysql_host = 'localhost';
  } else {
    $mysql_host = '192.168.0.1';
  }
  */
  if (!@mysql_connect($mysql_host, $mysql_user, $mysql_pass))
  { // could not connect to the database
    switch (mysql_errno())
    {
      case 1040:
      case 2002:
        die('d8:intervali'.rand(120, 600).'e5:peers0:e');
        //err('Database error (temporarily overloaded)');
      default:
        err('Database error ('.mysql_error().')');
    }
  }
  mysql_select_db($mysql_db) or err('Database error (could not open database)');
}

function hash_where($name, $hash) {
  $shhash = preg_replace('/ *$/s', "", $hash);
  return '('.$name.' = "' . mysql_real_escape_string($hash) . '" OR '.$name.' = "' . mysql_real_escape_string($shhash) . '")';
}

function getip()
{
  if (isset($_SERVER)) {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
      $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
      $ip = $_SERVER['HTTP_CLIENT_IP'];
    } else {
      $ip = $_SERVER['REMOTE_ADDR'];
    }
  } else {
    if (getenv('HTTP_X_FORWARDED_FOR')) {
      $ip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif (getenv('HTTP_CLIENT_IP')) {
      $ip = getenv('HTTP_CLIENT_IP');
    } else {
      $ip = getenv('REMOTE_ADDR');
    }
  }

  return $ip;
}

function validip($ip)
{
  // modified stuff from SKORPiUS tracker
  $ip = ip2long($ip);
  if (empty($ip) || $ip == '-1')
  {
    return false;
  }
  // reserved IANA IPv4 addresses
  // http://www.iana.org/assignments/ipv4-address-space
  $reserved_ips = array (
    array('0','50331647'), // '0.0.0.0','2.255.255.255'
    array('167772160','184549375'), // '10.0.0.0','10.255.255.255'
    array('2130706432','2147483647'), // '127.0.0.0','127.255.255.255'
    array('-1442971648','-1442906113'), // '169.254.0.0','169.254.255.255'
    array('-1408237568','-1407188993'), // '172.16.0.0','172.31.255.255'
    array('-1073741312','-1073741057'), // '192.0.2.0','192.0.2.255'
    array('-1062731776','-1062666241'), // '192.168.0.0','192.168.255.255'
    array('-256','-1') // '255.255.255.0','255.255.255.255'
  );

  foreach ($reserved_ips as $r)
  { // $r[0] = min, $r[1] = max
    if (($ip >= $r[0]) && ($ip <= $r[1]))
    {
      return false;
    }
  }
  return true;
}

?>