Допустим, у Вас есть некий сайт, написанный на языке Python и Вы хотите прикрутить к нему BitTorrent tracker, наподобие rutracker.org.
Разделение задачи
Задачу можно разделить на две большие функциональности:
- Каталог torrent-раздач на сайте (исторически обычно реализуется в виде форума),
- Сам Трекер, непосредственно участвующий в процессе раздач.
Трекер представляет собой http-приложение, согласно спецификации протокола BitTorrent сообщающее клиенту по запросу обо всех участниках раздачи. Поскольку клиенты шлют запросы постоянно периодически, то Трекер должен быть производительным: время ответа должно быть минимальным.
В мире PHP Каталог и Трекер зачастую не разделяются на два выделенных приложения. Например, популярный TBDev Tracker существует в виде приложения, объединяющего Каталог-форум и Трекер. (Автор видимо так устал от популярности своего приложения, что сайт давно не обновляется и скачать приложение с него нереально. Однако в Сети можно найти множество клонов.)
Некоторые реализации Трекера изначально были написаны на Python, но затем переписаны на C++ из соображений производительности. Так что в наши дни Python-трекеров не существует (по крайней мере мне найти не удалось). Поэтому единственное, что остается — установить отдельное приложение Трекера и интегрировать его с Python-Каталогом.
Контроль и защита
Если Вам никакой контроль не нужен, то есть:
- Вам все равно, какие раздачи будет поддерживать Ваш tracker — анонимный пользователь может добавить свою раздачу на Ваш Трекер,
- и Вы считаете в порядке вещей, что анонимный пользователь может получить доступ к любой существующей раздаче,
то Вам достаточно установить одну из программ, выбрав среди них самую легкую и самую производительную.
Далее, Вы просто даете возможность своим пользователям создавать записи каталога и загружать и скачивать созданные ими самими .torrent-файлы. Предварительно сообщив им правильный announce-адрес Вашего tracker-а, который они запишут в .torrent-файл при создании раздачи.
Если же Вы хотите установить хоть какой-то контроль, а тем более, как в моем случае, захотите ограничить доступ некоторых групп пользователей к некоторым раздачам, то для этого Вам понадобится так называемый «частный трекер».
В теории это делается следующим образом:
- Предотвращение добавления анонимных раздач: трекер должен поддерживать только те раздачи, которые Ваши авторизованные пользователи добавили в каталог. При создании записи Каталога и присоединении к ней .torrent-файла код Вашего сайта должен добавить раздачу в список разрешенных раздач для tracker-а.
- Предотвращение получения .torrent — файла анонимным пользователем — реализуется средствами Каталога
- Ограничение доступа пользователей к раздаче (а также отслеживание пользователей, участвующих в раздаче) выполняется по принципу: если некто может видеть запись каталога и скачать присоединенный .torrent-файл, то ему участие в раздаче разрешается. При этом трекер должен идентифицировать пользователя. И это делается путем прошивки уникального кода пользователя в announce URL трэкера — на лету, когда пользователь скачивает .torrent-файл. То есть это должен делать код Каталога.
- Запрет протоколов «широковещательного обмена раздачами»: Distribute Hash Table (DHT), Peer EXchange (PEX), Local Service Discovery (LSD), — позволяющих работать вообще без Трекера, а значит и без контроля доступа. Реализуется путем установки флага «private=1» в .torrent-файле.
Реализация
Учитывая все вышесказанное, мой выбор пал на XBT Tracker, как единственную реализацию частного Трекера, рассчитанную на интеграцию с любым сайтом-Каталогом.
XBT Tracker написан на C++ и устанавливается путем сборки из исходников. На Ubuntu Server 12.04 64-bit собирается с первого раза.
Зависимости
Взаимодействие с XBT Tracker предполагается через MySQL-базу данных Трекера, поэтому нашему Python-Каталогу понадобится уметь писать-читать БД MySQL. Для этого я использовал пакет pymysql
.
Для разбора и модификации .torrent-файлов также понадобится пакет BitTorrent-bencode
import bencode, pymysql
Функции
Добавление авторизованного пользователя, если еще такого нет. Здесь поле torrent_pass
таблицы xbt_users
используется для связки ID пользователя Вашего сайта и ID пользователя XBT Tracker. Ранее torrent_pass использовался для авторизации пользователя по ключу, прописанному в announce URL, однако теперь XBT Tracker использует другой алгоритм — см. ниже. Поле uid
— автоинкрементное.
c = db.cursor()
c.execute("SELECT uid, torrent_pass, torrent_pass_version, downloaded, uploaded FROM xbt_users WHERE torrent_pass = %s",
(request.user.id,))
rec = c.fetchone()
if not rec:
# insert a new user
c.execute("INSERT INTO xbt_users(torrent_pass) VALUES(%s)", (request.user.id))
c.execute("SELECT uid, torrent_pass, torrent_pass_version, downloaded, uploaded FROM xbt_users WHERE torrent_pass = %s",
(request.user.id,)) #rowcount =0
rec = c.fetchone()
db.commit()
uid, torrent_pass, torrent_pass_version, downloaded, uploaded = rec
Чтение и разбор .torrent-файла:
with open(fpath, 'rb') as f:
buf = f.read()
bt = bencode.bdecode(buf)
Насильно установить private-флаг и рассчитать info_hash (private-флаг входит в раздел info и влияет на его хэш):
if xbt_force_private:
bt['info']['private'] = 1
info_hash_obj = hashlib.sha1(bencode.bencode(bt['info']))
Регистрация раздачи в таблице xbt_files
если еще таковой нет.
sha = info_hash_obj.hexdigest()
c = db.cursor()
c.execute("SELECT flags FROM xbt_files WHERE info_hash=UNHEX(%s)", (sha,))
flag = c.fetchone()
if flag is None:
c.execute("INSERT INTO xbt_files(info_hash, mtime, ctime) VALUES(UNHEX(%s), UNIX_TIMESTAMP(), UNIX_TIMESTAMP())",
(sha,))
db.commit()
elif flag[0] % 2 :
error_msg(pagename, request, _("Torrent is marked for deletion!"))
return
Вычисление ключа авторизации для announce URL
XBT Tracker использует весьма хитрый алгоритм вычисления ключа. Берется набор значений:
- ID пользователя:
xbt_users.uid
, - версия ключа пользователя:
xbt_users.torrent_pass_version
, - секретный ключ сервера (генерируется автоматически при первом старте XBT Tracker): таблица
xbt_config
, значениеtorrent_pass_private_key
, - значение info_hash данной раздачи:
xbt_files.info_hash
.
Всё это магическим образом перемешивается по принципу «кручу-верчу, запутать хочу»:
c.execute("select value from xbt_config where name = 'torrent_pass_private_key'")
torrent_pass_private_key = c.fetchone()[0]
s = "%s %d %d %s" % (torrent_pass_private_key, torrent_pass_version, uid, info_hash_obj.digest())
sha = hashlib.sha1(s).hexdigest()[0:24]
pwd = "%08x%s" % (uid, sha)
bt['announce'] = 'http://xbt.host:port/%s/announce' % pwd
# remove other trackers
if bt.has_key('announce-list'):
del bt['announce-list']
Отсюда становится понятно назначение xbt_users.torrent_pass_version
: изменив это значение, можно сделать все ранее скачанные .torrent-файлы инвалидными. То есть это — некий аналог сброса пароля.
И, наконец, кодируем .torrent-файл обратно в строку, которую и будем посылать клиенту:
buf = bencode.bencode(bt)
Удаление раздачи
При удалении присоединенного файла мы должны удалить раздачу из списка зарегистрированных раздач (таблицы xbt_files
).
Есть способ вежливого удаления, при котором мы помечаем раздачу как удаленную, а реально она удаляется трекером, когда ее скачает полностью последний лич.
c.execute("update xbt_files set flags = 1 where info_hash = UNHEX(%s)", (info_hash, ))
Ну вот и всё. Дальнейшая докрутка сайта: отображение количества и списка раздающих, подсчет рейтинга, отображение списка файлов в раздаче я отношу к украшательствам. Они реализуются достаточно очевидно: вся необходимая информация, включая статистику, имеется в БД трекера, — и мне, вслед за Пьером Ферма, жаль тратить на них место.
Изложенное решение воплощено в виде плагина к популярному вики-движку: MoinMoin
Необходимо отметить, что предложенное решение имеет существенный недостаток:
Надеюсь, мой опыт кому-то пригодится.
Автор: alosev