Создание системы голосования на Perl/CGI (исходники)

Аллан Педа

В данной статье представлен реальный пример (Web-приложение для голосования), в котором минимальное число внешних модулей, не применяются cookies на стороне клиента и используются преимущества CGI-атрибутов.

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

В конце концов, если под рукой имеется только молоток, то каждая проблема кажется гвоздем.

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

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

CGI: Простота против сложности

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

Что касается лежащей в основе архитектуры, Common Gateway Interface (CGI) был первым широко используемым подходом для расширения возможностей Web-серверов в области предоставления интерактивного содержимого. Разработчики часто говорят о превосходстве более новых стандартов, таких как JSP, .NET, mod_perl, PHP и ISAPI, и они правы, указывая на недостатки CGI. Но в случае с данным проектом CGI-сценарий, который считает голоса для нескольких сотен пользователей, едва ли образует крупномасштабное приложение, поскольку вся информация о бюллетенях может легко сохраняться в системной RAM Web-сервера. Это позволяет загружать в память всю справочную таблицу каждый раз, когда пользователь передает запрос на чтение или запись данных.

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

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

Для данного проекта использование CGI предлагает несколько преимуществ:

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

Однако помните, что из-за ограничений платформы, CGI-приложения (создающие новые процессы) на Win32-системах работают намного медленнее. Кроме того, Web-сервер Apache все же считается Linux™/UNIX®-приложением, хотя может отлично работать под Windows®. В разделе "Ресурсы" приводятся ссылки на информацию о других (не IIS) Web-серверах для систем Win32, а также на классическое описание исходной спецификации CGI на сайте National Center for Supercomputing Applications (NCSA).

Функциональный дизайн

Давайте перейдем к главной задаче этого маленького проекта - функциональности дизайна.

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

Возникает проблема многократного голосования. В принципе, можно подумать о том, как сделать невозможным множественное голосование с использованием нескольких адресов e-mail, но можно, также, ограничить подсчет голосов таким образом, чтобы только один голос был разрешен для одного адреса e-mail. Это верификационное письмо содержит ссылку на исходный CGI-сценарий, который позволяет сравнить эту ссылку с записанной в локальном DBM-файле. Если две ссылки совпадают, делается запись о бюллетене в таблице castBallot и голос учитывается. Если ссылки не совпадают, никакой записи не делается и голос считается не подтвержденным. Генерируется новое верификационное письмо с новой записью в базе данных. При этом перезаписываются все записи о черновых бюллетенях, связанные с данным почтовым адресом, эффективно начиная процесс с самого начала.

Если ссылки совпадают, избиратель может подтвердить черновик бюллетеня. В данный момент, если пользователь передумает, он может просто вернуться на Web-форму и создать новый черновик бюллетеня, который заменяет предыдущий. Такой дизайн представляет благоразумно защищенную систему; до тех пор, пока каждый голосующий пользователь имеет один и только один адрес электронной почты, существует обоснованная уверенность в том, что пользователь не проголосует дважды (я вернусь к этому вопросу позже).

Давайте рассмотрим систему подробно.

Подробности: Hash-ключи

Использование хешированных ключей для создания ассоциативных массивов в Perl позволяет оперативно разрабатывать комплексные структуры данных. Комбинируя эту возможность с возможностью сохранять эти (произвольно комбинированные) структуры в бинарный DBM-файл, можно разработать эквивалент крошечной системы управления базами данных. Отсутствующий компонент, позволяющий все это сделать, предоставляется модулями MLDBM и MLDBM::Sync.

Модуль MLDBM позволяет плавно сохранить в локальный файл комплексные Perl хеш-значения. Модуль MLDBM::Sync предоставляет возможность надежно заблокировать этот файл, используя методы $sync->Lock и $sync->ReadLock. После загрузки или сохранения интересующей нас структуры последующий вызов метода UnLock() сбрасывает I/O и освобождает переменную (дополнительная информация о модуле MLDBM::Sync приведена в документации по Perl - man 3 MLDBM::Sync).

По существу, логика последовательности действий проста, что показано в листинге 1.

