Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых
[Назад] [Содержание] [Дальше]

Создание Web-приложений: Практическое руководство

© Александр Фролов, Григорий Фролов
М.: Русская Редакция, 2001, 1040 стр.

11. Расширения CGI и ISAPI сервера Web

11. Расширения CGI и ISAPI сервера Web.. 1

Программы CGI и базы данных.. 2

Немного о формах HTML. 2

Передача данных программе CGI 3

Метод GET.. 4

Метод POST.. 4

Выбор между GET и POST.. 5

Передача ответа из программы CGI 5

Переменные среды для программы CGI 5

Примеры программ CGI 7

Программа CGIHELLO.. 7

Программа CONTROLS. 8

Программа AREF.. 13

Доступ к базе данных из CGI 14

Создание приложений ISAPI 19

Принципы работы и структура расширения ISAPI 19

Вызов расширения ISAPI сервером Web. 20

Функция GetExtensionVersion. 20

Функция HttpExtensionProc. 21

Получение данных расширением ISAPI 23

Функция GetServerVariable. 23

Функция ReadClient 25

Отправка данных расширением ISAPI 26

Функция WriteCilent 26

Функция ServerSupportFunction. 26

Приложение ISHELLO.. 28

Вызов функций ODBC из ISAPI 30

Обращение к базе данных в отдельном потоке. 35

Загрузка файлов на сервер Web через браузер. 36

Исходные тексты приложения FILEUPL. 40

Загрузка файлов в Интернет-магазине ITBOOK.RU.. 45

Инициализация модуля. 46

Функция HttpExtensionProc. 48

Функция parseData. 49

Форма выбора загружаемого файла. 53

Perl и отправка данных формы HTML по электронной почте. 54

Форма для отправки почтового сообщения. 54

Исходный текст программы urgent_mail.pl 56

Функция win2koi 56

Функция send_mail 57

Обработка формы HTML. 58

 

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

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

Если процессинговая компания предоставляет в Ваше распоряжение интерфейсный модуль в виде объекта компонентной модели Microsoft COM, к нему можно обращаться непосредственно из серверного сценария, расположенного на страницах ASP. Однако чаще всего модуль поставляется в виде библиотеки динамической загрузки DLL, экспортирующей набор функций. Такие функции легко вызываются из программ, составленных на языке C, но они недоступны программам серверного сценария JScript или VB Script, встроенного в страницы ASP.

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

В этой главе мы расскажем Вам о том, как создать собственные расширения сервера Web, созданного на базе Microsoft Internet Information Server версии 4.0, в виде программ CGI и приложений ISAPI.

Так как наша книга посвящена базам данных, то мы рассмотрим вопросы интеграции приложений CGI и ISAPI с сервером базы данных Microsoft SQL Server.

Программы CGI и базы данных

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

Что кроется за аббревиатурой CGI?

CGI — это стандартный шлюзовой интерфейс (Common Gateway Interface) для запуска внешних программ под управлением сервера Web. Соответственно приложениями CGI называются программы, которые, пользуясь этим интерфейсом, получают через протокол HTTP информацию от удаленного пользователя, обрабатывают ее и возвращают результат обработки обратно в виде ссылки на уже существующий документ HTML или другой объект (например, графическое изображение) или в виде документа HTML, созданного динамически.

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

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

Когда пользователь заполняет форму и щелкает указанную кнопку, данные передаются приложению CGI, путь к которому задается в заголовке формы. Это приложение получает через протокол HTTP данные из полей формы в виде «имя поля/значение».

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

Так как приложение CGI представляет собой не что иное, как программу, Вы должны оттранслировать ее для той операционной системы, под управлением которой работает Ваш сервер Web. В нашем случае необходимо создать консольное приложение Win32 (не путайте его с консольной программой MS-DOS — это разные вещи).

Заметим, что программы CGI, создаваемые для операционной системы Unix, часто составляют с применением интерпретируемого языка Perl. Однако наша книга ориентирована на применение технологий Microsoft, поэтому мы не будем касаться этой темы. Заметим только, что для платформы Microsoft Windows NT также существуют реализации интерпретатора языка Perl.

Немного о формах HTML

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

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

Для того чтобы сделать форму в документе HTML, следует воспользоваться тегом <FORM>. Этот тег применяется в паре с тегом </FORM>, завершающим описание формы. Между тегами <FORM> и </FORM> находятся описания элементов управления в виде таких тегов, как <INPUT>, <TEXTAREA> и <SELECT> с соответствующими параметрами.

Вот пример определения простейшей формы:

<FORM METHOD=GET ACTION="http://www.myserver.ru/cgi/form.exe">
  <TABLE>
  <TR>
     <TD><INPUT TYPE=text NAME="text1" VALUE="Sample of text1"></TD>
  </TR>
  <TR>
     <TD><INPUT TYPE=text NAME="text2" VALUE="Sample of text2"></TD>
  </TR>
  <TR>
     <INPUT TYPE=submit VALUE="Send">
  </TR>
  </TABLE>
</FORM>

Здесь элементы управления размещаются в таблице, состоящей из одного столбца и трех строк. В верхних двух строках мы расположили поля ввода и редактирования текста, а в последней строке — кнопку Send.

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

Посредством параметра METHOD Вы можете выбрать один из двух методов передачи данных из формы серверу Web.

Если значение этого параметра равно GET (как в нашем примере), программа CGI, указанная в параметре ACTION, получит данные из формы через переменную среды с именем QUERY_STRING. В том случае, когда значение параметра METHOD равно POST, программа CGI получит данные из формы через стандартный поток ввода. Позже мы рассмотрим различия этих методов более подробно.

И наконец, третий параметр ENCTYPE, используется очень редко и только для метода POST. Он позволяет указать тип передаваемых данных и по умолчанию имеет значение application/x-www-form-urlencoded.

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

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

Передача данных программе CGI

Когда пользователь заполняет форму и щелкает кнопку типа SUBMIT либо графическую кнопку (которая выполняет аналогичную функцию), данные из полей формы вместе с именами этих полей передаются браузером серверу Web. Тот, в свою очередь, анализирует эти данные и запускает соответствующую программу CGI, путь к файлу которой указан в теге <FORM>.

Перед запуском программы CGI сервер Web выбирает в зависимости от значения параметра METHOD тега <FORM> один из двух способов передачи полученных данных для обработки — метод GET или POST.

Метод GET

Метод GET предполагает передачу данных программе CGI через переменные среды (environment variable). Это те самые переменные среды, которые устанавливаются в операционной системе MS-DOS командой SET.

Сервер Web создает для программы CGI довольно много переменных среды. Имена и назначение всех этих переменных Вы узнаете позже, а пока мы расскажем только о самых необходимых.

Прежде всего, метод GET предполагает использование переменной среды с именем QUERY_STRING. Именно сюда попадают данные из полей формы. Эти данные находятся в следующем формате:

“Имя1=Значение1&Имя2=Значение2&Имя3=Значение3”

Здесь в качестве имен используются значения параметров NAME, задающих имена полей формы. Вместо значений подставляются данные из соответствующих полей.

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

Адрес строки любой переменной среды в программе, составленной на C, легко получить с помощью функции getenv:

char * szQueryString;
szQueryString = getenv("QUERY_STRING");

Заметим, что если Вы собираетесь модифицировать строку переменной среды, то ее следует скопировать во внутренний буфер. Операционная система сервера Web иногда не допускает прямого редактирования блока памяти, содержащего переменные среды.

Строка, передаваемая в переменной среды QUERY_STRING, закодирована с использованием так называемой кодировки URL. В этом случае все символы пробелов заменяют символами «+». Кроме того, для представления кодов управляющих и некоторых других символов используется последовательность символов вида «%xx», где «xx» — это шестнадцатеричный код символа в виде двух символов ASCII. В нашей книге мы приведем исходные тексты функций, предназначенных для перекодирования информации, полученной из форм.

Метод POST

При использовании метода POST программа CGI получает данные из формы через стандартный поток ввода stdin. Если программа CGI составлена на языке программирования C, то для получения данных она может воспользоваться такими функциями, как fread или scanf.

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

Ниже мы приводим фрагмент кода для определения размера информации для ввода через стандартный поток stdin:

int Size;
Size = atoi(getenv("CONTENT_LENGTH"));

Входные данные можно затем получить, например, следующим образом:

char szBuf[8196];
fread(szBuf, Size, 1, stdin);

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

Если в теге <FORM> не указан параметр ENCTYPE (тип MIME передаваемых данных) или этот параметр имеет значение application/x-www-form-urlencoded, данные, полученные через стандартный поток ввода, закодированы в URL. Перед использованием Вы должны их перекодировать соответствующим образом.

Выбор между GET и POST

Метод GET обычно применяют для обработки небольших форм, так как браузеры и операционные системы накладывают ограничения на размер данных, передаваемых через переменную среды QUERY_STRING.

В этом отношении метод POST предпочтительнее, так как не ограничивает размер передаваемых данных.

Передача ответа из программы CGI

Вне зависимости от метода передачи данных (GET или POST) результат своей работы программа CGI направляет в стандартный поток вывода stdout. Если программа составлена на языке программирования C, для записи результат работы она может воспользоваться, например, функцией printf или fwrite.

Чаще всего программы CGI применяют для создания динамических документов HTML на основе данных, полученных из формы. В этом случае первой строкой, которую необходимо вывести в стандартный поток вывода stdout, будет следующая строка заголовка HTTP:

Content-type: text/html

Сразу за ней необходимо вывести еще одну пустую строку, которая послужит разделителем между заголовком HTTP и данными документа HTML.

Ниже мы привели фрагмент кода, в котором программа CGI динамически формирует документ HTML и выводит его в стандартный поток вывода:

