Библиотека сайта rus-linux.net
Sendmail – Архитектура и принципы разработки
Глава 17 из 1 тома книги "Архитектура приложений с открытым исходным кодом".
Оригинал: "Sendmail"
Автор: Eric Allman
Перевод: Vlad http://vlad8.com/
17.4. Проектные решения
Некоторые решения в процессе проектирования были правильными. Некоторые начинались правильно, но стали неверными, так как мир успел измениться. Некоторые были сомнительными и так и не стали менее сомнительными со временем.
17.4.1. Синтаксис конфигурационного файла
Синтаксис конфигурационного файла преследовало несколько проблем. Во-первых, все приложение должно было помещаться в 16-битном адресном пространстве, поэтому парсер должен был быть маленьким. Во-вторых, первые конфигурации были довольно небольшими по размеру (до одной страницы), поэтому несмотря на запутанный синтаксис, файл все равно оставался читаемым. Однако по прошествии времени все больше решений по работе программы переносились из кода C в конфигурационный файл, и файл начал разрастаться. Конфигурационный файл приобрел репутацию загадочного. Одной из проблем для многих был выбор символа табуляции в качестве активного синтаксического элемента. Это была одна из ошибок, скопированная с других систем того времени, в частности make. Эта ошибка стала более критичной с появлением оконных систем (а с ними копирования и вставки, которые обычно не сохраняют табуляцию).
Оглядываясь назад, я понимаю, что так как файл становился все больше и начали появляться 32-битные машины, имело смысл пересмотреть синтаксис. Было время, когда я раздумывал над этим, но решил не делать, т.к. не хотел изменять конфигурации на «большом» количестве машин (которое в то время составляло несколько сотен). Как оказалось это было ошибкой; я просто недооценил насколько вырастет количество инсталляций программы и сколько часов я бы сэкономил, если бы поменял синтаксис раньше. Также, когда стандарты устоялись, большое количество опций можно было вернуть в код программы, упростив ее настройку.
Отдельный интерес представляет то, насколько много функциональности добавилось в конфигурационный файл. Я разрабатывал sendmail одновременно с развитием стандарта SMTP. За счет перемещения функциональных решений в конфигурационный файл я мог быстро отвечать на изменения в стандарте – обычно в течение 24 часов. Я думаю, что это улучшило SMTP-стандарт, так как представлялось возможным получить опыт работы с предлагаемыми изменениями достаточно быстро, хотя ценой этого стал трудночитаемый конфигурационный файл.
17.4.2. Правила преобразования (Rewriting Rules)
Одним из трудных вопросов при написании sendmail было то, как делать необходимые преобразования для разрешения пересылки между сетями без нарушения стандартов получающей сети. Требовались изменения метасимволов (например, BerkNET использовала запятую в качестве разделителя, которую нельзя было применять в адресах в SMTP), перемещения компонентов адресов, добавления или удаления компонентов и так далее. Например, следующие преобразования были необходимы в определенных обстоятельствах:
Из | В |
a:foo | a.foo@berkeley.edu |
a!b!c | b!c@a.uucp |
<@a.net,@b.org:user@c.com> | <@b.org:user@c.com> |
Регулярные выражения не были лучшим выбором, потому что они не имели хорошей поддержки для границ слов, кавычек и т.д. Быстро стало ясно, что практически невозможно написать регулярные выражения, которые были бы точными, и уж тем более вразумительными. В частности регулярные выражения резервировали несколько метасимволов, включая “.”, “*”, “+”, “{[}" и "{]}”, а все они могли появляться в e-mail адресах.
Эти символы могли быть экранированы в конфигурационных файлах, но я считал это сложным, запутывающим и достаточно уродливым (такое решение попробовали в UPAS в Bell Laboratories, мейлер из Unix 8, но он никогда не завоевал популярность). Вместо этого, был необходим этап сканирования для создания токенов, которыми затем можно было манипулировать также как символами в регулярных выражениях. Единственного параметра, описывающего «управляющие символы», которые сами были и токенами, и разделителями токенов, было достаточно. Пустые пробелы разделяли токены, но сами токенами не были. Правила преобразования были просто паттернами сопоставления и замены пар, организованные по сути в подпрограммы.
Вместо большого количества метасимволов, которые нужно было экранировать, чтобы убрать их «магические» свойства (как они использовались в регулярных выражениях), я использовал один единственный «экранирующий» символ, который комбинировался с обычными символами для представления паттернов с произвольным символом (например, для поиска произвольного слова).
Традиционным подходом Unix было использование обратного слэша, но обратный слэш уже использовался как символ кавычек в некоторых адресах. Как оказалось, “$” был одним из немногих символов, которые еще не использовались как пунктуационные символы в синтаксисе email.
Одним из первональных неудачных решений было, как ни странно, то, как использовались пробелы. Символ пробела был разделителем, как и в большинстве вводимых с компьютера данных, и поэтому мог быть использован свободно между токенами в паттернах. Однако, первые распространяемые конфигурационные файлы не включали пробелы, что приводило к паттернам, которые было понять труднее, чем это было необходимо. Посмотрите на разницу между следующими двумя (семантически идентичными) паттернами:
$+ + $* @ $+ . $={mydomain} $++$*@$+.$={mydomain}
17.4.3. Использование преобразования для парсинга
Некоторые предлагали, что sendmail должен использовать для разбора адресов стандартные техники парсинга, основанные на грамматике, а не правила преобразования и оставить их для модификации адресов. На первый взгляд это имело смысл, учитывая, что стандарты, определяющие адреса, используют грамматику. Главной причиной повторного использования правил преобразования было то, что в некоторых случаях было необходимо разбирать адреса поля заголовка (например, чтобы выбрать конверт отправителя из заголовка при получении почты из сети, не имевшей формального конверта). Такие адреса нелегко распарсить, используя, скажем, парсер LALR(1) вроде YACC и традиционный сканер, из-за количества символов, которые необходимо просмотреть впереди.
Например, разбор адреса allman@foo.bar.baz.com <eric@example.com> требует просмотра вперед или сканером или парсером; вы не можете знать что первоначальный “allman@…” не является адресом до тех пор, пока не увидите “<”. Так как парсеры LALR(1) имеют только один токен для просмотра вперёд, это будет необходимо делать в сканере, что заметно его усложнит. По той причине, что правила преобразования уже поддерживали произвольный перебор с возвратами (то есть они могли смотреть вперёд произвольно далеко), их было достаточно.
Второй причиной было то, что через паттерны было относительно легко распознать и исправить неверный ввод. В конце концов, преобразования через правила были более чем мощными для данной задачи, а повторное использование кода было мудрым решением.
Один необычный момент о правилах преобразования: при сопоставлении с паттерном полезно и для ввода и для паттерна создать токены. По этой причине один и тот же сканер был использовани для вводимых адресов и для самих паттернов преобразования. Для этого сканер вызывался с различными таблицами типов символов для различных вводимых данных.
17.4.4. Внедрение SMTP и организации очереди в sendmail
«Очевидным» способом реализации исходящей (клиентской) части SMTP было бы сделать его как внешний отправитель, схожий с UUCP, но это привело бы к ряду вопросов. Например, где нужно было делать организацию очередей – в sendmail или другом клиентском SMTP-модуле? Если делать ее в sendmail, тогда отдельные копии сообщений нужно было рассылать каждому получателю (т.е. нельзя было бы открыть одно соединение и затем рассылать несколько RCPT-команд) или гораздо более сложный способ обратной коммуникации был бы необходим для передачи необходимого состояния каждого получателя, чем это было возможно с использованием простых кодов завершения Unix.
Если организация очереди была сделана в клиентском модуле, то был большой потенциал для репликации; в частности в то время другие сети, например, XNS еще были возможными соперниками. Кроме того, включение очереди в сам sendmail предоставляла более элегантный способ работы с определенного рода ошибками, в частности кратковременными проблемами, такими как нехватка ресурсов.
Входящая (серверная) часть SMTP подразумевала другой набор решений. В то время я считал важным реализовать VRFY и EXPN SMTP-команды точно, что требовало доступа к механизму алиасов. Это опять же потребовало бы гораздо более сложного протокола обмена между серверным модулем SMTP и sendmail, чем было возможно при использовании командных строк и кодов завершения (на самом деле, что-то вроде самого протокола SMTP для такой коммуникации).
Сегодня я бы гораздо более склонялся к тому, чтобы оставить реализацию очереди в ядре sendmail, но переместить обе части реализации SMTP в другие процессы. Одна из причин для этого в безопасности: как только на стороне сервера открыт 25 порт, нет необходимости в доступе администратора. Современные расширения, такие как TLS и DKIM-подписывание, усложняют клиентскую сторону (т.к. секретные ключи не должны быть доступны непривилегированным пользователям), но строго говоря доступ под администратором все равно так и не нужен. Несмотря на то, что если SMTP-клиент работает как пользователь-неадминистратор, имеющий возможность на чтение секретных ключей, проблема безопасности остается, у этого пользователя по определению есть особые привилегии, и поэтому он не должен напрямую осуществлять коммуникации с другими узлами. Все эти проблемы можно обойти довольно просто.
17.4.5. Реализация очереди
Sendmail следовал представлениям того времени о хранении файлов очереди. На самом деле используемый формат был чрезвычайно похож на подсистему lpr (линейного печатающего принтера) того времени. Каждое задание имело два файла, один с контрольной информацией и один с данными. Контрольный файл был простым текстовым файлом, первый символ каждой строки содержал информацию о значении этой строки.
Когда sendmail хотел обработать очередь, ему нужно было прочитать все контрольные файлы, сохраняя всю нужную информацию в память, и затем сортировать этот список. Это хорошо работало для относительно маленького количества сообщений, но при числе сообщений в очереди больше 10 000 не давало нужный результат. В частности, когда папка становилась настолько большой, что требовала блоков с непрямым доступом в файловой системе, возникала серьезная проблема с производительностью, которая могла снизить быстродействие на порядок. Было возможно улучшить данную ситуацию, заставив sendmail понимать несколько директорий с данными очередей, но это был в лучшем случае хак.
Альтернативным подходом было хранение всех контрольных файлов в базе данных. Это не было сделано, потому что в то время не было доступных пакетов баз данных, а когда появилась dbm(3), у нее было несколько недостатков, включая невозможность возвращения места на диске, требование о том, что все ключи, хэшируемые вместе, должны помещаться на одной странице (512 байт) и отсутствие блокировки. Надежные пакеты баз данных не появлялись еще многие годы.
Еще одной возможностью альтернативной реализации было создание отдельного демона, хранящего состояние очереди в памяти, возможно, записывающего также информацию в лог для возможности восстановления. Но по причине относительно небольшого объема почтового траффика в то время, недостатка памяти на большинстве машин, относительно высоких затрат на фоновые процессы и сложность реализации такого процесса, это решение вряд ли было хорошим в то время.
Еще одним проектировочным решением было хранение заголовков сообщения в контрольном файле очереди, а не в файле данных. Объяснением этому было то, что большинство заголовков требовали существенных изменений, зависящих от места назначения (а так как сообщения могли иметь более одного места назначения, их нужно было изменять несколько раз), и цена разбора заголовков оказывалась достаточно высокой, поэтому хранение их в формате, предварительно подготовленном для разбора, казалось экономией.
В ретроспективе это оказалось не лучшим решением, также как и хранение тела сообщения в стандартном формате Unix (с завершающими символами начала строки), а не в формате, в котором они были получены (который мог использовать символы начала новой строки, возврата каретки/перевода строки, просто возврата каретки или перевода строки/возврата каретки). С развитием мира электронной почты и принятием стандартов, необходимость в перезаписи сократилась, а даже кажущееся безобидным преобразование содержит риск ошибки.
17.4.6. Получение и исправление неверного ввода
Так как sendmail был создан в мире множества протоколов и на редкость малого количества стандартов, я решил приводить в порядок плохо сформированные сообщения, когда это возможно. Это соответствует “принципу устойчивости” (он же закон Постеля), сформулированному в RFC 7934. Некоторые из этих изменений были очевидными и даже обязательными: при отправке UUCP сообщения в Арпанет адреса UUCP нужно было конвертировать в адреса Арпанет, чтобы команда “Ответить” работала корректно, завершающие строки символы нужно было конвертировать между форматами, используемыми различными платформами, и так далее. Некоторые были менее очевидными: если сообщение было получено, но не включало заголовок “From:”, которое было обязательным согласно спецификациям Интернета, нужно ли было добавлять это поле заголовка, передавать сообщение без него или отказать в отправке?
В то время моей главной заботой была функциональная совместимость, поэтому sendmail исправлял сообщение, например, добавляя поле заголовка From:. Однако говорили о том, что это позволяло другим старым почтовым системам существовать еще долгое время после того, как они уже должны были быть исправлены или от них нужно было вовсе отказаться.
Я думаю, что мое решение было верным для того времени, но сегодня является причиной проблем. Высокая степень функциональной совместимости была важна, чтобы поток писем отправлялся без препятствий. Если бы я отклонял неверно сформированные сообщения, большинство сообщений в то время были бы отклонены. Если бы я пропускал их неисправленными, получатели бы получали сообщения, на которые не могли бы ответить и в некоторых случаях – сообщения, по которым нельзя было бы определить, кем они были отправлены, или же сообщения были бы отклонены другим почтовым отправителем.
Сегодня существуют стандарты, и по большей части эти стандарты точные и полные. Больше нет проблем в том, что многие сообщения будут отклонены. И тем не менее существует почтовое программное обеспечение, которое рассылает некорректно сформированные почтовые сообщения. Это создает многочисленные ненужные проблемы для других программ в Интернете.
17.4.7. Конфигурирование и использование M4
Какое-то время я одновременно регулярно вносил изменения в конфигурационные файлы sendmail и лично поддерживал многие машины.
Так как большое количество конфигурационных файлов на разных машинах оставалось одинаковым, становилось желательно использовать какой-то инструмент для создания конфигурационных файлов. Макропроцессор m4 был включен в Unix. Он был разработан как клиентская часть программных языков (в частности, ratfor). Что важнее, у него были возможности “include”, как инструкция “#include” в языке C. Первоначальные конфигурационные файлы использовали немногим больше этой возможности и небольшие макро-расширения.
IDA sendmail также использовала m4, но абсолютно другим образом. Оглядываясь назад, я думаю, что мне нужно было изучать эти образцы более детально. В них содержалось много умных идей, в частности то, как обрабатывалось экранирование.
Начиная с sendmail 6 конфигурационные файлы m4 были полностью переписаны в более декларативном стиле и стали гораздо меньше. Они использовали гораздо больше возможностей процессора m4, что привело к некоторым проблемам, когда появление GNU m4 изменило некоторую часть семантики в небольшой степени.
Изначально замысел был в том, что конфигурации m4 должны были следовать правилу 80/20: они должны были быть простыми (отсюда 20% работы) и должны покрывать 80% случаев. Довольно быстро стало понятно, что этого не получилось, по двум причинам.
Не самой важной причиной оказалось то, что было довольно легко конфигурировать большую часть случаев, по крайней мере по началу. Но это стало гораздо сложнее по мере развития sendmail и мира, особенно при включении таких функций как TLS-шифрования и SMTP-аутентификации.
Важная причина была в том, что использовать обычные конфигурационные файлы было слишком сложным для большинства людей. По сути формат файлов .cf (необработанный) стал компонующим автокодом – в принципе редактируемым, но в реальности довольно запутанным. “Исходный код” представлял собой m4 скрипт в файлах .mc
Еще одним важным различием было то, что конфигурационный файл в необработанном виде представлял собой язык программирования. В нем были процедуры (наборы правил), вызовы подпрограмм, детализация параметров и циклы (но без опереторов перехода goto). Синтаксис был запутанным, но во многих случаях напоминал команды sed и awk, по крайней мере по сути.
Формат m4 был декларативным: хотя было возможно использовать низкоуровневый язык, на практике эти детали были скрыты от пользователя.
Неясно, было ли это правильным или неправильным решением. Я считал в то время (и до сих пор считаю), что при создании сложных систем можно сделать то, что называется специализированным языком (Domain Specific Language – DSL) для создания определенных разделов этой системы. Однако, использование такого языка как методологии конфигурирования для конечного пользователя по сути превращает все попытки конфигурирования этой системы в проблему программирования. В этом заключены большие возможности, но и цена этого также высока.
Продолжение статьи: Другие соображения