Листинг 1. Псевдокод логической последовательности

                
1  unless( defined( $q->param( $vparm ) )){
2      # Отобразить исходный экран для голосования
3      # выбрать кандидата
4      $ballotBox->printForm( $q );
5  } else {
6      # если голос учтен, не передавать верификационное письмо
7      if( $castBallot->voteIsTallied( $q ) ){
8           print "Your vote has already been recorded"
9      } else {
10          #
11          # голос еще не учтен, проверить наличие черновика бюллетеня в файле
12          # и перенести draftBallot в объект castBallot 
13          #
14          if( $draftBallot->exactMatch( $q ) ){
15              # вбросить бюллетень
16              print $q->h2('Thank you, your vote has been recorded.');
17              # и проголосовать в файле cast ballot db 
18              $castBallot->tallyVote( $q );
19              # просуммировать голоса
20              $ballotBox->addVotes( $castBallot );
21              $cc_msg->send();
22          } elsif ( $draftBallot->voter_is_okay( $voter_email )){
23              # Послать e-mail для подтверждения голосования
24              $mime_msg->send()
25          } else {
26              print 'Only University ballots are acceptable';
27          }
28      }
29  }

После разработки условного алгоритма оставалось создать объекты, которые соответствовали бы этой последовательности действий. Как я уже упоминал, необходимые структуры hash-данных извлекались и обновлялись при помощи связанных переменных и блокировки MLDBM-файла. Используемые объекты были больше похожи на интеллектуальные структуры, чем на полноценные объекты; данные передавались между ними способом, который распараллеливал переход бюллетеня из состояния черновика в окончательно подсчитанный бюллетень.

Другими словами, список бюллетеней использовался для создания DraftBallot, который, в свою очередь, использовался для создания классов CastBallot и BallotBox. Таким образом, обеспечивалась минимальная взаимосвязь с главной CGI-программой.

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

Подробности: E-mail

Разрешение пользователям посылать e-mail с вашего Web-сервера является рискованным ходом, поскольку спаммеры потенциально могут использовать ваш хост для передачи незатребованных писем. Для минимизации такой возможности сценарий всегда выполняет проверку для определения того, передается ли письмо по допустимому адресу. Вы можете усилить защищенность системы, изменяя метод проверки voter_is_okay() в классе DraftBallot и сверяясь со списком допустимых адресов e-mail. В сущности, это потребовало бы предварительной регистрации пользователей для голосования.

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

Подробности: Несекретные бюллетени

Вызов метода $castBallot->dumpHTMLentrys() возвращает подробный отчет, кто за кого голосовал. На практике я закомментировал бы этот вызов, запланировав Web-сервер на отключение после завершения выборов, используя Linux-команду at.

При отключенном сервере можно раскомментировать этот раздел и перезапустить Web-сервер, временно установив его на прослушивание только адреса localhost. Затем полные результаты можно возвратить любому, кто переходит по предварительно переданной ссылке, которую можно взять через копию, переданную на выделенную для этого свободную учетную запись e-mail.

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

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

Этот идентификатор был основан на идентификаторе процесса операционной системы (process identifier - PID), выполняющегося сценария. Он объединялся со случайным числом, для того чтобы затруднить предсказание URL для подтверждения черновика бюллетеня. Я беспокоился об этом, потому что индивидуум со злыми намерениями мог бы разобрать на части видимые URL-шаблоны, чтобы создать фальшивые бюллетени для подтверждения. Это одна из частей кода, которая не транслировалась напрямую в версию mod_perl, поскольку она полагается на использование PID выполняющегося экземпляра Perl, который объединяется со случайными цифрами. Если форма генерируется из повторно используемого модуля mod_perl, PID-номер не обязательно будет меняться между активизациями модуля.

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

Подробности: Схема файлов

Требуется три типа каталогов на Web-сервере:

  • Записываемый каталог для сохранения передаваемых пользователем данных.
  • Область для запуска из нее CGI.
  • Область статических данных (например, CSS, изображений и, возможно, файла, содержащего подробные инструкции).

Также обратите внимание на то, что права доступа вероятнее всего должны быть скорректированы и Web-сервер мог выполнять запись в каталог для DBM-файлов.

В листинге 2 приведен процесс создания некоторых типичных каталогов на Web-сервере.

Листинг 2. Настройка каталогов на Web-сервере

                
  $ id   uid=500(allan) gid=500(allan) groups=10(wheel),48(apache),500(allan)
    $ sudo mkdir /var/www/db /var/www/javascript/ /var/www/css/
    $ sudo chmod 2775 /var/www/db
    $ sudo chmod 2755 /var/www/javascript/ /var/www/css/
    $ sudo chown apache.apache /var/www/db/

Собственно говоря, абсолютно необходимы только каталоги cgi-bin (/var/www/cgi-bin) и DBM (/var/www/db), поскольку они хранят исполняемые сценарии и данные о голосовании соответственно. Схема, приведенная в листинге 1, специфична для Linux, а имена пользователя и группы процесса Web-сервера могут быть другими. Существенным является то, что есть несколько компонентов, которые нужно поместить в корректную область файловой системы, чтобы они были доступны для Web-сервера. После копирования файлов поддержки в их каталоги обновите все псевдонимы в конфигурационных файлах Web-сервера (например, httpd.conf).

