8. Приложения ISAPI

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

Расширения ISAPI по своему назначению напоминают уже изученные вами программы CGI. Однако в отличие от последних эти расширения выполнены в виде библиотек динамической компоновки DLL, что имеет ряд преимуществ. Так же как и программы CGI, расширения ISAPI получают данные от навигатора (например, из заполненной удаленным пользователем формы), обрабатывают их и посылают навигатору ответ. Однако вместо чтения содержимого переменных среды и стандартного потока ввода STDIN расширение 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 работает в мультизадачном режиме, что приводит к дополнительным проблемам при отладке. Особенности программирования для мультизадачного режима мы описали в 26 и 27 томах “Библиотеки системного программиста”, которые называются “Программирование для Windows NT. Часть 1” и “Программирование для Windows NT. Часть 2”. Там же вы найдете информацию о том, как создавать библиотеки DLL, предназначенные для работы в среде Microsoft Windows NT и Microsoft windows 95.

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

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

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

Функция GetExtensionVersion

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


// =============================================================
// Функция GetExtensionVersion
// =============================================================
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVersion)
{
  pVersion->dwExtensionVersion = 
    MAKELONG(HSE_VERSION_MINOR,HSE_VERSION_MAJOR);

  lstrcpyn(pVersion->lpszExtensionDesc,
    "Remote File Upload", 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 2 // верхний номер версии
#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;

Рассмотрим отдельные поля этой структуры.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Программа CGI получает данные из переменных среды и стандартного потока ввода STDIN (в случае применения метода доступа 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 содержит тип идентификации, который применяется сервером.

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

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

Тип данных, присланных навигатором.

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

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

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

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

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

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

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

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

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

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

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

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

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

Название и версия программного обеспечения сервера WWW. Версия следует после названия и отделяется от последнего символом “/”.

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

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

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

В случае успешного завершения функция GetServerVariable возвращает значение TRUE, а при возникновении ошибки - значение FALSE. Код ошибки можно определить с помощью функции GetLastError, вызвав ее сразу после функции 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 еще один или несколько раз для чтения оставшихся данных.

Пример использования функции ReadClient вы найдете в разделе “Приложение FILEUPL”. Там мы привели исходные тексты расширения ISAPI, позволяющее использовать сервер WWW довольно необычным способом - для получения файлов от удаленных пользователей и записи их на диск сервера. Так как размеры передаваемых файлов могут быть значительны, приложение FILEUPL вызывает функцию ReadClient в цикле.

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

Вместо того чтобы записывать выходные данные в стандартный поток вывода STDOUT, как это делает программа 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, определяющего код выполняемой операции:

Эта операция предназначена для посылки удаленному пользователю стандартного заголовка 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, расширение ISAPI может послать удаленному пользователю данные, заданные адресом URL, как будто бы эти данные были запрошены непосредственно пользователем по этому адресу URL. Такая возможность удобна для посылки либо динамически созданных данных, либо для посылки предварительно подготовленных данных.

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

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

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

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

Приложение ISHELLO

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

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

Листинг 8.1. Файл chap8\ishello\ishello.htm


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML>
  <HEAD>
    <TITLE>ISAPI Script Test</TITLE>
  </HEAD>
  <BODY BGCOLOR=#FFFFFF>
    <H1>Вызов расширения ISAPI</H1>

    <FORM METHOD=POST     ACTION="http://frolov/scripts/ishello.dll?Param1|Param2|Param3">
      <INPUT TYPE=submit VALUE="Send">
    </FORM>

  </BODY>
</HTML>

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

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

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

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

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


// ===============================================
// Расширение ISAPI ishello.c
// Пример простейшего расширения ISAPI
//
// (C) Фролов А.В., 1997
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//         или
//         http://www.dials.ccas.ru/frolov
// ===============================================

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

// =============================================================
// Функция GetExtensionVersion
// Запись версии интерфейса ISAPI и
// строки описания расширения
// =============================================================
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
  // Записываем версию интерфейса ISAPI
  pVer->dwExtensionVersion = 
    MAKELONG(HSE_VERSION_MINOR,HSE_VERSION_MAJOR );

  // Записываем строку описания расширения
  lstrcpyn(pVer->lpszExtensionDesc,
    "Simple ISAPI DLL", HSE_MAX_EXT_DLL_NAME_LEN);

  return TRUE;
}

// =============================================================
// Функция HttpExtensionProc
// =============================================================
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB)
{
  CHAR  szBuff[4096];
  CHAR  szTempBuf[4096];
  
  DWORD  dwSize;

  // Нулевой код состояния - признак успешного выполнения
  lpECB->dwHttpStatusCode = 0;

  // Записываем в буфер заголовок HTTP и начальный
  // фрагмент формируемого динамически документа HTML
  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, "<HR>");
 
  // Добавляем версию интерфейса ISAPI
  wsprintf(szTempBuf, "<P>Extension Version: %d.%d", 
    HIWORD(lpECB->dwVersion), LOWORD(lpECB->dwVersion));
  strcat(szBuff, szTempBuf);
  
  // Название метода передачи данных
  wsprintf(szTempBuf, "<BR>Method: %s", lpECB->lpszMethod);
  strcat(szBuff, szTempBuf);
  
  // Строка параметров запуска расширения ISAPI
  wsprintf(szTempBuf, "<BR>QueryString: %s", 
    lpECB->lpszQueryString);
  strcat(szBuff, szTempBuf);
  
  // Физический путь к программному файлу расширения ISAPI
  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);

  // Отображаем содержимое переменных сервера
  strcat(szBuff, "<HR><P><B>Server Variables:</B><BR>");

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

  // Конечный фрагмент документа HTML
  strcat(szBuff, "</BODY></HTML>");  

  // Посылаем содержимое буфера удаленному пользователю
  if(!lpECB->ServerSupportFunction(lpECB->ConnID,
    HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL, 
    (LPDWORD)szBuff))
  {
    // Если послать данные не удалось, 
    // завершаем работу нашего расширения ISAPI 
    // с кодом ошибки
    return HSE_STATUS_ERROR;
  }

  // Записываем код успешного завершения
  lpECB->dwHttpStatusCode = 200;
  
  // Возвращаем признак успешного завершения  
  return HSE_STATUS_SUCCESS;
}