printf("Content-type: text/html\n\n");
printf("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//EN\">");
printf("<HTML><HEAD><TITLE>XYZ Incorporation</TITLE></HEAD>”
  ”<BODY BGCOLOR=#FFFFFF>");
printf("<H1>
Результаты обработки формы</H1>");
. . .
printf("</BODY></HTML>");

Обратите внимание на символы перевода строки «\n\n». Первый из них закрывает строку заголовка HTTP, а второй нужен для создания пустой разделительной строки.

Переменные среды для программы CGI

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

Рассмотрим по отдельности назначение переменных среды. Заметим, что набор переменных, создаваемых при запуске программы CGI, зависит от конкретной реализации сервера Web.

 

·         AUTH_TYPE

Технология Web допускает защиту страниц HTML, когда доступ к отдельным страницам предоставляется только для отдельных пользователей при предъявлении пароля. При этом используется система аутентификации, или проверки подлинности идентификатора пользователя.

Переменная среды AUTH_TYPE содержит тип идентификации, который применяется сервером. Например, для сервера Web типа Microsoft Information Server при включении аутентификации в этой переменной будет храниться строка «NTLM».

·         GATEWAY_INTERFACE

В этой переменной находится версия интерфейса CGI, с которой работает данный сервер.

·         HTTP_ACCEPT

В этой переменной перечислены типы данных MIME, которые могут быть приняты браузером от сервера Web. Например, сервер Microsoft Internet Information Server может передать браузеру растровые графические изображения в формате gif, jpeg, pjpeg, x-xbitmap. Подробно эти типы данных описаны в спецификации протокола MIME, рассмотрение которой выходит за рамки нашей книги.

·         HTTP_REFER

В переменную HTTP_REFER записывается адрес URL документа HTML, который инициировал работу программы CGI.

·         HTTP_ACCEPT_LANGUAGE

Переменная HTTP_ACCEPT_LANGUAGE содержит идентификатор предпочтительного национального языка для получения ответа от сервера Web.

·         HTTP_UA_PIXELS

Разрешение видеоадаптера, установленное в компьютере пользователя.

·         HTTP_UA_COLOR

Допустимое количество цветов в системе пользователя.

·         HTTP_UA_OS

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

·         HTTP_UA_CPU

Тип центрального процессора, установленного в компьютере пользователя.

·         HTTP_USER_AGENT

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

·         HTTP_HOST

Имя узла, на котором работает сервер Web.

·         HTTP_CONNECTION

Тип соединения.

·         HTTP_ACCEPT_ENCODING

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

·         HTTP_AUTHORIZATION

Информация авторизации от браузера. Используется браузером для собственной аутентификации в сервере Web.

·         HTTP_FROM

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

·         HTTP_PRAGMA

Специальные команды серверу Web.

·         CONTENT_LENGTH

Количество байт данных, которые программа CGI должна получить от браузера.

·         CONTENT_TYPE

Тип данных, присланных браузером.

·         PATH_INFO

Путь к виртуальному каталогу, в котором находится программа CGI.

Как правило, при настройке сервера Web администратор выделяет один или несколько каталогов для хранения расширений сервера в виде программ CGI или ISAPI. Для файлов, записанных в такие каталоги, устанавливается доступ на запуск.

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

·         PATH_TRANSLATED

Физический путь к программе CGI.

·         QUERY_STRING

Строка параметров, указанная в форме после адреса URL программы CGI после разделительного символа «?».

·         REMOTE_ADDR

Адрес IP узла, на котором работает браузер пользователя.

·         REMOTE_HOST

Доменное имя узла, на котором работает браузер пользователя. Если эта информация недоступна (например, для узла не определен доменный адрес), вместо доменного имени указывается адрес IP, как в переменной REMOTE_ADDR.

·         REMOTE_USER

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

·         REQUEST_METHOD

Метод доступа, который используется для передачи данных от браузера серверу Web. В своих примерах мы используем методы доступа GET и POST, хотя протокол HTTP допускает применение и других методов доступа, например, PUT и HEAD.

·         SCRIPT_NAME

В эту переменную записывается путь к виртуальному каталогу и имя программы CGI. Анализируя эту переменную, программа CGI может определить путь к своему загрузочному файлу.

·         SERVER_NAME

Доменное имя сервера Web или адрес IP сервера Web, если доменное имя недоступно или не определено.

·         SERVER_PROTOCOL

Имя и версия протокола, который применяется для выполнения запроса к программе CGI.

·         SERVER_PORT

Номер порта, на котором навигатор посылает запросы серверу Web.

·         SERVER_SOFTWARE

Название и версия программного обеспечения сервера Web. Версия следует после названия и отделяется от названия символом «/».

·         REMOTE_IDENT

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

Примеры программ CGI

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

Программа CGIHELLO

Программа CGIHELLO представляет собой простейшую программу CGI, которая запускается при помощи кнопки в форме, возвращая навигатору созданный динамически документ HTML.

Эта программа хороша для проверки возможности запуска программ CGI на Вашем сервере Web или на сервере Вашего поставщика услуг Интернета. Так как она очень проста, то существует немного причин, по которым она не работает. Это неправильная настройка прав доступа к виртуальному каталогу, содержащему загрузочный модуль программы CGI, а также ошибочная ссылка на этот каталог в параметре ACTION тега <FORM>.

Еще одна возможная причина — неправильное указание типа проекта в среде Visual C++. Напоминаем, что программа CGI, предназначенная для работы в среде Windows NT, должна обязательно создаваться как 32-разрядное консольное приложение, а не как консольная программа MS-DOS.

Наша первая программа CGI запускается из формы, расположенной в документе HTML с именем cgihello.html. Исходный текст этого документа представлен в листинге 11-1.

Листинг 11-1 Вы найдете в файле chap11\cgihello\cgihello.html на прилагаемом к книге компакт-диске.

В этом документе определена форма, содержащая единственную кнопку, созданную с применением тега <INPUT> и имеющую тип SUBMIT:

<form METHOD="GET" ACTION="http://saturn/cgi-bin/cgihello.exe">
  <p><input TYPE="submit" VALUE="Send"> </p>
</form>

В параметре ACTION тега <FORM> мы указали путь к программе CGI, причем этот путь является виртуальным. Для передачи данных используется метод GET.

В результате работы программы CGIHELLO динамически создается документ HTML, страница которого показан на рис. 11-1.

Рис. 11-1. Документ HTML, создаваемый динамически программой CGGIHELLO

Рассмотрим исходный текст программы CGIHELLO (листинг 11-2).

Листинг 11-2 Вы найдете в файле chap11\cgihello\cgihello.с на прилагаемом к книге компакт-диске.

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

void main(int argc, char *argv[])
{
  printf("Content-type: text/html\n\n");
  printf("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//EN\">");
  printf("<HTML><HEAD><TITLE>"
      "Программы CGI</TITLE></HEAD><BODY>");

  printf("<H1>Результат работы программы CGI</H1>");
  printf("<P>Эта страница создана динамически в результате"
     " работы программы CGI");
  printf("</BODY></HTML>");
}

В первый раз функция printf выводит заголовок HTTP и пустую строку-разделитель. Далее программа CGIHELLO записывает построчно в стандартный поток вывода текст документа HTML.

Программа CONTROLS

Более сложная программа CGI с названием CONTROLS выполняет обработку данных, полученных из формы, показанной на рис. 11-2.

Рис. 11-2. Поле с элементами различных типов

Исходный текст этой формы представлен в листинге 11-3.

Листинг 11-3 Вы найдете в файле chap11\controls\controls.html на прилагаемом к книге компакт-диске.

Программа CONTROLS отображает в динамически формируемом документе HTML метод, использованный для передачи (POST или GET), а также размер и тип данных, поступающих от формы. Принятые данные отображаются как в исходном виде, так и после перекодировки. Кроме того, в созданном динамически документе HTML Вы сможете увидеть список значений всех полей, определенных в форме (рис. 11-3).

Рис. 11-3. Документ HTML, сформированный динамически программой CONTROLS

Из рисунка видно, что браузер прислал серверу Web 127 байт информации. Так как при этом использовался метод POST, данные были направлены в стандартный поток ввода. Они представлены в кодировке URL, так как содержимое переменой среды CONTENT_TYPE равно application/x-www-form-urlencoded.

Обратите внимание на текстовое поле с именем text1. Все пробелы в соответствующей строке в кодировке URL заменены символом «+». Символы «&» и «,» приходят в виде «%26» и «%2C». Функция перекодирования возвращает строку в исходный вид — «Sample of text1».

Форма, показанная на рис. 11-2, имеет две кнопки, предназначенные для передачи данных серверу Web. Это обычная кнопка и кнопка в графическом виде. Мы щелкнули графическую кнопку, поэтому от формы пришла информация о координатах курсора мыши в виде переменных с именами x и y.

Исходный текст программы CONTROLS приведен в листинге 11-4.

Листинг 11-4 Вы найдете в файле chap11\controls\controls.с на прилагаемом к книге компакт-диске.

После запуска функция main программы CONTROLS выводит в стандартный поток вывода заголовок HTTP и начальные строки динамически формируемого документа HTML. Для вывода мы использовали функцию printf:

printf("Content-type: text/html\n\n");

printf("<!DOCTYPE HTML PUBLIC"
     " \"-//W3C//DTD HTML 3.2//EN\">");

printf("<HTML><HEAD><TITLE>
Программы CGI"
     "</TITLE></HEAD><BODY BGCOLOR=#FFFFFF>");

szMethod = getenv("REQUEST_METHOD");

Далее функция main определяет использованный метод передачи данных, анализируя содержимое переменной среды REQUEST_METHOD:

if(!strcmp(szMethod, "POST"))
{
  . . .
}
else if(!strcmp(szMethod, "GET"))
{
  . . .
}

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

Если данные передаются методом POST, программа будет считывать их из стандартного потока ввода. Размер данных находится в переменной среды CONTENT_LENGTH. Соответствующая текстовая строка получается функцией getenv и преобразуется в численное значение функцией atoi.

Чтение данных из входного потока выполняется за один вызов функции fread:

lSize = atoi(getenv("CONTENT_LENGTH"));
fread(szBuf, lSize, 1, stdin);

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

Программа CGI сохраняет принятые данные в файле для дальнейшей обработки. Наша программа создает в текущем каталоге файл с названием received.dat:

fileReceived = fopen("received.dat", "w");
fwrite(szBuf, lSize, 1, fileReceived);
fclose(fileReceived);

Текущим каталогом при запуске этой программы в среде сервера Microsoft Information Server будет каталог с загрузочным модулем программы CONTROLS. Заметим, что, для того чтобы программа CGI могла создать файл в каталоге, необходимо соответствующим образом настроить права доступа.

После сохранения принятых данных в файле программа CONTROLS выводит в стандартный поток вывода содержимое некоторых переменных среды: REQUEST_METHOD, CONTENT_LENGTH и CONTENT_TYPE:

printf("<H2>Переменные среды</H2>");
    
printf("REQUEST_METHOD = %s", getenv("REQUEST_METHOD"));
printf("<BR>CONTENT_LENGTH = %ld", lSize);
printf("<BR>CONTENT_TYPE = %s", getenv("CONTENT_TYPE"));

szBuf[lSize] = '\0';
strcpy(szSrcBuf, szBuf);

printf("<H2>Принятые данные</H2>");
printf("<P>%s", szSrcBuf);

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

Перекодирование выполняется функцией DecodeStr, определенной в нашем приложении. Эту функцию мы рассмотрим позже. Результат сохраняется в буфере szSrcBuf, откуда он и берется для отображения:

DecodeStr(szSrcBuf);
printf("<H2>Данные после перекодировки</H2>");
printf("<P>%s", szSrcBuf);

На завершающем этапе обработки данных, полученных от формы, программа CONTROLS записывает в выходной документ HTML значения отдельных полей. Напомним, что эти значения имеют формат &<Имя_поля>=<Значение>, при этом символ «&» используется как разделитель.

Наша программа закрывает исходный буфер с принятыми данными дополнительным символом «&» (для простоты сканирования), после чего запускает цикл по полям формы.

szBuf[lSize] = '&';
szBuf[lSize + 1] = '\0';

for(szParam = szBuf;;)
{
  szPtr = strchr(szParam, '&');
      
  if(szPtr != NULL)
  {
     *szPtr = '\0';
     DecodeStr(szParam);
     printf("%s<BR>", szParam);

     szParam = szPtr + 1;
     if(szParam >= (szBuf + lSize))
       break;
  }
  else
     break;
}

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

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

В конце программа CONTROLS закрывает документ HTML, записывая в него теги </BODY> и </HTML>:

printf("</BODY></HTML>");

Если данные передаются в программу CONTROLS методом GET, входные данные находятся в переменной среды QUERY_STRING, которую мы получаем следующим образом:

szQueryString = getenv("QUERY_STRING");

Размер строки определяется при помощи функции strlen:

lSize = strlen(szQueryString);

Обработка принятых данных выполняется аналогично тому, как это делается методом POST. Разница заключается лишь в том, что в документе мы отображаем содержимое только переменных REQUEST_METHOD и QUERY_STRING.

Теперь займемся перекодировкой принятых данных. Она выполняется функцией DecodeStr, исходный текст которой приведен ниже:

void DecodeStr(char *szString)
{
  int src;
  int dst;
  char ch;

  for(src=0, dst=0; szString[src]; src++, dst++)
  {
     ch = szString[src];
     ch = (ch == '+') ? ' ' : ch;
     szString[dst] = ch;
    
     if(ch == '%')
     {
       szString[dst] = DecodeHex(&szString[src + 1]);
       src += 2;
     }
  }
  szString[dst] = '\0';
}

Эта функция сканирует входную строку, заменяя символы «+» на пробелы. Если в перекодируемой строке встречается комбинация символов вида «%xx», мы заменяем ее однобайтовым кодом соответствующего символа с помощью функции DecodeHex.

Функция DecodeHex комбинирует значение кода символа из старшего и младшего разряда преобразуемой комбинации символов:

char DecodeHex(char *str)
{
  char ch;

  if(str[0] >= 'A')
     ch = ((str[0] & 0xdf) - 'A') + 10;
  else
     ch = str[0] - '0';

  ch <<= 4;

  if(str[1] >= 'A')
     ch += ((str[1] & 0xdf) - 'A') + 10;
  else
     ch += str[1] - '0';

  return ch;
}

Программа AREF

В примерах, приведенных выше, мы использовали программы CGI только для обработки данных из полей форм. При этом адрес URL загрузочного файла программы указывался в параметре ACTION тега <FORM>.

Однако есть и другая возможность вызова программ CGI: указать их адрес в параметре HREF тега ссылки <A>. В этом случае Вы можете передать программе CGI параметры, указав их после имени файла загрузочного модуля через разделительный символ «?». Программа CGI получит строку параметров методом GET и сможет извлечь ее из переменной среды с именем QUERY_STRING.

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

Пример документа HTML, в котором демонстрируется вызов программы CGI указанным выше способом, приведен в листинге 11-5.

Листинг 11-5 Вы найдете в файле chap11\aref\aref.html на прилагаемом к книге компакт-диске.

В этом документе есть три ссылки на программу CGI с именем aref.exe, причем каждый раз ей передаются разные параметры:

<a HREF="http://saturn/cgi-bin/aref.exe?page1">
  <p>
Издательство &quot;Русская Редакция&quot;</a><br>
<a HREF="http://saturn/cgi-bin/aref.exe?page2">Microsoft</a><br>
<a HREF="http://saturn/cgi-bin/aref.exe?page3">
Наш сервер</a><br>

Программа CGI принимает параметр и в зависимости от его значения отображает один из документов HTML. Например, при выборе первой строки в окне навигатора отображается главная страница сервера Web издательства «Русская Редакция».

Исходный текст программы AREF приведен в листинге 11-6.

Листинг 11-6 Вы найдете в файле chap11\aref\aref.c на прилагаемом к книге компакт-диске.

Программа получает значение переменной среды QUERY_STRING, пользуясь для этого функцией getenv:

char * szQueryString;
szQueryString = getenv("QUERY_STRING");

Далее она сравнивает значение параметра со строками «page1», «page2» и «page3»:

if(!strcmp(szQueryString, "page1"))
  printf("Location: http://www.rusedit.ru/\n\n");
 
else if(!strcmp(szQueryString, "page2"))
  printf("Location: http://www.microsoft.com/\n\n");
 
else if(!strcmp(szQueryString, "page3"))
  printf("Location: http://www.glasnet.ru/~frolov/\n\n");

else
  printf("Location: error.html\n\n");

При совпадении программа возвращает навигатору адрес URL соответствующего документа HTML, формируя заголовок HTTP специального вида:

Location: «Адрес URL»\n\n

Когда браузер получает от сервера Web такой заголовок, он отображает в своем окне документ или файл графического изображения, адрес URL которого указан в заголовке. В случае ошибки посетителю отправляется документ с именем error.html.

Таким образом, программа CGI анализирует параметры, поступающие от навигатора через ссылку или поля формы, а затем не только динамически формирует документ HTML или ASP для отображения в окне браузера, но и возвращает ссылки на уже существующие документы в виде их адресов URL.

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

Доступ к базе данных из CGI

Если необходимо, чтобы программа CGI обращалась к базе данных, то для этого можно использовать один из методов доступа, описанный в нашей книге, — ADO, OLE DB или ODBC. Когда Вы извлечете параметры из элементов формы, запустившей программу CGI, передайте их методам или функциям для доступа к базе данных.

В этом разделе мы рассмотрим исходные тексты программы CGI с названием CGICPPADO. Эту программу мы создали на основе консольной программы CPPADO — ее исходные тексты описаны ранее, в четвертой главе книги.

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

Для работы с программой CGICPPADO мы подготовили документ HTML с формой, показанной на рис. 11-4.

Рис. 11-4. Форма для ввода идентификатора и пароля

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

Если идентификатор и пароль указаны правильно и действительно принадлежат сотруднику с административными привилегиями, программа CGICPPADO динамически формирует документ HTML, в котором отображается содержимое таблицы managers базы данных BookStore (рис. 11-5).

Рис. 11-5. Список сотрудников, извлеченный из базы данных программой CGICPPADO

Если же пароль введен неправильно или если пользователь не обладает административными привилегиями, в окне браузера появится лишь сообщение об отказе в доступе (рис. 11-6).

Рис. 11-6. Пользователь не зарегистрирован как администратор, поэтому отображается сообщение об отказе в доступе

Исходные тексты документа HTML с формой приведены в листинге 11-7.

Листинг 11-7 Вы найдете в файле chap11\CGICppado\managers.html на прилагаемом к книге компакт-диске.

Здесь Вам следует, прежде всего, обратить внимание на параметры тега <FORM>:

<form method="POST" action="http://saturn/cgi-bin/cppado.exe">

Параметр METHOD задает метод передачи данных из формы как POST, а параметр ACTION определяет путь к загрузочному файлу программы CGICPPADO.

Поля ввода идентификатора и пароля пользователя определены в нашей форме следующим образом:

<tr>
  <td width="134">Идентификатор:</td> <td width="230">
  <input type="text" name="UserID" size="20"></td>
</tr>
<tr>
  <td width="134">Пароль:</td> <td width="230">
  <input type="password" name="Pwd" size="20"></td>
</tr>

Поле для ввода идентификатора пользователя называется UserID, а поле пароля — Pwd. Эти имена нам потребуются в программе CGICPPADO для извлечения текста из соответствующих элементов формы.

Теперь займемся программой CGICPPADO.

Исходные тексты главного модуля этой программы приведены в листинге 11-8.

Листинг 11-8 Вы найдете в файле chap11\CGICppado\CPPADO.cpp на прилагаемом к книге компакт-диске.

Когда пользователь щелкает кнопку Submit в форме, показанной на рис. 7-4, программа CGICPPADO запускается на сервере Web. При этом управление передается функции _tmain, выполняющей все действия по обращению к базе данных и по динамическому формированию результата этих обращений в виде документов HTML.

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

char szUserID[80];
char szUserPassword[80];

Перед началом работы программа CGICPPADO выводит в стандартный выходной поток заголовок документа HTML:

  cout <<   "Content-type: text/html\n\n";
  cout << "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//EN\">";
  cout << "<HTML><HEAD><TITLE>
Просмотр списка сотрудников"
     "</TITLE></HEAD><BODY BGCOLOR=#FFFFFF>";

Здесь мы пользуемся потоком класса cout, что вполне допустимо, так как он связан с потоком stdout.

На следующем этапе наша программа проверяет метод, использованный для передачи данных из формы:

char * szMethod;
szMethod = getenv("REQUEST_METHOD");
if(!strcmp(szMethod, "POST"))
{
  . . .
}
else
  cout << "<h2>Можно использовать только метод POST</h2>";

Если в форме по ошибке применен метод GET, программа запишет сообщение об ошибке в создаваемый документ HTML и завершит свою работу.

В том случае, если использован метод POST, программа определит размер данных, отправленной формой, а затем сохранит их в буфер szBuf:

int lSize;
char szBuf[8196];

lSize = atoi(getenv("CONTENT_LENGTH"));
fread(szBuf, lSize, 1, stdin);
szBuf[lSize] = '\0';

На следующем этапе принятые данные копируются в буфер szSrcBuf и там перекодируются:

char szSrcBuf[8196];
strcpy(szSrcBuf, szBuf);
DecodeStr(szSrcBuf);

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

szBuf[lSize] = '&';
szBuf[lSize + 1] = '\0';

char * szPtr;
char * szParam;
for(szParam = szBuf;;)
{
  szPtr = strchr(szParam, '&');
  if(szPtr != NULL)
  {
     *szPtr = '\0';

     DecodeStr(szParam);
     GetParam(szParam);

     szParam = szPtr + 1;
     if(szParam >= (szBuf + lSize))
       break;
  }
  else
     break;
}

Этот цикл Вам должен быть знаком по программе CONTROLS, описанной ранее в этом разделе. В частности, мы рассказывали о функции DecodeStr. Здесь, однако, мы дополнительно вызываем функцию GetParam, извлекающую идентификатор пользователя и пароль и сохраняющую эти данные в глобальных переменных szUserID и szUserPassword.

Исходный текст функции GetParam показан ниже:

void GetParam(char *szString)
{
  char szBuf[1024];
  char * szPtr;

  strcpy(szBuf, szString);
  szPtr = strchr(szBuf, '=');
  if(szPtr != NULL)
  {
     *szPtr = '\0';
     szPtr++;

     if(!strncmp(szBuf, "UserID", 6))
       strcpy((char*)szUserID, szPtr);

     else if(!strncmp(szBuf, "Pwd", 3))
       strcpy((char*)szUserPassword, szPtr);
  }
}

Получая через параметр szString указатель на пару вида «Имя_параметра=Значение», эта функция последовательно сравнивает имя со строками «UserID» и «Pwd». При совпадении извлеченное значение содержимого соответствующего поля формы сохраняется в глобальных переменных szUserID и szUserPassword.

После обработки данных, поступивших от формы, программа CGICPPADO проверяет идентификатор и пароль пользователя, вызывая функцию login:

if(login((char*)szUserID, (char*)szUserPassword))
  getManagers();
else
  cout << "<h2>
Доступ запрещен</h2>";

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

Функция login обращается к базе данных для проверки идентификатора, пароля, а также прав пользователя. Если запрос сделал пользователь с правами администратора, функция login возвращает значение true, а если нет — false.

После успешной проверки прав пользователя вызывается функция getManagers. Она записывает в динамически формируемый документ HTML сведения о содержимом таблицы managers.

Перед завершением своей работы программа CGICPPADO записывает в стандартный выходной поток теги, завершающие формирование документа HTML:

cout << "</BODY></HTML>";

Функция login практически совпадает с одноименной функцией из приложения CPPADO, описанной нами в четвертой главе. Однако есть и отличия — новый вариант функции принимает через свои параметры имя и пароль пользователя, а не вводит их из стандартного потока:

bool login(char* szUsername, char * szPassword)
{
  . . .
}

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

Перед началом цикла обработки набора записей эта функция формирует заголовок таблицы в документе HTML, записывая в него названия столбцов:

cout << "<h2>Список сотрудников магазина<h2>";
cout << "<table border=1>";
cout << "<th>Идентификатор</th><th>Имя</th>"
  "<th>Пароль</th><th>Последнее подключение</th><th>Права</th>";

Значения, извлеченные из полей текущей записи набора, форматируются для записи в ячейки таблицы, создаваемой в документе HTML:

while(rs->adoEOF == VARIANT_FALSE)
{
  vManagerID = rs->Fields->GetItem(_variant_t("ManagerID"))->Value;
  vName       = rs->Fields->GetItem(_variant_t("Name"))->Value;
  vPassword = rs->Fields->GetItem(_variant_t("Password"))->Value;
  vLastLogin = rs->Fields->GetItem(_variant_t("LastLogin"))->Value;
  vRights     = rs->Fields->GetItem(_variant_t("Rights"))->Value;

  strTmp.Format(
  "<td>%s</td><td>%10s</td><td>%10s</td><td>%20s</td><td>%10s</td>",
  v2str(vManagerID), v2str(vName),
  v2str(vPassword), v2str(vLastLogin),
  v2str(vRights));

  cout << "<tr>";
  cout << (LPCTSTR)strTmp << "\n";
  cout << "</tr>";

  hr = rs->MoveNext();
  if(!SUCCEEDED(hr))
     break;
}

При этом для каждой строки набора записей мы формируем одну строку таблицы в выходном документе HTML.

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

cout << "</table>";

Обработка ошибок, возникающих при обращении к базе данных, выполняется при помощи функции AdoErrHandler:

catch(_com_error ex)
{
  AdoErrHandler(cn);
  return;
}

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

Создание приложений ISAPI

В этом разделе речь пойдет о приложениях ISAPI, дополняющих возможности сервера Microsoft Information Server. Все эти приложения можно разделить на две группы: расширения ISAPI и фильтры ISAPI.

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

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

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

Принципы работы и структура расширения ISAPI

Как только что было сказано, расширение ISAPI создается в виде библиотеки динамической загрузки DLL. Обращение к такой библиотеке выполняется в документах HTML аналогично обращению к программам CGI — из форм или ссылок, созданных при помощи тегов <FORM> и <A>.

Когда пользователь обращается к расширению ISAPI, соответствующая библиотека DLL загружается в адресное пространство сервера Microsoft Information Server и становится его составной частью. Так как расширение ISAPI работает в рамках процесса сервера Microsoft Information Server, а не в рамках отдельного процесса (как это происходит при запуске программы CGI), оно может пользоваться всеми ресурсами, доступными серверу. Это благоприятно сказывается на производительности.

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

Если, скажем, 20 пользователей запустят одновременно одну и ту же программу CGI, то на сервере будет создано 20 процессов — по одному для каждого пользователя. Так как создание процесса отнимает достаточно много системных ресурсов, это приведет к потере производительности.

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

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

Так как расширения ISAPI работают в рамках серверного процесса, они должны отлаживаться особенно тщательно. Ошибка в расширении ISAPI способна вызвать аварийное завершение работы всего сервера Microsoft Information Server. Что же касается программы CGI, выполняющейся как отдельный процесс в своем собственном адресном пространстве, то она едва ли способна вывести из строя сервер. Если в программе CGI допущена критическая ошибка, это приведет всего лишь к аварийному завершению самой программы, но не сервера.

Напомним, что расширение ISAPI работает в многопоточном режиме, что приводит к дополнительным проблемам при отладке.

Вызов расширения ISAPI сервером Web

Структура расширения ISAPI очень проста. Библиотека DLL расширения должна экспортировать всего две функции с именами GetExtensionVersion и HttpExtensionProc. Первая предназначена для того, чтобы расширение могло сообщить серверу версию спецификации, которой оно соответствует, и строку описания расширения. Функция HttpExtensionProc выполняет всю работу по передаче данных между расширением и сервером.

Дополнительно расширение ISAPI способно экспортировать функцию TerminateExtension. Она вызывается сервером перед тем, как ненужное больше приложение ISAPI выгружается из памяти. Функция TerminateExtension должна освободить ресурсы, полученные при инициализации расширения ISAPI.

Функция GetExtensionVersion

Функция GetExtensionVersion очень проста в реализации и обычно выглядит следующим образом:

BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVersion)
{
  pVersion->dwExtensionVersion =
     MAKELONG(HSE_VERSION_MINOR,HSE_VERSION_MAJOR);

  lstrcpyn(pVersion->lpszExtensionDesc,
     "My ISAPI Application Name", HSE_MAX_EXT_DLL_NAME_LEN);

  return TRUE;
}

При вызове функции GetExtensionVersion передается через единственный параметр указатель на структуру типа HSE_VERSION_INFO. Эта структура и указатель на нее (типа LPHSE_VERSION_INFO) определены в файле httpext.h следующим образом:

#define   HSE_MAX_EXT_DLL_NAME_LEN 256
typedef struct _HSE_VERSION_INFO
{
  DWORD dwExtensionVersion;
  CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN];
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;

Константы HSE_VERSION_MINOR и HSE_VERSION_MAJOR указывают текущую версию интерфейса расширения ISAPI и также определены в файле httpext.h:

#define HSE_VERSION_MAJOR 4 // верхний номер версии
#define HSE_VERSION_MINOR 0 //
нижний номер версии

Функция HttpExtensionProc

Теперь рассмотрим вторую функцию, которую должна экспортировать библиотека DLL расширения ISAPI. Она называется HttpExtensionProc и имеет следующий прототип:

DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);

