Введение

Добрый день.
Использую, как и многие, крупный торрент-трекер — 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);
Исходник.
