Наши партнеры








Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

Технологии Интернет
Лабораторный практикум

Тема 6. CGI


Литература и ссылки

Используемое ПО:

  • Unix (Solaris 2.x),
  • Russian Apache (Apache 1.3.6PL28.16) (распространяется свободно).
  • Язык программирования Perl v. 5.005_03, библиотека CGI.pm и сопутствующие библиотеки (распространяется свободно).
  • Модуль mod_perl v. 1.21 (распространяется свободно).

См. архив на Уране.



Интерфейс CGI

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

CGI определяет каким образом данные, предоставленные клиентом в запросе, передаются программе, как программа возвращает сгенерированный HTML-контент серверу, и какие переменные окружения устанавливаются сервером при запуске программы. Переменные окружения несут дополнительную информацию о сервере и запросе (например, тип сервера, IP-адрес клиента и др.).

Данные из заполненной клиентом HTML-формы могут передаваться на сервер двумя методами: GET и POST, это определяется параметром method соответствующего тэга <form method=... action=...>. В первом случае (GET) данные присоединяются после вопросительного знака в конец URL, указанной в параметре action, во втором случае - передаются в теле запроса - в секции, предназначенной для данных (следует после всех заголовокв и пустой строки). В обоих случаях данные кодируются одинаково - см. след. пункт.

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

CGI-программа выдает содержимое ответа (как правило, HTML-контент) на свой стандартный вывод, который перехватывается веб-сервером с тем, чтобы отослать эти данные клиенту. Предварительно CGI-программа должна напечатать заголовок "Content-Type" и отделить его от данных пустой строкой. Например, вывод CGI-программы, генерирующей HTML, может выглядеть следующим образом:

Content-Type: text/html

<HTML>
  <BODY>
    <H1>Hello, world</H1>
  </BODY>	
<HTML>  

Конфигурирование сервера Apache для исполнения CGI-скриптов

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


ScriptAlias /виртуальный/путь/ /путь/к/каталогу/
ScriptAlias /cgi-bin/ /usr/local/www/cgi-bin/

Это означает, что для обработки запроса URL вида http://your.server.com/cgi-bin/dir/script будет взят не файл script из каталога DocumentRoot/cgi-bin/dir/, а запущена программа /usr/local/www/cgi-bin/dir/script.

Для смешанного хранения файлов, подлежащих просмотру, и CGI-скриптов в одном каталоге внутри дерева DocumentRoot следует присвоить CGI-скриптам одинаковые расширения (например, ".cgi") и указать серверу, что интерпретировать такие файлы следует как CGI-скрипты:


AddHandler cgi-script .cgi

Директива AddHandler может быть использована в любом контексте конфигурации Apache.

Структура URL и кодирование данных запроса

Для работы CGI-программ важное значение имеют части URL, называемые PATH_INFO и QUERY_STRING. Рассмотрим запрос с URL вида

http://my.server.com/cgi-bin/dir/prog/a/b?A=1&B=qwerty

Используя директиву ScriptAlias, приведенную в предыдущем пункте, сервер определяет что произошло обращение к CGI-программе и для поиска этой программы заменяет начальное /cgi-bin/ на /usr/local/www/cgi-bin/. Следуя запрошенному URL, сервер обнаруживает в этом каталоге подкаталог dir, однако подкаталога prog в каталоге /usr/local/www/cgi-bin/dir не обнаружено. В таком случае сервер предполагает, что prog - имя CGI-программы, подлежащей выполнению. Если программа /usr/local/www/cgi-bin/dir/prog не найдена или не может быть исполнена, сервер возвращает клиенту ошибку 403, 404 или 500. В противном случае программа prog запускается, а оставшаяся часть пути из URL - /a/b - передается программе prog в переменной окружения PATH_INFO. Таким способом можно передать в CGI-программу дополнительные параметры.

Все, что находится после вопросительного знака - A=1&B=qwerty - передается программе prog в переменной окружения QUERY_STRING. Это могут быть данные из заполненной пользователем формы, отправленные на сервер методом GET, либо какая-то другая информация (сервер не делает никаких предположений об интерпретации данных в QUERY_STRING, это задача вызываемой программы).

Данные из полей формы, заполненной пользователем - независимо от метода (POST или GET), которым они пересылаются на сервер - кодируются следующим образом:

имя_поля=значение_поля&имя_поля=значение_поля...

Пары имя-значение разделяются амперсандом. Алфавитно-цифровые символы и некоторые знаки препинания, не имеющие специального значения (тире, подчеркивание) передаются как есть. Остальные символы кодируются в виде "%NM", где NM - двузначный шестнадцатеричный код символа. Пробел может передаваться как "%20" или как символ "+". Кириллические символы также должны кодироваться указанным способом. Кодировка производится броузером при отправке полей заполненной формы.