Обратите внимание, что наряду с обычным для приложений Windows файлом windows.h мы включили в наш исходный текст файл httpext.h, в котором определены все необходимые константы, структуры данных и прототипы функций. Этот файл поставляется в составе Microsoft Visual C++ версии 4.2, а также в составе Internet SDK, который можно получить на сервере www.microsoft.com.

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

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

Прежде всего в буфер szBuff записывается заголовок HTTP и начальный фрагмент документа HTML, для чего используется функция wsprintf. Далее к буферу 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).

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

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


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

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


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

Если при посылке данных произошла ошибка, расширение завершает свою работу с кодом HSE_STATUS_ERROR.

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

Файл определения модуля для библиотеки DLL расширения ISAPI представлен в листинге 8.3.

Листинг 8.3. Файл chap8\ishello\ishello.def


LIBRARY	     ishello
DESCRIPTION  'Simple ISAPI DLL'
EXPORTS
    GetExtensionVersion
    HttpExtensionProc

Приложение ISFORM

Приложение ISFORM демонстрирует способ получения и обработки данных, полученных от формы, расширением ISAPI. Аналогичные действия выполняла программа CGI с именем CONTROLS, описанная нами в предыдущей главе.

Исходный текст документа HTML, содержащий форму, представлен в листинге 8.4. Эта форма уже использовалась нами ранее в предыдущей главе (рис. 7.2), поэтому мы не будем показывать ее внешний вид снова для экономии места.

Листинг 8.4. Файл chap8\isform\isform.htm


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML>
<HEAD>
 <TITLE>Органы управления в формах</TITLE>
</HEAD>
<BODY BGCOLOR=#FFFFFF>
<FORM METHOD=POST ACTION="http://frolov/scripts/isform.dll?Param1|Param2|Param3">
  <TABLE>
    <TR>
      <TD VALIGN=TOP>Текстовое поле TEXT</TD>
      <TD><INPUT TYPE=text NAME="text1" VALUE="Sample of text1" SIZE=30></TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Текстовое поле PASSWORD</TD>
      <TD><INPUT TYPE=password NAME="pwd" VALUE="Sample of password"></TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Текстовое поле TEXTAREA</TD>
      <TD><TEXTAREA NAME="text2" ROWS=4 COLS=30>Sample of text</TEXTAREA></TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Переключатели CHECKBOX</TD>
      <TD>
        <INPUT TYPE=CHECKBOX NAME="chk1" VALUE="on" CHECKED>Первый<BR>
        <INPUT TYPE=CHECKBOX NAME="chk2" VALUE="on">Второй<BR>
        <INPUT TYPE=CHECKBOX NAME="chk3" VALUE="on" CHECKED>Третий<BR>
      </TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Переключатели RADIO</TD>
      <TD>
        <INPUT TYPE=RADIO NAME="rad" VALUE="on1" CHECKED>Первый<BR>
        <INPUT TYPE=RADIO NAME="rad" VALUE="on2">Второй<BR>
        <INPUT TYPE=RADIO NAME="rad" VALUE="on3">Третий<BR>
      </TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Список</TD>
      <TD>
        <SELECT NAME="sel" SIZE="1">
          <OPTION Value="First Option">First Option</OPTION>
          <OPTION Value="Second Option">Second Option</OPTION>
          <OPTION Value="None">None Selected</OPTION>
        </SELECT>
      </TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Скрытый орган управления</TD>
      <TD><INPUT TYPE=HIDDEN NAME="hid" VALUE="Hidden"></TD>
    </TR>
  </TABLE>
<BR><INPUT TYPE=submit VALUE="Send">&nbsp;
<INPUT TYPE=reset VALUE="Reset">
<P><INPUT TYPE=IMAGE SRC="send.GIF" BORDER=0>
</FORM>
</BODY>
</HTML>

Вызов расширения ISAPI выполняется в форме с помощью параметра ACTION оператора <FORM>, как это показано ниже:


ACTION="http://frolov/scripts/isform.dll?Param1|Param2|Param3">

После разделительного символа “?” расширению передается строка параметров Param1|Param2|Param3.

Результат обработки формы показан на рис. 8.2.

Рис. 8.2. Результат обработки формы расширением ISAPI с именем isform.dll

Обратите внимание, что поля TotalBytes и Available содержат одинаковые значения. Следовательно, все принятые данные поместились в буфере предварительной загрузки. И это не удивительно - форма передала всего 127 байт данных.

Исходный текст расширения isform.dll показан в листинге 8.5.

Листинг 8.5. Файл chap8\isform\isform.c


