Русский
Русский
English
Статистика
Реклама

Файловый менеджер

Перевод Умрёт ли FTP? Расцвет и упадок протокола

05.10.2020 12:23:18 | Автор: admin


Вот небольшое известие, которое вы могли пропустить, восстанавливая свою жизнь после начала кризиса COVID: из-за того, что вирус перемешал всем карты, Google пропустила выпуск Chrome версии 82. Да кого это волнует?, спросите вы. Ну, хотя бы пользователей FTP, или File Transfer Protocol. Во время пандемии Google отложила свои планы по убийству FTP, и теперь, когда буря немного успокоилась, Google недавно объявила о том, что возвращается к мысли об убийстве в Chrome версии 86, которая снова сократит поддержку протокола, и окончательно убьёт его в Chrome 88. (Mozilla объявила о похожих планах на Firefox, утверждая, что дело в безопасности и возрасте поддерживающего протокол кода.) Это один из старейших протоколов, который поддерживает мейнстримный Интернет (в следующем году ему исполнится 50 лет), но эти популярные приложения хотят оставить его в прошлом. Сегодня мы поговорим об истории FTP, сетевого протокола, который продержался дольше, чем почти все остальные.

1971


Именно в этом году родившийся в Индии студент магистратуры MIT Абхай Бхушнан впервые разработал File Transfer Protocol. FTP, появившийся спустя два года после telnet, стал одним из первых примеров работающего пакета приложений для системы, которая в дальнейшем стала известна как ARPANET. Он обогнал электронную почту, Usenet и даже стек TCP/IP. Как и telnet, FTP по-прежнему используется, хоть и ограниченно. Однако в современном Интернете он потерял значимость, в основном из-за проблем с безопасностью, а его место занимают альтернативные протоколы с шифрованием в случае FTP это SFTP, протокол передачи файлов, работающий поверх протокола Secure Shell (SSH), по большей мере заменившего telnet.


FTP настолько стар, что появился раньше электронной почты, а в начале он сам играл роль email-клиента. Наверно, неудивительно, что среди множества программ прикладного уровня, созданных для раннего ARPANET именно FTP выделился и проложил себе дорогу в мир современных технологий.

Причина этого сводится к его базовой функциональности. По сути, это утилита, упрощающая передачу данных между хостами, однако секрет его успеха заключается в том, что он в определённой степени сгладил различия между этими хостами. Как говорит Бхушан в своём рабочем предложении (RFC), самая большая сложность использования telnet в то время заключалась в том, что каждый хост немного отличался от другого.

Различия в характеристиках терминалов учитываются программами систем хостов в соответствии со стандартными протоколами, пишет он, упоминая и telnet, и протокол удалённого ввода заданий той эпохи. Однако чтобы использовать их, вам необходимо знать различные условности удалённых систем.


Телетайпный терминал эпохи ARPANET.

Придуманный им протокол FTP пытался обойти сложности непосредственного подключения к серверу при помощи способа, который он назвал косвенным использованием; этот способ позволял передавать данные или исполнять программы удалённо. Первая сборка протокола Бхушана, которая десятки лет спустя по-прежнему используется, хотя и в видоизменённом виде, использовала структуру директорий для исследований различий между отдельными системами.

В своём RFC Бхушан пишет:

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

В интервью подкасту Mapping the Journey Бхушан сообщил, что приступил к разработке протокола из-за очевидной потребности в приложениях для зарождающейся системы ARPANET, в том числе из-за потребности в электронной почте и FTP. Эти первые приложения стали фундаментальными строительными блоками современного Интернета и за последующие десятилетия сильно усовершенствовались.

Бхушан рассказал, что из-за ограниченных возможностей компьютеров того времени сначала функции электронной почты были частью FTP и позволяли распространять письма и файлы по протоколу в более легковесной формате. И в течение четырёх лет FTP был своего рода электронной почтой.

Мы спросили: Почему бы не добавить в FTP две команды под названием mail и mail file? Команда mail будет использоваться для обычных текстовых сообщений, а mail file для вложений писем, которые существуют и сегодня, говорит он в интервью.