Функция HttpExtensionProc получает единственный параметр — указатель на структуру типа EXTENSION_CONTROL_BLOCK, определенную в файле httpext.h:

typedef struct _EXTENSION_CONTROL_BLOCK 
{
  DWORD cbSize;    // размер структуры в байтах
  DWORD dwVersion; // версия спецификации ISAPI
  HCONN ConnID;    // идентификатор канала
  DWORD dwHttpStatusCode; // код состояния HTTP

  CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; // текстовая строка,
          // закрытая двоичным нулем, в которой находится информация
          // протоколирования, специфичная для данного расширения

  LPSTR lpszMethod;         // переменная REQUEST_METHOD
  LPSTR lpszQueryString;    // переменная QUERY_STRING
  LPSTR lpszPathInfo;       // переменная PATH_INFO
  LPSTR lpszPathTranslated; // переменная PATH_TRANSLATED

  DWORD  cbTotalBytes;    // полный размер данных, полученных от
                          // браузера
  DWORD  cbAvailable;     // размер доступного блока данных
  LPBYTE lpbData;         // указатель на доступный блок данных
                          // размером cbAvailable байт
  LPSTR  lpszContentType; // тип принятых данных

  // Функция GetServerVariable для получения значения переменных
  BOOL (WINAPI * GetServerVariable)(HCONN hConn,
      LPSTR lpszVariableName, LPVOID lpvBuffer, LPDWORD lpdwSize);

  // Функция WriteClient для посылки данных посетителю
  BOOL (WINAPI * WriteClient)(HCONN ConnID,
      LPVOID Buffer, LPDWORD lpdwBytes, DWORD dwReserved);

  // Функция ReadClient для получения данных от посетителя
  BOOL (WINAPI * ReadClient) (HCONN ConnID,
      LPVOID lpvBuffer, LPDWORD lpdwSize);

  // Вспомогательная функция ServerSupportFunction
  // для выполнения различных операций
  BOOL (WINAPI * ServerSupportFunction)(HCONN hConn,
      DWORD dwHSERRequest, LPVOID lpvBuffer,
      LPDWORD lpdwSize, LPDWORD lpdwDataType);

} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;

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

·         cbSize

В самом начале структуры EXTENSION_CONTROL_BLOCK находится поле cbSize, в которое при вызове расширения сервер записывает размер структуры в байтах.

·         dwVersion

Поле dwVersion содержит номер версии расширения ISAPI. Верхнее и нижнее значения номера версии можно получить при помощи макрокоманд HIWORD и LOWORD, соответственно.

·         ConnID

В поле ConnID сервер записывает идентификатор канала, созданного для расширения. Это поле нельзя изменять.

·         dwHttpStatusCode

Поле dwHttpStatusCode должно заполняться расширением ISAPI. Вы должны записать сюда результат завершения операции (код состояния транзакции). В случае успеха в это поле записывается значение 200 (как указано в спецификации HTTP).

·         lpszLogData

Поле lpszLogData предназначено для записи сообщения о выполнении транзакции в журнал сервера Web. Это сообщение должно быть в виде текстовой строки, закрытой нулем. Размер строки в байтах не должен превышать значения HSE_LOG_BUFFER_LEN.

·         lpszMethod

Поле lpszMethod заполняется сервером и содержит название метода передачи данных от удаленного пользователя серверу в виде текстовой строки, закрытой двоичным нулем. Расширения ISAPI используют те же самые методы, что и программы CGI — метод GET и метод POST. Проводя аналогию с программами CGI дальше, скажем, что поле lpszMethod эквивалентно переменной среды с именем REQUEST_METHOD, создаваемой для программы CGI.

·         lpszQueryString

Аналогично, поле lpszQueryString соответствует переменной среды с именем QUERY_STRING. В это поле записываются данные, принятые от удаленного пользователя методом GET.

·         lpszPathInfo

В поле lpszPathInfo записывается виртуальный путь к программному файлу библиотеки DLL расширения ISAPI. Напомним, что аналогичная информация для программ CGI передавалась через переменную среды с именем PATH_INFO.

·         lpszPathTranslated

Это поле содержит физический путь к программному файлу библиотеки DLL расширения ISAPI. Оно соответствует переменной среды с именем PATH_TRANSLATED, создаваемой для программ CGI.

·         cbTotalBytes

В поле cbTotalBytes записывается общее количество байт данных, которое необходимо получить от удаленного пользователя. Часть этих данных (размером не более 48 кб) считывается сервером автоматически. Эти данные будут сразу доступны, после того как функция HttpExtensionProc получит управление. Остальные данные необходимо дочитать в цикле при помощи функции ReadClient, о которой мы еще будем говорить.

·         cbAvailable

В поле cbAvailable записывается размер блока данных, полученных автоматически от браузера посетителя сервера. Как мы только что сказали, размер этого блока не может превышать 48 кб. Этого, однако, вполне достаточно для обработки данных, полученных от форм обычного размера.