// ===============================================
// Расширение ISAPI isform.c
// Обработка данных, полученных от формы,
// при помощи расширения ISAPI
//
// (C) Фролов А.В., 1997
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//         или
//         http://www.dials.ccas.ru/frolov
// ===============================================

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

// Прототипы функций перекодировки
void DecodeStr(char *szString);
char DecodeHex(char *str);

// =============================================================
// Функция GetExtensionVersion
// Запись версии интерфейса ISAPI и
// строки описания расширения
// =============================================================
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
  // Записываем версию интерфейса ISAPI
  pVer->dwExtensionVersion = 
    MAKELONG(HSE_VERSION_MINOR,HSE_VERSION_MAJOR );

  // Записываем строку описания расширения
  lstrcpyn(pVer->lpszExtensionDesc,
    "Form Parser ISAPI DLL", HSE_MAX_EXT_DLL_NAME_LEN);

  return TRUE;
}

// =============================================================
// Функция HttpExtensionProc
// =============================================================
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB)
{
  CHAR  szBuff[4096];
  CHAR  szTempBuf[4096];
  char * szPtr;
  char * szParam;
  
  // Нулевой код состояния - признак успешного выполнения
  lpECB->dwHttpStatusCode = 0;

  // Записываем в буфер заголовок HTTP и начальный
  // фрагмент формируемого динамически документа HTML
  wsprintf(szBuff,
    "Content-Type: text/html\r\n\r\n"
    "<HTML><HEAD><TITLE>Simple ISAPI Extension</TITLE></HEAD>\n"
    "<BODY BGCOLOR=#FFFFFF><H2>Information from ECB</H2>\n");

  // Добавляем версию интерфейса ISAPI
  wsprintf(szTempBuf, "<P>Extension Version: %d.%d", 
    HIWORD(lpECB->dwVersion), LOWORD(lpECB->dwVersion));
  strcat(szBuff, szTempBuf);
  
  // Название метода передачи данных
  wsprintf(szTempBuf, "<BR>Method: %s", lpECB->lpszMethod);
  strcat(szBuff, szTempBuf);
  
  // Строка параметров запуска расширения ISAPI
  wsprintf(szTempBuf, "<BR>QueryString: %s", 
    lpECB->lpszQueryString);
  strcat(szBuff, szTempBuf);
  
  // Физический путь к программному файлу расширения ISAPI
  wsprintf(szTempBuf, "<BR>PathTranslated: %s", 
    lpECB->lpszPathTranslated);
  strcat(szBuff, szTempBuf);

  // Полный размер данных, которые нужно получить
  wsprintf(szTempBuf, "<BR>TotalBytes: %d", 
    lpECB->cbTotalBytes);
  strcat(szBuff, szTempBuf);

  // Сколько доступно предварительно прочитанных данных
  wsprintf(szTempBuf, "<BR>Available: %d", 
    lpECB->cbAvailable);
  strcat(szBuff, szTempBuf);

  // Тип данных
  wsprintf(szTempBuf, "<BR>ContentType: %s", 
    lpECB->lpszContentType);
  strcat(szBuff, szTempBuf);

  lstrcpyn(szTempBuf, lpECB->lpbData, lpECB->cbAvailable + 1);
  szTempBuf[lpECB->cbAvailable + 1] = '\0';
  
  strcat(szBuff, "<H2>Принятые данные</H2>");
  strcat(szBuff, szTempBuf);

  // Перекодируем данные и отображаем результат
  // перекодировки
  DecodeStr(szTempBuf);

  strcat(szBuff, "<H2>Данные после перекодировки</H2>");
  strcat(szBuff, szTempBuf);

  // Выводим в документ список значений полей формы
  strcat(szBuff, "<H2>Список значений полей</H2>");

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

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


  // Конечный фрагмент документа HTML
  strcat(szBuff, "</BODY></HTML>");  

  // Посылаем содержимое буфера удаленному пользователю
  if(!lpECB->ServerSupportFunction(lpECB->ConnID,
    HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL, 
    (LPDWORD)szBuff))
  {
    // Если послать данные не удалось, 
    // завершаем работу нашего расширения ISAPI 
    // с кодом ошибки
    return HSE_STATUS_ERROR;
  }

  // Записываем код успешного завершения
  lpECB->dwHttpStatusCode = 200;
  
  // Возвращаем принак успешного завершения  
  return HSE_STATUS_SUCCESS;
}

// ------------------------------------------------
// Функция DecodeStr
// Раскодирование строки из кодировки URL
// ------------------------------------------------
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;
    
    // Обработка шестнадцатеричных кодов вида "%xx"
    if(ch == '%')
    {
      // Выполняем преобразование строки "%xx"
      // в код символа
      szString[dst] = DecodeHex(&szString[src + 1]);
      src += 2;
    }
  }
  
  // Закрываем строку двоичным нулем
  szString[dst] = '\0';
}

// ------------------------------------------------
// Функция DecodeHex
// Раскодирование строки "%xx"
// ------------------------------------------------
char DecodeHex(char *str)
{
  char ch;

  // Обрабатываем старший разряд
  if(str[0] >= 'A')
    ch = ((str[0] & 0xdf) - 'A') + 10;
  else
    ch = str[0] - '0';

  // Сдвигаем его влево на 4 бита
  ch <<= 4;

  // Обрабатываем младший разряд и складываем
  // его со старшим
  if(str[1] >= 'A')
    ch += ((str[1] & 0xdf) - 'A') + 10;
  else
    ch += str[1] - '0';

  // Возвращаем результат перекодировки
  return ch;
}

