Pull to refresh

Torrent-файл. Что же у него внутри?

Reading time 7 min
Views 53K

Введение


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


Исходник.
Tags:
Hubs:
+116
Comments 56
Comments Comments 56

Articles