IIS的ISAPI接口

ISAPI(Internet Server Application Programming Interface)作爲一種可用來替代CGI的方法,是由微軟和Process軟件公司聯合提出的Web服務器上的API標準。ISAPI與Web服務器結合緊密,功能強大,能夠獲得大量的信息,因此利用ISAPI可以開發出靈活高效的Web服務器增強程序。由於ISAPI程序與Web服務器的關係,使得ISAPI接口在安全方面有一定的研究價值。本文主要討論ISAPI在IIS和VC++ 6.0中的實現。

一、ISAPI接口和CGI接口的不同。

ISAPI程序和CGI程序完成類似的功能,但是實現方法不同。

1、ISAPI程序以DLL形式被Web服務器加載到自己的進程空間中,因此和服務器共用同一個地址空間,且在沒有客戶請求時可以將其從內存中卸載;而對客戶端發來的每個對CGI程序的請求則需要服務器爲它單獨啓動一個進程,這需要耗費大量的時間和內存。當併發的請求數目很大時,使用CGI在效率上不如ISAPI。

2、CGI程序通過環境塊和標準輸入輸出與Web服務器進行通信,而ISAPI程序與服務器結合得更爲緊密,與服務器共享同一個進程上下文,主要通過一個參數塊與服務器進行交互,可以從服務器那裏獲得關於當前HTTP連接的大量信息。

ISAPI主要分爲ISA和ISAPI Filter兩部分。ISA方法相對而言要傳統一些,利用一些特殊的鏈接,指向服務器的作業,供程序開發人員設計一些擴展功能;而ISAPI過濾器則傾向於構造服務器直接調用的模塊,提供一種無縫鏈接部件用於監測直接來自於服務器的HTTP請求。


二、ISA

ISA(Internet Server Application)也可稱爲ISAPI DLL,其功能和CGI程序的功能直接相對應,使用方法和CGI也類似,由客戶端在URL中指定其名稱而激活。例如下面的請求將調用服務器的虛擬可執行目錄Scripts下的function.dll(ISAPI DLL必須放在服務器的虛擬可執行目錄下):
http://www.abc.com/Scripts/function.dll?

ISA和服務器之間的接口主要有兩個:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必須在其PE文件頭的引出表中定義這兩個引出函數,以供Web服務器在適當的時候調用。

1、當服務器剛加載ISA時,它會調用ISA提供的GetExtentionVersion( )來獲得該ISA所需要的服務器版本,並與自己的版本相比較,以保證版本兼容。函數原型如下:

BOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
typedef struct _HSE_VERSION_INFO
{
DWORD dwExtensionVersion; //版本號
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //關於ISA的描述字符串
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;

2、ISA的真正入口是HttpExtentionProc( ),它相當於普通C程序的main( )函數,在這個函數中根據不同的客戶請求作不同的處理。服務器和HttpExtentionProc( )之間是通過擴展控制塊(Extention Control Block)來進行通信的,即ECB中存放入口參數和出口參數,包括服務器提供的幾個回調函數的入口地址。函數原型如下:

DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );

ECB的結構定義如下(IN表示入口參數,OUT表示出口參數):

typedef struct _EXTENSION_CONTROL_BLOCK
{
DWORD cbSize; //IN,本結構的大小,只讀
DWORD dwVersion //IN,版本號,高16位爲主版本號,低16位爲次版本號
HCONN ConnID; //IN,連接句柄,由服務器分配,ISA只能讀取該值
DWORD dwHttpStatusCode; //OUT,當前完成的事務狀態
CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要寫入到日誌文件中的內容
LPSTR lpszMethod; //IN,等價於CGI的環境變量REQUEST_METHOD
LPSTR lpszQueryString; //IN,等價於環境變量QUERY_STRING
LPSTR lpszPathInfo; //IN,等價於環境變量PATH_INFO
LPSTR lpszPathTranslated; //IN,等價於環境變量PATH_TRANSLATED
DWORD cbTotalBytes; //IN,等價於環境變量CONTENT_LENGTH
DWORD cbAvailable; //IN,緩衝區中的可用字節數
LPBYTE lpbData; //IN,緩衝區指針,指向客戶端發來的數據
LPSTR lpszContentType; //IN,等價於環境變量CONTENT_TYPE

//回調函數,用於返回服務器的連接信息或特定的服務器詳細情況
BOOL ( WINAPI * GetServerVariable )
( HCONN hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSize );

BOOL ( WINAPI * WriteClient ) //回調函數,從客戶端的HTTP請求中讀取數據
( HCONN ConnID,
LPVOID Buffer,
LPDWORD lpdwBytes,
DWORD dwReserved );

BOOL ( WINAPI * ReadClient ) //回調函數,向客戶端發送數據
( HCONN ConnID,
LPVOID lpvBuffer,
LPDWORD lpdwSize );

BOOL ( WINAPI * ServerSupportFunction ) //回調函數,訪問服務器的一般和特定功能
( HCONN hConn,
DWORD dwHSERRequest,
LPVOID lpvBuffer,
LPDWORD lpdwSize,
LPDWORD lpdwDataType );

} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;