Для перекодирования принятых данных из кодировки URL мы использовали здесь функции DecodeStr и DecodeHex, описанные нами в разделе “Программа CONTROLS” предыдущей главы.

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


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

Сканирование и вывод значений отдельных полей формы выполняется аналогично тому, как это делалось в программе CGI с именем CONTROLS. Однако в отличие от указанной программы, значения отдельных полей не выводятся в стандартный поток STDOUT, а дописываются в конец буфера szBuff функцией strcat.

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

Листинг 8.6. Файл chap8\isform\isform.def


LIBRARY	     isform
DESCRIPTION  'Form Parser ISAPI DLL'
EXPORTS
    GetExtensionVersion
    HttpExtensionProc

Приложение FILEUPL

Доводилось ли вам пользоваться новой популярной услугой известной антивирусной фирмы АО “ДиалогНаука” - поиск вирусов в файлах через Internet?

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

Обсуждая проблемы антивирусной защиты сети Internet с генеральным директором АО “ДиалогНаука” Сергеем Григорьевичем Антимоновым, мы предложили идею организовать бесплатную антивирусную проверку файлов пользователей сети Internet самыми новыми версиями антивирусных программ, созданных в этой фирме. Эта идея была реализована сотрудником фирмы Spektrum Максимом Синевым в виде расширения ISAPI.

Каждый пользователь сети Internet, загрузив с помощью навигатора Netscape Navigator версии 2.0 или более поздней версии соответствующую страницу с сервера АО “ДиалогНаука” (адрес этого сервера http://www.dials.ccas.ru) может проверить любой свой файл или архив файлов на предмет зараженности вирусами. Страница антивирусной проверки показана на рис. 8.3.

Рис. 8.3. Страница сервера АО “ДиалогНаука”, предназначенная для поиска вирусов в локальных файлах пользователей

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

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

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

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

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

В данном случае был проверен файл A:\C-639.COM, и в нем был найден вирус Khizhnjak.639.

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

Если вас заинтересовали вопросы антивирусной защиты, прочитайте нашу книгу “Осторожно: компьютерные вирусы”, которая вышла в серии “Персональный компьютер. Шаг за шагом”. Эта книга содержит наиболее полную информацию по антивирусной защите и написана на базе опыта, накопленного в АО “ДиалогНаука”. Ну а сейчас мы вернемся к расширениям ISAPI.

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

На чем основана система удаленного приема файлов сервером WWW?

Она основана на экспериментальном протоколе, описанном в документе RFC1867. Если вы не знакомы с описаниями RFC, сходите на сервер http://www.cis.ohio-state.edu/htbin/rfc. Здесь вы найдете бесчисленное множество различного рода спецификаций и протоколов, на которых, собственно, и базируется работа Internet. Каждый документ имеет свой номер, по которому его можно легко найти на указанном сервере. Эти документы необходимы каждому профессиональному разработчику приложений для Internet, поэтому мы рекомендуем вам посмотреть хотя бы список их названий.

Документ 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 Explorer любой версии вплоть до 3.01. Именно этим объясняется требование на сервере АО “ДиалогНаука” выполнять удаленную антивирусную проверку навигатором Netscape Navigator.

Теперь мы можем перейти к описанию расширения ISAPI fileupl.dll, выполняющего удаленный прием файлов.

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

Листинг 8.7. Файл chap8\fileupl\fileupl.htm


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML>
<HEAD>
 <TITLE>File Upload via HTTP</TITLE>
</HEAD>
<BODY BGCOLOR=#FFFFFF>
<FORM ENCTYPE="multipart/form-data" METHOD=POST ACTION="http://frolov/scripts/fileupl.dll">
  <TABLE>
    <TR>
      <TD VALIGN=TOP>Text field TEXT</TD>
      <TD><INPUT TYPE=text NAME="text1" VALUE="Sample of text1" SIZE=30></TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Text field PASSWORD</TD>
      <TD><INPUT TYPE=password NAME="pwd" VALUE="Sample of password"></TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Text field  TEXTAREA</TD>
      <TD><TEXTAREA NAME="text2" ROWS=4 COLS=30>Sample of text</TEXTAREA></TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Switches CHECKBOX</TD>
      <TD>
        <INPUT TYPE=CHECKBOX NAME="chk1" VALUE="on" CHECKED>One<BR>
        <INPUT TYPE=CHECKBOX NAME="chk2" VALUE="on">Two<BR>
        <INPUT TYPE=CHECKBOX NAME="chk3" VALUE="on" CHECKED>Nothing<BR>
      </TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Action:</TD>
      <TD>
        <INPUT TYPE=RADIO NAME="rad" VALUE="on1" CHECKED>Virus Checking<BR>
        <INPUT TYPE=RADIO NAME="rad" VALUE="on2">Spell Checking<BR>
        <INPUT TYPE=RADIO NAME="rad" VALUE="on3">Translate<BR>
      </TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Select Uploaded File:</TD>
      <TD><INPUT TYPE=FILE NAME="fupload"></TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>List</TD>
      <TD>
        <SELECT NAME="sel" SIZE="1">
          <OPTION Value="First Option">First Option</OPTION>
          <OPTION Value="Second Option">Second Option</OPTION>
          <OPTION Value="None">None Selected</OPTION>
        </SELECT>
      </TD>
    </TR>
    <TR>
      <TD VALIGN=TOP>Hidden Control</TD>
      <TD><INPUT TYPE=HIDDEN NAME="hid" VALUE="Hidden"></TD>
    </TR>
  </TABLE>
<BR><INPUT TYPE=submit VALUE="Send">&nbsp;
<INPUT TYPE=reset VALUE="Reset">
</FORM>
</BODY>
</HTML>

Здесь вам нужно обратить внимание на параметры оператора <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.

Внешний вид формы, содержащий орган управления для выбора файла, показан на рис. 8.5.

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

На этом рисунке в поле Select Uploaded File уже выбран файл C:\UT\800.COM. Если нажать на кнопку Browse, на экране появится диалоговая панель File Upload, показанная на рис. 8.6.

Рис. 8.6. Диалоговая панель File Upload, с помощью которой можно выбрать файл для передачи серверу WWW

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

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

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

Листинг 8.8. Файл chap8\fileupl\fileupl.c


// ===============================================
// Расширение ISAPI fileupl.c
// Загрузка файла из локального компьютера
// на диск удаленного сервера WWW
// из документа HTML
//
// (C) Фролов А.В., 1997
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//         или
//         http://www.dials.ccas.ru/frolov
// ===============================================

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

// Прототипы функций, определенных в приложении
LPVOID ReadClientMIME(EXTENSION_CONTROL_BLOCK *lpECB, 
                      int *nStatus);
BOOL GetMIMEBoundary(LPVOID lpDataMIME, LPSTR lpBuffer, 
                     DWORD dwBufSize);

// =============================================================
// Функция GetExtensionVersion
// Запись версии интерфейса ISAPI и
// строки описания расширения
// =============================================================
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
  // Записываем версию интерфейса ISAPI
  pVer->dwExtensionVersion = 
    MAKELONG(HSE_VERSION_MINOR,HSE_VERSION_MAJOR );

  // Записываем строку описания расширения
  lstrcpyn(pVer->lpszExtensionDesc,
    "Remote File Upload", HSE_MAX_EXT_DLL_NAME_LEN);

  return TRUE;
}