Например:

http://my.server.com/cgi-bin/dir/prog?birthday=11%2F05%2F73&name=John+Smith
означает, что в поле birthday пользователь внес "11/05/73", а в поле name - "John Smith".

Декодирование данных формы является задачей CGI-программы.

При пересылке данных формы, закодированных вышеописанным способом, методом POST клиент должен установить заголовок запроса Content-Type следующим образом:


Content-Type: application/x-www-form-urlencoded

Переменные окружения CGI

При запуске CGI-скрипта веб-сервер устанавливает дополнительные переменные окружения:

Переменная Значение
AUTH_TYPE

Метод аутентифицирования, использованный для опознания пользователя. См. также REMOTE_USER и REMOTE_IDENT.

CONTENT_LENGTH

Длина данных запроса в байтах, переданных CGI-скрипту через стандартный ввод.

CONTENT_TYPE

MIME-тип данных запроса.

DOCUMENT_ROOT

Корневой каталог дерева документов веб-сервера (определяется директивой DocumentRoot).

GATEWAY_INTERFACE

Используемая версия CGI.

HTTP_ACCEPT

Список MIME-типов данных, которые клиент может принять.

HTTP_FROM

Адрес электронной почты пользователя, сделавшего запрос (многие броузеры не передают такие данные).

HTTP_REFERER

URL документа, в котором находилась ссылка, вызвавшая настоящий запрос.

HTTP_USER_AGENT

Броузер клиента.

PATH_INFO PATH_INFO (если есть) - см. выше "Структура URL и кодирование данных запроса"
PATH_TRANSLATED

PATH_INFO, преобразованное в полный путь в файловой системе сервера (PATH_INFO, добавленное к DOCUMENT_ROOT).

QUERY_STRING

Данные запроса, переданные в составе URL вслед за вопросительным знаком - см. выше "Структура URL и кодирование данных запроса".

REMOTE_ADDR

IP-адрес клиента.

REMOTE_HOST Имя DNS клиента.
REMOTE_USER Аутентифицированное имя пользователя.
REQUEST_METHOD

Метод запроса (GET, POST, HEAD и т.д.).

SCRIPT_NAME Виртуальный путь (например, /cgi-bin/program.pl) к исполняемому CGI-скрипту.
SERVER_NAME DNS-имя сервера или, при невозможности определить имя, его IP-адрес.
SERVER_PORT

Номер порта сервера.

SERVER_PROTOCOL Имя и версия протокола, через который был сделан запрос (например, HTTP/1.1).
SERVER_SOFTWARE

Тип и номер версии ПО веб-сервера.

С Apache поставляется стандартный тестовый скрипт test-cgi, выводящий занчения переменных окружения CGI.

Cookies и другие методы сохранения состояния

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

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

Существует несколько методов сохранения состояния:

  1. cookies - сохранение на компьютере клиента,
  2. скрытые поля - сохранение внутри формы, посылаемой клиенту,
  3. сохранение в файле какого-либо формата на сервере,
  4. сохранение в параллельно работающей базе данных.

Два последних метода реализуют сохранение состояния на стороне сервера.

База данных

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

Также существует решение в виде демона, который запускается параллельно с http-сервером, и сохраняет требуемую информацию в своей оперативной памяти в виде переменная=значение. Для записи или извлечения данных скрипт соединяется с демоном по заранее оговоренному порту TCP или UDP, идентифицирует себя и использует набор простых команд типа "save name=value" и "extract name" (возвращается value).

Интересно, что несмотря на сложность реализации, такое решение (или использование СУБД с возможностью доступа по сети) позволяет разделять данные между скриптами работающими на различных серверах (если реализуется какая-то сложная распределенная интерактивная веб-система), при этом не вовлекается сохранение данных на стороне пользователя.

Файл

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

Сохранение состояния на стороне пользователя

Сохранение данных состояния на стороне пользователя (cookies и, технически, скрытые поля) существенный недостаток: пользователь имеет полный доступ к сохраняемым данным и может их несанкционированно изменить (например, прочитать правильный ответ теста или изменить идентификатор пользователя). Достоинством является простая реализация.

Cookies

Cookies - это данные вида имя=значение, которые, будучи получены от сервера, сохраняются броузером на диске пользователя для их возврата серверу при последующих запросах к этому или другому URL. Поскольку данные сохраняются на диске, они могут быть использованы после перезапуска броузера.