Разумеется, Бхушан был не единственным, кто принял участие в разработке этого фундаментального раннего протокола, ведь после выпуска из вуза он получил должность в Xerox. Созданный им протокол продолжил своё развитие без него, получив в 1970-х и 1980-х серию обновлений в виде RFC; в том числе примерно в 1980 году появилась его реализация, позволявшая обеспечивать поддержку спецификации TCP/IP.

Хотя со временем появлялись незначительные обновления, чтобы протокол успевал за временем и мог поддерживать новые технологии, версия, которую мы используем сегодня, была выпущена в 1985 году, когда Джон Постел и Джойс К. Рейнольдс разработали RFC 959 обновление предыдущих протоколов, лежащих в основе современного ПО для работы с FTP. (Постел и Рейнольдс, среди прочего, примерно в то же время работали над системой доменных имён (DNS).) Хотя в документе эта версия описывается как предназначенная для исправления незначительных ошибок документации, улучшения объяснения некоторых функций протокола и добавления новых вспомогательных команд, стандартной стала именно она.

Учитывая его возраст, FTP обладает множеством неотъемлемых слабых мест, многие из которых проявляются и сегодня. Например, передача папки, содержащей множество крошечных файлов, выполняется в FTP чрезвычайно неэффективно, он гораздо лучше работает с крупными файлами, так как ограничивает количество необходимых отдельных соединений.

Во многих смыслах, из-за столь раннего появления в истории Интернета FTP повлиял на структуру множества последующих протоколов. Можно сравнить его с чем-то, что часто менялось и совершенствовалось в течение нескольких десятков лет, допустим, с баскетбольными кроссовками. Да, Converse All-Stars это хорошая обувь, и в нужных условиях с честью она послужит и сегодня, но с гораздо большей вероятностью успеха добьётся какая-нибудь модель от Nike, вероятно, под брендом Air Jordan.

File Transfer Protocol это Converse All-Star Интернета. Он передавал файлы ещё до того, как это стало круто, и по-прежнему сохраняет часть своей притягательности.

Никто не зарабатывал на Интернете. Скорее наоборот, на него тратили кучу денег. Мы смело сражались в этой битве и знали, что у него был потенциал. Но если кто-то скажет вам, что он знал, что будет дальше, то это ложь. Ведь я видел всё сам своими глазами.

Так Алан Эмтедж, создатель Archie, считающегося первым поисковым движком Интернета, рассказывал Internet Hall of Fame почему его изобретение, позволявшее пользователям искать на анонимных FTP-серверах файлы, не сделало его богатым. Если вкратце, то Интернет тогда был некоммерческим, а аспирант и работник техподдержки монреальского Университета Макгилла Эмтедж без разрешения использовал сеть вуза для работы Archie. Но именно так и лучше всего было поступить. Как гласит старая пословица, лучше просить прощения, чем разрешения. (Как и Бхушан, Эмтедж был иммигрантом, он родился и вырос на Барбадосе и приехал в Канаду, став студентом благодаря своим достижениям.)


Скриншот WS_FTP FTP-клиента для Windows, который был довольно популярен в 90-х.

Почему FTP может быть последней связью с нашим прошлым, до сих пор используемым онлайн


Как я писал несколько лет назад, если взять старую книгу об Интернете и попытаться пройти по старым ссылкам, то вероятность добраться до указанного в них ПО выше всего, если оно хранится на большом корпоративном FTP-сайте, потому что такие сайты обычно закрывают не очень часто.

Крупные технологические компании наподобие Hewlett-Packard, Mozilla, Intel и Logitech десятками лет использовали эти сайты для распространения среди конечных пользователей документации и драйверов. И по большей части эти сайты по-прежнему находятся онлайн, храня контент, который находился там долгие годы.

Во многих случаях эти сайты наиболее полезны, когда вам нужно найти нечто по-настоящему старое, например, драйвер или документацию. (Когда я пытался запустить свою Connectix QuickCam, то знал, что эти сайты мне пригодятся.)


Пример того, как выглядит FTP в современном веб-браузере (ftp.logitech.com).

В каком-то смысле, это даже может быть удобнее, чем навигация по веб-сайту, потому что интерфейс однороден и безупречно работает. (Многие веб-интерфейсы могут быть довольно кошмарными, если вам просто нужно добраться до драйвера.) Но у этого есть и недостатки такая простота означает, что FTP часто не очень хорошо справляется с современными стандартами и может быть намного медленнее современных способов передачи файлов.