// =============================================================
// Функция HttpExtensionProc
// =============================================================
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB)
{
  CHAR   szBuff[4096];
  HANDLE hOutFile;
  DWORD  dwWritten;
  LPVOID lpDataMIME;
  int    nStatus;

  // Нулевой код состояния - признак успешного выполнения
  lpECB->dwHttpStatusCode = 0;

  // Получаем данные от навигатора в кодировке MIME
  lpDataMIME = ReadClientMIME(lpECB, &nStatus);

  if(lpDataMIME != NULL)
  {
    // Создаем файл для записи принятых данных
    hOutFile = CreateFile("e:\\InetPub\\scripts\\uploaded.dat", 
      GENERIC_WRITE, FILE_SHARE_READ, NULL,
      CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	  
    if(hOutFile != INVALID_HANDLE_VALUE)
    {
      // Выполняем запись данных в файл
      WriteFile(hOutFile, lpDataMIME, 
        lpECB->cbTotalBytes, &dwWritten, NULL);
	    
      // Закрываем файл
      CloseHandle(hOutFile);
    }
      
    // Освобождаем память, заказанную для принятых
    // данных функцией ReadClientMIME
    LocalFree(lpDataMIME);
  }
  
  // Создаем документ HTML с сообщением об
  // успешной загрузке файла
  wsprintf(szBuff,
    "Content-Type: text/html\r\n\r\n"
    "<HTML><HEAD><TITLE>Remote File Upload</TITLE></HEAD>\n"
    "<BODY BGCOLOR=#FFFFFF><H1>Make your file upload!</H1>\n"
    "<HR>\n");

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

  // Отправляем созданный документ 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;
}

// =============================================================
// Функция ReadClientMIME
// Получение данных в кодировке MIME
// =============================================================
LPVOID ReadClientMIME(
	EXTENSION_CONTROL_BLOCK *lpECB, 
	int *nStatus)
{
  DWORD cbReaded;
  DWORD nBufferPos;
  DWORD nBytesToCopy;

  LPVOID lpTemp = NULL;
  
  // Код завершения
  *nStatus = 0;

  // Определяем, есть ли данные для чтения
  if(lpECB->cbTotalBytes != 0)
  {
    // Заказываем буфер памяти для чтения принятых данных
    if(!(lpTemp = (LPVOID)LocalAlloc(LPTR, 
      lpECB->cbTotalBytes)))
    {
      // Если памяти не хватает, завершаем работу
      // с установкой кода ошибки и возвращаем
      // значение NULL
      *nStatus = HSE_STATUS_ERROR;
      return NULL;
    }

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

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

    // В случае успешного копирования возвращаем
    // адрес буфера с прочитанными данными
    return lpTemp;
  }
  
  // Если данных для чтения нет, завершаем
  // работу с установкой кода ошибки
  else
  {
    *nStatus = HSE_STATUS_ERROR;

    // В случае ошибки вместо адреса буфера
    // с прочитанными данными возвращается
    // значение NULL
    return NULL;
  }
}

// =============================================================
// Функция GetMIMEBoundary
// Поиск разделителя в буфере.
// Параметры:
//   lpDataMIME - адрес буфера с данными MIME
//   lpBuffer   - адрес буфера для записи разделителя
//   dwBufSize  - размер буфера с данными MIME
// =============================================================
BOOL GetMIMEBoundary(LPVOID lpDataMIME, LPSTR lpBuffer, DWORD dwBufSize)
{
  LPSTR lpCurrent;
  DWORD dwOffset;
  BOOL  fFound;
  
  // Устанавливаем признак успешного поиска
  fFound = TRUE;

  // Ищем конец первой строки
  for(lpCurrent = lpDataMIME, 
    dwOffset = 0;;lpCurrent++, dwOffset++)
  {
    // Сравниваем с концом строки
    if(!memcmp(lpCurrent,"\r\n",2))
      break;
    
    // Если достигнут конец буфера,
    // сбрасываем признак успешного поиска
    // и прерываем работу цикла
    if(dwOffset >= dwBufSize)
    {
      fFound = FALSE;
      break;
    }

    // Копируем очередной символ разделителя
    *(lpBuffer + dwOffset) = *lpCurrent;
  }
  
  // Если разделитель найден, закрываем строку
  // разделителя двоичным нулем
  if(fFound)
    *lpBuffer = '\0';  

  // Возвращаем признак успешного или
  // неуспешного поиска
  return fFound;
}

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

Листинг 8.9. Файл chap8\fileupl\fileupl.def


LIBRARY	     fileupl
DESCRIPTION  'File Upload DLL'
EXPORTS
    GetExtensionVersion
    HttpExtensionProc

Рассмотрим функции нашего расширения 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, описанную нами в 26 томе “Библиотеки системного программиста”, который называется “Программирование для Windows NT. Часть 1”. Примеры использования этой и других функций, предназначенных для работы с файлами, вы можете найти в 27 томе этой же серии, который называется “Программирование для Windows NT. Часть 2”.

Запись принятых данных в файл выполняется за один вызов функции 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 байт. Описание этой функции вы найдете в только что упомянутом 26 томе “Библиотеки системного программиста”.

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

Вначале функция 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 в единственном экземпляре, оно работает в мультизадачном режиме. Это означает, что при обращении к критичным ресурсам вы должны использовать средства синхронизации задач, описанные нами в 26 томе “Библиотеки системного программиста”.

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

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

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

Подробную информацию о работе с библиотеками DLL в среде операционной системы Microsoft Windows NT вы найдете в 27 томе “Библиотеки системного программиста”.

Счетчик посещений XBMCNT

Еще один счетчик посещений, который мы рассмотрим в нашей книге, создан на основе приложения ISAPI, которое называется XBMCNT. Это приложение создает графическое изображение счетчика в виде файла формата XBM или X-Bitmap, что одно и тоже.

Внешний вид страницы, на которой расположен наш графический счетчик, показан на рис. 8.8.

Рис. 8.8. Графический счетчик на странице сервера WWW

Исходный текст соответствующего документа HTML приведен в листинге 8.10.

Листинг 8.10. Файл chap8\xbmcnt\xbmcnt.htm


<HTML>
  <BODY BGCOLOR="#FFFFFF">
    <H1>Главная страница фирмы XYZ Inc.</H1>
    <P>Добро пожаловать на нашу главную страницу!
    <HR>
    <P>Вы посетитель номер 
    <IMG ALT="Счетчик доступа" SRC=/scripts/xbmcnt.dll?">с 1 января 1913 года
  </BODY>
</HTML>

Обратите внимение, что ссылка на файл библиотеки приложения ISAPI здесь сделана с помощью оператора <IMG>. Когда навигатор отображает такую страницу, он запускает приложение xbmcnt.dll, а то, в свою очередь, посылает навигатору массив данных изображения.

В каком виде?

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

Для этого в заголовке HTTP, который отправляется навигатору, необходимо указать правильный тип данных. В нашем случае мы будем работать с графическим изображением в формате XBM, для которого определен тип данных MIME, обозначаемый как image/x-xbitmap. Поэтому заголовок Content-type протокола HTTP должен выглядеть следующим образом:


Content-type: image/x-xbitmap

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

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

Пусть например, у нас есть файл counter.xbm, в котором хранится черно-белое изображение цифры 0 высотой 8 пикселов и шириной 16 пикселов (формат XBM не позволяет хранить цветные изображения). Этот файл должен иметь следующий вид:


#define counter_width  8
#define counter_height 16
static unsigned char counter_bits[] =
{
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 
    0x42, 0x42, 0x42, 0x3C 
};

Файл определения изображения XBM всегда текстовый.

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

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

0x00

               

0x00

               

0x00

               

0x00

               

0x00

               

0x00

               

0x3C

    X X X X    

0x42

  X         X  

0x42

  X         X  

0x42

  X         X  

0x42

  X         X  

0x42

  X         X  

0x42

  X         X  

0x42

  X         X  

0x42

  X         X  

0x3C

    X X X X    

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

Теперь, когда мы познакомились с форматом XBM и знаем, какие данные должно вернуть приложение ISAPI навигатору, рассмотрим исходный текст приложения XBMCNT, приведенный в листинге 8.11. Мы сделали это приложение на основе приложения COUNTER из примеров Microsoft Visual C++ версии 4.2, упростив и изменив его таким образом, чтобы не использовать библиотеку классов MFC.

Листинг 8.11. Файл chap8\xbmcnt\xbmcnt.c


// ===============================================
// Расширение ISAPI xbmcnt.c
// Счетчик посещений страниц
//
// (C) Фролов А.В., 1997
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//         или
//         http://www.dials.ccas.ru/frolov
// ===============================================

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

// Ширина символа
#define char_width 8

// Высота символа
#define char_height 16

// Массив растровых изображений цифровых символов
static unsigned char char_bits[10][char_height] = 
{
  // Изображение цифры 0
  {
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x3C, 0x42, 0x42, 0x42, 0x42, 0x42, 
    0x42, 0x42, 0x42, 0x3C 
  },
	
  // Изображение цифры 1
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x10, 0x18, 0x14, 0x10,	0x10, 0x10, 
    0x10, 0x10, 0x10, 0x7C 
  },

  // Изображение цифры 2
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x3E, 0x22, 0x20, 0x20, 0x10, 0x08, 
    0x04, 0x02, 0x22, 0x3F 
  },

  // Изображение цифры 3
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x3E, 0x22, 0x40, 0x40, 0x3C, 0x40, 
    0x40, 0x40, 0x22, 0x3E 
  },

  // Изображение цифры 4
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x10, 0x18, 0x14, 0x12, 0x12, 0x3E, 
    0x10, 0x10, 0x10, 0x38 
  },

  // Изображение цифры 5
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x3E, 0x02, 0x02, 0x02, 0x3E, 0x20, 
    0x20, 0x20, 0x22, 0x2C 
  },

  // Изображение цифры 6
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x1C, 0x06, 0x02, 0x02, 0x1E, 0x22, 
    0x22, 0x22, 0x22, 0x1C 
  },

  // Изображение цифры 7
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x3E, 0x22, 0x20, 0x20, 0x10, 0x08, 
    0x08, 0x08, 0x08, 0x08 
  },

  // Изображение цифры 8
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x1C, 0x22, 0x22, 0x22, 0x1C, 0x22, 
    0x22, 0x22, 0x22, 0x1C 
  },

  // Изображение цифры 9
  { 
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
    0x1C, 0x22, 0x22, 0x22, 0x3C, 0x20, 
    0x20, 0x20, 0x20, 0x1C 
  },
};

