|
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。 , D' s" k8 C3 H6 ?8 a9 G2 U
. ]% A: Y! e) [* C" x; \一、ISAPI接口和CGI接口的不同。
) r1 ]; _7 v2 B0 C$ H1 z7 c% y
" t6 i$ [" A/ b- T/ SISAPI程序和CGI程序完成类似的功能,但是实现方法不同。
7 a; a" M: G1 f) s) X2 c" @
& F5 z2 U3 _+ }% n% W2 [2 u+ Z. J1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。 ) k" V$ }; ^: m s: U+ {" N6 I
2 n" l6 @9 s& _% D8 ]2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。 $ _0 N9 T' M( r& m& U
! e) g" _, q+ {; w8 H: I# zISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。 4 W$ R& _& _) S/ d$ t) _
7 F9 G }( o# L, Q- [" u2 D4 f$ b- \1 r$ `: m
二、ISA & K6 t$ q2 w( `# I
; `- F {5 p1 V8 [
ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):
+ a; d7 q- ]$ r [http://www.abc.com/Scripts/function.dll? ! W; S E% ?4 Q# c
- n! l( `3 i6 @9 y
ISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。
; e3 \. N3 [/ j) X8 D2 U& Z: ~" o
2 w* p9 v& s4 F. m1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:
3 j9 w& l) A5 a! K' L# s/ h! P( q
: o2 U, q! f8 e7 W QBOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version); 0 h! N4 l1 [+ B, |: @
typedef struct _HSE_VERSION_INFO
" \4 ~$ W! ~# a+ R' g3 k{
# J0 A4 f/ l! s% Y! P' @- G8 @: KDWORD dwExtensionVersion; //版本号
8 B' S+ @/ C6 Z7 A5 Q4 |CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串 4 R8 ^2 t, c' f& K1 C- M5 I+ a
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO; q% T# ^9 Y" z
. t M" u) x7 r
2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下: 5 n D# b! J2 u# a
" z% k, j3 a$ ^4 f. N
DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB ); * s2 `/ v/ ]7 ^6 U9 Y8 k1 ^7 Z/ C
2 C+ f, E$ B( y |- u
ECB的结构定义如下(IN表示入口参数,OUT表示出口参数): / [# S. H: c: G, j; m
9 ^5 l0 ^) `8 H1 M0 p6 btypedef struct _EXTENSION_CONTROL_BLOCK
4 h8 L4 x+ T" E' v; K: t; T/ z{
2 W, t; y* z: T; M, }DWORD cbSize; //IN,本结构的大小,只读 ! V# c7 _3 d( d2 d
DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号 4 P) [# r/ T% o' P, S' T
HCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值
1 w- f5 ? q! {DWORD dwHttpStatusCode; //OUT,当前完成的事务状态 1 k. |7 h. t4 y. t. u, B9 C
CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容
2 @0 J, s, u9 i: a: n: ~LPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD
$ t2 O G' k/ Q# w3 H, ILPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING , V( a: n* [9 ]: e# |; T# U1 p7 `
LPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO
( e! y. r& |0 \LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED & s. Y' d6 s8 M1 @( I1 K% `
DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH
v9 G* z: q4 n5 gDWORD cbAvailable; //IN,缓冲区中的可用字节数
! D# A6 a% B2 k% K+ e1 QLPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据
* z+ w6 R$ n9 |) G5 R7 ^3 S* L, xLPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE
* p' E, t/ \* `: [& d4 y2 b4 ]/ o* \7 o
//回调函数,用于返回服务器的连接信息或特定的服务器详细情况 ( e, i9 O/ ~; g' ^. g1 r
BOOL ( WINAPI * GetServerVariable ) 5 k, Y1 f; e" U8 l) q! i, @% e; @! u
( HCONN hConn,
4 @! d( j# V; R3 e8 p0 Z; |LPSTR lpszVariableName,
. p$ ^8 H4 j! ?% ]! U& H7 _0 CLPVOID lpvBuffer,
( [5 y' t+ r* K' T* xLPDWORD lpdwSize ); 0 l! p$ z; |% I, ~& Y; F" k% x
9 O* U" [1 _7 F" X; |) y3 {BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据 2 d7 o. Q, @5 _6 X
( HCONN ConnID, 2 ~/ f0 A1 e5 M& U" u
LPVOID Buffer,
* e* w4 K. ]3 [: t1 q6 p- pLPDWORD lpdwBytes, 6 T2 b0 _1 U3 Y% k ]7 k: R
DWORD dwReserved ); % C4 T, d2 I( A0 b7 X, y
: z" b) C$ m1 x; zBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据
+ Z3 w# g/ I6 v( HCONN ConnID,
( q. b" H8 ~& i4 z0 cLPVOID lpvBuffer, }2 P0 A$ q3 P
LPDWORD lpdwSize );
$ Z! y+ s! K5 R; E* V' i7 f" N7 ]9 e6 K
BOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能
4 w+ l! ~( J5 r3 Y( S+ ?( HCONN hConn,
8 G! D5 o* c# i8 t, M+ l3 c- }( H# yDWORD dwHSERRequest,
" v, e' C6 @; K8 H9 xLPVOID lpvBuffer,
# [- {1 I* h& q! R2 x; A/ ~LPDWORD lpdwSize, % b" M8 I2 M1 G' F8 s, a- v; r
LPDWORD lpdwDataType );
5 u' \5 C& f6 p/ r. `" o, O" j% I& p& T6 n' w9 w$ S g5 y E `
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
; d% B( ?4 A. z/ ~/ z; [5 Z- b& Z" M8 f- l( u' ^, c& H$ V
在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。
3 j; ]' [/ g/ y4 @. z0 n5 C- X8 ^ ?# p
三、ISAPI Filter - L: b- Q: `4 N5 Q" t$ U
9 K% `; C/ g7 @3 @ F: D$ |ISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。
4 j L9 M' V: o+ Z0 @
, R) j5 U3 w. B& I+ C5 ~- r' YISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何
3 T6 X1 d: k) LISAPI Filter都必须引出这两个函数以供服务器调用。
; m/ L/ B" l+ G: U2 w5 G: K. g; q% |$ Q7 W
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得 7 }( }0 v0 M0 u$ C5 {3 E
Filter的文件名并加载它们。
# v! }( l. W1 |/ v$ a6 p
( G/ G* a& V8 X/ }1 ]# THKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL
5 d5 M' R; |: D" M* a+ y6 Z& @9 z" W! j L0 c2 X
2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:
; v+ G1 ^: S) c6 i( q$ _- ~( y6 Y" ~6 L- D/ K5 S
BOOL WINAPI GetFilterVersion( ( J6 M/ P7 I f; }+ W
DWORD dwServerFilterVersion; //IN,服务器使用的版本规范
% H8 s( l9 T5 P% tDWORD dwFilterVersion; //OUT,过滤器使用的版本规范
a: \- V9 |7 q- DCHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串 I- c8 m9 ~1 l- u
DWORD dwFlags //OUT,事件和优先级标志 0 U' }, m6 {, u! s; _& u
); 9 m. z* h. v: s# V
( w* ?* U8 r8 Q6 \8 W# c, v
事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
" a# R% F& i% V# x/ _
" [3 v1 v9 Y) h) J. P7 M5 @3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。 8 ]2 j: D$ e% i! e
! p4 Y% H4 J1 Y* W
typedef struct _HTTP_FILTER_CONTEXT
: i7 f. c3 H( ]- c4 {{
/ u$ i. N& P$ B
: W. Q/ V* M& ZDWORD cbSize; //IN,本参数块的大小 8 R7 s% E5 l |) o
DWORD Revision; //IN
Q3 _. B) Q# I- X$ ?PVOID ServerContext; //IN,由server使用本参数
! Q5 Y* R* m0 I: p% e5 NDWORD ulReserved; //IN,由server使用本参数 & |& _& G" W9 @+ V% z' n
BOOL fIsSecurePort; //IN,事件是否发生在安全端口上 + u& P3 ^: c2 z# y4 p0 a6 h
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文
# n! d8 P5 j5 f; y9 [5 e1 d1 r s. q, R% U6 H
//回调函数,取得关于服务器和本次连接的信息
3 i* I: Q9 c3 \! @2 bBOOL (WINAPI * GetServerVariable) ( " A9 ]1 n6 h7 ?2 o' f5 h3 z U
struct _HTTP_FILTER_CONTEXT * pfc, + L6 P9 C" y9 [
LPSTR lpszVariableName, 1 i4 K8 t# S4 T/ |1 b4 l; L
LPVOID lpvBuffer, 6 Q2 n+ G' ?5 I7 c: @. Q6 W
LPDWORD lpdwSize . G: H' b# B; E$ D7 P, a
);
3 G8 P# |/ M8 U7 ?5 z$ _( d) [8 i
- i2 G; k( Y( W6 a& vBOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头 ( U4 w& |6 Q/ m. v3 p
struct _HTTP_FILTER_CONTEXT * pfc, 3 o4 f/ R+ r1 k- V. i/ R
LPSTR lpszHeaders,
& V4 e; R7 N, N4 H& h7 [DWORD dwReserved 7 i, e, e R- n2 M4 S; F. v
); 8 X+ Q/ x2 D: H7 R# h* ]8 ^
1 l9 ~6 z# @3 y/ t: m
BOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端
/ R2 d( x# w2 P+ | ]& X* Vstruct _HTTP_FILTER_CONTEXT * pfc, 0 v! b, j$ P1 a; Q2 S: r, k( \
LPVOID Buffer,
/ H. f/ s/ H, B( j: ~LPDWORD lpdwBytes, - \. [: s, x0 Q! S- s
DWORD dwReserved
2 _# N5 J2 [2 ~; R8 r, |" J2 i); 1 b2 V; S7 G+ M# W$ b- k
. Q+ R9 G9 v, o3 y+ TVOID * (WINAPI * AllocMem) ( //回调函数,分配内存。 4 F7 z* j) o! {' w$ b% e, r
struct _HTTP_FILTER_CONTEXT * pfc, ' L& C1 u+ K5 C9 H- e/ T
DWORD cbSize, " V/ x9 W7 o" @' r9 y+ k
DWORD dwReserved * m1 F6 i/ O5 V1 z9 k1 c, r
);
$ {& T% @8 J& U" e' E) i- f; K7 d$ j6 M% C/ w5 S7 A; v
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
$ _5 y8 J: {0 D6 Z, jstruct _HTTP_FILTER_CONTEXT * pfc,
! A$ D- U7 Z( Senum SF_REQ_TYPE sfReq,
8 b! A7 `* v1 sPVOID pData, 1 F# A0 }& n D- V8 M) o3 s. m
DWORD ul1, 3 v9 ]& P1 ?, |0 a' J
DWORD ul2
0 W! [( k, o+ m2 G: e y);
! G4 Q$ e+ U2 B9 i' n3 \! s' [& ~
5 p. I F7 V; g} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;
) g$ K8 @9 T3 H7 P9 i+ C. R6 G% z7 c) ^4 q/ V
四、VC++ 6.0中对ISAPI的支持
# A& f1 J3 D z! o6 e% H1 Q$ I5 S: g+ D& N
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实例,每个 ( t2 t- ^% K' K0 h3 n
CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。
" l1 ~6 V! g1 p" R
: a9 `2 A# i4 W4 O# t6 q6 m/ Q一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。 2 w3 i5 |5 M: v: h+ ~9 d. p& N
7 Q N- w( A4 g$ bParse 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为例,该例中有下面这样一个表单:
n w& V$ V9 p' r" N# B3 f, C! y9 ?" M D
<form method=get action="pinball.dll?">
, v1 B: i. N7 n2 W- y4 i<input type="hidden" name="MfcISAPICommand" VALUE="GetImage"> " d& Z6 Y0 i+ y$ @* {
<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br> 3 o. _5 P2 ]2 T! B4 z& U
<input type="radio" name="Favorite" value="2"> Twilight Zone<br> . {/ }0 E) A- j" Z
<input type="radio" name="Favorite" value="3"> The Addams Family<br>
/ q, L m% x: p8 Y \<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>
* S9 o1 O( P2 E" O) d: ]- } z<input type="radio" name="Favorite" value="0"> I don't see it here<br> 7 r# u/ p2 ^+ G; w1 i
<br>
5 K$ @( y+ O* x<input type="submit" value="Show Me!">
0 T+ s; _" R ~! f+ g</form> . j/ R0 ~! W" P. G8 Z, `
( U, t) d4 b) K/ H, ~4 w9 J
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端
- f! C3 u, g2 g" c最终将得到如下的URL串: 7 M6 Z% Y+ D. j7 v
' k, r7 n) H# D( D% Chttp://www.abc.com/pinball.dll?M ... mage&Favorite=1 5 f1 R( V7 A- S& t4 f( V4 N3 N
+ M9 D( N* u: s* f$ B: Y在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数
5 h. L( p/ I/ P; x" h将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite: 6 e1 W$ F( ~, z/ q! K
* t: ]9 H* n4 F, m5 l \/ {void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice); - ^! o" W8 L& u3 }2 E7 c
9 K1 w. L" `9 `" P$ A* U- A
而parse map需要按照下面的形式定义:
6 X% t6 k, p9 \6 D% v, w& H' d# V2 |) _+ \) K
//CPinballExtension从CHttpServer派生而来
/ h; p# D, v: _* u; ABEGIN_PARSE_MAP(CPinballExtension, CHttpServer) % v6 G. v/ }* \2 ~: N$ @
! o1 H( K+ b1 ]' c" _
//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice , O; r1 B g7 Y& t: X' L: J2 h
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) 7 Z @' X: U! r! H4 \
& L( q2 W: J8 B7 g
//该参数在URL中的名字为Favorite * [' B" }% t8 r f. {
ON_PARSE_COMMAND_PARAMS("Favorite")
4 u8 T+ Z3 S3 \# h' U- v6 q8 Y3 x& u3 L8 g# ^) \1 M1 Q" k
END_PARSE_MAP(CPinballExtension)
4 p {6 M; P5 v- H( J2 S9 G" C- H7 j$ J; d& }& L5 o! K" k
而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件: $ `# B) f- b3 E6 O' N' {4 x
1 |% ~7 K6 Q0 s( p! JOnPreprocHeaders . j' M) ]% I* ]6 ^
OnAuthentication 9 C6 _7 w0 G% A4 `) K4 |5 b
OnUrlMap
" w7 r9 ?0 Q6 Q7 ^: \' o6 b# BOnSendRawData
. R; \2 e' I" o: K, SOnReadRawData
1 t4 q- v2 T- k1 aOnLog 5 D0 e4 u# Y+ `2 v8 Y. m
OnEndOfNetSession
4 ~; V: y; A3 f. e$ o/ R5 U- a; ~5 | H$ q# _& f. o5 O- D) {' L
MSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。 ( I8 O" e: B2 V: ~
6 }6 m }* ?/ T参考资料:
$ l! m# U& W7 K2 C3 r1 b3 g V; K6 M/ E
1、MSDN
8 |" C) k; y0 M; H- n2、《精通CGI编程》,丁一强等,清华大学出版社 |
|