Как я писал в статье на эту тему в прошлом году, доступ к таким FTP-сайтам получить всё сложнее (хотя их и архивируют в разные места), ведь компании отходят от подобных моделей хранения информации и могут отключать старые сайты.

В статье, где приводится интервью с Джейсоном Скоттом из Internet Archive, архив предпринимает шаги для защиты этих старых публичных FTP-сайтов, которые сегодня в любой момент могут быть отключены.

В том интервью Скотт заметил, что долгая жизнь таких FTP-сайтов на самом деле больше исключение, чем правило.

Невероятно странно, что FTP-сайты накопили инерцию, позволившую им продолжать работать в течение 15-20 лет, сказал он.

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

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

Цитата из статьи 1997 года в Network World; в ней говорится, что FTP, несмотря на свою неуклюжесть, по-прежнему остаётся хорошим вариантом для надомных работников и корпоративных пользователей Интернета. Хотя автор статьи был заинтересованным лицом (Роджер Грин являлся президентом компании Ipswitch, крупного изготовителя программ для FTP), его аргументы, тем не менее, соответствовали духу эпохи. Протокол был отличным способом передачи больших файлов через сети и сохранения их на сервере. Проблема в том, что FTP, несмотря на своё постепенное совершенствование, будет вытеснен гораздо более изощрёнными альтернативами, как протоколами (BitTorrent, SFTP, rsync, git, даже современными вариантами HTTP), так и облачными системами наподобие Dropbox или Amazon Web Services.

Когда-то у меня был собственный FTP-сервер. В основном он использовался для хранения музыки во времена учёбы в колледже, когда студенты были одержимы идеей обмена музыкой. У меня было чрезвычайно быстрое подключение, а потому идеальная скорость для работы FTP-сервера.

Это был отличный способ делиться собственными музыкальными вкусами с миром, но университетская система однажды узнала об обмене файлами и начала ограничивать пропускную способность, поэтому на этом всё закончилось или, по крайней мере, так я думал. Дело в том, что летом я работал в общежитии, и оказалось, что после выпуска студентов ограничение больше не было проблемой, поэтому я смог снова на пару месяцев запустить FTP-сервер.


Panics Transmit современный пример FTP-клиента. Множество современных клиентов поддерживает широкий набор протоколов, а не только старый добрый FTP.

Позже я выпустился и FTP-сервер отключился навсегда; к тому же всё равно возникли более эффективные варианты его замены, например, BitTorrent, и более законные, типа Spotify и Tidal.

Так же, как и обмен файлами, сильно эволюционировавший спустя 15 лет, изменились и мы сами. Мы узнали о более эффективных и безопасных способах удалённого управления файлами. В 2004 году многие считали, что лучше всего управлять веб-сервером при помощи FTP. Сегодня, когда сервисы типа Git обеспечивают возможность эффективного контроля версий, такой способ кажется рискованным и неэффективным.

Даже если популярные браузеры в ближайшие месяцы избавятся от поддержки FTP, это не значит, что у нас совершенно исчезнут другие способы работы с этим протоколом. Специализированное ПО, разумеется, по-прежнему будет существовать. Но, что более важно, для замены устаревшего протокола FTP есть веские причины.

В отличие от ситуации с IRC (когда у протокола отобрали популярность коммерческие инструменты) и Gopher (рост которого погубил внезапный переход на коммерческую модель), FTP уходит из веб-браузеров потому, что его возраст подчёркивает отсутствие инфраструктуры безопасности.

Самые распространённые способы его использования, например, для организации публично доступных анонимных FTP-серверов, по сути, потеряли свою популярность. А в своей основной нише он в конечном итоге был заменён более защищёнными и современными версиями, например SFTP.

Уверен, что какой-нибудь технарь может заявить, что FTP никогда не умрёт, потому что для него всегда найдётся специализированная область применения. Да, это возможно. Но у подавляющего большинства людей после отключения FTP в браузере Chrome, вероятно, не будет причин искать способы подключиться к нему снова.

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



На правах рекламы


VDS с посуточной оплатой для любых целей это про наши эпичные серверы. Максимальная конфигурация 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe.

Подробнее..

Как мы делали универсальный сервис подписания для инфраструктуры Госуслуг на C и GO. Часть 1