void PrintCounter(LPSTR szCounter, LPSTR szBuffer);

// =============================================================
// Функция GetExtensionVersion
// Запись версии интерфейса ISAPI и
// строки описания расширения
// =============================================================
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
  // Записываем версию интерфейса ISAPI
  pVer->dwExtensionVersion = 
    MAKELONG(HSE_VERSION_MINOR,HSE_VERSION_MAJOR );

  // Записываем строку описания расширения
  lstrcpyn(pVer->lpszExtensionDesc,
    "ISAPI Counter", HSE_MAX_EXT_DLL_NAME_LEN);

  return TRUE;
}

// =============================================================
// Функция HttpExtensionProc
// =============================================================
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB)
{
  // Рабочий буфер
  CHAR  szBuff[4096];

  // Идентификатор файла счетчика
  HANDLE hCounterFile;

  // Количество прочитанных байт
  DWORD dwBytesRead;

  // Количество записанных байт
  DWORD dwBytesWritten;

  // Результат выполнения операции
  BOOL bResult;

  // Временный буфер для работы со счетчиком
  CHAR  szBuf[20];

  // Текущее значение счетчика
  INT   nCounter;

  // Нулевой код состояния - признак успешного выполнения
  lpECB->dwHttpStatusCode = 0;

  // -----------------------------------------------
  // Увеличиваем значение счетчика в файле
  // -----------------------------------------------

  // Открываем файл счетчика для чтения
  hCounterFile = CreateFile("CNTDAT.DAT", 
    GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
    FILE_FLAG_SEQUENTIAL_SCAN, NULL);

  // Читаем из файла строку значения счетчика
  bResult = ReadFile(hCounterFile, szBuf, 7, 
    &dwBytesRead, NULL);

  // Закрываем файл счетчика
  CloseHandle(hCounterFile);
  
  // Преобразуем значение счетчика из текстовой
  // строки в численную величину
  sscanf(szBuf, "%d", &nCounter);
  
  // Увеличиваем значение счетчика
  nCounter++;
  
  // Записываем в буфер szBuf пять цифр нового
  // значения счетчика
  sprintf(szBuf, "%05.5ld", nCounter);

  // Сохраняем новое значение счетчика в файле
  hCounterFile = CreateFile("CNTDAT.DAT", 
    GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
    FILE_FLAG_SEQUENTIAL_SCAN, NULL);

  WriteFile(hCounterFile, szBuf, strlen(szBuf),
    &dwBytesWritten, NULL);

  CloseHandle(hCounterFile);
  
  // Записываем в буфер заголовок HTTP
  wsprintf(szBuff, "Content-type: image/x-xbitmap\r\n\r\n");

  // Выводим биты графического изображения в формате XBM
  PrintCounter(szBuf, szBuff);

  // Посылаем содержимое буфера удаленному пользователю
  if(!lpECB->ServerSupportFunction(lpECB->ConnID,
    HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL, 
    (LPDWORD)szBuff))
  {
    // Если послать данные не удалось, 
    // завершаем работу нашего расширения ISAPI 
    // с кодом ошибки
    return HSE_STATUS_ERROR;
  }

  // Записываем код успешного завершения
  lpECB->dwHttpStatusCode = 200;
  
  // Возвращаем принак успешного завершения  
  return HSE_STATUS_SUCCESS;
}

