Делаем по быстрому битторрент-трекер на основе XBT и IPB

Дата публикации: 2007-01-04
Это статья - описание как я быстренько сделал битторрент-трекер с миниммум необходимых функций

Это статья - описание как я быстренько сделал битторрент-трекер с миниммум необходимых функций

Итак, как всегда - исходные условия задачи.

Имеем сервер (на самом деле даже два см. "Зеркалируем форумы по взрослому") с инвижиновскими форумами.
Недавно проапгрейженными до версии 2.2.2 (русская).
Сразу предупреждаю, что вообще-то я форумом самим никогда особенно не занимался, да и не занимаюсь до сих пор, но тут пришлось немного поковырятся в его внутренностях :).

Для начала надо поднять backend трекера - т.е. именно серверную часть.
Выбран был в этом качестве XBT - http://xbtt.sourceforge.net/
Процедура инсталяции там описана, скажу лишь что поскольку у меня была Slackware - мне пришлось скачать библиотеку libboost вручную.
libboost можно найти здесь - http://www.boost.org/

Скачал, стандартно комбинация из трех волшебных команд configure, make, make install - ничего необычного
Затем по инструкции с http://xbtt.sourceforge.net/tracker/ забрал и собрал XBT Tracker:

   svn co https://xbtt.svn.sourceforge.net/svnroot/xbtt/trunk/xbt/misc xbt/misc
   svn co https://xbtt.svn.sourceforge.net/svnroot/xbtt/trunk/xbt/Tracker xbt/Tracker
   cd xbt/Tracker
   ./make.sh

Тут тоже ничего необычного, все собралось, имеем в результате исполняемый файл xbt_tracker

Перед запуском серверной части сначала необходимо еще создать нужные таблицы.

Я немного модифицировал скрипт входящий в комплект - кое-где добавил дополнительные поля, плюс добавил триггеров.

Download file tracker.sql.txt

create table if not exists xbt_announce_log
(
  id int not null auto_increment,
  ipa int unsigned not null,
  port int not null,
  event int not null,
  info_hash blob not null,
  peer_id blob not null,
  downloaded bigint not null,
  left0 bigint not null,
  uploaded bigint not null,
  uid int not null,
  mtime int not null,
  primary key (id)
);
 
create table if not exists xbt_config
(
  name varchar(255) not null,
  value varchar(255) not null
);
 
create table if not exists xbt_deny_from_hosts
(
  begin int not null,
  end int not null
);
 
create table if not exists xbt_files
(
  fid int not null auto_increment,
  info_hash blob not null,
  leechers int not null default 0,
  seeders int not null default 0,
  completed int not null default 0,
  flags int not null default 0,
  mtime int not null,
  ctime int not null,
  files_size bigint NOT NULL default '0',
  attach_id int not null,
  primary key (fid),
  unique key (info_hash(20)),
  key (attach_id)
);
 
create table if not exists xbt_files_users
(
  fid int not null,
  uid int not null,
  active tinyint not null,
  announced int not null,
  completed int not null,
  downloaded bigint not null,
  `left` bigint not null,
  uploaded bigint not null,
  mtime int not null,
  unique key (fid, uid),
  key (uid)
);
 
create table if not exists xbt_scrape_log
(
  id int not null auto_increment,
  ipa int not null,
  info_hash blob,
  uid int not null,
  mtime int not null,
  primary key (id)
);
 
 
create table if not exists xbt_users
(
  uid int not null auto_increment,
  name varchar(255) not null,
  pass blob not null,
  can_leech tinyint not null default 1,
  can_create tinyint(4) NOT NULL default '0',
  can_delete tinyint(4) NOT NULL default '0',
  wait_time int not null default 0,
  peers_limit int not null default 0,
  torrents_limit int not null default 1,
  torrent_pass char(32) not null,
  torrent_pass_secret bigint not null,
  downloaded bigint not null,
  uploaded bigint not null,
  primary key (uid)
);
 
drop trigger trigger_ai_ipbf_members;
delimiter //
CREATE DEFINER = 'root'@'localhost' TRIGGER trigger_ai_ipbf_members AFTER INSERT on ipbf_members
FOR EACH ROW BEGIN
   REPLACE INTO xbt_users SET uid         = NEW.id, 
                             name         = NEW.name, 
                             torrent_pass = MD5(CONCAT_WS(':', NEW.name, NEW.member_login_key)),
                             pass         = MD5(CONCAT_WS(':', NEW.id, NEW.name,NEW.email,NEW.member_login_key));