在上述ECB中,服務器不但提供了當前HTTP連接的句柄和一些變量,而且提供了4個回調函數給ISA調用,從而使ISA可以獲得更詳盡的信息。

三、ISAPI Filter

ISAPI Filter位於服務器和客戶端之間,能夠對服務器和客戶端之間的通信進行預處理和後處理,比如對通信進行加密/解密、提供對客戶進行身份驗證的新方法、提供自定義的日誌記錄等,在CGI中沒有與ISAPI Filter直接相對應的部分。

ISAPI Filter與服務器之間的接口有兩個:GetFilterVersion( )和HttpFilterProc( )。任何
ISAPI Filter都必須引出這兩個函數以供服務器調用。

1、在註冊表的如下鍵值中存放着所有ISAPI Filter的文件名,IIS服務器啓動時從該鍵值中獲得
Filter的文件名並加載它們。

HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL

2、然後服務器調用每個Filter提供的GetFilterVersion( )函數,獲得版本號以及該Filter希望處理的事件,即ISAPI Filter通過引出GetFilterVersion( )函數來告知服務器自己希望處理什麼類型的事件,因爲ISAPI Filter是通過事件來激活的,當滿足條件的事件到達時,服務器就會調用Filter引出的主函數HttpFilterProc( )對該事件進行處理。GetFilterVersion( )的原型如下:

BOOL WINAPI GetFilterVersion(
DWORD dwServerFilterVersion; //IN,服務器使用的版本規範
DWORD dwFilterVersion; //OUT,過濾器使用的版本規範
CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,對該過濾器的描述字符串
DWORD dwFlags //OUT,事件和優先級標誌
);

事件和優先級標誌dwFlasg的取值在MSDN中有詳細解釋,其中包括該Filter被調用的優先級,一般應使用默認的低優先級,否則可能會對系統的性能造成很大影響。

3、HttpFilterProc( )是ISAPI Filter主要的入口函數,它根據當前的事件的不同作出不同的處理。服務器通過如下的參數塊和Filter進行交互,這個參數塊的作用和ISA中的ECB類似。

typedef struct _HTTP_FILTER_CONTEXT
{

DWORD cbSize; //IN,本參數塊的大小
DWORD Revision; //IN
PVOID ServerContext; //IN,由server使用本參數
DWORD ulReserved; //IN,由server使用本參數
BOOL fIsSecurePort; //IN,事件是否發生在安全端口上
PVOID pFilterContext; //IN/OUT,與本次請求相關的上下文

//回調函數,取得關於服務器和本次連接的信息
BOOL (WINAPI * GetServerVariable) (
struct _HTTP_FILTER_CONTEXT * pfc,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSize
);

BOOL (WINAPI * AddResponseHeaders) ( //回調函數,給HTTP響應添加一個標頭
struct _HTTP_FILTER_CONTEXT * pfc,
LPSTR lpszHeaders,
DWORD dwReserved
);

BOOL (WINAPI * WriteClient) ( //回調函數,將原始數據發送給客戶端
struct _HTTP_FILTER_CONTEXT * pfc,
LPVOID Buffer,
LPDWORD lpdwBytes,
DWORD dwReserved
);

VOID * (WINAPI * AllocMem) ( //回調函數,分配內存。
struct _HTTP_FILTER_CONTEXT * pfc,
DWORD cbSize,
DWORD dwReserved
);

BOOL (WINAPI * ServerSupportFunction) ( //回調函數,訪問服務器的一般和特定功能
struct _HTTP_FILTER_CONTEXT * pfc,
enum SF_REQ_TYPE sfReq,
PVOID pData,
DWORD ul1,
DWORD ul2
);

} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;