·         lpbData

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

·         lpszContentType

Поле lpszContentType содержит тип принятых данных, например, «text/html».

·         GetServerVariable

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

Поле GetServerVariable содержит указатель на функцию, средствами которой расширение ISAPI может получить информацию, доступную программам CGI через переменные среды.

·         WriteClient

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

·         ReadClient

Посредством функции, адрес которой передается в поле ReadClient, приложение может дочитать дополнительные данные, не поместившиеся в буфер предварительного чтения, имеющий адрес lpbData и размер, не превышающий 48 кб. Аналогичную операцию приема данных от пользователя выполняет программа CGI в случае применения метода передачи данных POST. Отличие заключается в том, что программа CGI получает данные через стандартный поток ввода, а расширение ISAPI берет эти данные из буфера предварительного чтения и при необходимости дочитывает данные при помощи функции ReadClient.

·         ServerSupportFunction

Посредством функции, адрес которой передается через поле ServerSupportFunction, расширение ISAPI может выполнять различные действия, такие как посылка стандартного заголовка протокола HTTP и некоторые другие.

При успешном завершении функция HttpExtensionProc должна вернуть значение HSE_STATUS_SUCCESS, а при ошибке — значение HSE_STATUS_ERROR. Соответствующие константы определены в файле httpext.h.

Получение данных расширением ISAPI

Программа CGI получает данные из переменных среды и стандартного потока ввода (в случае применения метода доступа POST). Расширение ISAPI делает это по-другому.

Функция HttpExtensionProc получает указатель на структуру типа EXTENSION_CONTROL_BLOCK. Некоторые поля этой структуры заполняются сервером и должны использоваться для получения входных данных. Прежде всего это поле lpszMethod, через которое передается метод, использованный для посылки данных (GET или POST), поле lpszQueryString, в котором передаются параметры запуска расширения или данные при использовании метода GET, а также другие поля, описанные выше.

Через структуру EXTENSION_CONTROL_BLOCK передаются адреса функций GetServerVariable и ReadClient, специально предназначенных для получения данных от браузера посетителя.

Функция GetServerVariable

Прототип функции GetServerVariable определен в структуре EXTENSION_CONTROL_BLOCK, описанной нами ранее:

BOOL (WINAPI * GetServerVariable)(HCONN hConn,
  LPSTR lpszVariableName, LPVOID lpvBuffer, LPDWORD lpdwSize);

Через параметр hConn Вы должны передать этой функции идентификатор канала, полученный через поле ConnID структуры EXTENSION_CONTROL_BLOCK.

Параметр lpszVariableName должен содержать указатель на строку имени переменной, содержимое которой необходимо получить. Это содержимое будет записано функцией в буфер, адрес которого передается через параметр lpvBuffer, а размер — через параметр lpdwSize.

Ниже перечислены возможные значения строк, передаваемых через параметр lpszVariableName.

·         AUTH_TYPE

Переменная среды AUTH_TYPE содержит тип идентификации, который применяется сервером.

·         HTTP_ACCEPT

В этой переменной перечислены типы данных MIME, которые могут быть приняты браузером от сервера Web.

·         CONTENT_LENGTH

Количество байт данных, которые расширение ISAPI должно получить от браузера.

·         CONTENT_TYPE

Тип данных, присланных браузером.

·         PATH_INFO

Путь к виртуальному каталогу, в котором находится библиотека DLL расширения ISAPI.

·         PATH_TRANSLATED

Физический путь к библиотеки DLL расширения ISAPI.

·         QUERY_STRING

Строка параметров, указанная в форме или теге ссылки <A>. Эта строка указывается после адреса URL библиотеки DLL расширения ISAPI вслед за разделительным символом «?».

·         REMOTE_ADDR

Адрес IP узла, на котором работает браузер посетителя.

·         REMOTE_HOST

Доменное имя узла, на котором работает браузер посетителя. Если эта информация недоступна (например, для узла не определен доменный адрес), то вместо доменного имени указывается адрес IP, как в переменной REMOTE_ADDR.

·         REMOTE_USER

Имя пользователя, которое применяет браузер для аутентификации.

·         UNMAPPED_REMOTE_USER

Имя пользователя до обработки фильтром ISAPI, которое применяет браузер для аутентификации.

·         REQUEST_METHOD

Метод доступа, который используется для передачи данных от браузера серверу Web.

·         SCRIPT_NAME

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

·         SERVER_NAME

Доменное имя сервера Web или адрес IP сервера Web, если доменное имя недоступно или не определено.

·         SERVER_PROTOCOL

Имя и версия протокола, который применяется для выполнения запроса к расширению ISAPI.

·         SERVER_PORT

Номер порта, на котором браузер посылает запросы серверу Web.

·         SERVER_PORT_SECURE

Если обработка запроса выполняется через защищенный порт, в этой строке записано значение 1, а если через незащищенный — значение 0.

·         SERVER_SOFTWARE

Название и версия программного обеспечения сервера Web. Версия следует после названия и отделяется символом «/».

·         ALL_HTTP

Строка, закрытая двоичным нулем, в которую записаны значения всех переменных, имеющих отношение к протоколу HTTP. Это, например, такие переменные как HTTP_ACCEPT, HTTP_CONNECTION, HTTP_USER_AGENT и т. д.

Извлекать содержимое отдельных переменных Ваша программа должна самостоятельно. При этом следует учесть, что названия переменных отделены от их значений символом двоеточия «:», а поля переменных разделены символом перевода строки.

Обратите внимание, что названия этих строк почти совпадают с названиями переменных среды, создаваемых для программ CGI, однако совпадение все же не полное.

В случае успешного завершения функция GetServerVariable возвращает значение TRUE, а при возникновении ошибки — значение FALSE. Код ошибки можно определить с помощью функции GetLastError, вызвав ее сразу после функции GetServerVariable.

Возможные коды ошибок для функции GetServerVariable приведены в таблице 11-2.

Таблица 11-2. Коды ошибок для функции GetServerVariable

Код ошибки

Описание

ERROR_INVALID_INDEX

Неправильное имя переменной, передаваемой через параметр lpszVariableName

ERROR_INVALID_PARAMETER

Неправильное значение параметра Hconn

ERROR_INSUFFICIENT_BUFFER

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

ERROR_MORE_DATA

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

ERROR_NO_DATA

Данные не получены

Ниже мы показываем пример использования функции GetServerVariable для получения содержимого переменной с именем ALL_HTTP в буфер szTempBuf.

CHAR szTempBuf[4096];
DWORD dwSize;
dwSize = 4096;
lpECB->GetServerVariable(lpECB->ConnID,
  (LPSTR)"ALL_HTTP", (LPVOID)szTempBuf, &dwSize);
strcat(szBuff, szTempBuf);

Функция ReadClient

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

BOOL (WINAPI * ReadClient) (HCONN ConnID,
  LPVOID lpvBuffer, LPDWORD lpdwSize);

Через параметр hConn этой функции надо передать идентификатор канала, полученный через поле ConnID структуры EXTENSION_CONTROL_BLOCK.

Функция ReadClient читает данные в буфер, адрес которого передается через параметр lpvBuffer, а размер — через параметр lpdwSize. В случае успеха функция возвращает значение TRUE, а при ошибке — значение FALSE. Код ошибки можно получить посредством функции GetLastError.

Работа с функцией ReadClient имеет некоторые особенности.

Когда расширение ISAPI получает управление, через структуру типа EXTENSION_CONTROL_BLOCK передается адрес предварительно прочитанного блока данных, полученного от браузера посетителя. Как Вы знаете, адрес и размер этого блока данных указаны соответственно в полях lpbData и cbAvailable структуры EXTENSION_CONTROL_BLOCK.

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

Прежде всего следует сравнить размер предварительно считанных данных с полным размером данных, которые нужно считать (этот размер передается в поле cbTotalBytes структуры EXTENSION_CONTROL_BLOCK).

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

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

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

Отправка данных расширением ISAPI

Вместо того чтобы записывать выходные данные в стандартный поток вывода, как это делает программа CGI, расширение ISAPI пользуется для пересылки данных функциями WriteCilent и ServerSupportFunction. Указатели на эти функции передаются расширению ISAPI через структуру типа EXTENSION_CONTROL_BLOCK.

Функция WriteCilent

Прототип функции WriteClient, взятый из определения структуры EXTENSION_CONTROL_BLOCK, приведен ниже:

BOOL (WINAPI * WriteClient)(HCONN ConnID,
  LPVOID Buffer, LPDWORD lpdwBytes, DWORD dwReserved);

Через параметр hConn функции WriteClient передается идентификатор канала, полученный через поле ConnID структуры EXTENSION_CONTROL_BLOCK.

Функция WriteClient посылает удаленному пользователю данные из буфера Buffer, причем размер передаваемого блока данных должен быть записан в переменную типа DWORD, адрес которой передается через параметр lpdwBytes. Параметр dwReserved зарезервирован для дальнейших расширений возможностей функции.

В случае успеха функция возвращает значение TRUE, а при ошибке — значение FALSE. Код ошибки можно получить посредством функции GetLastError.

Заметим, что после отпрвки данных функция WriteClient записывает в переменную, адрес которой был ей передан через параметр lpdwBytes, количество успешно переданных байт данных. В отличие от функции ReadClient, функция WriteClient посылает данные за один прием, поэтому нет необходимости вызывать ее в цикле. Если же эта функция смогла передать только часть данных, то это означает, что произошла ошибка.

Функция ServerSupportFunction

Прототип функции ServerSupportFunction, определенный в структуре типа EXTENSION_CONTROL_BLOCK, приведен ниже:

BOOL (WINAPI * ServerSupportFunction)(HCONN hConn,
 DWORD dwHSERRequest, LPVOID lpvBuffer,
 LPDWORD lpdwSize, LPDWORD lpdwDataType);

Через параметр hConn функции ServerSupportFunction передается идентификатор канала, полученный через поле ConnID структуры EXTENSION_CONTROL_BLOCK.

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

Через параметр lpvBuffer передается размер буфера, который используется при выполнении операции. Размер этого буфера должен быть записан в переменной типа DWORD, адрес которой передается через параметр lpdwSize. После выполнения операции передачи данных в эту переменную будет записан размер успешно переданного блока данных.

Параметр lpdwDataType применяется для указания дополнительной строки заголовка или дополнительных данных, которые будут добавлены к заголовку, передаваемому удаленному пользователю. Если для параметра lpdwDataType указать значение NULL (что допустимо), к заголовку будут добавлены символы конца строки «\r\n».

Какие операции допустимо выполнять при помощи функции ServerSupportFunction?

Ниже мы привели список возможных значений параметра dwHSERRequest, определяющего код выполняемой операции.

·   HSE_REQ_SEND_RESPONSE_HEADER

Эта операция предназначена для пересылки удаленному пользователю стандартного заголовка HTTP. Если надо добавить другие заголовки, следует воспользоваться параметром lpdwDataType. В качестве дополнительного заголовка Вы можете указать любую строку, закрытую символами конца строки «\r\n» и двоичным нулем.

Если Ваше расширение ISAPI динамически формирует документ HTML и отправляет его пользователю, то ей не нужно вызывать функцию WriteClient.

Все необходимые для этого действия можно сделать средствами одной только функции ServerSupportFunction. Ниже мы показали фрагмент кода, в котором эта функция используется для посылки документа HTML, подготовленного заранее в буфере szBuff:

CHAR szBuff[4096];
wsprintf(szBuff,
  "Content-Type: text/html\r\n\r\n"
  "<HTML><HEAD><TITLE>Simple ISAPI Extension</TITLE></HEAD>\n"
  "<BODY BGCOLOR=#FFFFFF><H1>Hello from ISAPI Extension!</H1>\n");