Сервер передает cookie через специальное поле заголовка HTTP-ответа "Set-Cookie". Броузер возвращает cookie также через специальное поле в загловке HTTP-запроса - "Cookie". На стороне сервера cookie формируется, как правило, скриптом, который просто выводит в STDOUT соответствующий заголовок. Передача данных, полученных через cookie, от броузера в скрипт производится сервером через установку переменной окружения HTTP_COOKIE, которая доступна внутри скрипта и содержит пары имя=значение, которые броузер передал внутри поля "Cookie" в заголовке своего запроса.

Формат поля Set-Cookie (HTTP-ответ)

Set-Cookie: имя=значение; Max-Age=секунды; Comment=текстовый_комментарий; Path=URI_или_часть_URI; Domain=домен_сервера; Secure ; Version=1

Все элементы, кроме имя=значение и Version, не являются обязательными. В заголовке одного ответа сервера может содержаться несколько полей Set-Cookie.

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

Max-Age=секунды
устанавливает срок годности данных (в секундах с момента получения cookie); по умолчанию - до окончания работы данного проуесса броузера.

Comment=текстовый_комментарий
комментарий сервера по поводу предназначения cookie; предполагается, что пользователь может отказаться работать с этим cookie, если комментарий ему не понравится.

Domain=домен_сервера
домен, для которого действительно данное cookie (броузер должен возвращать cookie при обращении ко всем серверам данного домена, с учетом параметра Path [см. ниже]); домен должен начинаться с точки; данный сервер должен находиться в этом домене. Если параметр Domain не указан - возвращать cookie только данному серверу.

Path=URI_или_часть_URI
путь от корня дерева документов сервера (URI); броузер должен возвращать cookie при обращении к данному URI и ко всем URI, начинающимся с данного; по умолчанию - URI, при запросе которого было сгенерировано cookie, минус имя файла.

Пример: при обращении на "http://s.vvsu.ru/a/b/c" сервер выдал ответ с установленным полем в заголовке:

SetCookie: X=5; Version=1
Это значит, что cookie должно возвращаться броузером при обращении на все URL вида "http://s.vvsu.ru/a/b/какое-то_имя_файла".
Если же SetCookie в ответе сервера выглядит вот так:
SetCookie: X=5; Domain=.vvsu.ru; Path=/a/; Version=1
то броузер должен присоединять это cookie ко всем запросам URL вида: "http://имя_без_точки.vvsu.ru/a/b/некий_путь_или_никакого".

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

Формат поля Cookie (HTTP-запрос)

Cookie: имя=значение; Path=URI_или_часть_URI; Domain=домен_сервера; Version=1

Параметры Path и Domain включаются только если они были установлены в заголовке Set-Cookie. Если несколько cookie удовлетворяют параметру Path, то они указываются в одном заголовке Cookie друг за другом (через точку с запятой) в следующем порядке: первыми передаются cookie с более длинным параметром Path. Порядок следования при равенстве параметров Path не определяется.

Скрытые поля

Скрытое поле создается внутри формы с помощью тега
<input type=hidden name=name1 value=value1>

Когда броузер получает документ с этой формой, содержимое полей типа "hidden" не отображается и пользователь не знает об их существовании (если только не посмотрит в HTML-текст присланного документа). После того, как пользователь отправляет форму на сервер, пара "name1=value1" присоединяется к данным формы, которые будут обработаны вновь запущенным скриптом. Таким образом скрипт может получить данные о предыстории своей работы с пользователем. Например, при электронном шоппинге в скрытых полях может сожержаться список товаров, выбранных для покупки в других отделах, которые пользователь уже посетил в данном сеансе работы.

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

Server Side Includes

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

Все директивы вставляются внутрь тэгов HTML-комментариев, что позволяет клиенту, в случае, если сервер не поддерживает SSI, игнорировать эти директивы. Директивы имеют следующий формат:


<!--#директива параметр(ы)="значение"-->

Ниже следует список основных директив SSI и их параметров.

echo
Подставляет в документ значение указанной в качестве параметра переменной окружения (см. также список CGI-переменных) или специальной переменной SSI (см. ниже):
        <H1>Вы пришли на сервер, находящийся по адресу
        <!--#echo var="SERVER_NAME"-->...</H1>

include
Вставляет в документ текст другого файла. Параметры: file - указывает путь к вставляемому файлу относительно расположения данного документа; virtual - указывает виртуальный путь (как он указывался бы в URL) к вставляемому файлу.
       <!--#include file="stuff.html"-->
       <!--#include virtual="/personal/stuff.html"-->
Эта директива очень удобна для создания стандартных шапок и подвалов веб-страниц.

fsize
Вставляет размер указанного в параметре файла (путь к файлу виртуальный):
        Размер файла archive.zip - 
        <!--#fsize file="/archive.zip"--> bytes.

flastmod
Вставляет в документ дату и время последней модификации указанного в параметре файла (путь к файлу виртуальный):
        Дата последнего изменения: 
        <!--#flastmod file="/this_dir/this_file.html"-->bytes.