四、VC++ 6.0中對ISAPI的支持

VC++ 6.0中定義了5個相關的類以簡化ISAPI的編程工作:CHttpServer、CHttpServerContext、CHttpFilter、CHttpFilterContext、CHtmlStream,這5個類都沒有父類。其中CHttpServer和CHttpServerContext主要用來編寫ISA,CHttpFilter和CHttpFilterContext則用來編寫ISAPI Filter,而CHtmlStream則用來操作內存中的HTML文件,爲其它的4個類提供服務。CHttpServer在每個ISA中只能有一個實例,一個CHttpServer可以對應多個CHttpServerContext實例,每個
CHttpServerContext處理一個客戶請求,這樣可以處理併發的HTTP請求;CttpFilter和CHttpFilterContext之間的關係與此類似,在每個ISAPI Filter中只能有一個CHttpFilter實例,但是可以有多個CHttpFilterContext來處理併發的事件。CHttpServer和CHttpFilter是獨立的類,它們可以共存於一個DLL中,也可以分別在不同的DLL中。

一個ISA可以提供多個命令,每個命令對應於CHttpServer(或其子類)的一個成員函數,客戶端可以在URL中指定命令名及其參數。在VC++ 6.0中是通過parse map來實現這種對應的。

Parse map類似MFC中的Windows消息分發機制,通過使用VC提供的DECLARE_PARSE_MAP、BEGIN_PARSE_MAP、ON_PARSE_COMMAND、ON_PARSE_COMMAND_PARAMS、DEFAULT_PARSE_COMMAND、END_PARSE_MAP等宏,可以實現對不同的命令的處理。每個CHttpServer中只能建立一個parse map,當客戶端給ISA發來命令的時候,parse map可以分析HTTP請求中的命令名及其參數,將該命令與相應的成員函數關聯起來,即由該成員函數處理該命令。以MSDN中的例子程序pinball爲例,該例中有下面這樣一個表單:

<form method=get action="pinball.dll?">
<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">
<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>
<input type="radio" name="Favorite" value="2"> Twilight Zone<br>
<input type="radio" name="Favorite" value="3"> The Addams Family<br>
<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>
<input type="radio" name="Favorite" value="0"> I don't see it here<br>
<br>
<input type="submit" value="Show Me!">
</form>

當客戶端選中了上面的表單中的“Attack from Mars”這一項並點擊了submit按鈕後,服務器端
最終將得到如下的URL串:

http://www.abc.com/pinball.dll?MfcISAPICommand=GetImage&Favorite=1

在該URL串中,命令名是GetImage,參數Favorite的值是1,因此pinball.dll中的如下成員函數
將被調用以處理該請求,其中參數dwChoice對應URL中的參數Favorite:

void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);

而parse map需要按照下面的形式定義:

//CPinballExtension從CHttpServer派生而來
BEGIN_PARSE_MAP(CPinballExtension, CHttpServer)

//GetImage是CPinballExtension的成員函數,且有一個long型的參數即dwChoice
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4)

//該參數在URL中的名字爲Favorite
ON_PARSE_COMMAND_PARAMS("Favorite")

END_PARSE_MAP(CPinballExtension)

而對於ISAPI Filter,在VC中可以通過重載CHttpFilter(或其子類)的不同的成員函數來實現對不同事件的處理。可重載的函數如下,每一個成員函數均對應一個或多個事件:

OnPreprocHeaders
OnAuthentication
OnUrlMap
OnSendRawData
OnReadRawData
OnLog
OnEndOfNetSession

MSDN提供了4個關於ISAPI的編程實例:counter、MFCUCASE、pinball、wwwquote,有興趣的可看看,本文主要不是介紹編程,所以不再贅述。

參考資料:

1、MSDN
2、《精通CGI編程》,丁一強等,清華大學出版社

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章