strcat(szBuff, "<H1>
Заголовок документа</H1>");
strcat(szBuff, "<HR>");
strcat(szBuff, "</BODY></HTML>");  

lpECB->ServerSupportFunction
(lpECB->ConnID,
  HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL, (LPDWORD)szBuff);

Заметим, однако, что функция ServerSupportFunction не позволяет посылать двоичные данные. Для отправки двоичных данных Вы обязательно должны применить функцию WriteClient.

·         HSE_REQ_SEND_URL

Используя операцию HSE_REQ_SEND_URL, расширение ISAPI может послать удаленному пользователю данные, хранящиеся по определенному адресу URL, как будто бы эти данные были запрошены непосредственно пользователем по этому адресу URL. Это удобно для отправки либо динамически созданных данных, либо предварительно подготовленных данных.

Адрес URL должен быть указан в виде текстовой строки, закрытой двоичным нулем, через параметр lpvBuffer. Размер строки задают в параметре lpdwSize. Что же касается параметра lpdwDataType, то при выполнении данной операции этот параметр игнорируется.

·         HSE_REQ_SEND_URL_REDIRECT_RESP

Отправка сообщения с номером 302 (URL Redirect). Адрес URL указывается аналогично тому, как это делается при выполнении операции HSE_REQ_SEND_URL.

·         HSE_REQ_MAP_URL_TO_PATH

Преобразование логического адреса URL в физический. Адрес логического пути передается через параметр lpvBuffer. По этому же адресу записывается результат преобразования. Размер буфера при вызове функции задается как обычно с помощью параметра lpdwSize.

После выполнения преобразования в переменную типа DWORD, адрес которой указан параметром lpdwSize, будет записана длина строки результата преобразования.

·         HSE_REQ_DONE_WITH_SESSION

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

Приложение ISHELLO

В качестве нашего первого расширения ISAPI мы предлагаем приложение ISHELLO, выполняющее простейшие функции.

Вызов расширения ishello.dll выполняется из формы, исходный текст которой приведен в листинге 11-9.

Листинг 11-9 Вы найдете в файле chap11\ISHELLO\ishello.html на прилагаемом к книге компакт-диске.

Расширение вызывается в параметре ACTION тега <FORM> аналогично тому, как это делается для программ CGI:

<form METHOD="POST"
ACTION="http://saturn/cgi-bin/ishello.dll?Param1|Param2|Param3">
<p><input TYPE="submit" VALUE="Send"> </p>
</form>

После вызова наше расширение ishello.dll динамически создает документ HTML, представленный на рис. 11-7.

Рис. 11-7. Документ HTML, созданный динамически расширением ishello.dll

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

Исходный текст расширения ishello.dll представлен в листинге 11-10.

Листинг 11-10 Вы найдете в файле chap11\ISHELLO\ishello.с на прилагаемом к книге компакт-диске.

Наряду с обычным для приложений Windows файлом windows.h мы включили в наш исходный текст файл httpext.h, в котором определены все необходимые константы, структуры данных и прототипы функций:

#include <windows.h>
#include <httpext.h>

Этот файл поставляется в составе Microsoft Visual C++.

В приложении определена функция GetExtensionVersion — ее мы уже рассматривали ранее. Она записывает версию интерфейса ISAPI и текстовую строку описания расширения в поля структуры типа HSE_VERSION_INFO с именами dwExtensionVersion и lpszExtensionDesc, соответственно. Адрес структуры HSE_VERSION_INFO передается функции GetExtensionVersion через параметр.

Функция HttpExtensionProc обращается к буферу szBuff для подготовки динамически создаваемого документа HTML, который будет послан удаленному пользователю в результате работы нашего расширения. В качестве вспомогательного буфера применяется буфер szTempBuf:

CHAR szBuff[4096];
CHAR szTempBuf[4096];

Прежде всего мы записываем в поле dwHttpStatusCode нулевое значение:

lpECB->dwHttpStatusCode = 0;

Потом в это поле мы запишем результат выполнения команды.

Далее в буфер szBuff копируется заголовок HTTP и начальный фрагмент документа HTML, для чего используется функция wsprintf:

wsprintf(szBuff,
  "Content-Type: text/html\r\n\r\n"
  "<HTML><HEAD><TITLE>Simple ISAPI Extension</TITLE></HEAD>\n"
  "<BODY BGCOLOR=#FFFFFF><H2>Hello from ISAPI Extension!</H2>\n");

После этого к буферу szBuff с помощью функции strcat добавляются другие строки документа. Например, разделительная линия:

strcat(szBuff, "<HR>");

После первой разделительной линии в документ вставляются несколько строк со значениями некоторых полей структуры типа EXTENSION_CONTROL_BLOCK. В следующем фрагменте кода показана строка версии интерфейса ISAPI:

wsprintf(szTempBuf, "<P>Extension Version: %d.%d",
  HIWORD(lpECB->dwVersion), LOWORD(lpECB->dwVersion));
strcat(szBuff, szTempBuf);

Далее в документ выводятся строка с названием метода передачи данных (поле lpszMethod), строка параметров запуска расширения ISAPI (поле lpszQueryString), физический путь к программному файлу библиотеки DLL расширения (поле lpszPathTranslated), полный размер данных, которые нужно прочитать (поле cbTotalBytes), а также тип данных (поле lpszContentType):

wsprintf(szTempBuf, "<BR>Method: %s", lpECB->lpszMethod);
strcat(szBuff, szTempBuf);
 
wsprintf(szTempBuf, "<BR>QueryString: %s",
  lpECB->lpszQueryString);
strcat(szBuff, szTempBuf);
 
wsprintf(szTempBuf, "<BR>PathTranslated: %s",
  lpECB->lpszPathTranslated);
strcat(szBuff, szTempBuf);

wsprintf(szTempBuf, "<BR>TotalBytes: %d",
  lpECB->cbTotalBytes);
strcat(szBuff, szTempBuf);

wsprintf(szTempBuf, "<BR>ContentType: %s",
  lpECB->lpszContentType);
strcat(szBuff, szTempBuf);

После этого в документ снова добавляется разделительная линия, и отображается содержимое переменных сервера с префиксом имени HTTP, для чего используется рассмотренная ранее функция GetServerVariable:

strcat(szBuff, "<HR><P><B>Server Variables:</B><BR>");

dwSize = 4096;
lpECB->GetServerVariable(lpECB->ConnID,
  (LPSTR)"ALL_HTTP", (LPVOID)szTempBuf, &dwSize);
strcat(szBuff, szTempBuf);

В завершение в документ записывается финальная строка:

strcat(szBuff, "</BODY></HTML>");

Сформированный таким образом документ отправляется посетителю сервера Web функцией ServerSupportFunction, как это показано ниже:

if(!lpECB->ServerSupportFunction(lpECB->ConnID,
  HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL, (LPDWORD)szBuff))
{
  return HSE_STATUS_ERROR;
}
lpECB->dwHttpStatusCode = 200;
return HSE_STATUS_SUCCESS;

Если при пересылке данных произошла ошибка, расширение завершает свою работу с кодом HSE_STATUS_ERROR. В случае успеха в поле состояния dwHttpStatusCode записывается код 200. Вслед за этим расширение завершает свою работу с кодом HSE_STATUS_SUCCESS.

Создавая проект расширения ISAPI, Вы должны подготовить файл определения модуля для соответствующей библиотеки DLL (листинг 11-11).

Листинг 11-11 хранится в файле chap11\ISHELLO\ishello.def на прилагаемом к книге компакт-диске.

В разделе EXORT этого файла нужно указать имена функций GetExtensionVersion и HttpExtensionProc:

LIBRARY       ishello
DESCRIPTION 'Simple ISAPI DLL'
EXPORTS
  GetExtensionVersion

  HttpExtensionProc

Вызов функций ODBC из ISAPI

Ранее мы рассказывали, как обращаться к базе данных из программы CGI с применением объектного интерфейса ADO. В этом разделе мы приведем исходные тексты расширения ISAPI, которое тоже работает с базой данных, но с применением программного интерфейса ODBC.

Для запуска этого приложения ISAPI (с названием ISFORM) мы используем форму, показанную на рис. 11-8.

Рис. 11-8. Форма ввода идентификатора и пароля

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

Расширение ISFORM обращается к базе данных BookStore, запуская на выполнение хранимую процедуру ManagerLogin. Если пользователь зарегистрирован в таблице managers, расширение ISFORM отправляет ему динамически созданный документ HTML, в котором отображается идентификатор пользователя, его пароль и права, извлеченные из этой таблицы (рис. 11-9).

Рис. 11-9. Перечень прав сотрудника магазина, извлеченные из базы данных

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

А теперь опишем исходные тексты приложения ISFORM.

Исходный текст документа HTML, предназначенного для запуска этого расширения, Вы найдете в листинге 11-12.

Листинг 11-12 хранится в файле chap11\ISFORM\managers.html на прилагаемом к книге компакт-диске.

В этом документе имеется форма, ссылающаяся на загрузочный файл расширения isform.dll:

<form method="POST" action="http://saturn/cgi-bin/isform.dll">

Помимо всего прочего, в этой форме определены два поля, предназначенные для ввода идентификатора и пароля:

<tr>
  <td width="134">Идентификатор:</td><td width="230">
  <input type="text" name="UserID" size="20"></td>
</tr>
<tr>
  <td width="134">Пароль:</td><td width="230">
  <input type="password" name="Pwd" size="20"></td>
</tr>

Данные из этих полей передаются расширению ISFORM.

Исходный текст главного модуля приложения ISFORM приведен в листинге 11-13.

Листинг 11-13 хранится в файле ch7\ISFORM\isform.c на прилагаемом к книге компакт-диске.

Рассмотрим наиболее важные фрагменты исходного текста этого модуля.

В области глобальных переменных мы определили массивы для хранения идентификатора пользователя szUserID, его пароля szUserPassword, прав szUserRights, а также текста сообщения об ошибках sErrMsg (если они возникнут при обращении расширения к базе данных):

char szUserID[80];
char szUserPassword[80];
char szUserRights[80];
char sErrMsg[8000];

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

Все основные события происходят внутри функции HttpExtensionProc, к описанию которой мы и приступаем.

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

CHAR szBuff[4096];
CHAR szTempBuf[4096];
char * szPtr;
char * szParam;

В начале работы мы помещаем нулевое значение в поле dwHttpStatusCode блока ECB, а затем записываем в буфер szBuff заголовок HTTP и начальный фрагмент документа HTML, формируемого динамически:

lpECB->dwHttpStatusCode = 0;

wsprintf(szBuff, "Content-Type: text/html\r\n\r\n"
  "<HTML><HEAD><TITLE>Права сотрудника</TITLE></HEAD>\n"
  "<BODY BGCOLOR=#FFFFFF><H2>Права сотрудника</H2>\n");

Данные, отправленные браузером, копируются в буфер szTempBuf:

lstrcpyn(szTempBuf, lpECB->lpbData, lpECB->cbAvailable + 1);
szTempBuf[lpECB->cbAvailable + 1] = '\0';

Эти данные затем перекодируются функцией DecodeStr:

DecodeStr(szTempBuf);

Исходный текст этой функции мы рассматривали в разделах, посвященных программам CGI.

Далее мы запускаем цикл извлечения из принятых данных содержимого полей формы с идентификатором сотрудника и его паролем:

szTempBuf[lpECB->cbAvailable] = '&';
szTempBuf[lpECB->cbAvailable + 1] = '\0';
 
for(szParam = szTempBuf;;)
{
  szPtr = strchr(szParam, '&');
  if(szPtr != NULL)
  {
     *szPtr = '\0';
     DecodeStr(szParam);
     GetParam(szParam);

     szParam = szPtr + 1;
     if(szParam >= (szTempBuf + lpECB->cbAvailable))
       break;
  }
  else
     break;
}

Здесь используется техника, с которой мы познакомили Вас в исходных текстах программ CGI. Она предполагает применение функций DecodeStr и GetParam.

Далее наша программа добавляет в буфер выходного документа HTML идентификатор пользователя и пароль:

strcat(szBuff, "User: ");
strcat(szBuff, szUserID);
strcat(szBuff, "<br>Pasword: ");
strcat(szBuff, szUserPassword);
strcat(szBuff, "<BR>");

Соответствующие строки извлечены из данных, отправленных формой, при помощи функции GetParam.

Затем наше расширение ISFORM вызывает функцию get_manager_table, передавая ей идентификатор, пароль, а также указатель szUserRights на буфер, в который эта функция должна записать права пользователя:

if(!get_manager_table(szUserID, szUserPassword, szUserRights))
{
  strcat(szBuff, "<br>Rights: ");
  strcat(szBuff, szUserRights);
  strcat(szBuff, "<BR>");
}
else
{
  strcat(szBuff, "<h2>Произошла ошибка</h2>");
  strcat(szBuff, sErrMsg);
}

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

При возникновении ошибок в выходной буфер копируется содержимое строки сообщения об ошибке sErrMsg.

Далее в буфер записывается завершающий фрагмент динамически формируемого документа HTML, после чего содержимое буфера отправляется браузеру посетителя при помощи функции ServerSupportFunction:

strcat(szBuff, "</BODY></HTML>");  

if(!lpECB->ServerSupportFunction(lpECB->ConnID,
  HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL,
  (LPDWORD)szBuff))
{
  return HSE_STATUS_ERROR;
}

lpECB->dwHttpStatusCode = 200;
return HSE_STATUS_SUCCESS;

Исходный текст функции get_manager_table определен в файле odbcparam.c, исходный текст которого приведен в листинге 11-14.

Листинг 11-14 хранится в файле chap11\ISFORM\odbcparam.c на прилагаемом к книге компакт-диске.

Функция get_manager_table во многом аналогична функции main консольной программы ODBCPARAM, о которой мы рассказывали в предыдущей главе нашей книги, поэтому мы поговорим об отличиях.

Приложение ISFORM написано на языке С, а не С++, поэтому мы отказались от использования библиотеки шаблонов STL и типа данных string.

Сообщение об ошибке записывается в переменную sErrMsg, объявленную в файле odbcparam.c как extern:

extern char sErrMsg[8000];

Прототип функции get_manager_table приведен ниже:

int get_manager_table(char* szUserID, char* szUserPassword,
  char* szRights);

Через первый параметр ей передается указатель на строку идентификатора пользователя, через второй — указатель на строку пароля, а через третий — указатель на буфер, куда функция get_manager_table должна записать результат своей работы (права пользователя).

Далее функция get_manager_table выполняет инициализирующие действия, необходимые для работы с источником данных через интерфейс ODBC и создает соединение с этим источником:

rc = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &hEnv);
rc = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION,
     (SQLPOINTER)SQL_OV_ODBC3, SQL_IS_INTEGER);
rc = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);

rc = SQLConnect(hDbc,
  szDSN,      (SWORD)strlen((const char*)szDSN),
  szUserName, (SWORD)strlen((const char*)szUserName),
  szPassword, (SWORD)strlen((const char*)szPassword));

Здесь мы убрали обработку ошибок, выполняемую функциями GetErrorMsg и GetErrorMsgConn.

На следующем этапе функция get_manager_table выполняет «привязку» параметров и вызов хранимой процедуры ManagerLogin:

rc = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
strcpy((char*)szAdminName, szUserID);

rc = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR,
  SQL_CHAR, 50, 0, szAdminName, 51, &cbAdminName);

strcpy((char*)szAdminPass, szUserPassword);

rc = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR,
  SQL_CHAR, 50, 0, szAdminPass, 51, &cbAdminPass);

rc = SQLBindParameter(hStmt, 3, SQL_PARAM_OUTPUT, SQL_C_CHAR,
  SQL_CHAR, 16, 0, szAdminRights, 51, &cbAdminRights);

rc = SQLExecDirect(hStmt,
  (unsigned char*)"{call ManagerLogin(?,?,?)}", SQL_NTS);

Значения входных параметров при этом копируются из параметров szUserID и szUserPassword функции get_manager_table.

Результат работы хранимой процедуры записывается по адресу, который был передан функции get_manager_table через последний параметр:

strcpy(szRights, szAdminRights);

SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
SQLDisconnect(hDbc);
SQLFreeHandle(SQL_HANDLE_DBC,  hDbc);
SQLFreeHandle(SQL_HANDLE_ENV,  hEnv);

Далее функция освобождает полученные ей ресурсы и возвращает управление.

Исходные тексты функций GetErrorMsgConn и GetErrorMsg аналогичны исходным текстам, использованным в программе ODBCPARAM. Мы внесли в них небольшие изменения: теперь сообщение об ошибке будет отображаться не в окне консольной программы, а в документе HTML. Кроме того, вместо переменной класса string, определенного в библиотеке шаблонов STL, мы использовали здесь обычные функции стандартной библиотеки С.

Обращение к базе данных в отдельном потоке

В предыдущем разделе для запуска хранимой процедуры мы обращались к функциям интерфейса ODBC непосредственно из функции HttpExtensionProc. Однако этот способ, хотя и работает, обладает одним существенным недостатком. Этот недостаток связан с тем способом, которым сервер Microsoft Internet Information Server обрабатывает запросы, получаемые от браузеров посетителей через протокол HTTP.

Для таких запросов сервер IIS создает пул идентификаторов потоков, обрабатывающих запросы. Если все потоки из этого пула заняты или находятся в состоянии ожидания, сервер отвергает вновь поступающие запросы.

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

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

Исходный текст главного модуля, измененный для использования отдельного потока, мы привели в листинге 11-15.

Листинг 11-15 хранится в файле chap11\ISFORM_THREAD\isform.c на прилагаемом к книге компакт-диске.

Рассмотрим внесенные изменения.

В области глобальных переменных мы определили переменные dwThreadID и g_dwThreadCount:

DWORD dwThreadID;
DWORD g_dwThreadCount = 0;

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

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

wsprintf(szBuff,
  "<HTML><HEAD><TITLE>Права сотрудника</TITLE></HEAD>\n"
  "<BODY BGCOLOR=#FFFFFF><H2>Права сотрудника</H2>\n");
  . . .

  strcat(szBuff, "User: ");
  strcat(szBuff, szUserID);
  strcat(szBuff, "<br>Pasword: ");
  strcat(szBuff, szUserPassword);
  strcat(szBuff, "<BR>");

Далее функция HttpExtensionProc запускает функцию ThreadProc в отдельном потоке, передавая ей в качестве параметра указатель на блок ECB:

CreateThread(NULL, 0, ThreadProc, lpECB, 0, &dwThreadID);

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

InterlockedIncrement(&g_dwThreadCount);
return HSE_STATUS_PENDING;

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

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

Получив управление, эта функция вызывает функцию get_manager_table, выполняющую обращение к базе данных:

if(!get_manager_table(szUserID, szUserPassword, szUserRights))
{
  strcat(szBuff, "<br>Rights: ");
  strcat(szBuff, szUserRights);
  strcat(szBuff, "<BR>");
}
else
{
  strcat(szBuff, "<h2>Произошла ошибка</h2>");
  strcat(szBuff, sErrMsg);
}
strcat(szBuff, "</BODY></HTML>");  

После вызова этой функции поток продолжает формирование содержимого буфера выходного документа szBuff.

На следующем этапе поток отправляет браузеру заголовок документа HTTP, вызывая для этого функцию ServerSupportFunction:

char szHeader[] =  "Content-type: text/html\r\n\r\n";

header_ex_info.pszStatus = "200 OK";
header_ex_info.pszHeader = szHeader;
header_ex_info.cchStatus = strlen("200 OK");
header_ex_info.cchHeader = strlen(szHeader);
header_ex_info.fKeepConn = FALSE;

success = lpECB->ServerSupportFunction(
  lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX,
  &header_ex_info, NULL, NULL);

Далее остается только отправить браузеру содержимое буфера выходного документа HTML, что можно сделать при помощи функции WriteClient:

dwSize = strlen(szBuff);
lpECB->WriteClient(lpECB->ConnID, szBuff, &dwSize, 0);

Теперь мы можем завершить сеанс средствами функции ServerSupportFunction, передав ей во втором параметре константу HSE_REQ_DONE_WITH_SESSION:

lpECB->ServerSupportFunction(lpECB->ConnID,
 HSE_REQ_DONE_WITH_SESSION, NULL, NULL, NULL );

Загрузка файлов на сервер Web через браузер

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

Как такое возможно?