Формат вывода даты и времени может быть специфицирован параметром timefmt директивы config.

exec
Выполняет внешнюю программу, указанную параметром, и вставляет вывод этой программы в документ. Параметры: cmd - выполняемая программа является неким обычным приложением; cgi - выполняемая программа является CGI-скриптом
       <!--#exec cmd="/bin/finger $REMOTE_USER@$REMOTE_HOST"-->
        На эту страницу заходили
        <!--#exec cgi="/cgi-bin/counter.pl"--> 
        раз.
		
В первом примере используется подстановка значений переменных окружения (см. CGI-переменные).

config
Модифицирует различные аспекты работы SSI. Параметры:
  • errmsg - сообщение об ошибке, выдаваемое при невозможности выполнить директиву:
              <!--#config errmsg="Файл не найден"-->
    
  • sizefmt - устанавливает формат вывода размера файла (подставляемого директивой fsize; значения: bytes - выводит в байтах; abbrev - округляет до целого числа килобайт.
            <!--#config sizefmt="abbrev"-->
    		Размер файла archive.zip - примерно 
            <!--#fsize file="/archive.zip"--> bytes.
    
  • timefmt - устанавливает формат вывода даты и времени, подробнее см. здесь.

Специальные переменные SSI

Ниже приведены переменные SSI, которые можно использовать в директиве echo в дополнение к переменным CGI.

DOCUMENT_NAME
Имя данного документа. Например:
         Вы читаете файл под названием:
         <!--#echo var="DOCUMENT_NAME"-->

DOCUMENT_URL
Виртуальный путь к данному документу. Например:
        Ссылка на этот документ:
        <!--#echo var="DOCUMENT_URL"-->

QUERY_STRING_UNESCAPED
Декодированные данные из QUERY_STRING (см "Структура URL и кодирование данных запроса"), при этом все метасимволы шелла экранированы обратным слэшем (\).

DATE_LOCAL
Текущие дата и время по местному времени. Например:
        Сейчас <!--#echo var="DATE_LOCAL"-->

DATE_GMT
Текущие дата и время по Гринвичу.

LAST_MODIFIED
Дата и время последней модификации данного документа. Например:
       Этот файл был последний раз изменен 
       <!--#echo var="LAST_MODIFIED"-->

Задание

Написать CGI-скрипт для игры в виселицу (угадывание слова по буквам).

Правила игры

Сервер загадывает слово из словаря и показывает его пользователю в замаскированном виде (буквы заменены звездочками). Пользователь имеет некоторое число попыток; во время каждой попытки он может угадать одну букву. Если пользователь правильно угадывает букву или называет букву, которую он уже использовал, попытка не засчитывается. Иначе число попыток уменьшается на единицу.

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

Игра прекращается, если число попыток стало равным нулю (пользователь проиграл) или если угаданы все буквы в слове (пользователь выиграл).

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

Реализация

Слова выбираются случайным образом из заданного текстового файла.

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

При первом обращении к скрипту выдается заставка и регистрационная форма вида:


Ваше имя:


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

Например, форма 6-й попытки при угадывании слова ПАРОВОЗ может выглядеть так:


Игрок: Сидоров

Слово: ПА*О*О*

Использованные буквы: АОЕПС

Осталось попыток: 5 из 10


При завершении игры сервер выводит соответствующее сообщение и предлагает начать следующую игру.

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

Программа скрипта может быть написана на любом языке программирования под Unix (немедленно доступные в классе языки: С, С++, Перл, и - для любителей острых ощущений - shell/awk).

Обсуждение

При написании скрипта следует непрерывно помнить следующее основное правило CGI-программиста:

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

В связи с вышесказанным требуется решить две проблемы при обработке каждого вновь поступившего запроса (или, что тоже, при каждом запуске программы):

  1. Идентификация пользователя
  2. Получение информации о состоянии игры (слово, использованные буквы, число попыток)

Если такую информацию получить не удается, считается, что это первое обращение пользователя и ему предлагается регистрационная форма.

О методах сохранения состояния в CGI-программировании см. соответствующий раздел выше.

В результате анализа информации о состоянии игры и содержимого заполненной пользователем формы (если таковое имеется в поступившем запросе) программа разветвляется для формирования одного из следующих ответов (HTML-текстов):

  • регистрационная форма,
  • форма попытки,
  • сообщение об окончании игры.
После выдачи сформированного HTML-текста и сохранения (каким-либо способом) нового состояния, программа прекращает свою работу.

Дополнительное задание

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

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




Следующая тема - Вопросы безопасности в Интернет

Курс "Технологии Интернет"
Кафедра КТС
Максим Мамаев