END;
//
delimiter ;
 
 
drop trigger trigger_ad_ipbf_attachments;
delimiter //
CREATE DEFINER = 'root'@'localhost' TRIGGER trigger_ad_ipbf_attachments AFTER DELETE on ipbf_attachments
FOR EACH ROW BEGIN
   UPDATE xbt_files SET flags = 1 WHERE attach_id = OLD.attach_id;
END;
//
delimiter ;
 
INSERT IGNORE INTO xbt_users (uid, name, torrent_pass, pass)
    SELECT id, name, MD5(CONCAT_WS(':',name,member_login_key)), MD5(CONCAT_WS(':', id, name,email,member_login_key))
      FROM ipbf_members
 
его можно скачать отдельно, Download file tracker.tar.gz либо сразу со всеми файлами в архиве

Создать таблицы я решил для простоты в той же БД что и сами форумы.

Почему использовал триггеры? От лени - ситуация когда добавляется новый пользователь в форумах - для того, чтобы он мог работать мне проще показалось решить не ковырянием кода форума - а просто навешиванием простейшего триггера. Обработать ситуацию удаления торрент-файла - тоже показалось проще через тригеры.

Все, теперь можно и запустить xbt_tracker. Рисуем ему в конфиге логины и пароли для доступа к нужным ему таблицам (а нужны ему только таблицы xbt_* - поэтому, можно и даже нужно, создать отдельного mysql пользователя имеющего доступ _только_ к этим таблицам) конфиг xbt_tracker.conf получился примерно таким

//--------------------------
   mysql_host = ip
   mysql_user = user
   mysql_password = pass
   mysql_database = ipb_db
   announce_interval = 1800
   anonymous_connect = 0
   anonymous_announce = 0
   anonymous_scrape = 0
   auto_register = 0
   clean_up_interval = 60
   listen_check = 0
   listen_ipa = *
   listen_port = 2710
   log_access = 0
   log_announce = 0
   log_scrape = 0
   pid_file =
   read_config_interval = 300
   read_db_interval = 60
   redirect_url =
   scrape_interval = 60
   update_files_method = 2
   write_db_interval = 60
   debug = 0
//--------------------------

Запускаем. Кстати, должен сказать - сам xbt_tracker у меня запущен на совсем другом сервере, отличном от того, на котором находится инвижновский форум и его БД.

Теперь начинаем поднимать frontend - т.е. править исходники инвижновского форума чтобы можно было работать с трекером :). Все нижесказанное для форумов версии 2.2.2, русской версии.

Для начала заходим в админскую часть и прописываем там разрещение для прикрепления файлов с расширением ".torrent" Закладка "Управление", раздел "Прикрепляемые файлы".

Затем не выходя из админки надо сделать изменения для интерфейса.

Закладка "Внешний вид", в каждом используемом стиле выбираем "изменить HTML-шаблоны". Находим skin_topic, там находим Show_attachments В самом конце добавляем код Download file skin_topic - Show_attachments.addon

<script type=\"text/javascript\">
<!--
  if({$data['is_torrent']}==1)
  {
     document.write("<fieldset><legend> Статистика торрента: </legend>Total <b>{$data['torrent_files_count']}</b> файл(а|ов) в торренте, общий размер = <b>{$data['torrent_files_size']}</b><br><font color=green><b>{$data['seeders']}</b></font> раздающих, <font color=red><b>{$data['leechers']}</b></font> качающих, <b>{$data['completed']}</b> завершено<br>ХЭШ торрента: {$data['info_hash']}</fieldset>");
  }
//-->
</script>
 
(надо скачать его к себе и открыть в любом текстовом редакторе - при открытии в интернет-эксплорер он покажет пустоту ;) )

Аналогично надо найти skin_profile, там найти personal_portal_main и добавить код Download file skin_profile - personal_portal_main.addon

<!-- IbZ's insert //-->
<script type=\"text/javascript\">
<!--
  if({$member['show_torrent_stats']}==1)
  {
     document.write("<div class='pp-header'>Статистика трекера</div><div class='row1' style='padding:6px; margin-bottom:1px; padding-left:10px'>Рейтинг: <b>{$member['show_torrent_ratio']}</b> </div><div class='row1' style='padding:6px; margin-bottom:1px; padding-left:10px'>Скачал(а): <b>{$member['show_torrent_downloaded']}</b></div><div class='row1' style='padding:6px; margin-bottom:1px; padding-left:10px'>Отдал(а): <b>{$member['show_torrent_uploaded']}</b></font></div><div class='row1' style='padding:6px; margin-bottom:1px; padding-left:10px'>Доп. инфо: <b>{$member['show_torrent_limit']}</b></font></div>");
  }