После создания каталогов по примеру листинга 2 скопируйте файлы из ZIP-архива в аналогичные подкаталоги вашей системы. Самое важное: файлы ballot, DraftBallot.pm, BallotBox.pm и CastBallot.pm должны находиться в каталоге cgi-bin. Необходимы только три нестандартных Perl-модуля. Процесс их установки представлен в листинге 3 (более подробно - в файлах README модуля).

Листинг 3. Установка Perl-модулей

                
  $ sudo perl -MCPAN -e 'install MLDBM'
  $ sudo perl -MCPAN -e 'install MLDBM::Sync'
  $ sudo perl -MCPAN -e 'install MIME::Lite'

Подробности: Статический DNS против динамического DNS

Хотя имелась возможность установить этот сервис на сайте с назначенным доменом и статическим IP-адресом, я чувствовал, что динамический DNS предложил бы определенные преимущества в плане защищенности. Обычно сервер не доступен из Web вовсе без статического IP-адреса, но динамический DNS-сервис позволяет мне установить временно разрешимое имя машины под другим доменом верхнего уровня. Это позволяет мне быстро появляться и исчезать из Интернета, минимизируя возможность встречи со злодеями. А лучше всего то, что данный сервис бесплатен.

Также стоит отметить, что может быть желательным настроить сервер на прослушивание порта с нестандартным номером (например, 8000), поскольку многие ISP блокируют входящие запросы на порту 80. Потом клиента (избирателя) можно было бы направить на сервер для голосования только через ссылку с хорошо известного статического адреса (например, Web-страницы школы). После завершения голосования сервер с Web-приложением мог бы быть полностью удален из Web удаленно без его остановки или перенастройки. Отсутствовали бы какие-либо уязвимости, влияющие на ссылающуюся страницу,которая может администрироваться кем-то другим (ссылки на дополнительную информацию по использованию динамического DNS-сервиса приведены в разделе "Ресурсы").

Подробности: Вреден ли GET?

Браузеры могут сохранять состояние, передавая данные на страницы, используя методы GET и POST, а также через куки, содержащиеся в передаваемых на сервер заголовках. Для того чтобы подтвердить, что бюллетень был передан от реальной личности (или, по крайней мере, с активного адреса e-mail), черновики бюллетеней передаются на e-mail для подтверждения. Кроме того, могли бы использоваться сообщения cc: или bcc:. Как я уже упоминал, самым прямым способом сделать это - передать по HTTP структурированную GET-ссылку избирателю. Тем не менее, некоторые авторы утверждают, что GET-запросы, обновляющие записи - это плохая практика. Но в данном случае при последующих нажатиях ссылки просто будут принимать обновления текущего количества голосов для каждого кандидата, поэтому никакого вреда не предвидится.

Другие возможные улучшения

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

Для фильтрации входящих данных я установил переменные $CGI::DISABLE_UPLOADS и $CGI::POST_MAX в очень строгие значения. Дополнительно я рекомендую следующее:

  • Удаляйте из всех входящих переменных все неожидаемые символы и укорачивайте их до разумных пределов.
  • Внутри сценария хранится много данных времени исполнения. Положительной стороной такой практики заключается в меньшем количестве файлов для распространения и установки прав доступа. Отрицательная сторона - пользователи могут не захотеть изменять код, и код станет менее понятным. Возможным компромиссом может быть использование таких клуджей (kludges) как DATA для сохранения данных в конце сценария.
  • Блокирование файлов - это очень коварная операция, изобилующая условиями для состязаний. Кажется, в каждую рекомендацию о корректном способе блокировки файлов, которую я находил, позже вносились исправления. Я попытался минимизировать время, когда файлы находятся в открытом состоянии, и использовал механизм, предоставляемый для MLDBM-модуля.
  • Perl-модули не помещаются в отдельное место, отличное от CGI, поэтому они теоретически могут быть выполнены из каталога cgi-bin. Рекомендуется эти модули не настраивать как исполняемые.
  • PHP практически вездесущ на платформах Linux, поэтому я подумал бы о портировании этого сценария на PHP, если возникнет необходимость его переделать. Однако я не уверен в том, что есть PHP-эквивалент MLDBM-модулю.
  • Схема формы для голосования некоторыми может посчитаться нечестной, поскольку первый кандидат выделяется по умолчанию.
  • Я не использовал perldoc. А должен был.

Заключение

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=7445