|
|
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。 ; m+ o4 j% Q5 ~+ r8 t
1 G! B" {1 k% |. d9 Y" \5 P
一、ISAPI接口和CGI接口的不同。 2 K) ^; W A$ G% f0 C# _( Q+ w6 Q
3 b* R1 r' C, _5 B/ q8 ]
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。
) h2 V( J3 H6 ], |' t
# d: v Z' N8 N1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。 + N4 q5 A& {3 I/ y: q6 C
* a# l. [' a8 K$ D2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。 , ]! o) A% O- k& O
& w9 i8 Z4 ^1 {8 ]+ X; [8 }: [0 gISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。
3 V& z9 ^" V) |% w3 u0 j* I& O, W A. D1 u, G0 N- ^
* T7 Y( b; w/ t/ y
二、ISA
- Z1 z1 f* Q5 o8 q0 ~" M O. j- N$ z- x; X: U |+ u
ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):
1 K/ i. Z# T0 r! W- @7 x4 S& Lhttp://www.abc.com/Scripts/function.dll?
: E7 X! k7 f0 ?; \
) }9 |- h0 r; W& H) e0 Y1 m: U3 G! lISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。 ; ?' i$ Y& U" m \3 n
" c0 m% t- Z' \5 m1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:
0 l3 f+ f e$ d
! @+ J- _9 I) V+ B5 F6 WBOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
7 h5 i! G5 z" t* }; W% T$ Qtypedef struct _HSE_VERSION_INFO
# [' j1 h* X* n9 Z{
+ b5 x7 @% C' |DWORD dwExtensionVersion; //版本号 ( n8 M# u: S$ {* f# e6 O% R
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串
; J% v, ~3 S5 {5 E9 Z4 t$ }} HSE_VERSION_INFO, *LPHSE_VERSION_INFO; 9 A( f+ P6 } V3 Y7 R5 p( i# G
4 Z( q) b& y4 X2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:
+ e6 s9 `8 r; X- T& O8 c' a1 l
4 f7 L. `# R% e$ ^4 `' J" _DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );
8 n$ \# O0 S9 P& H8 z$ _$ b
, |( N1 Q1 k: {" U; L# \ECB的结构定义如下(IN表示入口参数,OUT表示出口参数):
# H+ L0 i2 ~' [; S( `; w( F
1 C+ h* y4 @# n I/ c) ~1 e/ s9 ytypedef struct _EXTENSION_CONTROL_BLOCK
: v9 c/ V* B3 G3 W{
5 w) l! P$ k: C1 w: c; B/ \DWORD cbSize; //IN,本结构的大小,只读
8 J+ n$ Q6 {+ \2 }0 d: x. M; TDWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号 5 M* i9 H, K) Q A6 X1 y3 \
HCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值
+ z) M% v0 l9 F8 e: q s; cDWORD dwHttpStatusCode; //OUT,当前完成的事务状态 - h3 j$ n0 Z2 X0 J! c6 D0 c& ]+ U
CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容
' R' T1 N; z0 F$ V6 m& dLPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD
$ m+ ~4 _5 V9 V, C, JLPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING ; u! L, t+ X2 ~9 k. m
LPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO 6 ^4 A2 ?4 U+ ^* r* P
LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED ( B! ^0 Q, K4 x: E; W5 J) g
DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH ! v, X$ ?7 S; z1 R& P7 `2 @
DWORD cbAvailable; //IN,缓冲区中的可用字节数 ) a0 x) d+ V# l; [7 `
LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据 * M1 p1 z; C! o
LPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE
& F& }" ^# b$ L2 ~9 X8 C2 e$ d8 E
//回调函数,用于返回服务器的连接信息或特定的服务器详细情况
1 W& ^, R$ o; rBOOL ( WINAPI * GetServerVariable )
: o* g( s8 ^; W+ g; ?( HCONN hConn, / f: ?2 o2 ]& G9 H
LPSTR lpszVariableName,
0 S3 f2 @* Q! }3 j7 Y% B7 FLPVOID lpvBuffer, % ^" c6 X: X. V, R6 ^- F' x% |3 }
LPDWORD lpdwSize ); ( g; P) c0 p$ Z+ a( g" s
5 @" p" n2 b0 I# t5 B0 i. g) v8 q, KBOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
1 {* u+ z7 k# ]- m( HCONN ConnID,
; W9 t( i) J5 q5 W, Z7 ?1 bLPVOID Buffer, : E( [5 t% t! ]) K6 [: t+ J
LPDWORD lpdwBytes,
4 ~# `, R# \4 Y: u9 dDWORD dwReserved );
% E$ ]6 i" Z9 v
} b6 d5 Y+ @* aBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据
, i0 T& o, z) w- F( HCONN ConnID,
# A6 Z# B ~/ e* P! r0 x! T+ L9 YLPVOID lpvBuffer, ( `, l, A& `% t
LPDWORD lpdwSize );
# _/ O# C/ M! v+ R2 x
- R# _& D" `. O8 eBOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能
& y0 ? }# X. p3 y9 F$ j, `0 j( HCONN hConn, . ~4 U2 \% `" c+ [" b8 Y- z
DWORD dwHSERRequest,
: Y+ W1 C; r. N, Q* J; W; Q8 m! eLPVOID lpvBuffer,
/ x( _: v# L- Q- {2 H: M. iLPDWORD lpdwSize,
2 Q( }* T0 P* D0 t6 CLPDWORD lpdwDataType ); 3 l* g4 D9 L5 S! u) N. b3 w8 ?
5 c" y4 y6 m, n# g9 Q( V/ \1 q} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
; [# s8 h @8 }9 Z& |
5 }7 [ r$ L7 o在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。 + s! K& }& s& I! |5 S/ `
3 c( ^/ p/ E2 o' a3 n# w三、ISAPI Filter
& B- T2 y- e. R: v; t c" f
3 U. P8 k$ f" C* wISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。
# H3 _: L# h& G6 _0 s8 K
2 {% h8 M) v' |; _# ~2 H4 j- ]ISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何 4 b, Q. I3 V" B. S" n
ISAPI Filter都必须引出这两个函数以供服务器调用。 ( P) c! x; U4 z/ |6 G/ m
( S3 u& v z$ \. w3 K6 p# {
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得
* K5 k+ a+ {7 Y ` e8 t6 NFilter的文件名并加载它们。
0 x7 |0 P6 U; s- T7 r! W! Y A, T* f
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL . j$ X. T: O3 U
& t/ ~7 M% V& V6 t% K2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下: 0 O' Z `9 y% K. \6 S- L$ I% M
* h" k/ `* x9 Q D3 xBOOL WINAPI GetFilterVersion( 2 h5 _. G0 N$ \
DWORD dwServerFilterVersion; //IN,服务器使用的版本规范
' T o9 D6 q6 {" U( ]. kDWORD dwFilterVersion; //OUT,过滤器使用的版本规范
3 ~* t" g' s. k' V0 U K' g( RCHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串 . ?! a( C' C8 v P' I! G
DWORD dwFlags //OUT,事件和优先级标志 2 w) D( {; L6 O
);
- r6 e8 G* B. E8 f2 Y: U) J6 y z( v5 E$ r! G
事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
8 ?/ i2 v/ M U& @: c! U6 M4 _8 k7 j8 [+ l3 b- f" O0 p" K+ ?; o; w
3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。 7 t* D$ O# A# t7 v4 V3 L7 u
' c; w9 y. e: \+ ~* {4 |/ M
typedef struct _HTTP_FILTER_CONTEXT ' z8 K, _$ x* l: ^3 \- s0 Y
{
: E! H7 h6 w6 i. t8 n* G$ F" o6 z) Q) J4 P' r
DWORD cbSize; //IN,本参数块的大小
4 b# b( M2 }4 r3 L$ z3 d* YDWORD Revision; //IN $ ]* I' I) c. T8 n. Z
PVOID ServerContext; //IN,由server使用本参数
0 [+ S( G0 X$ p$ p! i; a6 ?DWORD ulReserved; //IN,由server使用本参数 0 A, S2 Z7 ]% @: J# g
BOOL fIsSecurePort; //IN,事件是否发生在安全端口上
3 e8 Z, r$ B6 Z: A! HPVOID pFilterContext; //IN/OUT,与本次请求相关的上下文 * u# Z) z% E9 ]% t
# j% O% l& n- Z# B+ t//回调函数,取得关于服务器和本次连接的信息
/ D" {1 m, `( g' { |BOOL (WINAPI * GetServerVariable) (
& u# W: {& K, v9 F% a. @struct _HTTP_FILTER_CONTEXT * pfc,
1 S* R0 b* k4 h' S' bLPSTR lpszVariableName,
" Q0 z2 D3 w9 ^+ \0 F2 u, KLPVOID lpvBuffer,
% w2 D& P5 W0 `6 a* t1 d, E2 W! LLPDWORD lpdwSize 4 Y$ V) P9 _2 F" m% G
);
3 s" C% E3 X- @( a7 p+ ? D6 e, b$ X0 z; k; H+ b! U
BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头 - k$ h! q" m( G) b- o! h
struct _HTTP_FILTER_CONTEXT * pfc, . v) `8 [! M9 d/ n- J
LPSTR lpszHeaders,
: f, j3 o( P7 N) \DWORD dwReserved , m: O' X% ]: p w' a! q6 b
); 1 ?1 @+ Y$ p1 H3 Q2 B
- _4 W1 Y. B* X! g8 n6 IBOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端 ! T9 l/ d8 [& {6 k$ Y$ K- N4 V
struct _HTTP_FILTER_CONTEXT * pfc, 3 T" @/ m. Q8 M" V8 r9 O
LPVOID Buffer, 5 [6 m" E5 p! d
LPDWORD lpdwBytes,
' G a! l7 k- L4 B; ODWORD dwReserved ( ~/ O9 `% r3 N- Y1 K1 P) A
);
5 D" x! i) O% d* r6 ~. [" q o
0 S4 o' |: q- F# y2 NVOID * (WINAPI * AllocMem) ( //回调函数,分配内存。
U& o2 f# Z Z" i# lstruct _HTTP_FILTER_CONTEXT * pfc, ; O( b, ~# f/ {9 t- k6 ~
DWORD cbSize,
% [8 E5 }' `) }) ]( q. h& GDWORD dwReserved
/ R7 U% J, N% g5 j) p8 d" O2 d);
3 A0 l8 C6 S+ [5 L. b- g: U2 A$ t3 Z( @
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
8 I# R$ f: X: S+ sstruct _HTTP_FILTER_CONTEXT * pfc, ' M. V3 X" k; i o# o5 V
enum SF_REQ_TYPE sfReq,
6 {$ O& n" N# H) ] z8 }PVOID pData,
; A. D' R( }+ Q& K! y% m$ U fDWORD ul1, + {# A/ H4 @, a4 [ P
DWORD ul2
1 s. S8 e7 N* e, e); u% R4 L; D- F; q% N
2 l7 b g& f0 X( ^* p3 d4 G} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT; 9 H$ \" G+ r. b+ H( t0 R9 Z; k# ]
; _7 `2 A0 a7 b/ P1 ?# Z1 z& h
四、VC++ 6.0中对ISAPI的支持 $ a" B" O- V J( n9 F4 \9 ~
" u+ m0 Y9 f* kVC++ 6.0中定义了5个相关的类以简化ISAPI的编程工作:CHttpServer、CHttpServerContext、CHttpFilter、CHttpFilterContext、CHtmlStream,这5个类都没有父类。其中CHttpServer和CHttpServerContext主要用来编写ISA,CHttpFilter和CHttpFilterContext则用来编写ISAPI Filter,而CHtmlStream则用来操作内存中的HTML文件,为其它的4个类提供服务。CHttpServer在每个ISA中只能有一个实例,一个CHttpServer可以对应多个CHttpServerContext实例,每个 / z- ~! ~. a4 L, p, w0 U
CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。 / W$ P- e+ \; D% A
% e7 U" A$ q: N* c一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。
, d% O; Q% {8 B/ _9 m+ I% a! ]: A1 {9 ^
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为例,该例中有下面这样一个表单:
8 s$ G" x+ K% u
8 S$ L0 V8 Z( k" | j$ s/ j<form method=get action="pinball.dll?"> # I8 ^" {& ~7 N/ Z
<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">
1 E, o4 ?# [ p( Y U<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>
0 j p F9 I, I' }) x<input type="radio" name="Favorite" value="2"> Twilight Zone<br> ( i- \) q8 e) ^; W" V( W" t3 C
<input type="radio" name="Favorite" value="3"> The Addams Family<br>
) _9 O( U5 k* C9 A6 U<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>
* l# p- `$ \. W<input type="radio" name="Favorite" value="0"> I don't see it here<br>
. s- s. u# F3 b9 Z- {<br>
" y; t/ z$ }$ R( g<input type="submit" value="Show Me!"> 5 s: z: q# j! D% v1 N4 g8 u
</form> - y/ \2 K* l) O( c
# \+ J7 m6 L9 J( K- @0 [
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端 & D# n7 ~1 D3 ?3 s8 O! |
最终将得到如下的URL串:
7 L- ~9 U2 L; b! k, L2 S
, h" B# L& G# v2 Q7 ^" Fhttp://www.abc.com/pinball.dll?M ... mage&Favorite=1 ! q/ D+ p) k9 W; M1 E& D+ w
# C, Q4 L3 b0 i: D h9 o在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数 " G+ v9 s( m- }9 r
将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite: 1 C4 `. J# W/ {3 {
0 B8 N! p2 e4 l" ~. K$ }6 T: g
void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice); ' ?& N F4 z1 d& Z
0 q; A/ K4 `( z# S
而parse map需要按照下面的形式定义:
* Y$ Q% k" ?" J$ R/ Y8 H! Y) i, x( i' ]8 h
//CPinballExtension从CHttpServer派生而来 ! f& N4 Y$ a: ]4 y8 o/ G
BEGIN_PARSE_MAP(CPinballExtension, CHttpServer) * \! }% i' J# b4 U- @2 S
$ H* w8 Q6 v4 b% ~; t) h//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice
0 C: j0 y( o# [% `1 e& @5 Y& d! hON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) ) v/ I# `% Y( a) n0 c
3 T. v# K% B. Z+ C# d1 l! v//该参数在URL中的名字为Favorite
0 _( p: S8 z( D7 N7 T1 oON_PARSE_COMMAND_PARAMS("Favorite")
! z! C8 A) ?8 d6 K7 Q( |6 M9 F
% @/ ]' R2 T9 ?! pEND_PARSE_MAP(CPinballExtension)
9 `5 [% z. o5 N
* Y6 u: m: G: |. [' e而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:
/ T0 D* v D( _6 S$ ?5 o7 ~8 K
( V ^; {* u5 @OnPreprocHeaders ) z" D7 f4 M* e) Z; Z& H
OnAuthentication " T2 N" M# }& Z" Z, R$ ?9 @; ]$ S
OnUrlMap
! l3 u4 B. `, n9 g( |OnSendRawData ( G7 ]) t, {. |1 K7 t9 R
OnReadRawData
/ g+ E5 ]8 ^! l7 T5 ~OnLog - c u5 O% l: o3 ~" U
OnEndOfNetSession 3 F* m: L# |6 E
+ |! W8 m& N7 I& E
MSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。
- X2 \7 N" }5 j* f& ^/ u8 I" ^1 H3 X" S4 K
参考资料: $ E; [9 y) t$ a+ S
# \6 m$ y& N, l4 z. t" u$ s
1、MSDN " F/ V0 J9 `/ n
2、《精通CGI编程》,丁一强等,清华大学出版社 |
|