//-->
</script>
 
<!-- end of IbZ //-->
 

Код лучше всего мне кажется добавить надо после раздела <!-- / Custom Fields --> и перед разделом <!-- Statistics -->

Все, в админке закончили, теперь надо править сам код инвижна Если вы скачаете архив со всеми файлами есть очень большая вероятность того что вам подойдут патчи, и все будет очень просто - надо будет просто их применить к двум файлам: class_attach.php из каталога sources/classes/attach и profile.php из каталога sources/action_public

Если нет - то пытаемся ручками все сделать :) Для начала необходимо скопировать библиотеку benc.php для работы с торрент-файлами в каталог /sources форума. Взять ее можно из этого архива, либо здесь - Download file benc.phps

<?
 
function benc($obj) {
    if (!is_array($obj) || !isset($obj["type"]) || !isset($obj["value"]))
	return;
    $c = $obj["value"];
    switch ($obj["type"]) {
	case "string":
	    return benc_str($c);
	case "integer":
	    return benc_int($c);
	case "list":
	    return benc_list($c);
	case "dictionary":
	    return benc_dict($c);
	default:
	    return;
    }
}
 
function benc_str($s) {
    return strlen($s) . ":$s";
}
 
function benc_int($i) {
    return "i" . $i . "e";
}
 
function benc_list($a) {
    $s = "l";
    foreach ($a as $e) {
	$s .= benc($e);
    }
    $s .= "e";
    return $s;
}
 
function benc_dict($d) {
    $s = "d";
    $keys = array_keys($d);
    sort($keys);
    foreach ($keys as $k) {
	$v = $d[$k];
	$s .= benc_str($k);
	$s .= benc($v);
    }
    $s .= "e";
    return $s;
}
 
function bdec_file($f, $ms) {
    $fp = fopen($f, "rb");
    if (!$fp)
	return;
    $e = fread($fp, $ms);
    fclose($fp);
    return bdec($e);
}
 
function bdec($s) {
    if (preg_match('/^(\d+):/', $s, $m)) {
	$l = $m[1];
	$pl = strlen($l) + 1;
	$v = substr($s, $pl, $l);
	$ss = substr($s, 0, $pl + $l);
	if (strlen($v) != $l)
	    return;
	return array(type => "string", value => $v, strlen => strlen($ss), string => $ss);
    }
    if (preg_match('/^i(\d+)e/', $s, $m)) {
	$v = $m[1];
	$ss = "i" . $v . "e";
	if ($v === "-0")
	    return;
	if ($v[0] == "0" && strlen($v) != 1)
	    return;
	return array(type => "integer", value => $v, strlen => strlen($ss), string => $ss);
    }
    switch ($s[0]) {
	case "l":
	    return bdec_list($s);
	case "d":
	    return bdec_dict($s);
	default:
	    return;
    }
}
 
function bdec_list($s) {
    if ($s[0] != "l")
	return;
    $sl = strlen($s);
    $i = 1;
    $v = array();
    $ss = "l";
    for (;;) {
	if ($i >= $sl)
	    return;
	if ($s[$i] == "e")
	    break;
	$ret = bdec(substr($s, $i));
	if (!isset($ret) || !is_array($ret))
	    return;
	$v[] = $ret;
	$i += $ret["strlen"];
	$ss .= $ret["string"];
    }
    $ss .= "e";
    return array(type => "list", value => $v, strlen => strlen($ss), string => $ss);
}
 
function bdec_dict($s) {
    if ($s[0] != "d")
	return;
    $sl = strlen($s);
    $i = 1;
    $v = array();
    $ss = "d";
    for (;;) {
	if ($i >= $sl)
	    return;
	if ($s[$i] == "e")
	    break;
	$ret = bdec(substr($s, $i));
	if (!isset($ret) || !is_array($ret) || $ret["type"] != "string")
	    return;
	$k = $ret["value"];
	$i += $ret["strlen"];
	$ss .= $ret["string"];
	if ($i >= $sl)
	    return;
	$ret = bdec(substr($s, $i));
	if (!isset($ret) || !is_array($ret))
	    return;
	$v[$k] = $ret;
	$i += $ret["strlen"];
	$ss .= $ret["string"];
    }
    $ss .= "e";
    return array(type => "dictionary", value => $v, strlen => strlen($ss), string => $ss);
}
 