18.03.2021 12:07:58 | Автор: admin

Это первая часть статьи, в которой будет рассмотрена GO-часть нового сервиса подписания для всем известного (а если не всем, то многим) портала Госуслуг. Сюда входит конфигурирование, тестирование, ресты, файловый менеджер и планировщик. В общем все то, что происходит до того, как данные будут переданы в C-часть для подписания и верификации. Я также кратко опишу основные предпосылки к созданию нового сервиса.

Дисклеймер!

Автор этой статьи не писатель, а разработчик, который пытается в первый раз интересно описать достаточно сложный проект.

Во второй части будут рассмотрены CGO-прослойка и C-часть, в которой происходит подписание и верификация данных. Там же будут описаны самые важные оптимизации, ради которых этот сервис и создавался.

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

А чтобы еще сильнее упростить дальнейшее чтение, я сделал схему, в которой есть все элементы обеих частей статьи. Те элементы, которые будут рассмотрены в этой статье, выделены красным.

Немного о терминах

Перед тем как начать, думаю, надо дать краткое описание некоторых терминов:

Единый портал государственных и муниципальных услуг (функций) (ЕПГУ, далее по тексту просто Госуслуги). Это портал, где у каждого гражданина есть возможность:

  • узнать информацию о государственных и муниципальных услугах;

  • заказать госуслуги в электронной форме;

  • записаться на приём в ведомство;

  • оплатить любым электронным способом штрафы Госавтоинспекции, судебные и налоговые задолженности, госпошлины, услуги ЖКХ;

  • оценить качество предоставления госуслуг.

Единая система межведомственного электронного взаимодействия (СМЭВ) информационная система, которая позволяет федеральным, региональным и местным органам власти, кредитным организациям (банкам), внебюджетным фондам и прочим участникам СМЭВ обмениваться данными, необходимыми для оказания государственных услуг гражданам и организациям в электронном виде, а также для реализации других своих функций

Существует 2 версии СМЭВ это СМЭВ2 и СМЭВ3, они отличаются методическими рекомендациями и разным набором возможностей, но для этой статьи понимание этих отличий не обязательно.

Мотивы и предпосылки

Новый сервис подписания изначально задумывался как более производительная версия аналогичного сервиса для СМЭВ. Но в ходе работ он перерос в универсальный сервис, работающий практически со всеми стандартами, которые используются в инфраструктуре Госуслуг. Например, сейчас новый сервис можно использовать для:

  1. Создания и верификации простых подписей

  2. Подписания и верификации запросов для СМЭВ2 по стандарту WS-Security + XMLDSig

  3. Подписания и верификации запросов для СМЭВ3 по стандарту XMLDSig

  4. Подписания и верификации файлов по стандартам CMS и CAdES-BES

  5. Подписания и верификации OAuth токенов для единой системы идентификации и аутентификации по стандартам CMS и CAdES-BES

  6. Подписания и верификации JWT

При этом поддерживаются ГОСТ-2012 как для 256, так и 512-битных ключей.

Это дало возможно использовать КриптоПро только в экземплярах нового сервиса подписания, которыми могут одновременно пользоваться несколько систем. Что, в свою очередь, позволило экономить много ресурсов на закупках лицензий. Но, самое главное, это позволило сделать процесс работы с подписанием одинаковым во всех системах.

Помимо всего вышесказанного, этот проект одновременно и эксперимент по использованию GO в связке с C для проектов, которые должны работать очень быстро, но при этом иметь некритичные по производительности места, в которых можно использовать более высокоуровневый язык. Поскольку до работы над сервисом мы с GO не сталкивались, некоторые решения могут быть не оптимальными. Тем не менее, итоговый результат более чем удовлетворительный.

Финальный объем проекта сравнительно небольшой, приблизительно 28000 строк кода. Это особенно удивительно с учетом того, что у нас всего 4 внешних зависимости (libxml2 и КриптоПро, github.com/google/uuid, ithub.com/spf13/viper), что обязало нас взять на себя очень много работы с криптографическими стандартами.

GO-часть

Теперь предлагаю перейти к самому сервису подписания на C и GO. Чтобы повествование шло более логично, я начну описывать все с самого верха, начиная с конфигурационного файла. А затем буду спускаться все ниже и ниже, чтобы ближе к концу описание самых низкоуровневых операций и оптимизаций, которые как раз и позволяют сервису работать очень быстро, не осталось без контекста.