Технология загрузки файлов посетителей на сервер Web (часто называемая технологией File Upload) основана на экспериментальном протоколе, описанном в документе RFC1867 (если Вы не знакомы с описаниями RFC, сходите на сервер http://www.cis.ohio-state.edu/htbin/rfc). Документ RFC1867 называется «Form-based file Upload in HTML»”, что можно перевести как «прием файлов через документы HTML».

В этом документе помимо всего прочего описывается применение строки FILE в качестве возможного значения атрибута TYPE тега <INPUT>, создающего элементы управления в формах. Этот элемент управления состоит из однострочного текстового поля и расположенной справа от него кнопки с надписью Просмотр (Browse), предназначенной для выбора файла на локальном диске.

Кроме того, в атрибуте ENCTYPE тега <FORM> для передачи файлов предлагается указывать тип данных multipart/form-data, что отличается от привычного формата application/x-www-form-urlencoded.

Формат данных multipart/form-data позволяет передавать данные типа MIME и, в частности, произвольные двоичные данные, которыми в общем случае являются все файлы. Что же касается формата application/x-www-form-urlencoded, используемого по умолчанию, то он пригоден только для передачи текстовых данных.

Документы RFC носят рекомендательный характер, поэтому разработчики программного обеспечения вправе принимать их или игнорировать. В частности, спецификация удаленного приема файлов RFC1867 использовалась браузером Netscape Navigator начиная с версии 2.0, но полностью игнорировалась браузером Microsoft Internet Explorer любой версии вплоть до 4.0. Тем не менее, современные версии браузера Microsoft Internet Explorer полностью соответствуют требованиям документа RFC1867.

Одна из идей использования технологии File Upload родилась у нас при обсуждении вопросов антивирусной защиты с генеральным директором АО «ДиалогНаука» Сергеем Григорьевичем Антимоновым. Мы предложили идею организовать бесплатную антивирусную проверку файлов пользователей Интернета самыми новыми версиями антивирусных программ, созданных в этой фирме. Эта идея была реализована Максимом Синевым (компания Spektrum Web Development, http://www.spektrum.org.ru) в виде расширения ISAPI для сервера IIS.

Каждый пользователь Интернета, загрузив с помощью браузера соответствующую страницу с сервера АО «ДиалогНаука» может проверить любой свой файл или архив файлов на предмет зараженности вирусами (рис. 11-10).

Рис. 11-10. Страница узла Web АО «ДиалогНаука», предназначенная для поиска вирусов в файлах посетителей

В нижней части этого рисунка расположена форма, состоящая из списка, элемента управления, предназначенного для выбора файла, и кнопки Go!. Список позволяет Вам выбрать антивирусную программу, с помощью которой будет выполняться проверка (Aidstest или Doctor Web).

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

Выбрав файл, щелкните кнопку Go!. Файл будет передан на сервер Web АО «ДиалогНаука», где его обработает соответствующее расширение ISAPI. После приема файла расширение запустит антивирусную программу, выбранную пользователем из списка, и проверит с ее помощью присланный файл.

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

Рис.11-11. Результаты удаленной антивирусной проверки

В данном случае был проверен файл h:\!!WebDevelopment\eicar.com — тестовый файл для проверки работы антивирусных программ.

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

Технология удаленной загрузки файлов через браузер используется практически всеми системами бесплатной электронной почты, сделанными на базе серверов Web. На рис. 11-12 мы показали форму, предназначенную для загрузки файлов, присоединяемых к отправляемым сообщениям (файлов вложений, attachment file) в популярной почтовой системе Microsoft Hotamail (http://www.hotmail.com).

Рис. 11-12. Форма для загрузки на сервер присоединенных файлов

Если щелкнуть кнопку Browse, на экране посетителя узла Microsoft Hotmail появится диалоговое окно выбора файла Choose file (рис. 11-13).

Рис. 11-13. Диалоговое окно выбора файла Choose file

Выбрав с помощью этого окна файл, присоединяемый к сообщению, нужно дождаться завершения его отправки на сервер и затем щелкнуть кнопку Attach to Message. Файл будет добавлен к сообщению. Таким способом Вы можете добавить несколько файлов.

Исходные тексты приложения FILEUPL

Мы подготовили для Вас исходные тексты расширения ISAPI fileupl.dll, способного передавать файлы описанным выше образом. Вы можете использовать их как прототип для разработки собственных систем удаленного приема и обработки файлов, аналогичных только что описанной системы удаленной антивирусной проверки файлов.

Ниже в разделе «Загрузка файлов в Интернет-магазине ITBOOK.RU» мы опишем исходные тексты более сложного модуля ISAPI с названием FileUpload версии 1.1. Этот модуль применяется в приложении Back-офиса Интернет-магазина для загрузки на сервер файлов изображений обложек книг и файлов PDF с примерами глав.

Исходный текст документа HTML, содержащий форму для приема файлов и элементы управления для ввода другой информации, представлен в листинге 11-16.

Листинг 11-16 хранится в файле chap11\fileupl\fileupl.htm на прилагаемом к книге компакт-диске.

Здесь Вам нужно обратить внимание на атрибуты тега <FORM>, с помощью которого в документе HTML создается форма:

<FORM ENCTYPE="multipart/form-data" METHOD=POST
ACTION="http://frolov/scripts/fileupl.dll">

Атрибут ENCTYPE задает тип кодировки передаваемых данных как multipart/form-data. Метод передачи данных указан как POST, а в теге ACTION указан адрес URL файла библиотеки DLL нашего расширения ISAPI.

Элемент управления, предназначенный для выбора локального файла, создается тегом <INPUT> следующим образом:

<TR>
  <TD VALIGN=TOP>Select Uploaded File:</TD>
  <TD><INPUT TYPE=FILE NAME="fupload"></TD>
</TR>

Здесь указан тип поля FILE и имя поля fupload.

Внешний вид формы, содержащий элемент управления для выбора файла, показан на рис. 11-14.

Рис. 11-14. Форма, позволяющая выбирать файл для передачи серверу WWW

На этом рисунке в поле Select Uploaded File уже выбран файл c:\arcsetup.exe. Если щелкнуть кнопку Browse, на экране появится диалоговое окно Choose File, показанное на рис. 11-15.

Рис. 11-15. Диалоговое окно Choose File, с помощью которого можно выбрать файл для передачи на сервер Web

Теперь, если выбрать файл и щелкнуть кнопку Send, файл и данные из других полей формы будут переданы расширению ISAPI fileupl.dll. Расширение запишет принятые данные без какой-либо обработки в файл и возвратит пользователю сообщение (в виде динамически созданного документа HTML) об успешном завершении пересылки файла, показанное на рис. 11-16.

Рис. 11-16. Сообщение об успешном завершении передача файла

Рассмотрим исходные тексты расширения, приведенные в листинге 11-17.

Листинг 11-17 хранится в файле chap11\fileupl\fileupl.с на прилагаемом к книге компакт-диске.

Файл определения модуля для библиотеки DLL представлен в листинге 11-18.

Листинг 11-18 хранится в файле chap11\fileupl\fileupl.def на прилагаемом к книге компакт-диске.

Рассмотрим функции нашего расширения ISAPI.

Функция GetExtensionVersion не имеет никаких особенностей.

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

Если данные были приняты успешно, функция HttpExtensionProc создает файл, в который будут записаны принятые данные:

hOutFile = CreateFile("e:\\InetPub\\scripts\\uploaded.dat",
      GENERIC_WRITE, FILE_SHARE_READ, NULL,
      CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

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

Для создания файла мы использовали функцию CreateFile.

Запись принятых данных в файл выполняется за один вызов функции WriteFile:

WriteFile(hOutFile, lpDataMIME, lpECB->cbTotalBytes, &dwWritten, NULL);

В качестве размера блока данных здесь указывается содержимое поля cbTotalBytes структуры типа EXTENSION_CONTROL_BLOCK. После выполнения записи файл закрывается функцией CloseHandle. Блок памяти, полученный от функции ReadClientMIME, освобождается при помощи функции LocalFree.

Далее расширение создает документ HTML в буфере szBuff и посылает его удаленному пользователю при помощи функции ServerSupportFunction с кодом операции HSE_REQ_SEND_RESPONSE_HEADER.

Теперь займемся функцией ReadClientMIME.

В качестве первого параметра эта функция получает указатель на блок EXTENSION_CONTROL_BLOCK, передаваемый функции HttpExtensionProc  через единственный параметр. Второй параметр nStatus используется для передачи результата работы функции ReadClientMIME вызывающей программе.

В самом начале своей работы функция ReadClientMIME анализирует содержимое поля cbTotalBytes структуры EXTENSION_CONTROL_BLOCK, в котором находится размер принимаемых данных. Если данных для чтения нет, функция ReadClientMIME передает код ошибки HSE_STATUS_ERROR и возвращает вызывающей программе значение NULL.

Если все нормально и данные для чтения есть, функция ReadClientMIME с помощью функции LocalAlloc заказывает блок памяти размером lpECB‑>cbTotalBytes байт.

После этого начинается процесс копирования данных в полученный буфер.

Вначале функция ReadClientMIME копирует блок данных из буфера предварительного чтения, вызывая для этого функцию memcpy:

memcpy(lpTemp, lpECB->lpbData, lpECB->cbAvailable);

Напомним, что размер этого буфера не превышает 48 Кбайт. Буфер предварительного чтения располагается по адресу lpECB->lpbData и в нем доступно для чтения lpECB->cbAvailable байт данных.

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

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

nBufferPos = lpECB->cbAvailable;

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

nBytesToCopy = lpECB->cbTotalBytes - lpECB->cbAvailable;

После проверки значения nBytesToCopy (оно должно быть больше нуля) мы запускаем цикл чтения дополнительных данных:

while(1)
{
  // Читаем очередную порцию данных в текущую
  // позицию буфера
  lpECB->ReadClient(lpECB->ConnID,
    (LPVOID)((LPSTR)lpTemp + nBufferPos), &cbReaded);
    
  // Уменьшаем содержимое переменной nBytesToCopy,
  // в которой находится размер непрочитанных данных
  nBytesToCopy -= cbReaded;

  // Продвигаем указатель текущей позиции в буфере
  // на количество прочитанных байт данных
  nBufferPos   += cbReaded;
    
  // Когда копировать больше нечего, прерываем цикл
  if(nBytesToCopy <= 0l)
    break;
}

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

Функция ReadClient не обязательно прочитает за один прием все входные данные. Размер прочитанного ей блока данных записывается в переменную cbReaded.

Далее в цикле уменьшается содержимое переменной nBytesToCopy, хранящей количество еще не прочитанных данных. После этого указатель текущей позиции nBufferPos в буфере lpTemp продвигается вперед на количество прочитанных байт cbReaded. Условием завершения цикла является уменьшение значения nBytesToCopy до нуля. Это произойдет, когда все данные будут приняты.

Теперь о функции GetMIMEBoundary, которая определена в файле исходных текстов, но нигде не используется. Эта функция предназначена для получения строки разделителя блоков принятого файла.

Как мы уже говорили, принятый файл имеет формат MIME. Полное описание этого формата Вы найдете в документах RFC2045, RFC2046, RFC2047, RFC2048 и RFC2049. Первый из этих документов называется «Multipurpose Internet Mail Extensions»”. Однако для работы с принятым файлом Вы можете обойтись без полной спецификации MIME.

Ниже мы привели в сокращенном виде содержимое файла e:\\InetPub\\scripts\\uploaded.dat после приема файла драйвера 800.com, предназначенного для работы с нестандартными форматами дискет. Помимо этого файла в принятых данных есть содержимое всех полей формы:

-----------------------------264872619131689
Content-Disposition: form-data; name="text1"

Sample of text1
-----------------------------264872619131689
Content-Disposition: form-data; name="pwd"

Sample of password
-----------------------------264872619131689
Content-Disposition: form-data; name="text2"

Sample of text
-----------------------------264872619131689
Content-Disposition: form-data; name="chk1"

on
-----------------------------264872619131689
Content-Disposition: form-data; name="chk3"

on
-----------------------------264872619131689
Content-Disposition: form-data; name="rad"

on1
-----------------------------264872619131689
Content-Disposition: form-data; name="fupload"; filename="C:\UT\800.com"

йtf
*“*t*—*—_
Ђьv_Ђъv_ъ.я.__V3цЋЮ‹тѓжЂь_sd_SRД_x
‹ыѕI№
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Здесь располагаются двоичные данные принятого файла.
Мы сократили листинг, выкинув из него часть двоичных данных
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
ђЗ___ffЗ___ !&Ђ>__Tt____ѕ5cї__№_
юЛЃщ 't_юЛГґ_І
. Vc&ўW°й‹
_ЊИ-

-----------------------------264872619131689
Content-Disposition: form-data; name="sel"

First Option
-----------------------------264872619131689
Content-Disposition: form-data; name="hid"

Hidden
-----------------------------264872619131689—

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

Каков дальнейший алгоритм получения данных из полей формы, а также принятого файла?

Он несложен и Вы сможете реализовать его самостоятельно. Сканируя секции с использованием разделителя, Вы можете в каждой секции искать строку name=<ИмяПоля>, где ИмяПоля — это имя поля, данные из которого Вам нужно получить. Для сканирования лучше не пользоваться функцией strstr, так как она рассчитана только на символьные данные, а в секциях, содержащих файлы, присутствуют двоичные данные. Найдя нужное поле, Вы можете извлечь его содержимое и записать его в память или файл на диске.

Сделаем еще одно замечание, касающееся многопоточного режима работы расширений ISAPI.

Так как для повышения производительности расширение ISAPI загружается в адресное пространство сервера Microsoft Information Server в единственном экземпляре, оно работает в многопоточном режиме. Это означает, что при обращении к критичным ресурсам Вы должны использовать средства синхронизации потоков.

В частности, расширение fileupl.dll выполняет запись в файл, а эта операция является критической, так как всем пользователям предлагается записывать свои данные только в один файл. Чтобы избежать взаимных коллизий, можно предложить простейшее средство синхронизации — критические секции.

Перед началом записи в файл расширение ISAPI должно войти в критическую секцию, а после использования — выйти из нее. В этом случае пользователи будут работать с файлом по очереди. Для работы с критическими секциями предназначены функции InitializeCriticalSection, EnterCriticalSection, LeaveCriticalSection, DeleteCriticalSection.

Критическая секция должна быть создана в момент инициализации библиотеки DLL расширения ISAPI, поэтому вызов функции InitializeCriticalSection должен быть размещен в функции DllMain. Удаление критической секции можно выполнить в обработчике функции TerminateExtension, которая вызывается перед удалением расширения из адресного пространства сервера Web.

Загрузка файлов в Интернет-магазине ITBOOK.RU

Коммерческая версия модуля загрузки файлов, использующего технологию из RFC1867, несколько сложнее описанного выше демонстрационного примера. На компакт-диске, прилагаемом к книге, есть исходные тексты приложения ISAPI с названием FileUpload версии 1.1, разработанного нами специально для Интернет-магазина ITBOOK.RU.

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

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

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

Вот пример файла, записывающего все необходимые параметры в регистрационную базу данных операционной системы:

REGEDIT4
[HKEY_LOCAL_MACHINE\SOFTWARE\Alexandre.Frolov]
[HKEY_LOCAL_MACHINE\SOFTWARE\Alexandre.Frolov\ISAPI]
[HKEY_LOCAL_MACHINE\SOFTWARE\Alexandre.Frolov\ISAPI\FileUpload]
"Version"="1.1"
"MaxFileSize"="3000"
"UploadRootPath"="c:\\web_projects\\my_project\\html\\upload"

Здесь ключ Version хранит версию расширения ISAPI, ключ MaxFileSize задает максимальный размер загружаемых файлов (в килобайтах), а ключ UploadRootPath задает физический путь к каталогу, в который выполняется загрузка файлов (этот каталог должен располагаться на том же компьютере, где установлен сервер IIS).

Инициализация модуля

Полный исходный текст модуля FileUpload Вы найдете в листинге 11-17. Мы рассмотрим только самые важные его фрагменты.

Листинг 11-17 хранится в файле chap11\FileIpload 1.1\fileupload.cpp на прилагаемом к книге компакт-диске.

При инициализации функция GetExtensionVersion модуля FileUpload открывает свой ключ регистрационной базы данных и проверяет версию модуля:

char szKeyName[] = "Alexandre.Frolov\\ISAPI\\FileUpload";
Registry reg;
. . .
try
{
  reg.CheckAndCreateKey(szKeyName);
  reg.open(szKeyName);
 
  if(reg.getVersion() != "1.1")
     throw ErrorException(1, "Incorrect version: " + reg.getVersion(), "GetExtensionVersion");
  reg.close();
}
catch(ErrorException ex)
{
  return FALSE;
}
return TRUE;

В блоке try-catch мы вызываем методы CheckAndCreateKey и open, определенные в классе Registry. Первый из этих методов проверяет существование ключа приложения Alexandre.Frolov\\ISAPI\\FileUpload. Если такого ключа нет, он создается автоматически.

Далее происходит извлечение версии модуля методом getVersion и сравнение его с текстовой строкой «1.1». Если версия правильная, инициализация считается успешной, если нет — происходит аварийное завершение инициализации.

После этого ключ закрывается методом close.

Класс Registry является составной частью приложения FileUpload. Его исходные тексты приведены в листинге 11-18.

Листинг 11-18 хранится в файле chap11\FileIpload 1.1\registry.cpp на прилагаемом к книге компакт-диске.

Ниже мы привели исходный текст метода CheckAndCreateKey с необходимыми комментариями:

void Registry::CheckAndCreateKey(string sKey)
{
  //
Открываем ключ HKEY_LOCAL_MACHINE\\SOFTWARE
  rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE", 0, KEY_CREATE_SUB_KEY, &hKeySoftware);
 
  if(rc != ERROR_SUCCESS)
     throw ErrorException(1, rc, "Registry::CheckAndCreateKey");
 
 
// Проверяем существование ключа приложения
  // ----------------------------------------

  // Открываем ключ приложения
  rc = RegOpenKeyEx(hKeySoftware, sKey.c_str(), 0, KEY_READ, &hKey);

  if(rc == ERROR_SUCCESS)
  {
    //
Закрываем ключи
    rc = RegCloseKey(hKeySoftware);
   
    if(rc != ERROR_SUCCESS)
       throw ErrorException(2, rc, "Registry::CheckAndCreateKey");

    rc = RegCloseKey(hKey);
   
    if(rc != ERROR_SUCCESS)
       throw ErrorException(3, rc, "Registry::CheckAndCreateKey");
    return;
 
}

  // Если ключа нет, пытаемся его создать
  // -------------------------------------
 

  // Создаем ключ приложения
  rc = RegCreateKeyEx(hKeySoftware, sKey.c_str(),
      0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, &dwDisposition);
 
  if(rc != ERROR_SUCCESS)
     throw ErrorException(4, rc, "Registry::CheckAndCreateKey");

  //
Закрываем ключ HKEY_LOCAL_MACHINE\\SOFTWARE
  rc = RegCloseKey(hKeySoftware);
  if(rc != ERROR_SUCCESS)
     throw ErrorException(5, rc, "Registry::CheckAndCreateKey");

  //
Версия приложения
  rc = RegSetValueEx(hKey, "Version", 0, REG_SZ, (BYTE *)"1.1", strlen("1.1") + 1);

  if(rc != ERROR_SUCCESS)
     throw ErrorException(6, rc, "Registry::CheckAndCreateKey");

 
// Максимальный размер загружаемых файлов (Кбайт)
  rc = RegSetValueEx(hKey, "MaxFileSize", 0, REG_SZ, (BYTE *)"300", strlen("300") + 1);
 
  if(rc != ERROR_SUCCESS)
     throw ErrorException(7, rc, "Registry::CheckAndCreateKey");

 
// Физический путь к каталогу, предназначенному для загрузки файлов
  rc = RegSetValueEx(hKey, "UploadRootPath", 0, REG_SZ, (BYTE *)"c:\\upload", strlen("c:\\upload") + 1);
 
  if(rc != ERROR_SUCCESS)
     throw ErrorException(8, rc, "Registry::CheckAndCreateKey");

 
// Записываем и закрываем ключ
  rc = RegFlushKey(hKey);
 
  if(rc != ERROR_SUCCESS)
     throw ErrorException(9, rc, "Registry::CheckAndCreateKey");

  rc = RegCloseKey(hKey);
 
  if(rc != ERROR_SUCCESS)
     throw ErrorException(10, rc, "Registry::CheckAndCreateKey");
}

Обратите внимание, что при создании нового ключа максимальный размер загружаемых файлов устанавливается равным 300 Кбайт, а путь для загрузки файлов — c:\upload.

Открытие ключа приложения в регистрационной базе данных выполняется методом open, исходный текст которого приведен ниже:

void Registry::open(string sKey)
{
  rc = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE", 0, KEY_ENUMERATE_SUB_KEYS, &hKeySoftware);
 
  if(rc != ERROR_SUCCESS)
     throw ErrorException(1, rc, "Registry::CheckAndCreateKey");

  rc = RegOpenKeyEx(hKeySoftware, sKey.c_str(), 0, KEY_READ, &hKey);
 
  if(rc != ERROR_SUCCESS)
     throw ErrorException(2, rc, "Registry::open");
}

Так как мы собираемся только читать данные, ключ открывается в режиме «только чтение». Для этого функции RegOpenKeyEx в четвертом параметре передается значение KEY_READ.

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

Завершив работу с ключом регистрационной базы данных, не забудьте его закрыть. Это можно сделать методом close:

void Registry::close()
{
  rc = RegCloseKey(hKey);
  if(rc != ERROR_SUCCESS)
     throw ErrorException(1, rc, "Registry::close");
}

Здесь мы просто вызываем функцию RegCloseKey, закрывающую ключ.

Функция HttpExtensionProc

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

DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB)
{
  try
  {
    reg.open(szKeyName);
    sUploadRootPath = reg.getUploadRootPath();
    dwMaxFileSize = reg.getMaxFileSize();
   
    if(dwMaxFileSize  == 0)
       throw ErrorException(1, "HttpExtensionProc");
    reg.close();
  }
  catch(ErrorException ex)
  {
    sErrID = "000";
    sErrMsg = "Invalid Registry Parameters";
    return replyError(lpECB);
  }

  LPVOID lpData;
  int  nStatus = 0;

  lpData = readClientData(lpECB, &nStatus);

  if(lpData != NULL && nStatus == 0)
  {
    try
    {
      parseData(lpData, lpECB->cbTotalBytes);
    }
    catch(ErrorException ex)
    {
      if(lpData != NULL)
        LocalFree(lpData);
      return replyError(lpECB);
    }

    if(lpData != NULL)
      LocalFree(lpData);
    return replyOK(lpECB);
  }
  else
  {
    if(lpData != NULL)
      LocalFree(lpData);
    return replyError(lpECB);
 
}
}

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

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

Функция parseData

Принятые данные анализируются функцией parseData. Именно эта функция извлекает файл и записывает его в нужный каталог.

Рассмотрим самые важные фрагменты исходного текста функции (полный текст Вы найдете в листинге 11-17).

Получив управление, функция parseData ищет в принятом блоке данных разделитель MIME:

CHAR szBoundary[256];
if(!GetMIMEBoundary(lpUploadedData, szBoundary, dwDataSize))
  throw ErrorException(1, "parseData");

Далее она выделяет из него разделитель для поиска конца файла:

CHAR szFileBoundary[256];
strcpy(szFileBoundary, "\r\n");
strcat(szFileBoundary, szBoundary);

После этого инициализируется текущий указатель во входном буфере данных lpCurrent и счетчик обработанных байт данных i:

LPSTR lpCurrent = (LPSTR)lpUploadedData;
DWORD i = 0;

Далее идет подготовка переменных к запуску цикла сканирования буфера, адрес начала которого хранится в переменной lpUploadedData, а адрес текущей позиции — в переменной lpCurrent:

CHAR szFieldName[MAX_PATH];     // Имя поля из формы
CHAR szFieldValue[4096];        // Данные поля из формы
CHAR szFieldFileName[MAX_PATH];
DWORD j;                        // Переманная цикла
BOOL bEndOfFile = FALSE;
char szUploadedFilePathRegName[MAX_PATH];
BOOL fUploadedFilePath = FALSE;
BOOL fURL_OK = FALSE;
BOOL fURL_ERR = FALSE;

Массивы szFieldName и szFieldValue предназначены, соответственно, для хранения имени поля, извлеченного из данных формы, и значения этого поля. В переменную szFieldFileName будет записано имя принятого файла.

В начале цикла вызываются макрокоманды FIND_BOUNDARY, FIND_FIELD_NAME и GET_FIELD_NAME:

while(TRUE)
{
  FIND_BOUNDARY
  FIND_FIELD_NAME
  GET_FIELD_NAME
  . . .

Первая из них находит разделитель, вторая извлекает имя поля формы HTML, а третья — значение этого поля. Имя записывается в переменную szFieldName, а значение — в переменную szFieldValue.

Далее функция перемещается вперед по буферу:

lpCurrent++; i++;

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

if(memcmp(lpCurrent, "; filename=", 10) == 0)
{
  // Проверяем, все ли поля формы присутствуют
  if(!fUploadedFilePath || !fURL_OK || !fURL_ERR)
  {
    sErrID = "001";
    sErrMsg = "Invalid upload form fields values";
    throw ErrorException(2, "parseData");
  }
  . . .

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

// Конструируем физический путь для сохранения файла
sUploadedFilePath = "";
sUploadedFilePath += (sUploadRootPath + "\\");
sUploadedFilePath += szUploadedFilePathRegName;

Теперь извлекается имя принятого файла и сохраняется в переменной szFieldFileName:

lpCurrent += 12;
//
Копируем имя файла
GET_FILE_NAME
FIND_HEADER_END

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

CRITICAL_SECTION csSaveFile;
InitializeCriticalSection(&csSaveFile);
EnterCriticalSection(&csSaveFile);

Как мы говорили ранее, это необходимо из-за многопоточного режима работы расширений ISAPI.

Войдя в критическую секцию, мы открываем выходной файл для записи:

FILE *downloaded;
downloaded = fopen(sUploadedFilePath.c_str(), "wb");
if(downloaded == NULL)
{
  LeaveCriticalSection(&csSaveFile);
  DeleteCriticalSection(&csSaveFile);
  sErrID = "002";
  sErrMsg = "Destination file open error";
  throw ErrorException(3, "parseData");
}

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

while(TRUE)                         

  // Пока не найден разделитель конца файла
  if(memcmp(lpCurrent, szFileBoundary, strlen(szFileBoundary)) == 0)     
    break;

  // Побайтное копирование в выходной файл
  if(EOF == fputc(*lpCurrent, downloaded))
  {
    fclose(downloaded);
    LeaveCriticalSection(&csSaveFile);
    DeleteCriticalSection(&csSaveFile);
    sErrID = "003";
    sErrMsg = "Destination file write error";
    throw ErrorException(4, "parseData");
  }

  lpCurrent++; i++;                 
  CHECK_DATA_END
}

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

После завершения цикла выходной файл закрывается функцией fclose:

fclose(downloaded);

После того как файл загружен, функция parseData проверяет его содержимое, вызывая для этого функцию getFileType:

try
{
  sUploadedFileType=getFileType(sUploadedFilePath);
}
catch(ErrorException ex)
{
  remove(sUploadedFilePath.c_str());

  LeaveCriticalSection(&csSaveFile);
  DeleteCriticalSection(&csSaveFile);
  throw ex;
}

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

Имя принятого файла не имеет расширения. Наша программа переименовывает его в соответствии с динамически определенным типом исходного файла (GIF, JPG или PDF):

string sNewName = sUploadedFilePath + "." + sUploadedFileType;
DeleteFile(sNewName.c_str());

for(int waitcnt=0; waitcnt < 18; waitcnt++) //
ждем 3 минуты
{
  // 0 -
если удалось переименовать
  if(0 ==rename(sUploadedFilePath.c_str(), sNewName.c_str()))
   break;
  Sleep(10000); //
задержка на 10 секунд
}
LeaveCriticalSection(&csSaveFile);
DeleteCriticalSection(&csSaveFile);

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

После завершения этой операции созданная ранее критическая секция освобождается и удаляется.

Обычные поля формы HTML обрабатываются следующим образом:

lpCurrent += 4;
GET_FIELD_DATA

// Данные из обычного поля записаны в szFieldValue
       
if(strlen(szFieldValue) < MAX_PATH)
{
  // Загружаем значения скрытых полей формы
  if(!strncmp(szFieldName, "upload_path", 11))
  {
    strcpy(szUploadedFilePathRegName, szFieldValue);

    // Избавляемся от попыток пробиться к корню диска
    if(!PathIsValid(szUploadedFilePathRegName))
      fUploadedFilePath = FALSE;
    else
      fUploadedFilePath = TRUE;
    }

    if(!strncmp(szFieldName, "url_ok", 11))
    {
      sURL_OK = szFieldValue;
      fURL_OK = TRUE;
    }

    if(!strncmp(szFieldName, "url_err", 11))
    {
      sURL_ERR = szFieldValue;
      fURL_ERR = TRUE;
    }
  }
}

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

Форма выбора загружаемого файла

На рис. 11-17 мы показали форму, которая применяется в Back-офисе Интернет-магазина ITBOOK.RU для загрузки на сервер Web файла графического изображения логотипа книжной серии. Подробнее о Back-офисе мы расскажем в 15 главе этой книги.

Рис. 11-17. Форма выбора загружаемого файла

В документе HTML, содержащем эту форму, имеется форма и клиентский сценарий JavaScript, предназначенный для отправки формы на сервер.

Исходный текст формы (в сокращенном виде) представлен ниже:

<form ENCTYPE="multipart/form-data" METHOD="post"
ACTION="http://localhost/scripts/fileupload.dll" id=form1 name=form1>
<input type="hidden" name="upload_path" value="series_pic\56">
<input type="hidden" name="url_ok"
value="http://localhost/book_series/add_series2.asp?ID=56&amp;IMG_TYPE=gif">
<input type="hidden" name="url_err"  value="http://localhost/book_series/update_series1.asp?ID=56&amp;IMG_TYPE=gif">
<p><INPUT name=fupload type=file ></p>
<p><font size=2>
Щелкните кнопку <STRONG>Browse </STRONG>и выберите файл изображения логотипа серии (gif или jpg).
<br>Максимальный размер файла, Кбайт: 3000
<br>Рекомендуемый размер файла, Кбайт: < 20</font></p>
<p><input TYPE="button" VALUE="Далее >>" style ="LEFT: 9px; TOP: 161px" id=submit1 name=submit1 LANGUAGE=javascript onclick="return submit1_onclick()">
<INPUT id=btn_cancel name=btn_cancel type=button value=
Отменить LANGUAGE=javascript onclick="return btn_cancel_onclick()">
</form>

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

Исходный текст сценария JavaScript представлен ниже:

<SCRIPT ID=clientEventHandlersJS LANGUAGE=javascript>
<!--
function btn_cancel_onclick() {
  window.location.href="default.asp"
}
function submit1_onclick() {
  if(window.form1.fupload.value == "")
    alert("Выберите файл изображения");
  else
    document.forms[0].submit();
}
//-->
</SCRIPT>

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

Perl и отправка данных формы HTML по электронной почте

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

Между тем программы CGI, а также автономные программы, выполняющие на сервере Web те или иные задачи, можно составлять на языке высокого уровня с названием Perl, разработанного еще в 1980-х годах.

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

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

К тому же, Perl бесплатен и доступен через Интернет. Вы можете найти в Интернете огромное количество исходных текстов готовых программ, имеющих самое резное назначение. Если Вы решили попробовать свои силы в программировании на Perl, ознакомьтесь с дополнительной литературой, после чего посетите узлы Web с адресами http://www.activestate.com и http://www.perl.com.

Форма для отправки почтового сообщения

Рассмотрим программу CGI, составленную на Perl и используемую на нашем узле Web службы восстановления данных DataRecovery.Ru для отправки срочной заявки на выполнение аварийных работ. Соответствующая страница узла http://www.datarecovery.ru показана на рис. 11-18.

Рис. 11-18. Форма для отправки срочного сообщения

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

Ниже мы привели фрагмент исходного текста страницы экстренной помощи с только что описанной формой:

<form method="POST" action="http://www.datarecovery.ru/urgent_mail.pl">
<table border="0" width="100%" cellspacing="5" cellpadding="0">
<tr>
<td width="148"><input type="text" name="PHONE" size="20" class="table"></td>
<td><FONT CLASS="table">
Номер Вашего телефона в Москве</FONT></td>
</tr>
<tr>
<td width="148"><input type="text" name="NAME" size="20" class="table"></td>
<td><font class="table">
Ваше имя и фамилия</font></td>
</tr>
<tr>
<td width="148">
<select size="1" name="FS" CLASS="table">
  <option selected value="NTFS">NTFS</option>
  <option value="FAT">FAT</option>
  <option value="OTHR">
Другая</option>
  <option value="UNKN">
Не знаю</option>
</select></td>
<td><FONT CLASS="table">
Файловая система</FONT></td>
</tr>
<tr>
<td width="148">
<select size="1" name="OS" CLASS="table">
  <option selected value="NT">Windows NT</option>
  <option value="2K">Windows 2000</option>
  <option value="98">Windows 95/98/ME</option>
  <option value="OTHR">
Другая</option>
  <option value="UNKN">
Не знаю</option>
  </select></td>
<td><FONT CLASS="table">
Операционная система</FONT></td>
</tr>
</table>
<p><input type="submit" value="
Экстренный вызов" name="B1" CLASS="table"></p>
</form>

Как видите, атрибут ACTION тега FORM задает путь к программе обработки данных формы как http://www.datarecovery.ru/urgent_mail.pl. Файл urgent_mail.pl представляет собой программу CGI, составленную на языке программирования Perl (листинг 11-19).

Поля формы с именами PHONE и NAME предназначены, соответственно, для ввода номера контактного телефона, а также имени контактного лица. Список FS позволяет выбрать тип файловой системы, а список OS — тип операционной системы.

Исходный текст программы urgent_mail.pl

Рассмотрим исходный текст программы urgent_mail.pl (листинг 11-19).

Листинг 11-19 хранится в файле chap11\urgentmail\urgentmail.pl на прилагаемом к книге компакт-диске.

Начальный фрагмент программы характерен для всех программ CGI, составленных на языке Perl:

#!/usr/bin/perl -w

В первой строке мы указываем путь к интерпретатору Perl. Если программа выполняется под управлением операционной системы Microsoft Windows NT или Microsoft Windows 2000, указанный в этой строке путь /usr/bin/perl не используется. Параметр –w указывает на то, что интерпретатор должен выводить все предупреждающие сообщения об ошибках.

Вторая строка заставляет интерпретатор Perl выполнять строгую проверку использования необъявленных и неинициализированных переменных:

use strict;

Далее мы подключаем к программе два дополнительных модуля CGI и Net (вторая строка направляет сообщения об ошибках программы в окно браузера, что облегчает отладку):

use CGI qw(:all);
use CGI::Carp qw(fatalsToBrowser);
use Net::SMTP;

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

В программе urgent_mail.pl определены две функции с именами win2koi и send_mail.

Функция win2koi

Функция win2koi перекодирует текстовую строку из кодировки Windows-1251 в КОИ-8, что необходимо для передачи символов кириллицы через электронную почту:

sub win2koi
{
  my($from)=@_;
  $_=$from;
  tr/\0\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\40\41\42\43\44\45\46\47\50\51\52\53\54\55\56\57\60\61\62\63\64\65\66\67\70\71\72\73\74\75\76\77\100\101\102\103\104\105\106\107\110\111\112\113\114\115\116\117\120\121\122\123\124\125\126\127\130\131\132\133\134\135\136\137\140\141\142\143\144\145\146\147\150\151\152\153\154\155\156\157\160\161\162\163\164\165\166\167\170\171\172\173\174\175\176\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377/\0\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\40\41\42\43\44\45\46\47\50\51\52\53\54\55\56\57\60\61\62\63\64\65\66\67\70\71\72\73\74\75\76\77\100\101\102\103\104\105\106\107\110\111\112\113\114\115\116\117\120\121\122\123\124\125\126\127\130\131\132\133\134\135\136\137\140\141\142\143\144\145\146\147\150\151\152\153\154\155\156\157\160\161\162\163\164\165\166\167\170\171\172\173\174\175\176\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\341\342\367\347\344\345\366\372\351\352\353\354\355\356\357\360\362\363\364\365\346\350\343\376\373\375\377\371\370\374\340\361\301\302\327\307\304\305\326\332\311\312\313\314\315\316\317\320\322\323\324\325\306\310\303\336\333\335\337\331\330\334\300\321/;
  return $_;
}

Записав входной параметр в служебную переменную с именем $_, эта функция перекодирует полученную текстовую строку при помощи оператора tr. При этом оператору tr передается полная таблица  перекодировки.

Функция win2koi возвращает результат перекодировки при помощи оператора return.

Функция send_mail

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

my $relay="mail.domain.ru";
sub send_mail
{
  my($to, $from, $subject, @body)=@_;

  my $smtp = Net::SMTP->new($relay);
  return 1  if (! defined $smtp);
 
  $smtp->mail($from);
  $smtp->to($to);
  $smtp->data();
  $smtp->datasend("To: $to\n");
  $smtp->datasend("From: $from\n");
  $smtp->datasend("Subject: $subject\n");
  $smtp->datasend("\n");
  foreach(@body)
  {
    $smtp->datasend("$_\n");
  }
  $smtp->dataend();
  $smtp->quit;
  return 0;
}

Функции send_mail передаются четыре параметра, которые сохраняются в переменных $to, $from, $subject и @body.

Переменные $to и $from задают, соответственно, адрес электронной почты получателя и отправителя сообщения. В переменную $subject записывается тело сообщения, а в переменную @body — текст сообщения.

Первое, что делает функция send_mail после обработки входных параметров, это создание нового объекта — канала передачи данных с почтовым сервером SMTP:

my $smtp = Net::SMTP->new($relay);
return 1  if (! defined $smtp);

Здесь в переменной $relay хранится доменное имя почтового сервера (имя mail.domain.ru мы привели только для примера, Вы должны заменить его именем своего почтового сервера).

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

Перед тем как вернуть управление, функция завершает процесс передачи данных на почтовый сервер методом dataend и закрывает канал связи с почтовым сервером, вызывая метод quit.

Обработка формы HTML

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

Вот как мы извлекаем из формы номер телефона, а также имя контактного лица:

my $phone=param("PHONE");
my $name=param("NAME");

Как видите, для этого нам потребовалось написать в программе всего две строки.

Чтобы не отправлять сообщение в тех случаях, когда посетитель забыл указать номер телефона или имя, мы выполняем простую проверку:

if(length($phone) < 7 || length($name) == 0)
{
 print header(-charset=>'windows-1251');
 open (INCOMPLETE, "mail_incomplete.htm") || die;
 while(<INCOMPLETE>) { print $_; }
 close (INCOMPLETE);
}
else
{
  . . .
 
# Отправка сообщения
  . . .
}

В номере телефона, хранящемся в переменной $phone, должно быть не менее 7 символов. Кроме того, длина имени контактного лица $name должна быть отлична от нуля.

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

Для этого вначале программа динамически формирует заголовок документа, указывая в нем кодировку символов Windows-1251:

print header(-charset=>'windows-1251');

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

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


$rc=send_mail('@'.$relay.':alexandre@frolov.pp.ru', 'web@dataRecovery.ru', 'Urgent!', param("PHONE")." ".win2koi(param("NAME"))." ".param("FS")." ".param("OS")." ");

$rc1=send_mail('@'.$relay.':datarecovery@sms_gate.ru',  'web@dataRecovery.ru', 'Urgent!', param("PHONE")." ".win2koi(param("NAME"))." ".param("FS")."
".param("OS")." ");

В первый раз сообщение отправляется по адресу alexandre@frolov.pp.ru, а во второй — на адрес шлюза с системой передачи сообщений SMS на мобильный телефон или пэйджер (адрес datarecovery@sms_gate.ru указан только для примера, в своих программах Вы должны заменить этот адрес другим).

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

Тема сообщения указана как «Urgent(«Срочно!»). Текст сообщения комбинируется из содержимого полей формы, заполненной посетителем узла Web службы восстановления данных.

Отправив сообщения, программа проверяет содержимое переменных $rc и $rc1, содержащие коды завершения функции send_mail:

if($rc != 0 || $rc1 != 0)
{
 print header(-charset=>'windows-1251');
 open (MAIL_ERROR, "mail_error.htm") || die;
 while(<MAIL_ERROR>) { print $_; }
 close (MAIL_ERROR);
}
else
{
 print header(-charset=>'windows-1251');
 open (MAIL_OK, "mail_ok.htm") || die;
 while(<MAIL_OK>) { print $_; }
 close (MAIL_OK);
}

Если оба сообщения отправлены без ошибок, посетителю отправляется содержимое файла mail_ok.htm, а в противном случае — содержимое файла mail_error.htm.

[Назад] [Содержание] [Дальше]