?>
 
В каталоге sources/classes/attach находим файл class_attach.php Находим функцию show_attachment (примерно 209 строка) Находим блок кода (пр. 298 строка)

    //-----------------------------------------
    // Open and display the file..
    //-----------------------------------------

    //$contents = file_get_contents( $file );

    header( "Content-Type: ".$this->ipsclass->cache['attachtypes'][ $attach['attach_ext'] ]['atype_mimetype'] );
    header( "Content-Disposition: inline; filename=\"".$attach['attach_file']."\"" );
    header( "Content-Length: ".(string)(filesize( $file ) ) );

    //print $contents;
    readfile( $file );
    exit();
Заменяем на свой

    //-----------------------------------------
    // Open and display the file..
    //-----------------------------------------

    //$contents = file_get_contents( $file );

    if(strcasecmp($attach['attach_ext'], "torrent"))
    {
      header( "Content-Type: ".$this->ipsclass->cache['attachtypes'][ $attach['attach_ext'] ]['atype_mimetype'] );
      header( "Content-Disposition: inline; filename=\"".$attach['attach_file']."\"" );
      header( "Content-Length: ".(string)(filesize( $file ) ) );
       
      readfile( $file );
      exit();
    }
    else
    {
      $ibz_sql = $this->ipsclass->DB->query("SELECT torrent_pass FROM xbt_users WHERE uid ='".addslashes($this->ipsclass->member['id'])."' LIMIT 1");
      $this->ipsclass->DB->simple_exec();
      $r = $this->ipsclass->DB->fetch_row($ibz_sql);

      header( "Content-Type: ".$this->ipsclass->cache['attachtypes'][ $attach['attach_ext']]['atype_mimetype'] );
      header( "Content-Disposition: inline; filename=\"".$attach['attach_file']."\"" );

      require_once( ROOT_PATH."sources/benc.php" );
      $torrent = bdec_file($file, 1 << 20);

// Для отключения DHT
//    $torrent["value"]["info"]["value"]["private"]["value"] = 1;

// 172.20.0.4 замените на свой ип-адрес
      $torrent["value"]["announce"]["value"] = "http://172.20.0.4:2710/".$r['torrent_pass']."/announce";

      header( "Content-Length: ".(string)(strlen( benc($torrent) ) ) );
      print benc($torrent);
      exit();
    }

Находим функцию render_attachments (приимерно 340-я строка оригинального файла)
Находим блок (примерно 520-я строка оригинального файла)

    // Full attachment thingy
    //-----------------------------------------

    $tmp = $this->ipsclass->compiled_templates[ $skin_name ]->Show_attachments( array (
            'attach_hits'  => $row['attach_hits'],
            'mime_image'   => $this->ipsclass->cache['attachtypes'][ $row['attach_ext'] ]['atype_img'],
	    'attach_file'  => $row['attach_file'],
            'attach_id'    => $row['attach_id'],
            'type'         => $this->type,
            'file_size'    => $this->ipsclass->size_format( $row['attach_filesize'] ),
         )       );
И заменяем на свой код

    if($row['attach_ext']=="torrent")
    {
        $ibz_sql = $this->ipsclass->DB->query("SELECT info_hash, leechers, seeders,completed, files_size, files_count FROM xbt_files WHERE attach_id = '".$row['attach_id']."' LIMIT 1");
        $this->ipsclass->DB->simple_exec();
        $r = $this->ipsclass->DB->fetch_row($ibz_sql);
        $tmp = $this->ipsclass->compiled_templates[ $skin_name ]->Show_attachments(array (
            'attach_hits'  => $row['attach_hits'],
            'mime_image'   => $this->ipsclass->cache['attachtypes'][ $row['attach_ext'] ]['atype_img'],
            'attach_file'  => $row['attach_file'],
            'attach_id'    => $row['attach_id'],
            'type'         => $this->type,
            'file_size'    => $this->ipsclass->size_format( $row['attach_filesize'] ),
            'is_torrent'   => 1,
            'seeders'      => (int)$r['seeders'],
            'leechers'     => (int)$r['leechers'],
            'completed'    => (int)$r['completed'],
            'torrent_files_size' => $this->ipsclass->size_format( $r['files_size'] ),
            'torrent_files_count' => (int)$r['files_count'],
            'info_hash' => bin2hex($r["info_hash"]),
         )  );
    }
    else
    {
        $tmp = $this->ipsclass->compiled_templates[ $skin_name ]->Show_attachments(array (
            'attach_hits'  => $row['attach_hits'],
            'mime_image'   => $this->ipsclass->cache['attachtypes'][ $row['attach_ext'] ]['atype_img'],
            'attach_file'  => $row['attach_file'],
            'attach_id'    => $row['attach_id'],
            'type'         => $this->type,
            'file_size'    => $this->ipsclass->size_format( $row['attach_filesize'] ),
            'is_torrent'   => 0,
         )  );
    }
