Программирование в IIS

Расширения ISAPI

Разбить на страницы
Показывать лекцию целиком

Интерфейс Internet Server Application Program Interface (ISAPI) предназначен для программирования приложения (API) информационных служб интернета (IIS). ISAPI состоит из классов поддержки и структур, участвующих в программной эксплуатации IIS. Веб-приложения, использующие ISAPI для взаимодействия с IIS, реализуют это взаимодействие на веб-сервере Windows наиболее эффективным образом. При работе с ISAPI уровень программного обеспечения поддержки или интерфейсов между IIS и веб-приложением сильно снижается. Все программное обеспечение веб-приложений Microsoft прямо или косвенно использует технологию ISAPI. Технологии Microsoft Application Server Pages (ASP) и .NET Framework построены как приложения ISAPI.

Изначально ISAPI распространялся среди разработчиков CGI как альтернатива программам CGI или как обновление исполняемого файла CGI. Многие исполняемые файлы CGI написаны на C++ или C, поэтому интеграция существующего веб-приложения CGI не очень сложна. Преобразование веб-приложения CGI для использования ISAPI увеличивает производительность веб-приложения. CGI при каждом HTTP-запросе создает новый процесс, что занимает много ресурсов несущего сервера. Расширения ISAPI загружаются в пространство процесса IIS, поэтому узлу не нужно создавать новый процесс при каждом HTTP-запросе. Поскольку Windows загружает динамически подключаемую библиотеку в пространство памяти один раз при первом вызове функции в DLL и хранит ее там неопределенный промежуток времени, расширение ISAPI остается загруженным и не удаляется, до тех пор пока сервер IIS не будет выключен или не будет выгружен экземпляр или виртуальная память. Таким образом, компания Microsoft дает программистам основание использовать ISAPI вместо CGI и легко обновлять ПО, созданное при помощи CGI.

ISAPI рекомендуется для программистов, создающих (или уже создавших) приложение на языке C++, предназначенное для продажи на рынке ПО. Если важным фактором является производительность, и на разработку выделяется больше времени, чем на создание обычного сценария для интернета, рассмотрите вариант использования ISAPI. Кроме всего прочего, ISAPI выполняет на несущем узле некоторые задачи, которые нельзя выполнить при помощи других технологий. Программное обеспечение ISAPI создано таким образом, что при его выполнении другие веб-приложения, написанные на языках сценариев с использованием других расширений ISAPI (например, .NET Framework или ASP.DLL), не рассматривают задачи, выполняемые расширением ISAPI.

К недостаткам рассматриваемой технологии относится сложность ISAPI в работе и в отладке. Отладка кода в интегрированной среде разработки (Integrated Design Environment, IDE) Visual Studio .NET довольно сложна, и, поскольку IIS представляет собой процесс с несколькими нитями, результаты отладки могут быть непредсказуемыми. Малейшая ошибка в приложении ISAPI катастрофически сказывается на производительности IIS. По сравнению со другими средами разработки ISAPI весьма чувствительна к ошибкам при построении веб-приложения.

Кроме всего прочего, код ISAPI создается с помощью неконтролируемого кода C++. Новые возможности, предлагаемые Visual Studio .NET для управляемого кода C++ в технологии .NET Framework, нельзя использовать в проекте ISAPI.

Примечание. Если программа создается с помощью контролируемого кода, то в этом случае реализуется технология.NET Framework. Эта технология используется языками C# и Visual Basic. Термин "контролируемый" означает, что технология .NET Framework контролирует очистку памяти, отведение памяти и другие процессы управления ресурсами низкого уровня. Программа на C++ не сможет работать с технологией .NET Framework, если не применяются Managed Extensions (Контролируемые расширения) для C++. Контролируемый C++ означает использование технологии .NET Framework и контролируемых расширений C++. Код C++, созданный без использования контролируемых расширений C++, является неконтролируемым кодом C++.

Обзор архитектуры ISAPI