// =============================================================
// Функция PrintCounter
// =============================================================
void PrintCounter(LPSTR szCounter, LPSTR szBuff)
{
  // Временный буфер
  CHAR  szTempBuf[4096];

  // Полная ширина изображения счетчика
  int nFinalWidth;

  // Полная высота изображения счетчика
  int nFinalHeight;

  // Номер текущей строки растра изображения
  int nLine;

  // Текущий символ
  unsigned int nChar;

  // Смещение в массиве изображений цифр
  int nDigitOffset;

  // Полная ширина изображения вычисляется исходя из
  // количества цифр отображаемой строки
  nFinalWidth  = char_width * strlen(szCounter);

  // Полная высота изображения равна высоте 
  // изображения цифр
  nFinalHeight = char_height;

  // Отменяем кэширование
  wsprintf(szTempBuf, "Expires: Thu, 03 Dec 1996 10:00:00 GMT\r\n");
  strcat(szBuff, szTempBuf);

  wsprintf(szTempBuf, "Pragma: no-cache\r\n");
  strcat(szBuff, szTempBuf);

  // Выводим заголовок изображения XBM
  wsprintf(szTempBuf, "#define counter_width %d\r\n",
    nFinalWidth);
  strcat(szBuff, szTempBuf);

  wsprintf(szTempBuf, "#define counter_height %d\r\n", 
    nFinalHeight);
  strcat(szBuff, szTempBuf);

  wsprintf(szTempBuf, "static unsigned char counter_bits[] = {\r\n");
  strcat(szBuff, szTempBuf);

  // Выводим в цикле биты изображения счетчика
  for(nLine=0; nLine < nFinalHeight; nLine++)
  {
    for(nChar=0; nChar < strlen(szCounter); nChar++)
    {
      nDigitOffset = szCounter[nChar] - '0';

      wsprintf(szTempBuf, "0x%02X, ", 
        char_bits[nDigitOffset][nLine]);

      strcat(szBuff, szTempBuf);
    }
  }
 
  // Выводим строку, закрывающую структуру в файле XBM
  strcat(szBuff, "};\r\n");
}