Конфигурационный файл

Конфигурационный файл у сервиса один, вследствие чего он получился достаточно большим, более 200 строчек.

И большую его часть занимает настройка тестов, потому что практически сразу стало очевидно, что без хорошо продуманных тестов разработка может сильно затянуться. Настроить в тестах можно практически все, начиная от количества потоков и списка ключей, с которыми будет проводится тестирование. И заканчивая очень подробным описанием параметров и файлов, с которыми будут запускаться сами тесты.

Такое внимание к тестам было важно потому, что GO для меня был новым языком, и предугадать все неправильные решения заранее было практически невозможно. А хорошие тесты позволили не бояться на ходу переписывать конкретные модули или даже всю архитектуру проекта целиком, что я и делал неоднократно.

Именно благодаря этому итоговый объем проекта 28000 строк, а не 60000 - 100000, что вполне естественно для проектов без четкой постановки и изменением планов в ходе разработки. Это также позволило создать легко расширяемую, универсальную архитектуру, благодаря которой добавление новых возможностей сейчас может занимать 1 2 дня, а не несколько недель.

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

На последнем я предлагаю задержаться и постараюсь объяснить, почему параметры в запросах это не просто необоснованное раздувание конфигурационного файла, а то, что может сильно упростить взаимодействие с сервисом.

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

И, самое главное, каждый экземпляр сервиса в зависимости от среды и задач можно настроить оптимальным образом, что может в итоге дать существенный выигрыш в производительности.

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

Как следствие того, что конфигурационный файл получился очень большим и того, что его можно менять прямо во время работы сервиса, возникла необходимость контролировать целостность и базовые правила его заполнения. Для этого при каждом запуске сервиса или при каждом получении сигнала SIGHUP вызывается метод, который валидирует данные и пишет ошибки в случае необходимости.

Тестирование

В первую очередь для сервиса были написаны стандартные GO-тесты. В них нет ничего особенного, поэтому останавливаться на этом мы не будем.

Но одними стандартными тестами все не обошлось. По ходу развития сервиса появилась необходимость еще и в тестах, которые позволили бы проверять стабильности сервиса при больших нагрузках. А поскольку стандартные средства GO не позволяли сделать все необходимое, для этих целей был выбран JMeter. Впоследствии JMeter так же оказался очень удобным для профилирования C-части в связке с pprof.

Но, как вы уже, наверное, поняли, поддержка двух больших, независимых наборов тестов в условиях постоянно дорабатывающейся архитектуры занятие очень трудозатратное. Для решения этой проблемы были сделаны универсальные наборы данных для обычных и нагрузочных тестов. А нагрузочные тесы были по возможности сделаны максимально похожими на интеграционные тесты, что затем позволило тратить заметно меньше времени на доработки.

Также в проекте используются линтеры из golangci-lint, которые позволяют лучше следить за качеством кода. Впрочем, на них можно подробно не останавливаться, на Хабре про линтеры много отдельных статей.

Ресты

Теперь наконец то можно перейти к тому, что эти самые тесты и тестируют, к рестам, которых на текущий момент в сервисе 17.

Один из них health-check, который возвращает основную информацию о текущем экземпляре сервиса. И еще 8 групп по 2 реста, которые отвечают за подпись и верификацию по конкретному стандарту. Сам список поддерживаемых стандартов есть в начале статьи.

Изначально была идея осветить каждый их них, но в процессе написания статьи стало понятно, что в этом нет смысла. В основном потому, что одной из главных целей при проектировании этого сервиса была однообразность везде, где это не вредит качеству и очевидности кода. Даже переменные, настройки в конфиге, параметры в методах и самих рестах, отвечающие за одно и тоже, называются одинаково. Благодаря однообразности можно приблизительно понять, как работают все ресты, просто рассмотрев самые большие и сложные из них ресты для подписания запросов в СМЭВ2 и СМЭВ3. Далее по тексту все примеры будут приводиться в контексте именно этих рестов.

Файловый менеджер

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

В новом сервисе мы постарались к ним подготовиться. Для этого все файлы были разделены на 2 категории:

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

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