Приложения ISAPI представляют собой библиотеки DLL, напрямую взаимодействующие с IIS API. Программное обеспечение ISAPI – это расширение или фильтр. Расширения ISAPI являются библиотеками DLL, вызываемыми посредством квалифицированного запроса в IIS. Фильтры ISAPI вызываются независимо от других запросов IIS. Запросы HTTP передаются напрямую расширению ISAPI с помощью ссылки URL или данных, отправляемых из формы HTML. Расширение ISAPI может вызываться косвенно посредством связывания файла с определенным расширением ISAPI в IIS. При установке связей файлов выполняются действия, аналогичные ассоциированию файлов ответа сервера (SRF) с конкретным расширением ISAPI в ATL Server (см. лекцию 4). Можно настроить реагирование фильтров ISAPI на запросы согласно приоритету; это отличает их от других фильтров, загружаемых в IIS. Фильтры используются в специализированных приложениях, связанных с IIS, и обычно выполняют следующие задачи:

  • шифрование;
  • ведение журналов;
  • аутентификация;
  • сжатие данных.
  • Расширения ISAPI – наиболее частый способ применения ISAPI. Фильтры ISAPI довольно сложны в создании, и сфера их использования ограничена. Данная тема выходит за рамки книги и рассматриваться не будет.

    Анатомия URL

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

    <схема>://<пользователь>:<пароль>@<узел>:<порт>/<url-путь>/<дополнительная информация>

    Ниже приведен URL, реализующий запрос файла ISAPI DLL с именем SEUX.dll. В URL указаны параметры parm1 и parm2. В таблице 5.1 данный URL разбит на части, чтобы показать, как определяется обычный URL, осуществляющий запрос библиотеки ISAPI DLL.

    http://amd1700v2/simpleisapi/folder1/folder2/SEUX.dll/PATH_INFO?
    parm1=value1parm2=value

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

    Если ISAPI DLL запрашивается напрямую через ссылку URL, в секции url-путь определяется имя файла библиотеки DLL расширения ISAPI. В секции URL <дополнительная информация> располагаются пары параметр = значение, передаваемые расширению ISAPI с помощью этой секции или посредством отправки данных HTTP из формы HTML.

    Расширения ISAPI во взаимодействии с IIS

    Если IIS получает запрос и считает, что необходимо использовать расширение ISAPI (запрошен файл, связанный с расширением ISAPI или само расширение ISAPI), то IIS передает запрос HTTP вместе с расширением ISAPI. Чтобы расширение ISAPI получило запрос HTTP, используется определенный программный интерфейс. Как известно, приложение ISAPI содержит файлы заголовков ISAPI, определяющих структуры и классы. Расширение ISAPI примет запрос посредством используемого интерфейса API, и данные запроса HTTP будут загружены в структуры и классы, являющиеся компонентами ISAPI.

    Компоненты URL
    Компонент URLЗначение из примера
    схема http
    пользователь Отсутствует в демонстрационном URL
    пароль Отсутствует в демонстрационном URL
    узел amd1700v2
    порт Отсутствует в демонстрационном URL (подразумевается значение 80)
    url-путь simpleisapi/folder1/folder2/SEUX.dll/PATH_INFO
    дополнительная информация ?parm1=value1parm2=value

    Расширение ISAPI анализирует данные HTTP-запроса и направляет вызовы другому программному обеспечению, например, программе бизнес-уровня (см. рис. 5.1). Ответы этой программы формируются в виде HTTP-ответов и возвращаются в IIS. IIS возвращает ответ расширения ISAPI веб-пользователю, направившему изначальный запрос.

    (рис 5.1) Обзор архитектуры расширения ISAPI

    Архитектура, показанная на рисунке 5.1, позволяет использовать расширения ISAPI двумя возможными способами, но не является единственным вариантом технологии. Единственным ограничением является вызов расширения ISAPI при HTTP-запросе и наличие структур и классов, содержащих HTTP-запрос и поддерживающих программное взаимодействие с ответом HTTP и запросом HTTP. Построение абстракции логики – задача разработчика. Направление вызовов библиотеке бизнес-логики необязательно, если бизнес-логика заключена в самом расширении ISAPI. Так как ISAPI напрямую записывает данные в HTTP-запрос, следует создавать архитектуру, абстрагирующую логику представления для исключения повторной компиляции ISAPI DLL при изменении логики представления.

    Сравнение ISAPI с сервером ATL

    При сравнении ATL Server и ISAPI основным различием является то, что ISAPI в меньшей степени поддерживает инфраструктуру и наличие архитектуры. ISAPI для расширений (в отличие от фильтров) состоит из функций API и структур и классов, являющихся параметрами функций API. ATL Server представляет много новых вспомогательных классов, макросов и функций и выполняет по отношению к ISAPI задачи, аналогичные классам Microsoft Foundation Classes (MFC) применительно к Windows API. ATL Server является дополнительным уровнем программирования, позволяющим оптимально использовать технологии. ATL Server поддерживает разработчика, который создает взаимодействующий с ISAPI программный продукт, предоставляя ему соответствующие методы и вспомогательное программное обеспечение. Разработчик может выбирать используемое ПО (иногда это является сложной задачей). В подобных обстоятельствах ISAPI послужит удобной альтернативой. В отличие от ATL Server ISAPI не предлагает никакой абстракции типа логики, и, кроме этого, имеется не очень много информации об ISAPI. Можно использовать классы ATL Server в приложении ISAPI без применения шаблона проекта ATL Server. В качестве альтернативы шаблон проекта ATL Server настраивается на использование одной библиотеки DLL без каких-либо опций, что создает структуру проекта, похожую на проект ISAPI.

    Построение простого расширения ISAPI

    Расширения ISAPI имеют такую несложную структуру, что могут создаваться без помощи MFC или ATL. Расширение ISAPI можно написать, используя файл включения расширения httpext.h и файл DLL definition export file (DEF) с экспортом двух следующих функций:

    HttpExtensionProc;
    GetExtensionVersion.

    Каждое расширение ISAPI должно поддерживать эти функции. Оно может также экспортировать функцию TerminateExtension (это не является обязательным). После настройки проекта DLL расширение нужно применить к IIS. В Visual Studio .NET имеется шаблон проекта ISAPI, запускающий мастер расширения ISAPI. Этот мастер создает класс, названный по имени проекта, наследуемый из класса CHTTPServer и содержащий необходимые функции ISAPI. Мастер расширения ISAPI использует файлы заголовков для поддержки от MFC, ATL и httpext.h для получения классов и структур ISAPI.

    Работа с мастером расширения ISAPI будет описана позже. Рассмотрим метод создания простого расширения ISAPI с минимальной поддержкой MFC и ATL без помощи мастера.

  • В Visual Studio .NET выберите команду File\New\Project (Файл\Создать\Проект) для открытия диалогового окна New Project (Новый проект).
  • В диалоговом окне New Project (Новый проект) щелкните на узле Visual C++ Projects (Проекты Visual C++) в левой части окна и выберите шаблон проекта Win32. Присвойте проекту имя HelloWorld и выберите место расположения создаваемого проекта.

    При нажатии на кнопку More (Больше) диалоговое окно New Project (Новый проект) отобразит дополнительную информацию о том, в каком месте будет создан проект. Название данной кнопки изменится на Less (Меньше). По умолчанию Visual Studio создает каталог с именем, идентичным имени проекта, в папке, указанной в текстовом поле Location (Расположение), в котором располагаются все файлы проекта. На рисунке 5.2 показано, что в текстовом поле Location (Расположение) введено значение C:\bookMaterial\IISBook\17ISAPIExtension\code, а именем проекта является HelloWorld.

    (рис 5.2) Диалоговое окно New Project (Новый проект) с выбранным проектом Win32
  • Нажмите на кнопку OK для запуска мастера приложений Win32 (Win32 Application Wizard) (см. рис 5.3(рис 5.3) Обзор в мастере приложения Win32
  • Откройте вкладку Application Settings (Параметры приложения) в левой части мастера приложений Win32.
  • Выберите DLL в качестве типа приложения (см. рис 5.4(рис 5.4) Создание проекта Win32 DLL.
  • Нажмите на кнопку Finish (Готово); Visual Studio создаст каталог C:\bookMaterial\IISBook\17ISAPIExtension\code\HelloWorld и разместит в нем файлы проекта.
  • Выберите команду Project\Properties (Проект\Свойства) для открытия окна свойств страницы.
  • Выберите в левой части окна свойств узел Precompiled Header в узле C/C++ для отображения свойств проекта.
  • Visual Studio .NET не имеет шаблона проекта, который реализуется без заранее скомпилированного заголовка, поэтому выберите шаблон проекта Win32, создайте из проекта библиотеку DLL и удалите параметр заранее скомпилированного заголовка. В ниспадающем списке Create\Use Precompiled Header (Создать\Использовать готовый заголовок) выберите Not Using Precompiled Headers (Не использовать готовые заголовки) в ниспадающем списке Create\Use Precompiled Header (Создать\Использовать готовый заголовок) (см. рис 5.5(рис 5.5) Изменение параметра готового заголовка проекта
  • Нажмите на кнопку OK в окне свойств. Откроется окно Solution Explorer (Обозреватель решения); если это не так, то выберите в меню команду View\Solution Explorer (Вид\Обозреватель решения).
  • Готовый заголовок не используется, поэтому удалите файлы stdafx, созданные Visual Studio .NET для его поддержки. В окне Solution Explorer выделите заголовок stdafx и файлы реализации, щелкните правой кнопкой мыши и выберите Remove (Удалить) (см. рис. 5.6).(рис 5.6) Удаление файлов stdafx из проекта
  • Теперь нужно добавить в проект DEF. Для этого щелкните правой кнопкой мыши на имени проекта в Solution Explorer и выберите команду Add\Add New Item (Добавить\Добавить новый элемент).
  • В диалоговом окне Add New Item (Добавление нового элемента) выделите DEF File (Файл DEF) и присвойте файлу имя Hello World в соответствии с именем проекта (см. рис. 5.7). Имя файла должно соответствовать имени DLL, иначе компилятор C++ отобразит предупреждение о несоответствии имен файлов. Рассматриваемый файл DEF (файл экспорта определения) описывает функции, экспортируемые из DLL, чтобы другое приложение могло загрузить DLL и найти адреса функций.(рис 5.7) Добавление в проект файла экспорта определения
  • Нажмите на кнопку Open (Открыть) в диалоговом окне Add New Item (Добавить новый элемент) для добавления в проект нового файла DEF.
  • Шаблоны проекта Visual Studio используют встроенный механизм MFC для экспорта функций и использования библиотеки DLL расширения ISAPI программой-потребителем, каковой является IIS. В Visual Studio .NET нет шаблона проекта, создающего конечный продукт без готового заголовка, поэтому следует выбрать шаблон проекта Win32, преобразовать его в DLL, после чего удалить ненужные файлы и настройки. При создании проекта без поддержки MFC иногда возникают некоторые сложности. Однако MFC сами по себе достаточно сложны, поэтому небольшие проекты (как в нашем примере) можно упростить, отказавшись от использования MFC.

    Следующим шагом является настройка файла DEF.

    Файл экспорта определения

    После добавления в проект файла DEF имя библиотеки в файле будет идентично имени проекта (иначе Visual Studio .NET отобразит предупреждение). В нашем проекте имя выглядит следующим образом:

    LIBRARY    HelloWorld

    В файл DEF добавим блок DESCRIPTION для описания назначения библиотеки DLL. Описание заключается в одинарные кавычки. Блок DESCRIPTION не является обязательным в отличие от блока EXPORTS. Имена функций следуют после объявления блока EXPORTS. В листинге 5.1 приведен код файла экспорта определения, использованный в нашем проекте. Функции приводятся в произвольном порядке, однако они должны соответствовать именам в реализации расширения.

    LIBRARY    HelloWorld
    
    DESCRIPTION 'Demonstration of a simple Hello World ISAPI extension'
    
    EXPORTS
        HttpExtensionProc
        GetExtensionVersion

    Главная точка входа расширения ISAPI

    Файл HelloWorld.cpp необходимо настроить на поддержку функций, необходимых для обеспечения работы расширения ISAPI.

  • В Visual Studio .NET откройте файл HelloWorld.cpp для редактирования посредством двойного щелчка на имени файла в окне Solution Explorer.
  • Удалите функцию DLLMain (она не нужна в данном примере).
  • В верхней части файла с кодом добавьте препроцессорную директиву включения для файла заголовка httpext.h и <string> (см. листинг 5.2).
  • Добавьте функцию GetExtensionVersion (см. листинг 5.2).
  • Добавьте функцию HttpExtensionProc (см. листинг 5.2).
  • // HelloWorld.cpp : Defines the entry point for 
    //the DLL application.
    
    #include <httpext.h>      //for ISAPI classes and structures
    
    // tell the compiler to shut up about using STL
    #pragma warning(disable:4786)
    #include <string>       //to build response to send back to user
    using namespace std;
    
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:GetExtensionVersion
    
    In: pVer - Pointer to ISAPI structure HSE_VERSION_INFO. 
    
    Out: Returns true if you want IIS to use the extension, otherwise
       if another value is returned, IIS will not use the extension
    
    Purpose:
       Called when the extension is loaded into IIS. The member 
       variables of HSE_VERSION_INFO, dwExtensionVersion and
       lpszExtensionDesc should be filled with the extension 
       version and description. 
    
       Other initialization functionality could be called from this 
       function to set up the server to use this extension.
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
    {
       //ISAPI version 
       const DWORD VERSION_NUMBER = 0.9;
       const char* VERSION_NAME = "Hello World";
    
        pVer->dwExtensionVersion = VERSION_NUMBER;
    
        strncpy(   pVer->lpszExtensionDesc, 
                VERSION_NAME, 
                HSE_MAX_EXT_DLL_NAME_LEN);
    
        return TRUE;
    }
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:HttpExtensionProc
    
    In:      pECB - pointer to the Extension control block structure
    
    Out:   DWORD - HSE status code 
    
    Purpose:
       main entry point for HTTP request
    
       the possible return codes are: 
       HSE_STATUS_SUCCESS   - everything worked great
       HSE_STATUS_SUCCESS_AND_KEEP_CONN - same as HSE_STATUS_SUCCESS 
                                  since IIS 4
       HSE_STATUS_PENDING - wait until effort completed 
       HSE_STATUS_ERROR   - sends a 500 error code
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
    {   
       //HTTP headers
       const char* BASIC_HEADER = "Content-type: text/html\r\n\r\n";
       //output values
       const DWORD BUFFER_LENGTH = 4096;
       TCHAR szTempBuffer[BUFFER_LENGTH];
       DWORD dwBufferSize = BUFFER_LENGTH;
       string sResponse;
    
       //start our HTML document
       sResponse = "<HTML><HEAD></HEAD><BODY><P>";
       sResponse += "Hi! Hello World";
       sResponse += "</P></BODY></HTML>";
    
       // set content-type header
       strcpy(szTempBuffer, BASIC_HEADER);
       DWORD dwHeaderSize = strlen(szTempBuffer);
       pECB->ServerSupportFunction(pECB->ConnID, 
                            HSE_REQ_SEND_RESPONSE_HEADER, 
                            NULL, 
                            dwHeaderSize, 
                            (LPDWORD) szTempBuffer);
    
       //write value to http response
       DWORD dwLength=sResponse.length();
       pECB->WriteClient(   pECB->ConnID, 
                      (PVOID)sResponse.c_str(), 
                      dwLength, 
                      0);
    
       //return a success code
       return HSE_STATUS_SUCCESS;
    }

    Функция GetExtensionVersion

    Функция GetExtensionVersion использует указатель на структуру HSE_VERSION_INFO в качестве параметра и возвращает значение BOOL в зависимости от того, использует ли IIS расширение ISAPI. Если функция GetExtensionVersion возвращает значение "истина", то расширение ISAPI используется. Функция GetExtensionVersion вызывается при загрузке расширения ISAPI в пространство процесса IIS. После загрузки расширения ISAPI эта функция повторно не вызывается. Она отлично подходит для включения кода, выполняющего проверку ключа лицензии, или для процедур инициализации, проверяющих использование расширения. В листинге 5.2 файла HelloWorld.cpp функция GetExtensionVersion только получает информацию о версии для расширения ISAPI.

    Структура HSE_VERSION_INFO содержит две переменные dwExtensionVersion и lpszExtensionDesc. Этим переменным присваиваются значения при вызове функции GetExtensionVersion. В листинге 5.2 переменной dwExtensionVersion присваивается значение 0,9 типа DWORD, а переменной lpszExtensionDesc – значение HelloWorld. Эта функция всегда возвращает значение "истина", так как в IIS всегда загружается расширение ISAPI. Если расширение ISAPI зависит от наличия действительного файла лицензии, другой программной библиотеки или специальной конфигурации, тогда в функции можно выполнять соответствующую проверку и возвращать значение "ложь" в случае отрицательного результата.

    Функция HttpExtensionProc

    Функция HttpExtensionProc в качестве параметра использует одну из важнейших структур ISAPI – блок контроля расширения (Extension Control Block, ECB). Эта структура содержит следующие компоненты:

  • данные о запросе HTTP;
  • данные веб-экземпляра IIS, через которое поступил запрос;
  • вспомогательные функции для анализа запроса HTTP;
  • вспомогательные функции для управления ответом HTTP.
  • Изучая код листинга 5.2, следует отметить, что указатель на блок ECB используется только для отправки заголовка ответа клиенту и для отображения клиенту фразы "Hi! Hello World". Код C++ похож на C в структурах и вспомогательных функциях, поскольку ISAPI не сильно изменился со времени выхода первой версии IIS для обеспечения обратной совместимости. Заметным изменениям ISAPI стало добавление новых функциональных возможностей. Трудность работы с ISAPI состоит в том, что некоторые API требуют программирования с использованием решений языка C.

    Первой функцией, вызываемой в блоке ECB расширения HelloWorld, является функция ServerSupportFunction. Она устанавливает заголовок в ответе HTTP для обозначения вида содержимого (текст или HTML) с помощью следующей строки:

    Content-type: text/html\r\n\r\n

    За заголовками ответов HTTP должны следовать две пары символов возврата каретки и перехода на новую строку ( /n и /r ).

    Следующей функцией, вызываемой с помощью ECB, является функция WriteClient. Фраза "Hi! Hello World" записывается в браузер с помощью данной функции посредством передачи указателя char (char*) строковой переменной, содержащей код HTML и строку "Hi! Hello World". Функции WriteClient нужен пустой указатель ( void* ), поэтому указатель char приводится к форме пустого указателя с помощью макроса PVOID.

    Реализация ISAPI "HelloWorld"

    После успешной компиляции DLL расширение ISAPI можно отгружать на веб-сервер. Если в качестве расположения конечного файла DLL указан экземпляр веб-сервера или корень виртуального каталога, можно немедленно запросить файл из IIS. При выполнении отладки Visual Studio .NET браузер не откроется, и автоматического запроса ISAPI DLL из IIS (как при отладке веб-службы ASP.NET) не произойдет. Visual Studio считает проект библиотекой DLL, поэтому выдаст запрос на присвоение исполняемого файла, который использует данную библиотеку.

    Запомните. Если разработчик осуществляет построение библиотеки DLL расширения ISAPI и затем тестирует DLL посредством ее запроса через IIS, при втором построении ISAPI DLL в Visual Studio .NET, скорее всего, не удастся создать DLL. IIS блокирует библиотеку DLL расширения ISAPI при ее запросе из IIS, поскольку DLL загружается в пространство процесса IIS. Для разблокирования ISAPI DLL необходимо выгрузить экземпляр веб-сайта или виртуального каталога либо заново запустить приложение. После разблокирования ISAPI DLL поверх нее в процессе построения осуществляется запись.

    При первом запросе ISAPI DLL из IIS 6 в операционной системе Windows Server 2003, скорее всего, возвратится ошибка 404. В WS03 и IIS6 существует новая возможность, ограничивающая по умолчанию всякую программную поддержку серверной части, если она не включена вручную. В предыдущих версиях Windows Server компонент IIS поставлялся с включенной программной поддержкой серверной части (ASP). Эта функция включается в окне Web Service Extensions (Расширения веб-служб) консоли MMC Computer Management (Управление компьютером) (см. рис. 5.8).

    (рис 5.8) Включение программной функциональности серверной части в окне Web Service Extensions (Расширения веб-служб)

    Настроим IIS на разрешение запросов расширения ISAPI HelloWorld.

  • В окне администрирования расширения веб-служб щелкните на ссылке Add A New Web Service Extension (Добавить новое расширение веб-службы). Откроется диалоговое окно New Web Service Extension (Новое расширение веб-службы).
  • В текстовом поле Extension Name (Имя расширения) введите имя, которое будет отображаться в окне администрирования расширения веб-служб. В нашем примере это имя (рис 5.9) Диалоговое окно New Web Service Extension (Новое расширение веб-службы)
  • Нажмите на кнопку Add (Добавить), чтобы указать путь к файлу расширения ISAPI. Откроется диалоговое окно Add File (Добавление файла), в котором вручную вводится путь к файлу, либо укажите его расположение в окне Open File (Открыть файл) после нажатия на кнопку Browse (Обзор).
  • Установите путь к файлу расширения ISAPI, затем нажмите на кнопку OK в диалоговом окне Add File (Добавление файла). Файла расширения ISAPI отобразится в диалоговом окне New Web Service Extension (Новое расширение веб-службы).
  • Отметьте опцию Set Extension Status To Allowed (Разрешить использование расширения), после чего нажмите на кнопку OK.
  • Расширение HelloWorld появится в окне администрирования расширения веб-служб консоли Copmuter Management (Управление компьютером), и в поле Status (Состояние) отобразится значение Allowed (Разрешено).

    В качестве альтернативы установите Allowed (Разрешено) для расширения веб-службы All Unknown ISAPI Extensions (Все неизвестные расширения ISAPI). Данный параметр разрешает выполнение любого запроса относительно любого расширения ISAPI.

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

    Разрешения на выполнение веб-экземпляра IIS или виртуального каталога устанавливаются на разрешение выполнения библиотек ISAPI DLL. По умолчанию параметр Execute Permissions (Разрешения на выполнение) в окне свойства веб-экземпляра IIS или виртуального каталога не позволяет выполнять сценарии и исполняемые файлы.

  • Откройте оснастку IIS MMC Computer Management (Управление компьютером) и щелкните правой кнопкой мыши на веб-экземпляре или виртуальном каталоге для отображения контекстного меню.
  • Выберите Properties (Свойства) для открытия окна свойств веб-экземпляра или виртуального каталога.
  • Во вкладке Virtual Directory (Виртуальный каталог) окна свойств виртуального каталога или во вкладке Home Directory (Домашний каталог) веб-экземпляра расположено поле со списком Execute Permissions (Разрешения на выполнение) (см. рис. 5.10). Убедитесь, что отмечена опция Scripts And Executables (Сценарии и исполняемые файлы).
  • (рис 5.10) Установка разрешений для виртуального каталога

    Если в IIS все настроено должным образом, ISAPI Hello World будет функционировать. Запросите библиотеку ISAPI DLL в адресе URL из браузера, как если бы это был файл HTML – и DLL выполнится (см. рис. 5.11).

    (рис 5.11) Выполнение расширения ISAPI HelloWorld

    Извлечение информации из IIS

    ECB обычно используется для извлечения информации о запросе HTTP и экземпляре сервера IIS, посредством чего расширение ISAPI выполняет определенные программные действия на основе события запроса. В коде листинга 5.3, взятого из расширения ISAPI SEUX (Простое расширение с использованием XML), функция HttpExtensionProc выполняет следующие задачи.

  • Построение документа XML со множеством общих серверных переменных, получаемых из функции GetServerVariable.
  • Добавление свойств ECB в документ XML.
  • Возврат документа XML запрашивающей стороне.
  • /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:HttpExtensionProc
    
    In:        pECB - pointer to the Extension control block structure
    
    Out:    DWORD - HSE status code 
    
    Purpose:
        main entry point for HTTP request
    
        the possible return codes are: 
        HSE_STATUS_SUCCESS    - everything worked great
        HSE_STATUS_SUCCESS_AND_KEEP_CONN - same as HSE_STATUS_SUCCESS 
                                            since IIS 4
        HSE_STATUS_PENDING - wait until effort completed 
        HSE_STATUS_ERROR   - sends a 500 error code
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
    {   
        string sDoc;
    
        //start our XML document
        sDoc = string(HEAD) + string(NEW_LINE) + string(XML_L) + 
                    string(MAIN_ELEMENT_NAME) + string(XML_R) + 
                    string(NEW_LINE);
    
        //START THE ECBServerVariable VARIABLES
        sDoc += string(XML_L) + 
            string("ECBServerVariable") + string(XML_R) + 
            string(NEW_LINE);
        //GET the ALL_HTTP
        sDoc += string(XML_L) + string("ALL_HTTP");
        GetALLHTTPHeader(pECB, sDoc);    
        sDoc += string(XML_R_END) + 
            string(NEW_LINE);//end the first main element
    
        GetECBElement(pECB, string("AUTH_TYPE"), sDoc);
        GetECBElement(pECB, string("APPL_MD_PATH"), sDoc);
        GetECBElement(pECB, string("APPL_PHYSICAL_PATH"), sDoc);
        GetECBElement(pECB, string("CONTENT_LENGTH"), sDoc);
        GetECBElement(pECB, string("CONTENT_TYPE"), sDoc);
        GetECBElement(pECB, string("GATEWAY_INTERFACE"), sDoc);
        GetECBElement(pECB, string("HTTP_ACCEPT"), sDoc);
        GetECBElement(pECB, string("HTTPS"), sDoc);
        GetECBElement(pECB, string("HTTP_AUTHORIZATION"), sDoc);
        GetECBElement(pECB, string("LOGON_USER"), sDoc);
        GetECBElement(pECB, string("AUTH_PASSWORD"), sDoc);
        GetECBElement(pECB, string("AUTH_TYPE"), sDoc);
        GetECBElement(pECB, string("AUTH_USER"), sDoc);
        GetECBElement(pECB, string("APPL_PHYSICAL_PATH"), sDoc);
        GetECBElement(pECB, string("INSTANCE_ID"), sDoc);
        GetECBElement(pECB, string("INSTANCE_META_PATH"), sDoc);
        GetECBElement(pECB, string("PATH_INFO"), sDoc);
        GetECBElement(pECB, string("PATH_TRANSLATED"), sDoc);
        GetECBElement(pECB, string("QUERY_STRING"), sDoc);
        GetECBElement(pECB, string("REMOTE_ADDR"), sDoc);
        GetECBElement(pECB, string("REMOTE_HOST"), sDoc);
        GetECBElement(pECB, string("REMOTE_USER"), sDoc);
        GetECBElement(pECB, string("REQUEST_METHOD"), sDoc);
        GetECBElement(pECB, string("SCRIPT_NAME"), sDoc);
        GetECBElement(pECB, string("SERVER_NAME"), sDoc);
        GetECBElement(pECB, string("SERVER_PORT"), sDoc);
        GetECBElement(pECB, string("SERVER_PORT_SECURE"), sDoc);
        GetECBElement(pECB, string("SERVER_PROTOCOL"), sDoc);
        GetECBElement(pECB, string("SERVER_SOFTWARE"), sDoc);
        GetECBElement(pECB, string("URL"), sDoc);
    
        //End THE ECBServerVariable VARIABLES
        sDoc += string(XML_L_END) + 
            string("ECBServerVariable") + string(XML_R) + 
            string(NEW_LINE);
    
        //START THE ECBProperties
        sDoc += string(XML_L) + 
            string("ECBProperties") + string(XML_R) + 
            string(NEW_LINE);
        
        GetElement(string("lpszLogData"),
                   string(pECB->lpszLogData),sDoc);
        GetElement(string("lpszMethod"),
                   string(pECB->lpszMethod),sDoc);
        GetElement(string("lpszQueryString"),
                   string(pECB->lpszQueryString),sDoc);
        GetElement(string("lpszPathInfo"),
                   string(pECB->lpszPathInfo),sDoc);
        GetElement(string("lpszContentType"),
                   string(pECB->lpszContentType),sDoc);
    
        //end THE ECBProperties
        sDoc += string(XML_L_END) + 
            string("ECBProperties") + string(XML_R) + 
            string(NEW_LINE);
    
           //end our XML document
        sDoc += string(XML_L_END) + 
            string(MAIN_ELEMENT_NAME) + string(XML_R) + 
            string(NEW_LINE);
    
        //write it!
        SendResponse(pECB,sDoc);
    
       return HSE_STATUS_SUCCESS;
    }

    Примечание. Исходный код SEUX доступен на веб-сайте автора книги (см. введение).

    Построение XML для представления значений серверных переменных

    Первой задачей (см. листинг 5.3) является открытие документа XML посредством присоединения некоторых констант, представляющих собой части документа XML. Построение XML происходит вручную, и константы объявляются для общих частей документа XML, так как они используются во многих местах. Документ XML размещается в строковой переменной с именем sDoc. Ниже приведены константы XML:

    //xml parts
        const char* MAIN_ELEMENT_NAME = "HTTPRequestRaw";
        const char* QUOTE        ="\"";
        const char* XML_L        ="<";
        const char* XML_R        =">";
        const char* XML_L_END    ="</";
        const char* XML_R_END    ="/>";
        const char* NEW_LINE    ="\n";
        const char* HEAD = "<?xml version=\"1.0\"?>";

    Рассматриваемый документ XML достаточно прост, поэтому его построение вручную не вызывает никаких трудностей. Для более сложного документа рекомендуется использовать аналитическую библиотеку XML, такую как MSXML.

    После инициализации документа XML начинается работа по размещению в элементах XML всех серверных переменных. Создаваемый документ XML имеет родительский элемент HTTPRequestRaw. Внутри HTTPRequestRaw содержатся два дочерних элемента с ECBServerVariable и ECBProperties. Для каждого значения серверной переменной, запрашиваемого из ECB, будет создаваться дочерний элемент для элемента ECBServerVariable, независимо от получения значения соответствующей серверной переменной. Для каждого запрошенного свойства ECB создается дочерний элемент под элементом ECBProperties, которое содержит значение независимо от получения значения свойства. При вызове расширения ISAPI SEUX с помощью IE 6.0 отобразится документ XML (см. рис. 5.12). В других версиях браузера XML может отобразится по-другому или не отобразиться вовсе.

    (рис 5.12) IE 6.0, отображающий документ XML серверных переменных из расширения SEUX ISAPI

    Специальный случай серверной переменной ALL_HTTP

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

    Заголовки в значении, возвращаемом из ALL_HTTP, ограничены символами новой строки, поэтому требуется дополнительный анализ для размещения каждого заголовка в элементе XML в качестве атрибута. Функция GetALLHTTPHeader выполняет данную задачу с помощью функции ECB GetServerVariable (см. листинг 5.4).

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:GetALLHTTPHeader
    
    In:    pECB - pointer to the Extension control block structure
    
        psElement - string pointer to XML document being built that 
                    will be updated with the element for the ALL_HTTP
                    server variable value.
    
    Out:    nothing returned but the string psElement points to 
            will be updated.
    
    Purpose:
        Updates the XML document string by adding an element 
        for the ALL_HTTP server variable. This variable contains 
        all of the http headers so some additional parsing must 
        take place on the headers to format them into XML. 
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    void GetALLHTTPHeader(EXTENSION_CONTROL_BLOCK *pECB, 
                          string *psElement)
    {
        TCHAR szTempBuffer[BUFFER_LENGTH];
        DWORD dwBufferSize = BUFFER_LENGTH;
        const string EQUAL("=");
        const string SPACE(" ");
    
        string sAllHeaders;    //used to cut up 'all headers' returned
        int nNewLinePos = 0;
        int nEndLinePos = 0;
        string sName;
        string sValue;
    
    
        //pull the whole HTTP header 
        if (pECB->GetServerVariable(    pECB->ConnID, 
                                        "ALL_HTTP", 
                                        szTempBuffer, 
                                        dwBufferSize))
        {
           //if the whole http header was pulled then parse it
            sAllHeaders.assign(szTempBuffer);
    
            //get the name / value pairs
            while (GetHeaderValuePair(    sAllHeaders, 
                                        nNewLinePos, 
                                        sName, 
                                        sValue, 
                                        nEndLinePos))
            {
                //add the attribute to the element
                psElement->append(    SPACE + sName + EQUAL + 
                                    QUOTE + 
                                    ValidateValue(sValue) + 
                                    QUOTE); 
    
                //reset the newline to the last endline
                nNewLinePos = nEndLinePos;                    
                
                //dump the values
                sName.erase();
                sValue.erase();
            }
        }
    }

    Функция GetServerVariable

    Функция GetServerVariable возвращает значение "истина" при успешном выполнении и значение "ложь" при возникновении ошибки, как показано в следующем примере:

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

    Прототип GetServerVariable в данном случае требует передачи четырех параметров.

  • hConn. Поддержка соединения, полученная от ECB.
  • lpszVariableName. Строка с символом конца строки запрашиваемой серверной переменной.
  • lpvBuffer. Пустой указатель на буфер, который заполняется результирующим значением имени переменной и байтом конца строки.
  • lpdwSizeofBuffer. Указатель на значение DWORD, отражающее размер буфера.
  • При успешном выполнении функция GetServerVariable возвращает значение "истина". Указатель lpvBuffer указывает значение запрашиваемой серверной переменной, а lpdwSizeofBuffer – на новое значение DWORD, отражающее текущий размер значения, включая байт конца строки. При неудачном завершении работы функция GetServerValue возвращает значение "ложь". В этом случае вызывается функция GetLastError, которая возвращает значение DWORD, представляющее собой код ошибки. В таблице 5.2 показаны возможные ошибки функции GetServerVariable ; это константы, определяемыми во вспомогательном файле заголовка расширения ISAPI.

    Значения серверных переменных

    Возможные значения запрашиваемых серверных переменных приведены в листинге 5.3 в коде функции HttpExtensionProc в расширении ISAPI SEUX ; они являются аргументами в вызовах GetECBElement. Значения серверных переменных могут изменяться в процессе текущего события запроса HTTP в IIS, и зачастую переменной не присваивается значение. Серверные переменные содержат значения только при определенных настройках IIS. В таблице 5.2 приведены серверные переменные, запрашиваемые с помощью функции GetServerVariable.

    Обзор переменных сервера, запрашиваемых функцией GetServerVariable
    Константа ошибкиОписание ошибки
    ERROR_INVALID_PARAMETER Значение hConn является некорректным или закрытым, либо неверны параметры серверной переменной.
    ERROR_INVALID_INDEX Запрашиваемая серверная переменная не поддерживается.
    ERROR_INSUFFICIENT_BUFFER Размер lpdwSizeofBuffer слишком мал для содержания значения запрашиваемой серверной переменной.
    ERROR_NO_DATA Запрашиваемая серверная переменная недоступна.
    ALL_HTTP Все HTTP-заголовки (разделены символами новой строки), переданные в запросе HTTP в строке с символом конца строки. Заголовки имеют вид <имя заголовка> : <значение>.
    ALL_RAW Все заголовки HTTP в том виде, в котором они были отправлены запрашивающей HTTP-стороной.
    APPL_MD_PATH Путь метабазы веб-приложения. Например, /LMW3SVC/1/Root/SimpleISAPI.
    APPL_PHYSICAL_PATH Физический путь корневого веб-каталога для веб-приложения. Например: C:\ISAPI\.
    AUTH_PASSWORD Пароль, вводимый веб-пользователем в диалоговом окне аутентификации браузера, если установлена базовая аутентификация.
    AUTH_TYPE Используемый тип аутентификации. Пустое значение при отсутствии аутентификации либо значение, соответствующее Kerberos, пользовательской аутентификации, SSL/PCT, базовой или интегрированной аутентификации Windows.
    AUTH_USER Имя пользователя, вводимое пользователем в диалоговом окне аутентификации браузера в случае, если установлена базовая аутентификация.
    CERT_COOKIE Уникальный идентификатор сертификата клиента.
    CERT_FLAGS Битовые флаги бюро сертификатов (CA) сертификата клиента. Если bit0 равен 1, то CA отсутствует в списке распознаваемых бюро сертификатов данного сервера и признается недействительным.
    CERT_ISSUER Содержит имя сертификата клиента. Например, O=Schmidlaps, OU=House, CN= имя пользователя, C=USA.
    CERT_KEYSIZE Размер ключа в битах при соединении SSL.
    CERT_SERCRETKEYSIZE Размер секретного ключа сертификата сервера в битах.
    CERT_SERIALNUMBER Серийный номер сертификата клиента.
    CERT_SERVER_ISSUER Подробное имя издателя сертификата сервера.
    CERT_SERVER_SUBJECT Подробное имя субъекта сертификата сервера.
    CERT_SUBJECT Субъект сертификата клиента.
    CONTENT_LENGTH Количество байт, исключая заголовки HTTP-запроса.
    LOGON_USER Если конечный пользователь успешно аутентифицировался в Windows, используется учетная запись входа в систему.
    HTTPS Возвращает значение off, если в HTTPS не используется SSL, в противном случае возвращается значение on.
    HTTPS_KEYSIZE Размер ключа SSL-соединения в битах.
    HTTP_SECRETKEYSIZE Размер секретного ключа сертификата сервера в битах.
    HTTPS_SERVER_ISSUER Подробное имя издателя сертификата сервера.
    HTTPS_SERVER_SUBJECT Подробное имя субъекта сертификата сервера.
    INSTANCE_ID Номер экземпляра сервера. Значения идентификатора сервера в метабазе, например, 1.
    INSTANCE_META_PATH Путь веб- экземпляра в метабазе, например: LM/W3SVC/1.
    PATH_INFO Часть URL, расположенная между ISAPI DLL и началом секции с дополнительной информацией в URL. Как правило, в этом месте нет никаких данных, если запрашивающее ПО не добавляет свое значение.
    PATH_TRANSLATED Часть веб-экземпляра, связанная с физическим жестким диском, с конкатенацией значения PATH_INFO.
    QUERY_STRING Строка символов, следующих за символом "?" в секции дополнительной информации URL.
    REMOTE_ADDR IP-адрес хоста или шлюза запрашивающего ПО.
    REMOTE_HOST Имя узла или шлюза запрашивающего ПО, если включен обратный поиск DNS; иначе возвращается значение IP-адреса узла или шлюза запрашивающего ПО.
    REMOTE_USER Имя пользователя, осуществляющего HTTP-запрос и аутентифицируемого несущим сервером. Представляет собой пустую строку для анонимного пользователя.
    REQUEST_METHOD Команда метода HTTP-запроса.
    SCRIPT_NAME Имя исполняемого двоичного файла, например, имя ISAPI DLL или исполняемого файла CGI.
    SERVER_NAME Имя узла сервера или IP-адрес.
    SERVER_PORT Порт TCP/IP, по которому получен запрос.
    SERVER_PORT_SECURE Значение 0 или 1. Запросы по безопасному порту возвращают 1; иначе возвращается 0.
    SERVER_PROTOCOL Имя и версия протокола запроса, например, HTTP 1.1.
    SERVER_SOFTWARE Имя и версия IIS, под которой выполняется программа DLL расширения ISAPI, например, Microsoft-IIS/6.0.
    URL Значение части url-путь адреса URL, исключая значение PATH_INFO. Например: /simpleisapi/folder1/folder2/SEUX.dll.

    Для демонстрации значений таблицы 5.2 в листинге 5.5 приведен документ XML, созданный из расширения ISAPI SEUX.DLL. В данном примере несущий сервер и IIS настроены таким образом, что многие значения серверных переменных получаются в процессе HTTP-запроса. Имя несущего узла – amd1700v2. IIS 6 на amd1700v2 настроен с использованием следующих значений параметров и файловых расположений.

  • Физическое расположение расширения ISAPI SEUX.DLL. C:\ISAPI\папка1\папка2\SEUX.dll.
  • Корень веб- экземпляра. C:\inetpub\wwwroot.
  • Связанный виртуальный каталог. C:\ISAPI.
  • Анонимный доступ. Не включен для виртуального каталога.
  • Базовая аутентификация. Включена для виртуального каталога.
  • Расширение ISAPI SEUX.dll запрошено с amd1700v2 с помощью следующих данных браузера, расположенного на отдельном компьютере.

  • Пользователь, осуществивший вход на веб-сайт под именем normaluser.
  • Пользователь, осуществивший вход на веб-сайт с использованием пароля normaluser.
  • URL в браузере: http://amd1700v2/simpleisapi/folder1/folder2/SEUX.dll/PATH_INFO?parm1=value1parm2=value
  • <?xml version="1.0" ?> 
    <HTTPRequestRaw>
     <ECBServerVariable>
      <ALL_HTTP HTTP_CONNECTION="Keep-Alive" 
       HTTP_ACCEPT=
     "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, 
    application/vnd.ms-powerpoint, application/vnd.ms-excel, 
    application/msword, */*" 
       HTTP_ACCEPT_ENCODING="gzip, deflate" 
       HTTP_ACCEPT_LANGUAGE="en-us" 
       HTTP_AUTHORIZATION="Basic bm9ybWFsdXNlcjpub3JtYWx1c2Vy"
       HTTP_COOKIE="ASPCLIENTDEBUG=1" HTTP_HOST="amd1700v2" 
       HTTP_USER_AGENT=
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 
    1.0.3705)" /> 
      <AUTH_TYPE>Basic</AUTH_TYPE> 
      <APPL_MD_PATH>/LM/W3SVC/1/Root/SimpleISAPI</APPL_MD_PATH> 
      <APPL_PHYSICAL_PATH>C:\ISAPI\</APPL_PHYSICAL_PATH> 
      <CONTENT_LENGTH>0</CONTENT_LENGTH> 
      <CONTENT_TYPE /> 
      <GATEWAY_INTERFACE>CGI/1.1</GATEWAY_INTERFACE> 
      <HTTP_ACCEPT>image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
    application/vnd.ms-powerpoint, application/vnd.ms-excel, 
    application/msword, */*</HTTP_ACCEPT> 
      <HTTPS>off</HTTPS> 
      <HTTP_AUTHORIZATION>
    Basic bm9ybWFsdXNlcjpub3JtYWx1c2Vy
      </HTTP_AUTHORIZATION> 
      <LOGON_USER>normaluser</LOGON_USER> 
      <AUTH_PASSWORD>normaluser</AUTH_PASSWORD> 
      <AUTH_TYPE>Basic</AUTH_TYPE> 
      <AUTH_USER>normaluser</AUTH_USER> 
      <APPL_PHYSICAL_PATH>C:\ISAPI\</APPL_PHYSICAL_PATH> 
      <INSTANCE_ID>1</INSTANCE_ID> 
      <INSTANCE_META_PATH>/LM/W3SVC/1</INSTANCE_META_PATH> 
      <PATH_INFO>/PATH_INFO</PATH_INFO> 
      <PATH_TRANSLATED>c:\inetpub\wwwroot\PATH_INFO</PATH_TRANSLATED> 
      <QUERY_STRING>parm1=value1parm2=value</QUERY_STRING> 
      <REMOTE_ADDR>169.254.176.147</REMOTE_ADDR> 
      <REMOTE_HOST>169.254.176.147</REMOTE_HOST> 
      <REMOTE_USER>normaluser</REMOTE_USER> 
      <REQUEST_METHOD>GET</REQUEST_METHOD> 
      <SCRIPT_NAME>/simpleisapi/folder1/folder2/SEUX.dll</SCRIPT_NAME> 
      <SERVER_NAME>amd1700v2</SERVER_NAME> 
      <SERVER_PORT>80</SERVER_PORT> 
      <SERVER_PORT_SECURE>0</SERVER_PORT_SECURE> 
      <SERVER_PROTOCOL>HTTP/1.1</SERVER_PROTOCOL> 
      <SERVER_SOFTWARE>Microsoft-IIS/6.0</SERVER_SOFTWARE> 
      <URL>/simpleisapi/folder1/folder2/SEUX.dll</URL> 
     </ECBServerVariable>
     <ECBProperties>
      <lpszLogData /> 
      <lpszMethod>GET</lpszMethod> 
      <lpszQueryString>parm1=value1parm2=value</lpszQueryString> 
      <lpszPathInfo>/PATH_INFO</lpszPathInfo> 
      <lpszContentType /> 
     </ECBProperties>
    </HTTPRequestRaw>

    Анализ пары "Заголовок-Значение"

    После получения заголовка ALL_HTTP из функции GetServerVariable (см. листинг 5.4) для отображения содержимого в XML необходимо реализовать обработку строки. Заголовки разделяются символами новой строки и возврата каретки. Двоеточие (":") разделяет имя заголовка и его значение. GetHeaderValuePair инициализирует поиск в указанной позиции и возвращает имя и соответствующее ему значение для данного заголовка, а также позицию, в которой прерван поиск заголовков. GetHeaderValuePair единовременно осуществляет поиск одного значения заголовка.

    Как видно из листинга 5.6 функция GetHeaderValuePair осуществляет поиск символа ":" в HTTP-заголовке, начиная с позиции nStart в строке sHeader. Значение nStart – это счетчик (начинается с нуля). Если символ " :" не найден, функция завершает работу с возвращением значения "ложь". При обнаружении символа " :" (т.е. заголовок существует) в sHeader продолжается поиск новой строки, начиная с позиции, в которой обнаружен данный символ. При поиске используется функция find строки Standard Template Library (STL) с символом новой строки \n в качестве аргумента и с указанием в качестве начальной позиции символа " :". Позиция новой строки становится конечной позицией, возвращаемой указателем pnEnd, посредством чего вызывающей функции становится известно, в каком месте остановлен поиск. С помощью всех параметров позиции, выявленных в процессе вызовов функции поиска строки sHeader, имя заголовка и значение извлекаются в место расположения памяти, связанное с указателями psName и psValue.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name: GetHeaderValuePair
    
    In: sHeader - string HTTP Header 
        nStart - integer search start location
        psName - pointer to name of header that is being sought 
        psValue - pointer to string that will be filled if 
                    value found
        pnEnd - pointer to integer of the final search position  
    
    Out: bool    true returned if the header was found, 
                false returned otherwise
    Purpose:
            Searches through the HTTP header passed in for a 
            header value. Returns data about the search 
            parameters if found or not.
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    bool GetHeaderValuePair(const string sHeader, 
                            const int nStart, 
                            string *psName, 
                            string *psValue, 
                            int *pnEnd)
    {
        const string sColon(":");
    
        //determine if header is a post header
        string::size_type idxColonPosition = nStart;
    
        //start looking at beginning 
        idxColonPosition = sHeader.find(sColon, idxColonPosition);
    
        if (idxColonPosition == string::npos)//no more headers found
            return false;//this is failure
    
        //get the name
        psName->assign(sHeader.substr
                       (nStart, idxColonPosition - nStart));
    
        //find next newline
        string::size_type idxNewLine;
        idxNewLine = sHeader.find('\n', idxColonPosition);
    
        //get the end even if it means not found
        *pnEnd = idxNewLine;
    
        if (idxNewLine == string::npos)    //a newline was not found
            return true;//not a failure - might be the last header
    
        //get the value
        //adjust colon position so we do not assign colon in value
        idxColonPosition = idxColonPosition +1; 
        psValue->assign(sHeader.substr(idxColonPosition, 
            idxNewLine - idxColonPosition));
    
        return true;

    Построение остальных элементов XML

    После обработки функции HttpExtensionProc значения заголовка ALL_HTTP остальные серверные переменные обрабатываются с помощью функции GetECBElement (см. листинг 5.7). Каждая серверная переменная, передаваемая в GetECBElement, извлекается с помощью функции GetServerVariable и записывается в элемент XML, присоединяемый к строке, на которую указывает psElement. Указатель psElement указывает на документ XML, конструируемый в HttpExtensionProc.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:GetECBElement
    
    In:    pECB - Pointer to the extension control block for the 
        purposes of calling the GetServerVariable function.
        
        sName - string name of the server variable that is 
                being sought.
    
        psElement - string pointer to XML document being built that 
                    will be updated with the name and value for the 
                    server variable extracted from the extension 
                    control block.
    
    Out:    nothing returned but the string psElement points to 
            will be updated.
    
    Purpose:
        appends a string of an XML element to the string psElement 
        points to. The XML element that is created is in the form of
    <Server Variable Name>Server Variable Value</Server Variable Name>
    
        for example:
     <GATEWAY_INTERFACE>CGI/1.1</GATEWAY_INTERFACE> + newline
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/    
    void GetECBElement(    EXTENSION_CONTROL_BLOCK *pECB, 
                                        const string sName, 
                                        string *psElement)
    {
    
    TCHAR szTempBuffer[BUFFER_LENGTH];
    DWORD dwBufferSize = BUFFER_LENGTH;
    
        //get the server variable value
       if (pECB->GetServerVariable(    pECB->ConnID, 
                        (LPSTR)sName.c_str(), 
                        szTempBuffer, 
                        dwBufferSize))
       {
           //build the XML element and 
           //add it to the XML document passed in
            psElement->append(string(XML_L) + sName + string(XML_R));
    
            psElement->append(ValidateValue((string)szTempBuffer));
    
            psElement->append(string(XML_L_END) + sName + string(XML_R) + 
                string(NEW_LINE));
       }
    
    }

    Функция ValidateValue проверяет, что специальные символы указаны с помощью альтернативных комбинаций символов. Функция применяется к строке, перед тем как строке присваивается статус значения элемента. Для подтверждения значения атрибута можно применять ValidateValue. XML не разрешает использование определенных специальных символов в позиции значения, если они не представлены в альтернативном виде. Как видно из листинга 5.8, символы, используемые для реализации XML-структуры: " ?" и " ?" – заменяются альтернативными эквивалентами.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name: ValidateValue
    
    In: Constant reference to a string variable sValue. sValue is the 
        value being checked to see if it has a character requiring 
        escaping
    
    Out: returns a string with the escaped characters in place
    
    Purpose:
        blindly replaces all special characters 
        with the escape sequence character so that XML will
        be valid. 
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    string ValidateValue(const string sValue)
    {
        string sReturn;
        sReturn = sValue;
    
        FindAndReplace(sReturn, string(""),string("amp;"));
        FindAndReplace(sReturn, string("="),string("#61;"));
        FindAndReplace(sReturn, string("<"),string("lt;"));
        FindAndReplace(sReturn, string(">"),string("gt;"));
        FindAndReplace(sReturn, string("'"),string("apos;"));
        FindAndReplace(sReturn, string("\""),string("quot;"));
    
        return sReturn;
    }

    Функция FindAndReplace представляет собой утилиту для замещения всех вхождений строки. В расширении ISAPI SEUX она является идеальным механизмом для замещения одной фразы внутри строки другой фразой. Аргументы представляют собой указатели на строки:

  • изменяемая строка (контейнер);
  • строка, которую нужно заменить внутри контейнера (цель);
  • строка, заменяющая цель в контейнере (замещение).
  • Строка STL содержит функции find и replace, используемые функцией FindAndReplace для поиска контейнера всех вхождений цели (см. листинг 5.9). Каждый раз при обнаружении цели в контейнере происходит ее замена, и начинается новый поиск. По завершении работы функции FindAndReplace контейнер обновляется замещениями, если таковые имеются.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name: FindAndReplace
    
    In: psContainer - pointer to a string that will be searched 
                      and edited if a value is discovered.
         psTarget - pointer to a string that is being sought for 
                    replacement.
         psReplacement - pointer to a string that will replace the 
                         the string pointed to in psTarget.
    
    Out: nothing - but psContainer will be changed
    
    Purpose:
         searches string psContainer pointer for the string that 
         psTarget points to and replaces it with the string that 
         psReplacement points to.
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    void FindAndReplace(string *psContainer, 
                             string *psTarget, 
                             string *psReplacement)
    {
    
         string::size_type idx;
         idx = psContainer->find(*psTarget);
         while (idx != string::npos)//an instance was found
         {          
              //are we at the end of the string
              if (psContainer->size() == idx)
              {
                   *psContainer += *psReplacement;
                   break;
              }
              else
              {
                   psContainer->replace
                             (idx, psTarget->size() , *psReplacement);
                   
                   //advance beyond the current character
                   idx += psReplacement->size();
              }
    
              //look for next occurance
              idx = psContainer->find(*psTarget, idx);
         }
    
    }

    Функция GetElement работает аналогично функции GetECBElement ; она вызывается из функции HttpExtensionProc для конкатенации элементов из свойств ECB. Свойства извлекаются из ECB, после чего передаются вместе со своими именами и документом XML в функцию GetElement. GetElement размещает свойство ECB и соответствующее значение в XML документе и конкатенирует его с указателем документа XML, переданным функции GetElement. Осуществляется запрос следующих свойств:

  • lpszLogData. Буфер размера HSE_LOG_BUFFER_LEN, используемый для размещения информации, добавляемой в файл журнала для данной транзакции HTTP-запроса.
  • lpszMethod. Строковое значение используемого метода HTTP, например, GET, PUT или HEAD.
  • lpszQueryString. Строковое значение символов в секции дополнительной информации адреса URL, исключая символ " ?". Аналогично значению переменной сервера QUERY_STRING.
  • lpszPathInfo. Строковое значение секции URL, находящейся между библиотекой DLL расширения ISAPI и началом секции дополнительной информации URL. Обычно не содержит данных, если запрашивающее ПО не разместило в этом месте определенное значение.
  • lpszContentType. Строковое значение типа содержимого отправленных по HTTP данных. Аналогично значению серверной переменной CONTENT_TYPE.
  • Когда функция HttpExtensionProc завершает получение содержимого всех возможных серверных переменных и свойств ECB, в документе XML указываются закрывающие тегов XML, и он передается функции SendResponse. SendResponse направляет запрашивающей программе строковое значение, переданное в функцию. Заголовок типа содержимого передается запрашивающему клиенту с помощью функции ECB ServerSupportFunction (см. листинг 5.10). Передаваемый заголовок представляется константой BASIC_HEADER, эквивалентной следующей строке: Content-type: text/html\r\n\r\n. За заголовками HTTP следуют два символа возврата каретки и новой строки, поскольку возвращаемые данные представляют собой текст. Можно указать и XML, однако если в запрашивающем браузере в XML зарегистрированы типы Multipurpose Internet Mail Extensions (MIME), то для отображения XML откроется зарегистрированная программа.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name: SendResponse
    
    In:    pECB - pointer to the extension control block
        sValue - string reference to the value to be
                 written to the HTTP response
    
    Out:    nothing
    
    Purpose:
            writes the intended value to the HTTP response 
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    void SendResponse(EXTENSION_CONTROL_BLOCK *pECB, string sValue)
    {
        TCHAR szTempBuffer[BUFFER_LENGTH];
        DWORD dwBufferSize = BUFFER_LENGTH;
    
        // set content-type header
        strcpy(szTempBuffer, BASIC_HEADER);
        DWORD dwHeaderSize = strlen(szTempBuffer);
        pECB->ServerSupportFunction(pECB->ConnID, 
                                    HSE_REQ_SEND_RESPONSE_HEADER, 
                                    NULL, 
                                    dwHeaderSize, 
                                    (LPDWORD) szTempBuffer);
    
        //write value to http response
        DWORD dwLength=sValue.length();
        pECB->WriteClient(    pECB->ConnID, 
                            (PVOID)sValue.c_str(), 
                            dwLength, 
                            HSE_IO_SYNC);
    }

    После отправки заголовка возврата отправляется содержимое с помощью функции WriteClient. Как показано в следующем примере, при передаче данных клиенту отправляется идентификатор соединения ConnID. Имеющееся значение, полученное из текущего экземпляра указателя, использовано в листинге 5.10. Содержимое передается функцией WriteClient с помощью пустого указателя в параметре Buffer. Содержимое, на которое ссылается указатель Buffer, должно равняться количеству байт, передаваемому клиенту, и указываться в параметре lpdwBytes. По завершении вызова lpdwBytes содержит количество переданных байт, если запись не осуществлялась асинхронно. Значение параметра dwSync определяет способ передачи данных клиенту. В листинге 5.10 с помощью макроса HSE_IO_SYNC указывается значение 0х00000001, т.е. запись выполняется синхронно, и пространство памяти, на которое ссылается указатель lpdwBytes, обновится по завершении WriteClient количеством байт, переданным клиенту. Если в макросе HSE_IO_ASYNC представлено значение 0х00000002, то данные, отправляемые клиенту, и функция обратной связи зафиксируют события передачи информации клиенту. Асинхронное использование функции WriteClient требует объявления функции обратной связи, а также отправки функцией ServerSupportFunction значения HSE_REQ_IO_COMPLETION для установки с клиентом транзакции асинхронной записи.

    Функция WriteClient является членом ECB. Может показаться странным, что используемое описание ECB передается функции. Поскольку приложение IIS включает несколько нитей, в любой момент времени может потребоваться несколько экземпляров ECB, и вероятно выполнение записи в экземпляре ECB в другой экземпляр ECB. Ниже приведен пример WriteClient:

    BOOL WriteClient(
      HCONN ConnID,    
      LPVOID Buffer,   
      LPDWORD lpdwBytes, 
      DWORD dwSync   
    );

    Мастер шаблона проекта ISAPI

    При создании расширения ISAPI с использованием поддержки MFC воспользуйтесь мастером шаблона проекта ISAPI (ISAPI Project Template Wizard). Мастер настроит проект на необходимую поддержку для библиотек ISAPI и создаст класс, унаследованный из CHttpServer. Для ISAPI предоставляются следующие классы MFC.

  • CHttpArgList. Класс, содержащий функции и структуры для анализа URL.
  • CHtmlStream. Класс, управляющий памятью по отношению к данным, предназначенных для передачи клиенту.
  • CHttpFilter. Класс, расширяющий интерфейс IIS для создания фильтра ISAPI. Фильтры представляют собой тип приложения ISAPI, вызываемый при каждом запросе IIS, поэтому они отвечают на события внутри IIS.
  • CHttpFilterContext. Класс, являющийся параметром в функциях класса CHttpFilter, для обработки содержимого данного события HTTP. Служит для обработки связанных с рассматриваемым событием HTTP данных, проходящих через фильтр.
  • CHttpServer. Класс, расширяющий интерфейс IIS для рассматриваемого события HTTP-запроса.
  • ChttpServerContext. Класс, обеспечиваемый классом CHttpServer, содержащий методы управления данными, связанными с рассматриваемым событием HTTP.
  • Классы, предоставляемые в MFC, обеспечивают хороший уровень абстракции для первоначального взаимодействия со структурами ISAPI и функциями, приведенными выше. С помощью MFC разработчик осуществляет доступ к "сырому" ISAPI. Некоторые классы облегчают анализ заголовков и данных HTTP, выполняемый вручную в расширении ISAPI SEUX. Мастер создаст класс, которому будет присвоено имя согласно следующей схеме: C<Имя_проекта>Расширение. Функция с именем Default является главной точкой входа для ISAPI DLL:

    void Default(CHttpServerContext* pCtxt);

    Функция Default заменяет функцию HttpExtensionProc, используемую в расширении ISAPI SEUX в качестве главной точки входа. Указатель на класс CHttpServerContext, передаваемый в функцию Default, обеспечивает функциональность и требования разработчика к управлению данными и HTTP-транзакцией (осуществлялось посредством ECB в расширении ISAPI SEUX ).

    Создание расширения ISAPI в Visual Studio .NET

    Для создания расширения ISAPI с помощью шаблона проекта Vusal Studio .NET откройте Visual Studio .NET и выберите File\New\Project (Файл\Создать\Проект). Откроется диалоговое окно New Project (Новый проект) (см. рис. 5.13). Выберите шаблон проекта MFC ISAPI Extension DLL, введите имя проекта и нажмите на кнопку OK.

    (рис 5.13) Выбор шаблона проекта MFC ISAPI Extension DLL в Visual Studio .NET

    Примечание. Диалоговое окно (см. рис. 5.13) отображает значки, отличающиеся от тех, которые показаны на рис. 5.2. В обоих случаях использованы одинаковые версии Visual Studio .NET. Рисунок 5.2 получен при нажатии на кнопку Large Icons (Большие значки) в правом верхнем углу, рисунок 5.13 – при нажатии на кнопку Small Icons (Мелкие значки). Кнопки Large Icons (Большие значки) и Small Icons (Мелкие значки) являются взаимоисключающими, и они влияют на отображение значков проекта в правой части диалогового окна New Project (Новый проект).

    По аналогии с мастером проекта сервера ATL (см. лекцию 4) работа мастера расширения ISAPI выполняется в одном окне, и его работа завершается после нажатия на кнопку Finish (Готово) (см. рис. 5.14). Мастер приводит обзор параметров проекта в области Overview (Обзор). Типом создаваемого проекта по умолчанию является библиотека DLL расширения ISAPI с MFC в общей DLL. Данные настройки подходят для большинства проектов ISAPI. Эти параметры можно изменить в области Object Settings (Параметры объекта).

    (рис 5.14) Область Overview (Обзор) мастера расширения ISAPI с проектом ISAPI по умолчанию

    В области Object Settings (Параметры объекта) настраиваются имена классов, сгенерированных мастером, содержатся опции, позволяющие расширению ISAPI подключаться к MFC статически или динамически. Выбор опции Use MFC In A Shared DLL (Использовать MFC в общей DLL) (см. рис. 5.15) определяет динамическое подключение MFC в ISAPI DLL, генерируемой во время компиляции. Подразумевается, что MFC существует в среде разработки, что достигается исключением содержимого из ISAPI DLL.

    (рис 5.15) Область Object Settings (Параметры объекта) мастера расширения ISAPI с проектом ISAPI по умолчанию.

    При выборе опции Use MFC In A Static Library (Использовать MFC в статической библиотеке) компоненты MFC компилируются в DLL и подключаются статически. В этом случае подразумевается, что MFC не существует в среде разработки. Размер библиотеки при статическом подключении больше, чем при динамическом подключении. В большинстве случаев MFC располагаются в целевой среде, так как речь идет о сервере Windows. Для WS03 используйте динамическое подключение, поскольку MFC находятся на сервере.

    Можно сгенерировать объект фильтра, включив опцию Generate A Filter Object (Генерировать объект фильтра). Фильтры загружаются в IIS для обработки каждого запроса, имеющего место в IIS. Они работают по аналогии с расширением ISAPI, как если бы оно было связано с каждым файлом корневого веб-каталога. Фильтр и расширение не являются взаимоисключающими функциями. Одна библиотека DLL может содержать как расширение, так и фильтр, что позволяет каждому из этих объектов использовать состояние посредством глобальных переменных.

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

    После нажатия на кнопку Finish (Готово) и завершения мастером генерации файлов проект можно скомпилировать и запустить, как только библиотека DLL будет отгружена на сервер. Если все выполнится правильно, вы увидите в браузере сообщение, имя класса в котором соответствует выбранному имени класса:

    This default message was produced by the Internet Server DLL
    Wizard. Edit your CMFCISAPIExtension::Default() implementation
    to change it.
    Страницы:

    Интерфейс Internet Server Application Program Interface (ISAPI) предназначен для программирования приложения (API) информационных служб интернета (IIS). ISAPI состоит из классов поддержки и структур, участвующих в программной эксплуатации IIS. Веб-приложения, использующие ISAPI для взаимодействия с IIS, реализуют это взаимодействие на веб-сервере Windows наиболее эффективным образом. При работе с ISAPI уровень программного обеспечения поддержки или интерфейсов между IIS и веб-приложением сильно снижается. Все программное обеспечение веб-приложений Microsoft прямо или косвенно использует технологию ISAPI. Технологии Microsoft Application Server Pages (ASP) и .NET Framework построены как приложения ISAPI.

    Изначально ISAPI распространялся среди разработчиков CGI как альтернатива программам CGI или как обновление исполняемого файла CGI. Многие исполняемые файлы CGI написаны на C++ или C, поэтому интеграция существующего веб-приложения CGI не очень сложна. Преобразование веб-приложения CGI для использования ISAPI увеличивает производительность веб-приложения. CGI при каждом HTTP-запросе создает новый процесс, что занимает много ресурсов несущего сервера. Расширения ISAPI загружаются в пространство процесса IIS, поэтому узлу не нужно создавать новый процесс при каждом HTTP-запросе. Поскольку Windows загружает динамически подключаемую библиотеку в пространство памяти один раз при первом вызове функции в DLL и хранит ее там неопределенный промежуток времени, расширение ISAPI остается загруженным и не удаляется, до тех пор пока сервер IIS не будет выключен или не будет выгружен экземпляр или виртуальная память. Таким образом, компания Microsoft дает программистам основание использовать ISAPI вместо CGI и легко обновлять ПО, созданное при помощи CGI.

    ISAPI рекомендуется для программистов, создающих (или уже создавших) приложение на языке C++, предназначенное для продажи на рынке ПО. Если важным фактором является производительность, и на разработку выделяется больше времени, чем на создание обычного сценария для интернета, рассмотрите вариант использования ISAPI. Кроме всего прочего, ISAPI выполняет на несущем узле некоторые задачи, которые нельзя выполнить при помощи других технологий. Программное обеспечение ISAPI создано таким образом, что при его выполнении другие веб-приложения, написанные на языках сценариев с использованием других расширений ISAPI (например, .NET Framework или ASP.DLL), не рассматривают задачи, выполняемые расширением ISAPI.

    К недостаткам рассматриваемой технологии относится сложность ISAPI в работе и в отладке. Отладка кода в интегрированной среде разработки (Integrated Design Environment, IDE) Visual Studio .NET довольно сложна, и, поскольку IIS представляет собой процесс с несколькими нитями, результаты отладки могут быть непредсказуемыми. Малейшая ошибка в приложении ISAPI катастрофически сказывается на производительности IIS. По сравнению со другими средами разработки ISAPI весьма чувствительна к ошибкам при построении веб-приложения.

    Кроме всего прочего, код ISAPI создается с помощью неконтролируемого кода C++. Новые возможности, предлагаемые Visual Studio .NET для управляемого кода C++ в технологии .NET Framework, нельзя использовать в проекте ISAPI.

    Примечание. Если программа создается с помощью контролируемого кода, то в этом случае реализуется технология.NET Framework. Эта технология используется языками C# и Visual Basic. Термин "контролируемый" означает, что технология .NET Framework контролирует очистку памяти, отведение памяти и другие процессы управления ресурсами низкого уровня. Программа на C++ не сможет работать с технологией .NET Framework, если не применяются Managed Extensions (Контролируемые расширения) для C++. Контролируемый C++ означает использование технологии .NET Framework и контролируемых расширений C++. Код C++, созданный без использования контролируемых расширений C++, является неконтролируемым кодом C++.

    Обзор архитектуры ISAPI

    Приложения ISAPI представляют собой библиотеки DLL, напрямую взаимодействующие с IIS API. Программное обеспечение ISAPI – это расширение или фильтр. Расширения ISAPI являются библиотеками DLL, вызываемыми посредством квалифицированного запроса в IIS. Фильтры ISAPI вызываются независимо от других запросов IIS. Запросы HTTP передаются напрямую расширению ISAPI с помощью ссылки URL или данных, отправляемых из формы HTML. Расширение ISAPI может вызываться косвенно посредством связывания файла с определенным расширением ISAPI в IIS. При установке связей файлов выполняются действия, аналогичные ассоциированию файлов ответа сервера (SRF) с конкретным расширением ISAPI в ATL Server (см. лекцию 4). Можно настроить реагирование фильтров ISAPI на запросы согласно приоритету; это отличает их от других фильтров, загружаемых в IIS. Фильтры используются в специализированных приложениях, связанных с IIS, и обычно выполняют следующие задачи:

  • шифрование;
  • ведение журналов;
  • аутентификация;
  • сжатие данных.
  • Расширения ISAPI – наиболее частый способ применения ISAPI. Фильтры ISAPI довольно сложны в создании, и сфера их использования ограничена. Данная тема выходит за рамки книги и рассматриваться не будет.

    Анатомия URL

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

    <схема>://<пользователь>:<пароль>@<узел>:<порт>/<url-путь>/<дополнительная информация>

    Ниже приведен URL, реализующий запрос файла ISAPI DLL с именем SEUX.dll. В URL указаны параметры parm1 и parm2. В таблице 5.1 данный URL разбит на части, чтобы показать, как определяется обычный URL, осуществляющий запрос библиотеки ISAPI DLL.

    http://amd1700v2/simpleisapi/folder1/folder2/SEUX.dll/PATH_INFO?
    parm1=value1parm2=value

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

    Если ISAPI DLL запрашивается напрямую через ссылку URL, в секции url-путь определяется имя файла библиотеки DLL расширения ISAPI. В секции URL <дополнительная информация> располагаются пары параметр = значение, передаваемые расширению ISAPI с помощью этой секции или посредством отправки данных HTTP из формы HTML.

    Расширения ISAPI во взаимодействии с IIS

    Если IIS получает запрос и считает, что необходимо использовать расширение ISAPI (запрошен файл, связанный с расширением ISAPI или само расширение ISAPI), то IIS передает запрос HTTP вместе с расширением ISAPI. Чтобы расширение ISAPI получило запрос HTTP, используется определенный программный интерфейс. Как известно, приложение ISAPI содержит файлы заголовков ISAPI, определяющих структуры и классы. Расширение ISAPI примет запрос посредством используемого интерфейса API, и данные запроса HTTP будут загружены в структуры и классы, являющиеся компонентами ISAPI.

    Компоненты URL
    Компонент URLЗначение из примера
    схема http
    пользователь Отсутствует в демонстрационном URL
    пароль Отсутствует в демонстрационном URL
    узел amd1700v2
    порт Отсутствует в демонстрационном URL (подразумевается значение 80)
    url-путь simpleisapi/folder1/folder2/SEUX.dll/PATH_INFO
    дополнительная информация ?parm1=value1parm2=value

    Расширение ISAPI анализирует данные HTTP-запроса и направляет вызовы другому программному обеспечению, например, программе бизнес-уровня (см. рис. 5.1). Ответы этой программы формируются в виде HTTP-ответов и возвращаются в IIS. IIS возвращает ответ расширения ISAPI веб-пользователю, направившему изначальный запрос.

    (рис 5.1) Обзор архитектуры расширения ISAPI

    Архитектура, показанная на рисунке 5.1, позволяет использовать расширения ISAPI двумя возможными способами, но не является единственным вариантом технологии. Единственным ограничением является вызов расширения ISAPI при HTTP-запросе и наличие структур и классов, содержащих HTTP-запрос и поддерживающих программное взаимодействие с ответом HTTP и запросом HTTP. Построение абстракции логики – задача разработчика. Направление вызовов библиотеке бизнес-логики необязательно, если бизнес-логика заключена в самом расширении ISAPI. Так как ISAPI напрямую записывает данные в HTTP-запрос, следует создавать архитектуру, абстрагирующую логику представления для исключения повторной компиляции ISAPI DLL при изменении логики представления.

    Сравнение ISAPI с сервером ATL

    При сравнении ATL Server и ISAPI основным различием является то, что ISAPI в меньшей степени поддерживает инфраструктуру и наличие архитектуры. ISAPI для расширений (в отличие от фильтров) состоит из функций API и структур и классов, являющихся параметрами функций API. ATL Server представляет много новых вспомогательных классов, макросов и функций и выполняет по отношению к ISAPI задачи, аналогичные классам Microsoft Foundation Classes (MFC) применительно к Windows API. ATL Server является дополнительным уровнем программирования, позволяющим оптимально использовать технологии. ATL Server поддерживает разработчика, который создает взаимодействующий с ISAPI программный продукт, предоставляя ему соответствующие методы и вспомогательное программное обеспечение. Разработчик может выбирать используемое ПО (иногда это является сложной задачей). В подобных обстоятельствах ISAPI послужит удобной альтернативой. В отличие от ATL Server ISAPI не предлагает никакой абстракции типа логики, и, кроме этого, имеется не очень много информации об ISAPI. Можно использовать классы ATL Server в приложении ISAPI без применения шаблона проекта ATL Server. В качестве альтернативы шаблон проекта ATL Server настраивается на использование одной библиотеки DLL без каких-либо опций, что создает структуру проекта, похожую на проект ISAPI.

    Построение простого расширения ISAPI

    Расширения ISAPI имеют такую несложную структуру, что могут создаваться без помощи MFC или ATL. Расширение ISAPI можно написать, используя файл включения расширения httpext.h и файл DLL definition export file (DEF) с экспортом двух следующих функций:

    HttpExtensionProc;
    GetExtensionVersion.

    Каждое расширение ISAPI должно поддерживать эти функции. Оно может также экспортировать функцию TerminateExtension (это не является обязательным). После настройки проекта DLL расширение нужно применить к IIS. В Visual Studio .NET имеется шаблон проекта ISAPI, запускающий мастер расширения ISAPI. Этот мастер создает класс, названный по имени проекта, наследуемый из класса CHTTPServer и содержащий необходимые функции ISAPI. Мастер расширения ISAPI использует файлы заголовков для поддержки от MFC, ATL и httpext.h для получения классов и структур ISAPI.

    Работа с мастером расширения ISAPI будет описана позже. Рассмотрим метод создания простого расширения ISAPI с минимальной поддержкой MFC и ATL без помощи мастера.

  • В Visual Studio .NET выберите команду File\New\Project (Файл\Создать\Проект) для открытия диалогового окна New Project (Новый проект).
  • В диалоговом окне New Project (Новый проект) щелкните на узле Visual C++ Projects (Проекты Visual C++) в левой части окна и выберите шаблон проекта Win32. Присвойте проекту имя HelloWorld и выберите место расположения создаваемого проекта.

    При нажатии на кнопку More (Больше) диалоговое окно New Project (Новый проект) отобразит дополнительную информацию о том, в каком месте будет создан проект. Название данной кнопки изменится на Less (Меньше). По умолчанию Visual Studio создает каталог с именем, идентичным имени проекта, в папке, указанной в текстовом поле Location (Расположение), в котором располагаются все файлы проекта. На рисунке 5.2 показано, что в текстовом поле Location (Расположение) введено значение C:\bookMaterial\IISBook\17ISAPIExtension\code, а именем проекта является HelloWorld.

    (рис 5.2) Диалоговое окно New Project (Новый проект) с выбранным проектом Win32
  • Нажмите на кнопку OK для запуска мастера приложений Win32 (Win32 Application Wizard) (см. рис 5.3(рис 5.3) Обзор в мастере приложения Win32
  • Откройте вкладку Application Settings (Параметры приложения) в левой части мастера приложений Win32.
  • Выберите DLL в качестве типа приложения (см. рис 5.4(рис 5.4) Создание проекта Win32 DLL.
  • Нажмите на кнопку Finish (Готово); Visual Studio создаст каталог C:\bookMaterial\IISBook\17ISAPIExtension\code\HelloWorld и разместит в нем файлы проекта.
  • Выберите команду Project\Properties (Проект\Свойства) для открытия окна свойств страницы.
  • Выберите в левой части окна свойств узел Precompiled Header в узле C/C++ для отображения свойств проекта.
  • Visual Studio .NET не имеет шаблона проекта, который реализуется без заранее скомпилированного заголовка, поэтому выберите шаблон проекта Win32, создайте из проекта библиотеку DLL и удалите параметр заранее скомпилированного заголовка. В ниспадающем списке Create\Use Precompiled Header (Создать\Использовать готовый заголовок) выберите Not Using Precompiled Headers (Не использовать готовые заголовки) в ниспадающем списке Create\Use Precompiled Header (Создать\Использовать готовый заголовок) (см. рис 5.5(рис 5.5) Изменение параметра готового заголовка проекта
  • Нажмите на кнопку OK в окне свойств. Откроется окно Solution Explorer (Обозреватель решения); если это не так, то выберите в меню команду View\Solution Explorer (Вид\Обозреватель решения).
  • Готовый заголовок не используется, поэтому удалите файлы stdafx, созданные Visual Studio .NET для его поддержки. В окне Solution Explorer выделите заголовок stdafx и файлы реализации, щелкните правой кнопкой мыши и выберите Remove (Удалить) (см. рис. 5.6).(рис 5.6) Удаление файлов stdafx из проекта
  • Теперь нужно добавить в проект DEF. Для этого щелкните правой кнопкой мыши на имени проекта в Solution Explorer и выберите команду Add\Add New Item (Добавить\Добавить новый элемент).
  • В диалоговом окне Add New Item (Добавление нового элемента) выделите DEF File (Файл DEF) и присвойте файлу имя Hello World в соответствии с именем проекта (см. рис. 5.7). Имя файла должно соответствовать имени DLL, иначе компилятор C++ отобразит предупреждение о несоответствии имен файлов. Рассматриваемый файл DEF (файл экспорта определения) описывает функции, экспортируемые из DLL, чтобы другое приложение могло загрузить DLL и найти адреса функций.(рис 5.7) Добавление в проект файла экспорта определения
  • Нажмите на кнопку Open (Открыть) в диалоговом окне Add New Item (Добавить новый элемент) для добавления в проект нового файла DEF.
  • Шаблоны проекта Visual Studio используют встроенный механизм MFC для экспорта функций и использования библиотеки DLL расширения ISAPI программой-потребителем, каковой является IIS. В Visual Studio .NET нет шаблона проекта, создающего конечный продукт без готового заголовка, поэтому следует выбрать шаблон проекта Win32, преобразовать его в DLL, после чего удалить ненужные файлы и настройки. При создании проекта без поддержки MFC иногда возникают некоторые сложности. Однако MFC сами по себе достаточно сложны, поэтому небольшие проекты (как в нашем примере) можно упростить, отказавшись от использования MFC.

    Следующим шагом является настройка файла DEF.

    Файл экспорта определения

    После добавления в проект файла DEF имя библиотеки в файле будет идентично имени проекта (иначе Visual Studio .NET отобразит предупреждение). В нашем проекте имя выглядит следующим образом:

    LIBRARY    HelloWorld

    В файл DEF добавим блок DESCRIPTION для описания назначения библиотеки DLL. Описание заключается в одинарные кавычки. Блок DESCRIPTION не является обязательным в отличие от блока EXPORTS. Имена функций следуют после объявления блока EXPORTS. В листинге 5.1 приведен код файла экспорта определения, использованный в нашем проекте. Функции приводятся в произвольном порядке, однако они должны соответствовать именам в реализации расширения.

    LIBRARY    HelloWorld
    
    DESCRIPTION 'Demonstration of a simple Hello World ISAPI extension'
    
    EXPORTS
        HttpExtensionProc
        GetExtensionVersion

    Главная точка входа расширения ISAPI

    Файл HelloWorld.cpp необходимо настроить на поддержку функций, необходимых для обеспечения работы расширения ISAPI.

  • В Visual Studio .NET откройте файл HelloWorld.cpp для редактирования посредством двойного щелчка на имени файла в окне Solution Explorer.
  • Удалите функцию DLLMain (она не нужна в данном примере).
  • В верхней части файла с кодом добавьте препроцессорную директиву включения для файла заголовка httpext.h и <string> (см. листинг 5.2).
  • Добавьте функцию GetExtensionVersion (см. листинг 5.2).
  • Добавьте функцию HttpExtensionProc (см. листинг 5.2).
  • // HelloWorld.cpp : Defines the entry point for 
    //the DLL application.
    
    #include <httpext.h>      //for ISAPI classes and structures
    
    // tell the compiler to shut up about using STL
    #pragma warning(disable:4786)
    #include <string>       //to build response to send back to user
    using namespace std;
    
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:GetExtensionVersion
    
    In: pVer - Pointer to ISAPI structure HSE_VERSION_INFO. 
    
    Out: Returns true if you want IIS to use the extension, otherwise
       if another value is returned, IIS will not use the extension
    
    Purpose:
       Called when the extension is loaded into IIS. The member 
       variables of HSE_VERSION_INFO, dwExtensionVersion and
       lpszExtensionDesc should be filled with the extension 
       version and description. 
    
       Other initialization functionality could be called from this 
       function to set up the server to use this extension.
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
    {
       //ISAPI version 
       const DWORD VERSION_NUMBER = 0.9;
       const char* VERSION_NAME = "Hello World";
    
        pVer->dwExtensionVersion = VERSION_NUMBER;
    
        strncpy(   pVer->lpszExtensionDesc, 
                VERSION_NAME, 
                HSE_MAX_EXT_DLL_NAME_LEN);
    
        return TRUE;
    }
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:HttpExtensionProc
    
    In:      pECB - pointer to the Extension control block structure
    
    Out:   DWORD - HSE status code 
    
    Purpose:
       main entry point for HTTP request
    
       the possible return codes are: 
       HSE_STATUS_SUCCESS   - everything worked great
       HSE_STATUS_SUCCESS_AND_KEEP_CONN - same as HSE_STATUS_SUCCESS 
                                  since IIS 4
       HSE_STATUS_PENDING - wait until effort completed 
       HSE_STATUS_ERROR   - sends a 500 error code
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
    {   
       //HTTP headers
       const char* BASIC_HEADER = "Content-type: text/html\r\n\r\n";
       //output values
       const DWORD BUFFER_LENGTH = 4096;
       TCHAR szTempBuffer[BUFFER_LENGTH];
       DWORD dwBufferSize = BUFFER_LENGTH;
       string sResponse;
    
       //start our HTML document
       sResponse = "<HTML><HEAD></HEAD><BODY><P>";
       sResponse += "Hi! Hello World";
       sResponse += "</P></BODY></HTML>";
    
       // set content-type header
       strcpy(szTempBuffer, BASIC_HEADER);
       DWORD dwHeaderSize = strlen(szTempBuffer);
       pECB->ServerSupportFunction(pECB->ConnID, 
                            HSE_REQ_SEND_RESPONSE_HEADER, 
                            NULL, 
                            dwHeaderSize, 
                            (LPDWORD) szTempBuffer);
    
       //write value to http response
       DWORD dwLength=sResponse.length();
       pECB->WriteClient(   pECB->ConnID, 
                      (PVOID)sResponse.c_str(), 
                      dwLength, 
                      0);
    
       //return a success code
       return HSE_STATUS_SUCCESS;
    }

    Функция GetExtensionVersion

    Функция GetExtensionVersion использует указатель на структуру HSE_VERSION_INFO в качестве параметра и возвращает значение BOOL в зависимости от того, использует ли IIS расширение ISAPI. Если функция GetExtensionVersion возвращает значение "истина", то расширение ISAPI используется. Функция GetExtensionVersion вызывается при загрузке расширения ISAPI в пространство процесса IIS. После загрузки расширения ISAPI эта функция повторно не вызывается. Она отлично подходит для включения кода, выполняющего проверку ключа лицензии, или для процедур инициализации, проверяющих использование расширения. В листинге 5.2 файла HelloWorld.cpp функция GetExtensionVersion только получает информацию о версии для расширения ISAPI.

    Структура HSE_VERSION_INFO содержит две переменные dwExtensionVersion и lpszExtensionDesc. Этим переменным присваиваются значения при вызове функции GetExtensionVersion. В листинге 5.2 переменной dwExtensionVersion присваивается значение 0,9 типа DWORD, а переменной lpszExtensionDesc – значение HelloWorld. Эта функция всегда возвращает значение "истина", так как в IIS всегда загружается расширение ISAPI. Если расширение ISAPI зависит от наличия действительного файла лицензии, другой программной библиотеки или специальной конфигурации, тогда в функции можно выполнять соответствующую проверку и возвращать значение "ложь" в случае отрицательного результата.

    Функция HttpExtensionProc

    Функция HttpExtensionProc в качестве параметра использует одну из важнейших структур ISAPI – блок контроля расширения (Extension Control Block, ECB). Эта структура содержит следующие компоненты:

  • данные о запросе HTTP;
  • данные веб-экземпляра IIS, через которое поступил запрос;
  • вспомогательные функции для анализа запроса HTTP;
  • вспомогательные функции для управления ответом HTTP.
  • Изучая код листинга 5.2, следует отметить, что указатель на блок ECB используется только для отправки заголовка ответа клиенту и для отображения клиенту фразы "Hi! Hello World". Код C++ похож на C в структурах и вспомогательных функциях, поскольку ISAPI не сильно изменился со времени выхода первой версии IIS для обеспечения обратной совместимости. Заметным изменениям ISAPI стало добавление новых функциональных возможностей. Трудность работы с ISAPI состоит в том, что некоторые API требуют программирования с использованием решений языка C.

    Первой функцией, вызываемой в блоке ECB расширения HelloWorld, является функция ServerSupportFunction. Она устанавливает заголовок в ответе HTTP для обозначения вида содержимого (текст или HTML) с помощью следующей строки:

    Content-type: text/html\r\n\r\n

    За заголовками ответов HTTP должны следовать две пары символов возврата каретки и перехода на новую строку ( /n и /r ).

    Следующей функцией, вызываемой с помощью ECB, является функция WriteClient. Фраза "Hi! Hello World" записывается в браузер с помощью данной функции посредством передачи указателя char (char*) строковой переменной, содержащей код HTML и строку "Hi! Hello World". Функции WriteClient нужен пустой указатель ( void* ), поэтому указатель char приводится к форме пустого указателя с помощью макроса PVOID.

    Реализация ISAPI "HelloWorld"

    После успешной компиляции DLL расширение ISAPI можно отгружать на веб-сервер. Если в качестве расположения конечного файла DLL указан экземпляр веб-сервера или корень виртуального каталога, можно немедленно запросить файл из IIS. При выполнении отладки Visual Studio .NET браузер не откроется, и автоматического запроса ISAPI DLL из IIS (как при отладке веб-службы ASP.NET) не произойдет. Visual Studio считает проект библиотекой DLL, поэтому выдаст запрос на присвоение исполняемого файла, который использует данную библиотеку.

    Запомните. Если разработчик осуществляет построение библиотеки DLL расширения ISAPI и затем тестирует DLL посредством ее запроса через IIS, при втором построении ISAPI DLL в Visual Studio .NET, скорее всего, не удастся создать DLL. IIS блокирует библиотеку DLL расширения ISAPI при ее запросе из IIS, поскольку DLL загружается в пространство процесса IIS. Для разблокирования ISAPI DLL необходимо выгрузить экземпляр веб-сайта или виртуального каталога либо заново запустить приложение. После разблокирования ISAPI DLL поверх нее в процессе построения осуществляется запись.

    При первом запросе ISAPI DLL из IIS 6 в операционной системе Windows Server 2003, скорее всего, возвратится ошибка 404. В WS03 и IIS6 существует новая возможность, ограничивающая по умолчанию всякую программную поддержку серверной части, если она не включена вручную. В предыдущих версиях Windows Server компонент IIS поставлялся с включенной программной поддержкой серверной части (ASP). Эта функция включается в окне Web Service Extensions (Расширения веб-служб) консоли MMC Computer Management (Управление компьютером) (см. рис. 5.8).

    (рис 5.8) Включение программной функциональности серверной части в окне Web Service Extensions (Расширения веб-служб)

    Настроим IIS на разрешение запросов расширения ISAPI HelloWorld.

  • В окне администрирования расширения веб-служб щелкните на ссылке Add A New Web Service Extension (Добавить новое расширение веб-службы). Откроется диалоговое окно New Web Service Extension (Новое расширение веб-службы).
  • В текстовом поле Extension Name (Имя расширения) введите имя, которое будет отображаться в окне администрирования расширения веб-служб. В нашем примере это имя (рис 5.9) Диалоговое окно New Web Service Extension (Новое расширение веб-службы)
  • Нажмите на кнопку Add (Добавить), чтобы указать путь к файлу расширения ISAPI. Откроется диалоговое окно Add File (Добавление файла), в котором вручную вводится путь к файлу, либо укажите его расположение в окне Open File (Открыть файл) после нажатия на кнопку Browse (Обзор).
  • Установите путь к файлу расширения ISAPI, затем нажмите на кнопку OK в диалоговом окне Add File (Добавление файла). Файла расширения ISAPI отобразится в диалоговом окне New Web Service Extension (Новое расширение веб-службы).
  • Отметьте опцию Set Extension Status To Allowed (Разрешить использование расширения), после чего нажмите на кнопку OK.
  • Расширение HelloWorld появится в окне администрирования расширения веб-служб консоли Copmuter Management (Управление компьютером), и в поле Status (Состояние) отобразится значение Allowed (Разрешено).

    В качестве альтернативы установите Allowed (Разрешено) для расширения веб-службы All Unknown ISAPI Extensions (Все неизвестные расширения ISAPI). Данный параметр разрешает выполнение любого запроса относительно любого расширения ISAPI.

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

    Разрешения на выполнение веб-экземпляра IIS или виртуального каталога устанавливаются на разрешение выполнения библиотек ISAPI DLL. По умолчанию параметр Execute Permissions (Разрешения на выполнение) в окне свойства веб-экземпляра IIS или виртуального каталога не позволяет выполнять сценарии и исполняемые файлы.

  • Откройте оснастку IIS MMC Computer Management (Управление компьютером) и щелкните правой кнопкой мыши на веб-экземпляре или виртуальном каталоге для отображения контекстного меню.
  • Выберите Properties (Свойства) для открытия окна свойств веб-экземпляра или виртуального каталога.
  • Во вкладке Virtual Directory (Виртуальный каталог) окна свойств виртуального каталога или во вкладке Home Directory (Домашний каталог) веб-экземпляра расположено поле со списком Execute Permissions (Разрешения на выполнение) (см. рис. 5.10). Убедитесь, что отмечена опция Scripts And Executables (Сценарии и исполняемые файлы).
  • (рис 5.10) Установка разрешений для виртуального каталога

    Если в IIS все настроено должным образом, ISAPI Hello World будет функционировать. Запросите библиотеку ISAPI DLL в адресе URL из браузера, как если бы это был файл HTML – и DLL выполнится (см. рис. 5.11).

    (рис 5.11) Выполнение расширения ISAPI HelloWorld

    Извлечение информации из IIS

    ECB обычно используется для извлечения информации о запросе HTTP и экземпляре сервера IIS, посредством чего расширение ISAPI выполняет определенные программные действия на основе события запроса. В коде листинга 5.3, взятого из расширения ISAPI SEUX (Простое расширение с использованием XML), функция HttpExtensionProc выполняет следующие задачи.

  • Построение документа XML со множеством общих серверных переменных, получаемых из функции GetServerVariable.
  • Добавление свойств ECB в документ XML.
  • Возврат документа XML запрашивающей стороне.
  • /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:HttpExtensionProc
    
    In:        pECB - pointer to the Extension control block structure
    
    Out:    DWORD - HSE status code 
    
    Purpose:
        main entry point for HTTP request
    
        the possible return codes are: 
        HSE_STATUS_SUCCESS    - everything worked great
        HSE_STATUS_SUCCESS_AND_KEEP_CONN - same as HSE_STATUS_SUCCESS 
                                            since IIS 4
        HSE_STATUS_PENDING - wait until effort completed 
        HSE_STATUS_ERROR   - sends a 500 error code
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
    {   
        string sDoc;
    
        //start our XML document
        sDoc = string(HEAD) + string(NEW_LINE) + string(XML_L) + 
                    string(MAIN_ELEMENT_NAME) + string(XML_R) + 
                    string(NEW_LINE);
    
        //START THE ECBServerVariable VARIABLES
        sDoc += string(XML_L) + 
            string("ECBServerVariable") + string(XML_R) + 
            string(NEW_LINE);
        //GET the ALL_HTTP
        sDoc += string(XML_L) + string("ALL_HTTP");
        GetALLHTTPHeader(pECB, sDoc);    
        sDoc += string(XML_R_END) + 
            string(NEW_LINE);//end the first main element
    
        GetECBElement(pECB, string("AUTH_TYPE"), sDoc);
        GetECBElement(pECB, string("APPL_MD_PATH"), sDoc);
        GetECBElement(pECB, string("APPL_PHYSICAL_PATH"), sDoc);
        GetECBElement(pECB, string("CONTENT_LENGTH"), sDoc);
        GetECBElement(pECB, string("CONTENT_TYPE"), sDoc);
        GetECBElement(pECB, string("GATEWAY_INTERFACE"), sDoc);
        GetECBElement(pECB, string("HTTP_ACCEPT"), sDoc);
        GetECBElement(pECB, string("HTTPS"), sDoc);
        GetECBElement(pECB, string("HTTP_AUTHORIZATION"), sDoc);
        GetECBElement(pECB, string("LOGON_USER"), sDoc);
        GetECBElement(pECB, string("AUTH_PASSWORD"), sDoc);
        GetECBElement(pECB, string("AUTH_TYPE"), sDoc);
        GetECBElement(pECB, string("AUTH_USER"), sDoc);
        GetECBElement(pECB, string("APPL_PHYSICAL_PATH"), sDoc);
        GetECBElement(pECB, string("INSTANCE_ID"), sDoc);
        GetECBElement(pECB, string("INSTANCE_META_PATH"), sDoc);
        GetECBElement(pECB, string("PATH_INFO"), sDoc);
        GetECBElement(pECB, string("PATH_TRANSLATED"), sDoc);
        GetECBElement(pECB, string("QUERY_STRING"), sDoc);
        GetECBElement(pECB, string("REMOTE_ADDR"), sDoc);
        GetECBElement(pECB, string("REMOTE_HOST"), sDoc);
        GetECBElement(pECB, string("REMOTE_USER"), sDoc);
        GetECBElement(pECB, string("REQUEST_METHOD"), sDoc);
        GetECBElement(pECB, string("SCRIPT_NAME"), sDoc);
        GetECBElement(pECB, string("SERVER_NAME"), sDoc);
        GetECBElement(pECB, string("SERVER_PORT"), sDoc);
        GetECBElement(pECB, string("SERVER_PORT_SECURE"), sDoc);
        GetECBElement(pECB, string("SERVER_PROTOCOL"), sDoc);
        GetECBElement(pECB, string("SERVER_SOFTWARE"), sDoc);
        GetECBElement(pECB, string("URL"), sDoc);
    
        //End THE ECBServerVariable VARIABLES
        sDoc += string(XML_L_END) + 
            string("ECBServerVariable") + string(XML_R) + 
            string(NEW_LINE);
    
        //START THE ECBProperties
        sDoc += string(XML_L) + 
            string("ECBProperties") + string(XML_R) + 
            string(NEW_LINE);
        
        GetElement(string("lpszLogData"),
                   string(pECB->lpszLogData),sDoc);
        GetElement(string("lpszMethod"),
                   string(pECB->lpszMethod),sDoc);
        GetElement(string("lpszQueryString"),
                   string(pECB->lpszQueryString),sDoc);
        GetElement(string("lpszPathInfo"),
                   string(pECB->lpszPathInfo),sDoc);
        GetElement(string("lpszContentType"),
                   string(pECB->lpszContentType),sDoc);
    
        //end THE ECBProperties
        sDoc += string(XML_L_END) + 
            string("ECBProperties") + string(XML_R) + 
            string(NEW_LINE);
    
           //end our XML document
        sDoc += string(XML_L_END) + 
            string(MAIN_ELEMENT_NAME) + string(XML_R) + 
            string(NEW_LINE);
    
        //write it!
        SendResponse(pECB,sDoc);
    
       return HSE_STATUS_SUCCESS;
    }

    Примечание. Исходный код SEUX доступен на веб-сайте автора книги (см. введение).

    Построение XML для представления значений серверных переменных

    Первой задачей (см. листинг 5.3) является открытие документа XML посредством присоединения некоторых констант, представляющих собой части документа XML. Построение XML происходит вручную, и константы объявляются для общих частей документа XML, так как они используются во многих местах. Документ XML размещается в строковой переменной с именем sDoc. Ниже приведены константы XML:

    //xml parts
        const char* MAIN_ELEMENT_NAME = "HTTPRequestRaw";
        const char* QUOTE        ="\"";
        const char* XML_L        ="<";
        const char* XML_R        =">";
        const char* XML_L_END    ="</";
        const char* XML_R_END    ="/>";
        const char* NEW_LINE    ="\n";
        const char* HEAD = "<?xml version=\"1.0\"?>";

    Рассматриваемый документ XML достаточно прост, поэтому его построение вручную не вызывает никаких трудностей. Для более сложного документа рекомендуется использовать аналитическую библиотеку XML, такую как MSXML.

    После инициализации документа XML начинается работа по размещению в элементах XML всех серверных переменных. Создаваемый документ XML имеет родительский элемент HTTPRequestRaw. Внутри HTTPRequestRaw содержатся два дочерних элемента с ECBServerVariable и ECBProperties. Для каждого значения серверной переменной, запрашиваемого из ECB, будет создаваться дочерний элемент для элемента ECBServerVariable, независимо от получения значения соответствующей серверной переменной. Для каждого запрошенного свойства ECB создается дочерний элемент под элементом ECBProperties, которое содержит значение независимо от получения значения свойства. При вызове расширения ISAPI SEUX с помощью IE 6.0 отобразится документ XML (см. рис. 5.12). В других версиях браузера XML может отобразится по-другому или не отобразиться вовсе.

    (рис 5.12) IE 6.0, отображающий документ XML серверных переменных из расширения SEUX ISAPI

    Специальный случай серверной переменной ALL_HTTP

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

    Заголовки в значении, возвращаемом из ALL_HTTP, ограничены символами новой строки, поэтому требуется дополнительный анализ для размещения каждого заголовка в элементе XML в качестве атрибута. Функция GetALLHTTPHeader выполняет данную задачу с помощью функции ECB GetServerVariable (см. листинг 5.4).

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:GetALLHTTPHeader
    
    In:    pECB - pointer to the Extension control block structure
    
        psElement - string pointer to XML document being built that 
                    will be updated with the element for the ALL_HTTP
                    server variable value.
    
    Out:    nothing returned but the string psElement points to 
            will be updated.
    
    Purpose:
        Updates the XML document string by adding an element 
        for the ALL_HTTP server variable. This variable contains 
        all of the http headers so some additional parsing must 
        take place on the headers to format them into XML. 
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    void GetALLHTTPHeader(EXTENSION_CONTROL_BLOCK *pECB, 
                          string *psElement)
    {
        TCHAR szTempBuffer[BUFFER_LENGTH];
        DWORD dwBufferSize = BUFFER_LENGTH;
        const string EQUAL("=");
        const string SPACE(" ");
    
        string sAllHeaders;    //used to cut up 'all headers' returned
        int nNewLinePos = 0;
        int nEndLinePos = 0;
        string sName;
        string sValue;
    
    
        //pull the whole HTTP header 
        if (pECB->GetServerVariable(    pECB->ConnID, 
                                        "ALL_HTTP", 
                                        szTempBuffer, 
                                        dwBufferSize))
        {
           //if the whole http header was pulled then parse it
            sAllHeaders.assign(szTempBuffer);
    
            //get the name / value pairs
            while (GetHeaderValuePair(    sAllHeaders, 
                                        nNewLinePos, 
                                        sName, 
                                        sValue, 
                                        nEndLinePos))
            {
                //add the attribute to the element
                psElement->append(    SPACE + sName + EQUAL + 
                                    QUOTE + 
                                    ValidateValue(sValue) + 
                                    QUOTE); 
    
                //reset the newline to the last endline
                nNewLinePos = nEndLinePos;                    
                
                //dump the values
                sName.erase();
                sValue.erase();
            }
        }
    }

    Функция GetServerVariable

    Функция GetServerVariable возвращает значение "истина" при успешном выполнении и значение "ложь" при возникновении ошибки, как показано в следующем примере:

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

    Прототип GetServerVariable в данном случае требует передачи четырех параметров.

  • hConn. Поддержка соединения, полученная от ECB.
  • lpszVariableName. Строка с символом конца строки запрашиваемой серверной переменной.
  • lpvBuffer. Пустой указатель на буфер, который заполняется результирующим значением имени переменной и байтом конца строки.
  • lpdwSizeofBuffer. Указатель на значение DWORD, отражающее размер буфера.
  • При успешном выполнении функция GetServerVariable возвращает значение "истина". Указатель lpvBuffer указывает значение запрашиваемой серверной переменной, а lpdwSizeofBuffer – на новое значение DWORD, отражающее текущий размер значения, включая байт конца строки. При неудачном завершении работы функция GetServerValue возвращает значение "ложь". В этом случае вызывается функция GetLastError, которая возвращает значение DWORD, представляющее собой код ошибки. В таблице 5.2 показаны возможные ошибки функции GetServerVariable ; это константы, определяемыми во вспомогательном файле заголовка расширения ISAPI.

    Значения серверных переменных

    Возможные значения запрашиваемых серверных переменных приведены в листинге 5.3 в коде функции HttpExtensionProc в расширении ISAPI SEUX ; они являются аргументами в вызовах GetECBElement. Значения серверных переменных могут изменяться в процессе текущего события запроса HTTP в IIS, и зачастую переменной не присваивается значение. Серверные переменные содержат значения только при определенных настройках IIS. В таблице 5.2 приведены серверные переменные, запрашиваемые с помощью функции GetServerVariable.

    Обзор переменных сервера, запрашиваемых функцией GetServerVariable
    Константа ошибкиОписание ошибки
    ERROR_INVALID_PARAMETER Значение hConn является некорректным или закрытым, либо неверны параметры серверной переменной.
    ERROR_INVALID_INDEX Запрашиваемая серверная переменная не поддерживается.
    ERROR_INSUFFICIENT_BUFFER Размер lpdwSizeofBuffer слишком мал для содержания значения запрашиваемой серверной переменной.
    ERROR_NO_DATA Запрашиваемая серверная переменная недоступна.
    ALL_HTTP Все HTTP-заголовки (разделены символами новой строки), переданные в запросе HTTP в строке с символом конца строки. Заголовки имеют вид <имя заголовка> : <значение>.
    ALL_RAW Все заголовки HTTP в том виде, в котором они были отправлены запрашивающей HTTP-стороной.
    APPL_MD_PATH Путь метабазы веб-приложения. Например, /LMW3SVC/1/Root/SimpleISAPI.
    APPL_PHYSICAL_PATH Физический путь корневого веб-каталога для веб-приложения. Например: C:\ISAPI\.
    AUTH_PASSWORD Пароль, вводимый веб-пользователем в диалоговом окне аутентификации браузера, если установлена базовая аутентификация.
    AUTH_TYPE Используемый тип аутентификации. Пустое значение при отсутствии аутентификации либо значение, соответствующее Kerberos, пользовательской аутентификации, SSL/PCT, базовой или интегрированной аутентификации Windows.
    AUTH_USER Имя пользователя, вводимое пользователем в диалоговом окне аутентификации браузера в случае, если установлена базовая аутентификация.
    CERT_COOKIE Уникальный идентификатор сертификата клиента.
    CERT_FLAGS Битовые флаги бюро сертификатов (CA) сертификата клиента. Если bit0 равен 1, то CA отсутствует в списке распознаваемых бюро сертификатов данного сервера и признается недействительным.
    CERT_ISSUER Содержит имя сертификата клиента. Например, O=Schmidlaps, OU=House, CN= имя пользователя, C=USA.
    CERT_KEYSIZE Размер ключа в битах при соединении SSL.
    CERT_SERCRETKEYSIZE Размер секретного ключа сертификата сервера в битах.
    CERT_SERIALNUMBER Серийный номер сертификата клиента.
    CERT_SERVER_ISSUER Подробное имя издателя сертификата сервера.
    CERT_SERVER_SUBJECT Подробное имя субъекта сертификата сервера.
    CERT_SUBJECT Субъект сертификата клиента.
    CONTENT_LENGTH Количество байт, исключая заголовки HTTP-запроса.
    LOGON_USER Если конечный пользователь успешно аутентифицировался в Windows, используется учетная запись входа в систему.
    HTTPS Возвращает значение off, если в HTTPS не используется SSL, в противном случае возвращается значение on.
    HTTPS_KEYSIZE Размер ключа SSL-соединения в битах.
    HTTP_SECRETKEYSIZE Размер секретного ключа сертификата сервера в битах.
    HTTPS_SERVER_ISSUER Подробное имя издателя сертификата сервера.
    HTTPS_SERVER_SUBJECT Подробное имя субъекта сертификата сервера.
    INSTANCE_ID Номер экземпляра сервера. Значения идентификатора сервера в метабазе, например, 1.
    INSTANCE_META_PATH Путь веб- экземпляра в метабазе, например: LM/W3SVC/1.
    PATH_INFO Часть URL, расположенная между ISAPI DLL и началом секции с дополнительной информацией в URL. Как правило, в этом месте нет никаких данных, если запрашивающее ПО не добавляет свое значение.
    PATH_TRANSLATED Часть веб-экземпляра, связанная с физическим жестким диском, с конкатенацией значения PATH_INFO.
    QUERY_STRING Строка символов, следующих за символом "?" в секции дополнительной информации URL.
    REMOTE_ADDR IP-адрес хоста или шлюза запрашивающего ПО.
    REMOTE_HOST Имя узла или шлюза запрашивающего ПО, если включен обратный поиск DNS; иначе возвращается значение IP-адреса узла или шлюза запрашивающего ПО.
    REMOTE_USER Имя пользователя, осуществляющего HTTP-запрос и аутентифицируемого несущим сервером. Представляет собой пустую строку для анонимного пользователя.
    REQUEST_METHOD Команда метода HTTP-запроса.
    SCRIPT_NAME Имя исполняемого двоичного файла, например, имя ISAPI DLL или исполняемого файла CGI.
    SERVER_NAME Имя узла сервера или IP-адрес.
    SERVER_PORT Порт TCP/IP, по которому получен запрос.
    SERVER_PORT_SECURE Значение 0 или 1. Запросы по безопасному порту возвращают 1; иначе возвращается 0.
    SERVER_PROTOCOL Имя и версия протокола запроса, например, HTTP 1.1.
    SERVER_SOFTWARE Имя и версия IIS, под которой выполняется программа DLL расширения ISAPI, например, Microsoft-IIS/6.0.
    URL Значение части url-путь адреса URL, исключая значение PATH_INFO. Например: /simpleisapi/folder1/folder2/SEUX.dll.

    Для демонстрации значений таблицы 5.2 в листинге 5.5 приведен документ XML, созданный из расширения ISAPI SEUX.DLL. В данном примере несущий сервер и IIS настроены таким образом, что многие значения серверных переменных получаются в процессе HTTP-запроса. Имя несущего узла – amd1700v2. IIS 6 на amd1700v2 настроен с использованием следующих значений параметров и файловых расположений.

  • Физическое расположение расширения ISAPI SEUX.DLL. C:\ISAPI\папка1\папка2\SEUX.dll.
  • Корень веб- экземпляра. C:\inetpub\wwwroot.
  • Связанный виртуальный каталог. C:\ISAPI.
  • Анонимный доступ. Не включен для виртуального каталога.
  • Базовая аутентификация. Включена для виртуального каталога.
  • Расширение ISAPI SEUX.dll запрошено с amd1700v2 с помощью следующих данных браузера, расположенного на отдельном компьютере.

  • Пользователь, осуществивший вход на веб-сайт под именем normaluser.
  • Пользователь, осуществивший вход на веб-сайт с использованием пароля normaluser.
  • URL в браузере: http://amd1700v2/simpleisapi/folder1/folder2/SEUX.dll/PATH_INFO?parm1=value1parm2=value
  • <?xml version="1.0" ?> 
    <HTTPRequestRaw>
     <ECBServerVariable>
      <ALL_HTTP HTTP_CONNECTION="Keep-Alive" 
       HTTP_ACCEPT=
     "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, 
    application/vnd.ms-powerpoint, application/vnd.ms-excel, 
    application/msword, */*" 
       HTTP_ACCEPT_ENCODING="gzip, deflate" 
       HTTP_ACCEPT_LANGUAGE="en-us" 
       HTTP_AUTHORIZATION="Basic bm9ybWFsdXNlcjpub3JtYWx1c2Vy"
       HTTP_COOKIE="ASPCLIENTDEBUG=1" HTTP_HOST="amd1700v2" 
       HTTP_USER_AGENT=
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; .NET CLR 
    1.0.3705)" /> 
      <AUTH_TYPE>Basic</AUTH_TYPE> 
      <APPL_MD_PATH>/LM/W3SVC/1/Root/SimpleISAPI</APPL_MD_PATH> 
      <APPL_PHYSICAL_PATH>C:\ISAPI\</APPL_PHYSICAL_PATH> 
      <CONTENT_LENGTH>0</CONTENT_LENGTH> 
      <CONTENT_TYPE /> 
      <GATEWAY_INTERFACE>CGI/1.1</GATEWAY_INTERFACE> 
      <HTTP_ACCEPT>image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
    application/vnd.ms-powerpoint, application/vnd.ms-excel, 
    application/msword, */*</HTTP_ACCEPT> 
      <HTTPS>off</HTTPS> 
      <HTTP_AUTHORIZATION>
    Basic bm9ybWFsdXNlcjpub3JtYWx1c2Vy
      </HTTP_AUTHORIZATION> 
      <LOGON_USER>normaluser</LOGON_USER> 
      <AUTH_PASSWORD>normaluser</AUTH_PASSWORD> 
      <AUTH_TYPE>Basic</AUTH_TYPE> 
      <AUTH_USER>normaluser</AUTH_USER> 
      <APPL_PHYSICAL_PATH>C:\ISAPI\</APPL_PHYSICAL_PATH> 
      <INSTANCE_ID>1</INSTANCE_ID> 
      <INSTANCE_META_PATH>/LM/W3SVC/1</INSTANCE_META_PATH> 
      <PATH_INFO>/PATH_INFO</PATH_INFO> 
      <PATH_TRANSLATED>c:\inetpub\wwwroot\PATH_INFO</PATH_TRANSLATED> 
      <QUERY_STRING>parm1=value1parm2=value</QUERY_STRING> 
      <REMOTE_ADDR>169.254.176.147</REMOTE_ADDR> 
      <REMOTE_HOST>169.254.176.147</REMOTE_HOST> 
      <REMOTE_USER>normaluser</REMOTE_USER> 
      <REQUEST_METHOD>GET</REQUEST_METHOD> 
      <SCRIPT_NAME>/simpleisapi/folder1/folder2/SEUX.dll</SCRIPT_NAME> 
      <SERVER_NAME>amd1700v2</SERVER_NAME> 
      <SERVER_PORT>80</SERVER_PORT> 
      <SERVER_PORT_SECURE>0</SERVER_PORT_SECURE> 
      <SERVER_PROTOCOL>HTTP/1.1</SERVER_PROTOCOL> 
      <SERVER_SOFTWARE>Microsoft-IIS/6.0</SERVER_SOFTWARE> 
      <URL>/simpleisapi/folder1/folder2/SEUX.dll</URL> 
     </ECBServerVariable>
     <ECBProperties>
      <lpszLogData /> 
      <lpszMethod>GET</lpszMethod> 
      <lpszQueryString>parm1=value1parm2=value</lpszQueryString> 
      <lpszPathInfo>/PATH_INFO</lpszPathInfo> 
      <lpszContentType /> 
     </ECBProperties>
    </HTTPRequestRaw>

    Анализ пары "Заголовок-Значение"

    После получения заголовка ALL_HTTP из функции GetServerVariable (см. листинг 5.4) для отображения содержимого в XML необходимо реализовать обработку строки. Заголовки разделяются символами новой строки и возврата каретки. Двоеточие (":") разделяет имя заголовка и его значение. GetHeaderValuePair инициализирует поиск в указанной позиции и возвращает имя и соответствующее ему значение для данного заголовка, а также позицию, в которой прерван поиск заголовков. GetHeaderValuePair единовременно осуществляет поиск одного значения заголовка.

    Как видно из листинга 5.6 функция GetHeaderValuePair осуществляет поиск символа ":" в HTTP-заголовке, начиная с позиции nStart в строке sHeader. Значение nStart – это счетчик (начинается с нуля). Если символ " :" не найден, функция завершает работу с возвращением значения "ложь". При обнаружении символа " :" (т.е. заголовок существует) в sHeader продолжается поиск новой строки, начиная с позиции, в которой обнаружен данный символ. При поиске используется функция find строки Standard Template Library (STL) с символом новой строки \n в качестве аргумента и с указанием в качестве начальной позиции символа " :". Позиция новой строки становится конечной позицией, возвращаемой указателем pnEnd, посредством чего вызывающей функции становится известно, в каком месте остановлен поиск. С помощью всех параметров позиции, выявленных в процессе вызовов функции поиска строки sHeader, имя заголовка и значение извлекаются в место расположения памяти, связанное с указателями psName и psValue.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name: GetHeaderValuePair
    
    In: sHeader - string HTTP Header 
        nStart - integer search start location
        psName - pointer to name of header that is being sought 
        psValue - pointer to string that will be filled if 
                    value found
        pnEnd - pointer to integer of the final search position  
    
    Out: bool    true returned if the header was found, 
                false returned otherwise
    Purpose:
            Searches through the HTTP header passed in for a 
            header value. Returns data about the search 
            parameters if found or not.
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    bool GetHeaderValuePair(const string sHeader, 
                            const int nStart, 
                            string *psName, 
                            string *psValue, 
                            int *pnEnd)
    {
        const string sColon(":");
    
        //determine if header is a post header
        string::size_type idxColonPosition = nStart;
    
        //start looking at beginning 
        idxColonPosition = sHeader.find(sColon, idxColonPosition);
    
        if (idxColonPosition == string::npos)//no more headers found
            return false;//this is failure
    
        //get the name
        psName->assign(sHeader.substr
                       (nStart, idxColonPosition - nStart));
    
        //find next newline
        string::size_type idxNewLine;
        idxNewLine = sHeader.find('\n', idxColonPosition);
    
        //get the end even if it means not found
        *pnEnd = idxNewLine;
    
        if (idxNewLine == string::npos)    //a newline was not found
            return true;//not a failure - might be the last header
    
        //get the value
        //adjust colon position so we do not assign colon in value
        idxColonPosition = idxColonPosition +1; 
        psValue->assign(sHeader.substr(idxColonPosition, 
            idxNewLine - idxColonPosition));
    
        return true;

    Построение остальных элементов XML

    После обработки функции HttpExtensionProc значения заголовка ALL_HTTP остальные серверные переменные обрабатываются с помощью функции GetECBElement (см. листинг 5.7). Каждая серверная переменная, передаваемая в GetECBElement, извлекается с помощью функции GetServerVariable и записывается в элемент XML, присоединяемый к строке, на которую указывает psElement. Указатель psElement указывает на документ XML, конструируемый в HttpExtensionProc.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name:GetECBElement
    
    In:    pECB - Pointer to the extension control block for the 
        purposes of calling the GetServerVariable function.
        
        sName - string name of the server variable that is 
                being sought.
    
        psElement - string pointer to XML document being built that 
                    will be updated with the name and value for the 
                    server variable extracted from the extension 
                    control block.
    
    Out:    nothing returned but the string psElement points to 
            will be updated.
    
    Purpose:
        appends a string of an XML element to the string psElement 
        points to. The XML element that is created is in the form of
    <Server Variable Name>Server Variable Value</Server Variable Name>
    
        for example:
     <GATEWAY_INTERFACE>CGI/1.1</GATEWAY_INTERFACE> + newline
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/    
    void GetECBElement(    EXTENSION_CONTROL_BLOCK *pECB, 
                                        const string sName, 
                                        string *psElement)
    {
    
    TCHAR szTempBuffer[BUFFER_LENGTH];
    DWORD dwBufferSize = BUFFER_LENGTH;
    
        //get the server variable value
       if (pECB->GetServerVariable(    pECB->ConnID, 
                        (LPSTR)sName.c_str(), 
                        szTempBuffer, 
                        dwBufferSize))
       {
           //build the XML element and 
           //add it to the XML document passed in
            psElement->append(string(XML_L) + sName + string(XML_R));
    
            psElement->append(ValidateValue((string)szTempBuffer));
    
            psElement->append(string(XML_L_END) + sName + string(XML_R) + 
                string(NEW_LINE));
       }
    
    }

    Функция ValidateValue проверяет, что специальные символы указаны с помощью альтернативных комбинаций символов. Функция применяется к строке, перед тем как строке присваивается статус значения элемента. Для подтверждения значения атрибута можно применять ValidateValue. XML не разрешает использование определенных специальных символов в позиции значения, если они не представлены в альтернативном виде. Как видно из листинга 5.8, символы, используемые для реализации XML-структуры: " ?" и " ?" – заменяются альтернативными эквивалентами.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name: ValidateValue
    
    In: Constant reference to a string variable sValue. sValue is the 
        value being checked to see if it has a character requiring 
        escaping
    
    Out: returns a string with the escaped characters in place
    
    Purpose:
        blindly replaces all special characters 
        with the escape sequence character so that XML will
        be valid. 
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    string ValidateValue(const string sValue)
    {
        string sReturn;
        sReturn = sValue;
    
        FindAndReplace(sReturn, string(""),string("amp;"));
        FindAndReplace(sReturn, string("="),string("#61;"));
        FindAndReplace(sReturn, string("<"),string("lt;"));
        FindAndReplace(sReturn, string(">"),string("gt;"));
        FindAndReplace(sReturn, string("'"),string("apos;"));
        FindAndReplace(sReturn, string("\""),string("quot;"));
    
        return sReturn;
    }

    Функция FindAndReplace представляет собой утилиту для замещения всех вхождений строки. В расширении ISAPI SEUX она является идеальным механизмом для замещения одной фразы внутри строки другой фразой. Аргументы представляют собой указатели на строки:

  • изменяемая строка (контейнер);
  • строка, которую нужно заменить внутри контейнера (цель);
  • строка, заменяющая цель в контейнере (замещение).
  • Строка STL содержит функции find и replace, используемые функцией FindAndReplace для поиска контейнера всех вхождений цели (см. листинг 5.9). Каждый раз при обнаружении цели в контейнере происходит ее замена, и начинается новый поиск. По завершении работы функции FindAndReplace контейнер обновляется замещениями, если таковые имеются.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name: FindAndReplace
    
    In: psContainer - pointer to a string that will be searched 
                      and edited if a value is discovered.
         psTarget - pointer to a string that is being sought for 
                    replacement.
         psReplacement - pointer to a string that will replace the 
                         the string pointed to in psTarget.
    
    Out: nothing - but psContainer will be changed
    
    Purpose:
         searches string psContainer pointer for the string that 
         psTarget points to and replaces it with the string that 
         psReplacement points to.
    
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    void FindAndReplace(string *psContainer, 
                             string *psTarget, 
                             string *psReplacement)
    {
    
         string::size_type idx;
         idx = psContainer->find(*psTarget);
         while (idx != string::npos)//an instance was found
         {          
              //are we at the end of the string
              if (psContainer->size() == idx)
              {
                   *psContainer += *psReplacement;
                   break;
              }
              else
              {
                   psContainer->replace
                             (idx, psTarget->size() , *psReplacement);
                   
                   //advance beyond the current character
                   idx += psReplacement->size();
              }
    
              //look for next occurance
              idx = psContainer->find(*psTarget, idx);
         }
    
    }

    Функция GetElement работает аналогично функции GetECBElement ; она вызывается из функции HttpExtensionProc для конкатенации элементов из свойств ECB. Свойства извлекаются из ECB, после чего передаются вместе со своими именами и документом XML в функцию GetElement. GetElement размещает свойство ECB и соответствующее значение в XML документе и конкатенирует его с указателем документа XML, переданным функции GetElement. Осуществляется запрос следующих свойств:

  • lpszLogData. Буфер размера HSE_LOG_BUFFER_LEN, используемый для размещения информации, добавляемой в файл журнала для данной транзакции HTTP-запроса.
  • lpszMethod. Строковое значение используемого метода HTTP, например, GET, PUT или HEAD.
  • lpszQueryString. Строковое значение символов в секции дополнительной информации адреса URL, исключая символ " ?". Аналогично значению переменной сервера QUERY_STRING.
  • lpszPathInfo. Строковое значение секции URL, находящейся между библиотекой DLL расширения ISAPI и началом секции дополнительной информации URL. Обычно не содержит данных, если запрашивающее ПО не разместило в этом месте определенное значение.
  • lpszContentType. Строковое значение типа содержимого отправленных по HTTP данных. Аналогично значению серверной переменной CONTENT_TYPE.
  • Когда функция HttpExtensionProc завершает получение содержимого всех возможных серверных переменных и свойств ECB, в документе XML указываются закрывающие тегов XML, и он передается функции SendResponse. SendResponse направляет запрашивающей программе строковое значение, переданное в функцию. Заголовок типа содержимого передается запрашивающему клиенту с помощью функции ECB ServerSupportFunction (см. листинг 5.10). Передаваемый заголовок представляется константой BASIC_HEADER, эквивалентной следующей строке: Content-type: text/html\r\n\r\n. За заголовками HTTP следуют два символа возврата каретки и новой строки, поскольку возвращаемые данные представляют собой текст. Можно указать и XML, однако если в запрашивающем браузере в XML зарегистрированы типы Multipurpose Internet Mail Extensions (MIME), то для отображения XML откроется зарегистрированная программа.

    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Name: SendResponse
    
    In:    pECB - pointer to the extension control block
        sValue - string reference to the value to be
                 written to the HTTP response
    
    Out:    nothing
    
    Purpose:
            writes the intended value to the HTTP response 
    /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    void SendResponse(EXTENSION_CONTROL_BLOCK *pECB, string sValue)
    {
        TCHAR szTempBuffer[BUFFER_LENGTH];
        DWORD dwBufferSize = BUFFER_LENGTH;
    
        // set content-type header
        strcpy(szTempBuffer, BASIC_HEADER);
        DWORD dwHeaderSize = strlen(szTempBuffer);
        pECB->ServerSupportFunction(pECB->ConnID, 
                                    HSE_REQ_SEND_RESPONSE_HEADER, 
                                    NULL, 
                                    dwHeaderSize, 
                                    (LPDWORD) szTempBuffer);
    
        //write value to http response
        DWORD dwLength=sValue.length();
        pECB->WriteClient(    pECB->ConnID, 
                            (PVOID)sValue.c_str(), 
                            dwLength, 
                            HSE_IO_SYNC);
    }

    После отправки заголовка возврата отправляется содержимое с помощью функции WriteClient. Как показано в следующем примере, при передаче данных клиенту отправляется идентификатор соединения ConnID. Имеющееся значение, полученное из текущего экземпляра указателя, использовано в листинге 5.10. Содержимое передается функцией WriteClient с помощью пустого указателя в параметре Buffer. Содержимое, на которое ссылается указатель Buffer, должно равняться количеству байт, передаваемому клиенту, и указываться в параметре lpdwBytes. По завершении вызова lpdwBytes содержит количество переданных байт, если запись не осуществлялась асинхронно. Значение параметра dwSync определяет способ передачи данных клиенту. В листинге 5.10 с помощью макроса HSE_IO_SYNC указывается значение 0х00000001, т.е. запись выполняется синхронно, и пространство памяти, на которое ссылается указатель lpdwBytes, обновится по завершении WriteClient количеством байт, переданным клиенту. Если в макросе HSE_IO_ASYNC представлено значение 0х00000002, то данные, отправляемые клиенту, и функция обратной связи зафиксируют события передачи информации клиенту. Асинхронное использование функции WriteClient требует объявления функции обратной связи, а также отправки функцией ServerSupportFunction значения HSE_REQ_IO_COMPLETION для установки с клиентом транзакции асинхронной записи.

    Функция WriteClient является членом ECB. Может показаться странным, что используемое описание ECB передается функции. Поскольку приложение IIS включает несколько нитей, в любой момент времени может потребоваться несколько экземпляров ECB, и вероятно выполнение записи в экземпляре ECB в другой экземпляр ECB. Ниже приведен пример WriteClient:

    BOOL WriteClient(
      HCONN ConnID,    
      LPVOID Buffer,   
      LPDWORD lpdwBytes, 
      DWORD dwSync   
    );

    Мастер шаблона проекта ISAPI

    При создании расширения ISAPI с использованием поддержки MFC воспользуйтесь мастером шаблона проекта ISAPI (ISAPI Project Template Wizard). Мастер настроит проект на необходимую поддержку для библиотек ISAPI и создаст класс, унаследованный из CHttpServer. Для ISAPI предоставляются следующие классы MFC.

  • CHttpArgList. Класс, содержащий функции и структуры для анализа URL.
  • CHtmlStream. Класс, управляющий памятью по отношению к данным, предназначенных для передачи клиенту.
  • CHttpFilter. Класс, расширяющий интерфейс IIS для создания фильтра ISAPI. Фильтры представляют собой тип приложения ISAPI, вызываемый при каждом запросе IIS, поэтому они отвечают на события внутри IIS.
  • CHttpFilterContext. Класс, являющийся параметром в функциях класса CHttpFilter, для обработки содержимого данного события HTTP. Служит для обработки связанных с рассматриваемым событием HTTP данных, проходящих через фильтр.
  • CHttpServer. Класс, расширяющий интерфейс IIS для рассматриваемого события HTTP-запроса.
  • ChttpServerContext. Класс, обеспечиваемый классом CHttpServer, содержащий методы управления данными, связанными с рассматриваемым событием HTTP.
  • Классы, предоставляемые в MFC, обеспечивают хороший уровень абстракции для первоначального взаимодействия со структурами ISAPI и функциями, приведенными выше. С помощью MFC разработчик осуществляет доступ к "сырому" ISAPI. Некоторые классы облегчают анализ заголовков и данных HTTP, выполняемый вручную в расширении ISAPI SEUX. Мастер создаст класс, которому будет присвоено имя согласно следующей схеме: C<Имя_проекта>Расширение. Функция с именем Default является главной точкой входа для ISAPI DLL:

    void Default(CHttpServerContext* pCtxt);

    Функция Default заменяет функцию HttpExtensionProc, используемую в расширении ISAPI SEUX в качестве главной точки входа. Указатель на класс CHttpServerContext, передаваемый в функцию Default, обеспечивает функциональность и требования разработчика к управлению данными и HTTP-транзакцией (осуществлялось посредством ECB в расширении ISAPI SEUX ).

    Создание расширения ISAPI в Visual Studio .NET

    Для создания расширения ISAPI с помощью шаблона проекта Vusal Studio .NET откройте Visual Studio .NET и выберите File\New\Project (Файл\Создать\Проект). Откроется диалоговое окно New Project (Новый проект) (см. рис. 5.13). Выберите шаблон проекта MFC ISAPI Extension DLL, введите имя проекта и нажмите на кнопку OK.

    (рис 5.13) Выбор шаблона проекта MFC ISAPI Extension DLL в Visual Studio .NET

    Примечание. Диалоговое окно (см. рис. 5.13) отображает значки, отличающиеся от тех, которые показаны на рис. 5.2. В обоих случаях использованы одинаковые версии Visual Studio .NET. Рисунок 5.2 получен при нажатии на кнопку Large Icons (Большие значки) в правом верхнем углу, рисунок 5.13 – при нажатии на кнопку Small Icons (Мелкие значки). Кнопки Large Icons (Большие значки) и Small Icons (Мелкие значки) являются взаимоисключающими, и они влияют на отображение значков проекта в правой части диалогового окна New Project (Новый проект).

    По аналогии с мастером проекта сервера ATL (см. лекцию 4) работа мастера расширения ISAPI выполняется в одном окне, и его работа завершается после нажатия на кнопку Finish (Готово) (см. рис. 5.14). Мастер приводит обзор параметров проекта в области Overview (Обзор). Типом создаваемого проекта по умолчанию является библиотека DLL расширения ISAPI с MFC в общей DLL. Данные настройки подходят для большинства проектов ISAPI. Эти параметры можно изменить в области Object Settings (Параметры объекта).

    (рис 5.14) Область Overview (Обзор) мастера расширения ISAPI с проектом ISAPI по умолчанию

    В области Object Settings (Параметры объекта) настраиваются имена классов, сгенерированных мастером, содержатся опции, позволяющие расширению ISAPI подключаться к MFC статически или динамически. Выбор опции Use MFC In A Shared DLL (Использовать MFC в общей DLL) (см. рис. 5.15) определяет динамическое подключение MFC в ISAPI DLL, генерируемой во время компиляции. Подразумевается, что MFC существует в среде разработки, что достигается исключением содержимого из ISAPI DLL.

    (рис 5.15) Область Object Settings (Параметры объекта) мастера расширения ISAPI с проектом ISAPI по умолчанию.

    При выборе опции Use MFC In A Static Library (Использовать MFC в статической библиотеке) компоненты MFC компилируются в DLL и подключаются статически. В этом случае подразумевается, что MFC не существует в среде разработки. Размер библиотеки при статическом подключении больше, чем при динамическом подключении. В большинстве случаев MFC располагаются в целевой среде, так как речь идет о сервере Windows. Для WS03 используйте динамическое подключение, поскольку MFC находятся на сервере.

    Можно сгенерировать объект фильтра, включив опцию Generate A Filter Object (Генерировать объект фильтра). Фильтры загружаются в IIS для обработки каждого запроса, имеющего место в IIS. Они работают по аналогии с расширением ISAPI, как если бы оно было связано с каждым файлом корневого веб-каталога. Фильтр и расширение не являются взаимоисключающими функциями. Одна библиотека DLL может содержать как расширение, так и фильтр, что позволяет каждому из этих объектов использовать состояние посредством глобальных переменных.

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

    После нажатия на кнопку Finish (Готово) и завершения мастером генерации файлов проект можно скомпилировать и запустить, как только библиотека DLL будет отгружена на сервер. Если все выполнится правильно, вы увидите в браузере сообщение, имя класса в котором соответствует выбранному имени класса:

    This default message was produced by the Internet Server DLL
    Wizard. Edit your CMFCISAPIExtension::Default() implementation
    to change it.
    Вернуться к учебному плану