Файл определения модуля нашего расширения ISAPI представлен в листинге 8.12.

Листинг 8.12. Файл chap8\xbmcnt\xbmcnt.def


LIBRARY	     xbmcnt

DESCRIPTION  'ISAPI Counter'

EXPORTS
    GetExtensionVersion
    HttpExtensionProc

Константы char_width и char_height задают, соответственно, ширину и высоту изображений цифр, из которых формируется графическое изображение счетчика.

В массиве char_bits определено десять растровых изображений - по одному для каждой цифры от 0 до 9.

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

Функция HttpExtensionProc выполняет всю полезную работу.

Прежде всего, она записывает код успешного завершаения в поле dwHttpStatusCode структуры EXTENSION_CONTROL_BLOCK. Если произойдет ошибка, этот код следует изменить.

Далее функция HttpExtensionProc увеличивает значение счетчика посещений в файле. Заметим, что так как библиотека ISAPI работает в мультизадачном режиме, вы должны использовать критическую секцию для предотвращения конфликтов при попытке одновременной работы с файлом счетчика. Когда вы будете создавать свой счетчик на базе нашего примера, не забудьте об этом. О критических секциях мы рассказывали в 26 томе “Библиотеки системного программиста”, который называется “Программирование для Windows NT. Часть 1”. Там же вы найдете соответствующие примеры программ.

Приложение XBMCNT открывает файл счетчика с именем CNTDAT.DAT для чтения в текущем каталоге, пользуясь для этого функцией CreateFile. Далее из этого файла функцией ReadFile приложение считывает семь байт данных в буфер szBuf и закрывает файл функцией CloseHandle.

При помощи функции sscanf прочитанное текстовое значение счетчика преобразуется в двоичную форму и записывается в переменную nCounter.

Далее значение счетчика в переменной nCounter увеличивается на единицу и сохраняется в файле счетчика в текстовом виде.

Все использованные нами функции, предназначенные для работы с файлами, были подробно описаны в 26 и 27 томах “Библиотеки системного программиста”.

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

Заголовок записывается в буфер szBuff при помощи функции wsprintf:


wsprintf(szBuff, "Content-type: image/x-xbitmap\r\n\r\n");

Далее к этому буферу функция PrintCounter, определенная в нашем приложении, дописывает биты изображения, после чего функция ServerSupportFunction с параметром HSE_REQ_SEND_RESPONSE_HEADER отправляет данные навигатору.

Теперь о функции PrintCounter.

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

Вначале функция PrintCounter вычисляет размеры изображения счетчика, умножая количество цифр в строке szCounter на ширину изображения одной цифры.

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


wsprintf(szTempBuf, "Expires: Thu, 03 Dec 1996 10:00:00 GMT\r\n");
strcat(szBuff, szTempBuf);
wsprintf(szTempBuf, "Pragma: no-cache\r\n");
strcat(szBuff, szTempBuf);

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


wsprintf(szTempBuf, "#define counter_width %d\r\n", nFinalWidth);
strcat(szBuff, szTempBuf);
wsprintf(szTempBuf, "#define counter_height %d\r\n", nFinalHeight);
strcat(szBuff, szTempBuf);
wsprintf(szTempBuf, "static unsigned char counter_bits[] = {\r\n");
strcat(szBuff, szTempBuf);

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

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


strcat(szBuff, "};\r\n");