Введение

Добрый день.
Использую, как и многие, крупный торрент-трекер — rutracker.org, однако есть одна особенность которая меня раздражает.
Это добавление в список трекеров адреса ix*.rutracker.net, который служит для непонятных мне целей. Однако который часто (у меня — практически всегда) выдаёт ошибки (502 Bad Gateway и 0 No Response). Торрент-клиент (у меня Transmission) помечает торрент сломанным. Что само собой довольно сильно мне мешает. Особенно если учесть особенность Transmission — она задаёт статус торрента по последнему ответу трекера. То есть опрашиваем ix*, он возвращает ошибку, торрент помечается как Broken, через n минут/секунд опрашивается следующий трекер из списка — bt*.rutracker.org или retracker.local, которые возвращают успешный код и торрент снова становится нормальным. Такая чехарда не особо меня радует.
Решение банально — убрать этот нехороший адрес из списка. Однако файлов у меня много, из каждого вручную вырезать совсем не хочется, да и дополнительное действие при добавлении нового торрента выполнять тоже не было никакого желания. Поэтому принял решение разобраться в формате и автоматизировать удаление трекера из списка.
Bencode
Именно так называется формат кодирования данных в .torrent-файлах. Больше он почти нигде и не используется, мне попадался он на глаза так же в формате хранения resume-информации в Transmission.
Для большинства актуальных языков написаны библиотеки для работы с этим форматом, но не для C++, да, конечно, есть такая штука, но это чистый Си и кроме того форма представления мне не показалась удачной, поэтому написал простенький свой велосипед, ибо формат крайне прост.
Описываются 4 типа данных — массив байт, число, список, ассоциативный массив.
Пойдем по порядку:
- Числа задаются в форме i<последовательность цифр>e, <последовательность цифр> — это цифры в ascii представлении, то есть 1 задаётся как '1' или 0x31. Заметно что так мы можем задавать огромные числа, которые не влезут ни в long, ни в long long, однако большинство пренебрегают отсутствием лимита и используют 64-битные числа.
- Массив байт — <длина массива>:<сам массив>. Длина массива так же формируется неограниченной последовательностью цифр.
- Список — l<элемeнты списка>e. Элементом может являться любой из типов данных. В том числе и вложенный список. Конец, как видно из формата, отмечается литералом 'e'.
- Ассоциативный массив — d<элемeнты массива>e. Каждый элемент массива выглядит таким образом — <массив байт><элемент>. Массив байт — это имя записи в форме из пункта 2. Элемент опять же может быть любым — список, массив, ассоциативный массив, число.
Это всё. Сам файл это последовательность таких записей. Поэтому декодирование крайне просто выполняется:
void CTorrentFile::ReadBencElement(ifstream & fin, tree <BencElement>::pre_order_iterator & parent,
string name)
{
BencElement el;
char c = fin.get();
el.name = name;
if (c == 'i')
{
el.type = BencInteger;
fin >> el.integer;
m_tree.append_child(parent, el);
} else if (c == 'l')
{
int l = fin.peek();
el.type = BencList;
tree <BencElement>::pre_order_iterator it = m_tree.append_child(parent, el);
while (l != 'e')
{
ReadBencElement(fin, it, string(""));
l = fin.peek();
}
fin.seekg(1, ios_base::cur);
} else if (c == 'd')
{
int l = fin.peek();
el.type = BencDict;
tree <BencElement>::pre_order_iterator it = m_tree.append_child(parent, el);
while (l != 'e')
{
string name;
int len;
fin >> len;
fin.seekg(1, ios_base::cur);
while (len--)
{
char s = fin.get();
name += s;
}
ReadBencElement(fin, it, name);
l = fin.peek();
}
fin.seekg(1, ios_base::cur);
} else if (c >= '0' && c <= '9')
{
fin.seekg(-1, ios_base::cur);
int len;
el.type = BencString;
fin >> len;
el.bstr.len = len;
// skip ':'
fin.seekg(1, ios_base::cur);
el.bstr.byteStr = new char[len + 1];
for (int i = 0; i < len; i++)
{
char s = fin.get();
el.bstr.byteStr[i] = s;
}
el.bstr.byteStr[el.bstr.len] = 0;
m_tree.append_child(parent, el);
}
}
Кодирование тоже несложно:
void CTorrentFile::WriteBencElement(std::ofstream & fout, tree <BencElement>::sibling_iterator & el)
{
tree <BencElement>::sibling_iterator it;
switch (el->type)
{
case BencInteger:
fout << 'i' << el->integer << 'e';
break;
case BencString:
fout << el->bstr.len << ':';
fout.write(el->bstr.byteStr, el->bstr.len);
break;
case BencList:
fout << 'l';
it = m_tree.child(el, 0);
for (size_t i = 0; i < m_tree.number_of_children(el); i++, ++it)
WriteBencElement(fout, it);
fout << 'e';
break;
case BencDict:
fout << 'd';
tree <BencElement>::sibling_iterator it = m_tree.child(el, 0);
for (size_t i = 0; i < m_tree.number_of_children(el); i++, ++it)
{
fout << it->name.length() << ':' << it->name.c_str();
WriteBencElement(fout, it);
}
fout << 'e';
break;
}
}
Структура .torrent-файла.
Как я уже писал выше для кодирования используется Bencode.
Стоит добавить что если массив байт может быть интерпретирован как строка (имена элементов в ассоциативном массиве, просто строковые поля), то используется кодировка utf-8.
Содержимое является одним большим ассоциативным массивом со следующими полями:
- info — вложенный ассоциативный массив который собственно и описывает файлы, которые передаёт торрент.
- announce — URL для трекера. Наряду с info является обязательным полем, всё остальное — опционально.
- announce-list — список трекеров, если их несколько. В Bencode-виде — список списков.
- creation date — дата создания. UNIX Timestamp.
- comment — текстовое описание торрента. rutracker.org хранит здесь ссылку на тему форума.
- created by — говорит нам о том, кем создан данный торрент.
Необходимо упомянуть то, что файлы представлены в протоколе кусками. То есть файлы содержащиеся в торренте объединены в единый массив, и затем этот массив разделили на относительно небольшие кусочки. В таком виде данные обрабатывает BitTorrent-протокол.
Ассоциативный массив info состоит из:
- piece length — размер одного кусочка — 512 килобайт, 1 метр, и так далее. Слишком большое число кусков будет «раздувать» .torrent-файл.
- pieces — строка, которая содержит конкатенацию SHA1-хешей, описывающих каждый кусочек. Длина этой строки равна 20 * количество кусков.
- name — рекомендательное имя файла (если файл один) или директории. Увы многие торрент-клиенты воспринимают это как аксиому.
- length — если файл один, то будет задано это поле, которое содержит длину файла.
- files — если файлов несколько, то появится список ассоциативных массивов.
Формат элементов списка files:
- length — длина файла.
- path — список из строк, которые задают путь. Каждая строка — элемент пути, относительно корневой директории торрента. Для пути a/b/c/d.jpg будет 4 строки в данном списке — ['a', 'b', 'c', 'd.jpg'].
В общем-то это всё.
Нам в данный момент нужно только одно поле — announce-list. Пробегаясь по этому списку находим неугодный трекер и вырезаем его:
int CTorrentFile::RemoveTracker(const char * mask)
{
int deletedCount = 0;
tree <BencElement>::pre_order_iterator root = m_tree.child(m_tree.begin(), 0);
tree <BencElement>::sibling_iterator it = m_tree.child(root, 0);
for (size_t i = 0; i < m_tree.number_of_children(root); i++, ++it)
{
if (it->type == BencString && !it->name.compare("announce") && it->bstr.len > 0 &&
it->bstr.byteStr)
{
if (wildcardMatch(it->bstr.byteStr, mask))
{
it->bstr.len = 0;
it->bstr.byteStr[0] = 0;
deletedCount++;
}
} else if (it->type == BencList && !it->name.compare("announce-list"))
{
tree <BencElement>::sibling_iterator trackerList = m_tree.child(it, 0);
for (size_t j = 0; j < it.number_of_children(); j++)
{
if (trackerList->type != BencList)
{
++trackerList;
continue;
}
tree <BencElement>::sibling_iterator tracker = m_tree.child(trackerList, 0);
for (size_t k = 0; k < trackerList.number_of_children(); k++)
{
if (tracker->type != BencString || tracker->bstr.len <= 0 ||
!tracker->bstr.byteStr)
{
++tracker;
continue;
}
if (wildcardMatch(tracker->bstr.byteStr, mask))
{
tracker = m_tree.erase(tracker);
deletedCount++;
} else
++tracker;
}
if (trackerList.number_of_children() == 0)
trackerList = m_tree.erase(trackerList);
else
++trackerList;
}
}
}
return deletedCount;
}
Скомпонуем всё в один исходник:
Скачать — кроссплатформенный (win + *nix), нужен boost::filesystem.
Пользоваться просто:
torrentEditor <имя_файла> <шаблон>, где шаблон — это wildcard-строка ('*' и '?'), для моего случая — http://ix*rutracker.net/*
Если в качестве имени файла подставить имя директории, то будет совершен рекурсивный обход по этой директории и модификация *.torrent файлов.
Бэкап для <имя>.torrent сохраняется в <имя>.old.
Демоны и watch-directory.
Таким образом мы можем пробежаться по существующим .torrent-файлам и вырезать трекер, однако что делать с новыми файлами?
Я использую удобную штуку — watch directory. Кидаем туда .torrent и клиент обнаружив его в этой папке, сам автоматически добавит его к себе.
Однако мне совсем не хочется предварительно вырезать трекер, а желаю автоматизировать это дело.
Поэтому написал простенький демон, который мониторит собственную watch directory, удаляет трекер и кидает файл в watch directory торрент-клиента.
Для меня как пользователя абсолютно ничего не поменялось, кидаю файлы в ту же папку, получаю на выходе торрент в клиенте.
Демона пишем на Си с использованием замечательной штуки — inotify,
notifyDesc = inotify_init();
if (notifyDesc < 0)
exit(EXIT_FAILURE);
watchDesc = inotify_add_watch(notifyDesc, argv[1], IN_CREATE);
if (watchDesc < 0)
exit(EXIT_FAILURE);
// endless loop
while (1)
{
processEvents(notifyDesc, argv[2], argv[3], argv[1]);
}
Инициализируем модуль с помощью inotify_init(), затем добавляем директорию для слежения inotify_add_watch(), нас интересует только создание файла, поэтому указываем флажок IN_CREATE. А затем крутим бесконечный цикл слежения за директорией.
static void processEvents(int wd, char * moveDir, char * pattern, char * watchDir)
{
#define BUF_SIZE ((sizeof(struct inotify_event) + FILENAME_MAX) * 10)
int len, i = 0;
char buf[BUF_SIZE];
// blocked read, we wake up when directory changed
len = read(wd, buf, BUF_SIZE);
while (i < len)
{
struct inotify_event * ev;
ev = (struct inotify_event *)&buf[i];
processNewFile(ev->name, moveDir, pattern, watchDir);
i += sizeof(struct inotify_event) + ev->len;
}
}
Блокирующий вызов read() вернёт нам управление как только произойдут нужные нам изменения в одной из директорий, за которыми следим. Таким образом мы абсолютно не грузим процессор во время ожидания.
Сама обработка файла не представляет из себя ничего интересного — пара вызовов rename() и один вызов system().
Демонизация тоже стандартна:
// create child-process
pid = fork();
// error?
if (pid < 0)
exit(EXIT_FAILURE);
// parent?
if (pid > 0)
exit(EXIT_SUCCESS);
// new session for child
sid = setsid();
if (sid < 0)
exit(EXIT_FAILURE);
// change current directory
if (chdir("/") < 0)
exit(EXIT_FAILURE);
// close opened descriptors
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
Исходник.