После разделения файлов на 2 категории тут же возникла другая проблема. Как передавать большие объемы данных между двумя языками?

Вот тут и возникла необходимость в файловом менеджере, который будет работать как в C, так и в GO-частях. Вообще, при проектировании файлового менеджера главная идея была в том, чтобы код в C и в GO-частях не заботился о том, где и как хранятся файлы. А мог просто по uuid из запроса получить требуемые ему данные. Например, подписанный файл или конкретный чанк неподписанного файла. При этом все это должно было работать одинаково на обеих сторонах сервиса.

Для этого файловый менеджер пришлось разделять на 2 части. Первая часть находится на стороне GO. Она умеет делать все необходимые операции, например, записывать или удалять файлы и чанки, собирать из чанков итоговый файл для отправки и т.д. Синхронизация между С и GO-частями также происходит на стороне GO, за счет возможности блокировать директорию по uuid перед вызовом C-кода.

Вторая часть находится на стороне C. Она намного скромнее по возможностям умеет всего лишь читать определенные чанки и разбивать на чанки итоговый подписанный xml, но именно благодаря этой части удается экономить очень много памяти при работе с большими файлами.

Например, при чтении больших файлов по чанкам самым очевидным было бы сначала вычитать все в память, а потом уже по этим данным построить DOM. Но вместо этого libxml2 позволяет почанково передавать парсеру все необходимые данные, тем самым существенно экономя максимально потребляемый одним запросом объем памяти

Вот так, например, выглядит метод для парсинга многочанкового документа:

В примере вызовом xmlCreatePushParserCtxt создается контекст парсера, а для добавления в него чанков вызывается xmlParseChunk.

//Парсинг всех доступных чанковfor(int i = 1; i <= numberOfChunks; i++){    //Чтение данных из файла в буфер    result = fread(data, sizeof(char), fileSize, pFile);    if (result != fileSize)    {      res = GENERAL_ERROR;      errorLog(uuid, LOGGER_FILE_INFO, "fread failed");      goto err;    }    if(1 == i)    {        //Инициализация парсера        ctxt = xmlCreatePushParserCtxt(NULL, NULL, chars, sizeof(chars), filePath);        if (NULL == ctxt) {            res = XML_PARSE_ERROR;            errorLog(uuid, LOGGER_FILE_INFO, "xmlCreatePushParserCtxt failed");          goto err;        }            //Добавление поддержки больших файлов        res = xmlCtxtUseOptions(ctxt, XML_PARSE_HUGE);        if(OK != res)        {           res = XML_PARSE_ERROR;            errorLog(uuid, LOGGER_FILE_INFO, "xmlCtxtUseOptions failed");            goto err;        }        //Парсинг первого чанка        res = xmlParseChunk(ctxt, data + sizeof(chars), fileSize - sizeof(chars), 0);        if(OK != res)        {          res = XML_PARSE_ERROR;            errorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed");            goto err;        }        if (1 != ctxt->wellFormed || 0 != ctxt->errNo)        {           res = XML_PARSE_ERROR;           formatErrorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed, wellFormed: %d, err: %d", ctxt->wellFormed, ctxt->errNo);           goto err;        }    }    else if(numberOfChunks == i)    {        //Парсинг последнего чанка        res = xmlParseChunk(ctxt, data, fileSize, 1);        if(OK != res)        {          res = XML_PARSE_ERROR;            errorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed");            goto err;        }        if (1 != ctxt->wellFormed || 0 != ctxt->errNo)        {          res = XML_PARSE_ERROR;          formatErrorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed, wellFormed: %d, err: %d", ctxt->wellFormed, ctxt->errNo);           goto err;        }             //Возвращение ссылки на распаршеный документ        *doc = ctxt->myDoc;    }    else    {       //Парсинг промежуточных чанков        res = xmlParseChunk(ctxt, data, fileSize, 0);        if(OK != res)        {            res = XML_PARSE_ERROR;            errorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed");            goto err;        }        if (1 != ctxt->wellFormed || 0 != ctxt->errNo)        {            res = XML_PARSE_ERROR;            formatErrorLog(uuid, LOGGER_FILE_INFO, "xmlParseChunk failed, wellFormed: %d, err: %d", ctxt->wellFormed, ctxt->errNo);            goto err;        }    }}

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