Находим функцию process_upload (примерно 766 строка оригинального файла)
В самом ее конце между кодом

    $this->ipsclass->DB->do_insert( 'attachments', $attach_data );
                        
    $newid = $this->ipsclass->DB->get_insert_id();
И строкой (примерно 961-я строка оригинального файла)

    return $newid;
Вставляем свой код

   if (!strcasecmp($attach_data['attach_ext'], 'torrent'))
   {
       require_once( ROOT_PATH."sources/benc.php" );
       $torrent = bdec_file($upload->saved_upload_name, 1 << 20);
       if (!isset($torrent))
               return $attach_data;
       $bt_info_hash = pack('H*', sha1($torrent['value']['info']['string']));
       $torrent_files_count = count($torrent["value"]["info"]["value"]["files"]["value"]);
       if($torrent_files_count == 0)
       {
           $torrent_files_count = 1;
           $bt_size = $torrent["value"]["info"]["value"]["length"]["value"];
       }
       else
       {
           $bt_size = 0;
           for($torrent_i=0;$torrent_i<$torrent_files_count;$torrent_i++)
           {
               $bt_size += $torrent["value"]["info"]["value"]["files"]["value"][$torrent_i]["value"]["length"]["value"];
           }
       }
       $this->ipsclass->DB->query("insert into xbt_files (info_hash, ctime, files_size, attach_id,files_count) values ('".addslashes($bt_info_hash)."', Now(), '$bt_size', $newid, $torrent_files_count)");
   }
Редактирование class_attach.php завершено
Переходим к редактированию файла sources/action_public/profile.php
Находим функцию personal_portal_view (примерно 2446-я строка)
Находим блок (примерно 3017-я строка)

    //-----------------------------------------
    // Online location
    //-----------------------------------------
    
    $member = $this->personal_portal_get_user_location( $member );^M
Добавляем после него свой код

    $ibz_sql = $this->ipsclass->DB->query("SELECT can_leech, wait_time, torrents_limit, downloaded, uploaded FROM xbt_users WHERE uid = '".$member['id']."' LIMIT 1");
    $this->ipsclass->DB->simple_exec();
    $r = $this->ipsclass->DB->fetch_row($ibz_sql);

    if(($r['downloaded'] <> 0)||($r['uploaded']<>0))
    {
      $member['show_torrent_stats'] = 1;
    }
    else
    {
      $member['show_torrent_stats'] = 0;
    }
    $member['show_torrent_uploaded']   = $this->ipsclass->size_format( $r['uploaded'] );
    $member['show_torrent_downloaded'] = $this->ipsclass->size_format( $r['downloaded'] );
    if($r['downloaded']==0)
    {
      $member['show_torrent_ratio'] = 'inf';
    }
    else
    {
      $member['show_torrent_ratio'] = number_format($r['uploaded'] / $r['downloaded'], 2);
    }
    if( $r['can_leech'] != 1 )
    {
      $member['show_torrent_limit'] = "запрещено скачивать";
    }
    else
    {
      if( $r['torrents_limit'] == 0 )
      {
        $member['show_torrent_limit'] = "разрешено скачивать сколько угодно торентов";
      }
      else
      {
        $member['show_torrent_limit'] = "разрешено скачивать ".$r['torrents_limit']." торент(ов|а)";
      }
    }
    if(  $r['wait_time'] != 0 )
    {
      $member['show_torrent_limit'] .= "<br>Пауза для новых торрентов = ".$r['wait_time']." секунд";
    }
Все, редактирование завершено

Теперь - создаем торрент-файл (в uTorrent'е я создавал с announce url="http://" и сам ставил флаг приватного торрента). Прикрепляем его к своему сообщению. Затем заново его скачиваем (при этом в этот торрент-файл уже автоматически заполнится мой хеш, и урл трекера) - указываем исходный файл, и вуаля. Вот картинки как это примерно выглядит:

Конечно все очень просто - но минимум необходимого для быстрого старта есть. Есть куда и развивать дальше. :)