Ниже пример метода для дампа xml в несколько чанков.

В примере из метода dumpXmlDocAndSplitIntoChunks вызывается метод xmlOutputBufferCreateIO, позволяющий создать буфер, при заполнении которого будут вызываться переданные нами callback-и dumpToFileWriteCallback и dumpToFileCloseCallback. В которые первым параметром будет приходить переданный нами контекст callbackData. Для простоты пример с dumpToFileCloseCallback и writeToFile я приводить не стал.

Впоследствии этот буфер передается в метод xmlNodeDumpOutput, который и будет заниматься дампом.

static error_code dumpXmlDocAndSplitIntoChunks(const char *uuid, xmlDocPtr *doc, const char *tempDirPath, int *signedXmlLen, const char *chunkType, const int chunkSizeInByte, int *numberOfChunks){xmlNodePtr             rootNode = NULL;xmlOutputBufferPtr              buf = NULL;dumpToFileWriteCallbackTempData callbackTempData = {0};dumpToFileWriteCallbackData     callbackData = {uuid, tempDirPath, chunkType, chunkSizeInByte, numberOfChunks, signedXmlLen, &callbackTempData};int                    bufRes = 0;error_code            res = OK;debugLog(uuid, LOGGER_FILE_INFO, "started");if(NULL == doc || NULL == tempDirPath || NULL == signedXmlLen || NULL == chunkType || 0 == chunkSizeInByte || 0 == numberOfChunks){res = BAD_INPUT;errorLog(uuid, LOGGER_FILE_INFO, "bad input");goto err;}rootNode = xmlDocGetRootElement(*doc);if(NULL == rootNode){res = GENERAL_ERROR;errorLog(uuid, LOGGER_FILE_INFO, "xmlDocGetRootElement failed");goto err;}buf = xmlOutputBufferCreateIO(dumpToFileWriteCallback, dumpToFileCloseCallback, &callbackData, NULL);if(NULL == buf){res = GENERAL_ERROR;errorLog(uuid, LOGGER_FILE_INFO, "xmlOutputBufferCreateIO failed");goto err;}xmlNodeDumpOutput(buf, *doc, rootNode, 0, 0, NULL);bufRes = xmlOutputBufferFlush(buf);if(0 > bufRes){res = GENERAL_ERROR;errorLog(uuid, LOGGER_FILE_INFO, "xmlOutputBufferFlush failed");goto err;};err:if(NULL != buf)xmlOutputBufferClose(buf);return res;}

Пример определения структур dumpToFileWriteCallbackTempData и dumpToFileWriteCallbackData:

typedef struct{    char   *tempBuf;    int    tempBufLen;    int    tempBufSize;    bool   tempBufAllocated;} dumpToFileWriteCallbackTempData;typedef struct{    const char                       *uuid;    const char                       *tempDirPath;    const char                       *chunkType;    const int                        chunkSizeInByte;    int                              *numberOfChunks;    int                              *signedXmlLen;    dumpToFileWriteCallbackTempData  *tempData;} dumpToFileWriteCallbackData;

Пример dumpToFileWriteCallback:

static int dumpToFileWriteCallback(void *context, const char *buffer, int len){    dumpToFileWriteCallbackData *callbackData = (dumpToFileWriteCallbackData*) context;    dumpToFileWriteCallbackTempData *tempData = (dumpToFileWriteCallbackTempData*) callbackData->tempData;    int internalLen = 0;    error_code res = OK;    //Вызывается при флуше    if(len <= 0 && tempData->tempBufSize != 0)    {        //Запись данных в файл        res = writeToFile(callbackData->uuid, callbackData->tempDirPath, callbackData->chunkType, *callbackData->numberOfChunks + 1, tempData->tempBuf, tempData->tempBufSize);        if(OK != res)        {            errorLog(callbackData->uuid, LOGGER_FILE_INFO, "writeToFile failed");            return -1;        }        *callbackData->numberOfChunks += 1;        *callbackData->signedXmlLen += tempData->tempBufSize;        tempData->tempBufSize = 0;        return len;    }    if(len <= 0)        return len;    if(!tempData->tempBufAllocated)    {        tempData->tempBuf = (char*) malloc(callbackData->chunkSizeInByte);        if(NULL == tempData->tempBuf)        {        errorLog(callbackData->uuid, LOGGER_FILE_INFO, "malloc failed");            return -1;        }        tempData->tempBufLen = callbackData->chunkSizeInByte;        tempData->tempBufSize = 0;        tempData->tempBufAllocated = true;        *callbackData->numberOfChunks = 0;        *callbackData->signedXmlLen = 0;    }    internalLen = len;    while(0 != internalLen)    {        if(internalLen == tempData->tempBufLen)        {            //Запись данных в файл            res = writeToFile(callbackData->uuid, callbackData->tempDirPath, callbackData->chunkType, *callbackData->numberOfChunks + 1, buffer, len);            if(OK != res)            {                errorLog(callbackData->uuid, LOGGER_FILE_INFO, "writeToFile failed");                return -1;            }            internalLen -= len;            *callbackData->numberOfChunks += 1;            *callbackData->signedXmlLen += len;            break;        }        if(tempData->tempBufSize == tempData->tempBufLen)        {            //Запись данных в файл            res = writeToFile(callbackData->uuid, callbackData->tempDirPath, callbackData->chunkType, *callbackData->numberOfChunks + 1, tempData->tempBuf, tempData->tempBufSize);            if(OK != res)            {                errorLog(callbackData->uuid, LOGGER_FILE_INFO, "writeToFile failed");                return -1;            }            *callbackData->numberOfChunks += 1;            *callbackData->signedXmlLen += tempData->tempBufSize;            tempData->tempBufSize = 0;        }        int freeSpaceInBuf = tempData->tempBufLen - tempData->tempBufSize;        if( internalLen < freeSpaceInBuf )        {            memcpy(tempData->tempBuf + tempData->tempBufSize, buffer, internalLen);            tempData->tempBufSize +=internalLen;            internalLen = 0;            break;        }        else if( internalLen > freeSpaceInBuf )        {            memcpy(tempData->tempBuf + tempData->tempBufSize, buffer, freeSpaceInBuf);            tempData->tempBufSize +=freeSpaceInBuf;            buffer += freeSpaceInBuf;            internalLen -= freeSpaceInBuf;            continue;        }    }    return len;}

Самое главное, что позволяет этим двум частям обходится без передачи какой-либо информации, кроме uuid и количества чанков это универсальные имена для файлов.

Например, когда первый чанк файла приходит на подписание, то он будет называться unsignedXmlChunk_1 (если файл передавался без разбиения на чанки, то он будет файлом, состоящим из одного чанка) и будет лежать в директории, имя которой это uuid из запроса. Соответственно, когда из GO части вызывается метод для подписания запроса, в который предается количество чанков и uuid, то C-часть точно может сказать файловому менеджеру какие файлы ей нужны для подписания. Когда итоговый подписанный файл нужно записать на диск, он будет сохраняться в ту же директорию, но уже с именем signedXmlChunk_1. И GO-часть при отсутствии ошибок будет пытаться вернуть именно этот файл в качестве ответа.

Вот так, например, выглядит метод для добавления чанков для файлов разных типов:

func AddSignedXmlChunk(uuid string, tempDirPath string, chunkName string, content io.Reader, lockDir bool) (err error) {return addChunk(uuid, tempDirPath, "signedXmlChunk_"+chunkName, content, lockDir)}func AddUnsignedXmlChunk(uuid string, tempDirPath string, chunkName string, content io.Reader, lockDir bool) (err error) {return addChunk(uuid, tempDirPath, "unsignedXmlChunk_"+chunkName, content, lockDir)}func addChunk(uuid string, tempDirPath string, chunkName string, content io.Reader, lockDir bool) (err error) {file, err := os.OpenFile(filepath.Join(tempDirPath, chunkName), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)if err != nil {logger.Error.Println(uuid, err)return err}defer func() {defErr := file.Close()if defErr != nil {logger.Error.Println(uuid, defErr)err = defErr}}()n, err := io.Copy(file, content)if err != nil {logger.Error.Println(uuid, "Copy error", err, ", bytes written", n)return err}return err}

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

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

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

Заключение

Несмотря на то, что эта часть статьи получилась достаточно объемной, это только предисловие ко второй части статьи, которая выйдет в ближайшее время.

А пока буду рад обсудить все вышесказанное в комментариях.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru