|
|
作者:netguy (mailto:netguy@nsfocus.com)
4 _0 q& O8 D6 v0 Q2 K: Y& s5 i0 ?# A5 v
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。
5 ~2 @( t& P: ]0 v/ n# X6 d3 ^& r6 H2 p1 ~4 T1 `
一、ISAPI接口和CGI接口的不同。
6 \# W9 E0 B6 p
3 W! ]/ ^+ u; n: N+ O0 [ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。
6 W% O, `( s' t( f6 l# [) N1 d8 |; ]: B) S5 ?: P
1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。0 |$ g5 e" F4 \' N' i
- F1 i$ S( @8 S9 t0 j/ N( Q2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。
% j! b* D+ W* F7 H2 m6 ^1 S) H' @1 _8 J6 O8 |% G1 g
ISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。
/ E; n" ~% P: n/ @0 @% t$ B& J: W) D' \) m! x
" [3 a0 ]4 \; U6 w2 P# [. I二、ISA
! S) |6 u$ a+ j* o) N- }' D3 G- _4 o) M) V
ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):
& V0 u9 e6 d& I, ehttp://www.abc.com/Scripts/function.dll?
$ U. h2 u& B! P4 Y! g9 u
' v1 m( c& _! h# uISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。
5 O& _: A. [" ?+ I
$ k7 d! {, f9 K3 _$ e6 q; ~4 \1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:) _, ]3 \) ]4 ]
6 C, l; J/ C! m& V. u# W! {
BOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
* ?/ Y2 s1 j9 c/ S' _2 O) x Ytypedef struct _HSE_VERSION_INFO
8 |3 n5 ]+ U% }/ O. F{' ~+ g3 Z* T% X- T& v+ F4 S, x
DWORD dwExtensionVersion; //版本号' E6 j8 V9 L' V( [7 x) f: m0 n8 u
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串
. A% t {# I: p9 ^} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;. r. C; H3 N! s7 t
+ ^8 P3 F }; x. f" }3 Y
2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:6 P' z' T7 F( S6 T# h. Z0 R
1 ~7 T$ t, ]" J4 ]DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );9 t+ J' Y3 _2 ?0 F3 w/ _ D" I
1 J% J1 }. o* S ~2 @ECB的结构定义如下(IN表示入口参数,OUT表示出口参数):# j0 {) X# u& C! u
. ?( ^/ }6 H4 A5 W# t# V, Qtypedef struct _EXTENSION_CONTROL_BLOCK
, i$ R, r9 T) o, B5 n4 s{$ R/ w9 ]* I9 \: D! t
DWORD cbSize; //IN,本结构的大小,只读
% @3 [2 F: F2 H$ r. ~2 \9 A( vDWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号
" f3 J# ~ m8 s% H% s$ K) ~HCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值5 I6 }7 M9 H; D3 Y
DWORD dwHttpStatusCode; //OUT,当前完成的事务状态* w Q/ ~" `. L$ ^2 k& r; q( q
CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容
+ i' g2 h7 v) X! R, gLPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD
8 |7 a/ r- z% PLPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING
+ z0 e3 c0 l: S' ^* J( BLPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO4 }) R( L3 U3 N0 v$ z
LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED( [* Q; d0 |+ s% _
DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH
+ r( e6 Z U/ ]" T8 b9 `DWORD cbAvailable; //IN,缓冲区中的可用字节数
+ Q) ~0 H4 k& \7 V/ x$ S1 ?LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据/ I; v+ M; f; y6 _0 k* B
LPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE' [* |, k2 ~7 J. j( u& N2 C
) `# W/ n- u1 ~' i3 w- m' X
//回调函数,用于返回服务器的连接信息或特定的服务器详细情况) i5 Y5 g+ F* e4 M
BOOL ( WINAPI * GetServerVariable )
p2 M L$ k7 X: {) S( HCONN hConn,5 K3 l) T8 Z# a4 a! t2 q( |6 C5 @
LPSTR lpszVariableName,
" N) \2 W7 @0 z6 s6 w: TLPVOID lpvBuffer,6 H$ e, s! n C! M' G" }8 E
LPDWORD lpdwSize );
6 Y/ d" m S# B- K; q3 _. O% \% J. v$ [8 R( C. I4 p+ H
BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
+ ?& J; b+ O" g, Z7 z( HCONN ConnID,+ d" {+ p5 ], |" u9 J& L6 s0 Y
LPVOID Buffer,* ~* v: @7 B) g! c
LPDWORD lpdwBytes,1 i. p& K. |) U8 I' j
DWORD dwReserved );( D& j; E; k! E" Y. R |5 A8 o
& H V5 x1 C3 N' n9 wBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据5 `4 a& F, u3 u. k% z5 r* G" e; H
( HCONN ConnID,
x6 U& |3 `; Y R4 z3 H) M+ ?LPVOID lpvBuffer,
% I6 ]* d; y6 C$ F6 H' ^LPDWORD lpdwSize );' o5 u. a; e" a7 X- C7 q8 h- {
2 n% p6 t0 W {9 q2 p2 U( E
BOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能, N {9 e: S3 [9 i1 L- M1 u1 m( H
( HCONN hConn,
0 P8 D( ]* r+ }# rDWORD dwHSERRequest,. w" h( h p$ h
LPVOID lpvBuffer,7 }2 v8 _1 H3 D" A+ x+ x
LPDWORD lpdwSize,. @3 o" `( J" q) ?# {
LPDWORD lpdwDataType );
& E% I' E, w N) V9 G
: Z) d' U- ^; A$ y} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;" L, `) F# b+ d. Y3 o9 P: j
: Q# I3 C6 D7 q5 f3 ]在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。
" K6 n8 h+ j, K. N* W' [, L& M2 e4 x, o# v( P
三、ISAPI Filter8 B# L& c0 j- `
% c1 W W; A4 P6 g" [
ISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。
- U0 \' `, X6 I4 X. Z. c
( P/ |$ Z# K6 O' ~& R5 C ]ISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何
9 V2 y& J J1 x) HISAPI Filter都必须引出这两个函数以供服务器调用。
k/ |# R R" i+ `4 E- Y# `5 t& h0 a1 A- p% Y
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得
+ `- T8 Z( c- y( I5 Z3 ZFilter的文件名并加载它们。
3 r& @2 C6 `6 m4 v: f3 i7 J5 s
+ `1 L7 d1 }/ `% c" v; C- l5 HHKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL w4 p( C3 K* ~" H1 o( w o
8 O2 ~) X5 F. G8 V% l; `
2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:: V& L& o$ _' I; h& \8 D
, m( r) S* e# p. x2 _BOOL WINAPI GetFilterVersion(
, Z, m, j3 m' xDWORD dwServerFilterVersion; //IN,服务器使用的版本规范 . q5 N. U5 K. |* W. F/ n
DWORD dwFilterVersion; //OUT,过滤器使用的版本规范
" M/ |$ r5 a. {0 {CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串) o. P; V+ f' q5 s& D! ?
DWORD dwFlags //OUT,事件和优先级标志
& e; P# ~7 x' A! [% T$ E; [);
1 b1 M7 [( U6 P) Y7 ^# M& p: h) a! ~3 `
事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
0 c2 h4 v' {* [$ i4 E' U: a1 u" Y
3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。+ I0 x6 ?3 v W& P2 H7 L$ ^5 D
5 J6 k% Y, g1 p( v$ u
typedef struct _HTTP_FILTER_CONTEXT
# n8 z; n' i# ]5 m6 S{) v: C( o/ ~' k$ |9 C3 {9 o8 c3 P
* v. S9 j9 s( W. v0 ]' ZDWORD cbSize; //IN,本参数块的大小. ~9 c# N: Y$ X
DWORD Revision; //IN9 i3 P9 b% z$ h! o
PVOID ServerContext; //IN,由server使用本参数5 u p2 J& d# y/ r
DWORD ulReserved; //IN,由server使用本参数
' |: ^9 u/ ~3 P; OBOOL fIsSecurePort; //IN,事件是否发生在安全端口上
* r9 N# J8 P& G+ s4 XPVOID pFilterContext; //IN/OUT,与本次请求相关的上下文! X4 M1 }' j- E) W1 p. l
- {* X$ E! f, ?0 z6 ~//回调函数,取得关于服务器和本次连接的信息
. j8 q* h- _" N7 c- \1 d/ HBOOL (WINAPI * GetServerVariable) ( ( q6 L( M' o: c1 T
struct _HTTP_FILTER_CONTEXT * pfc,
, e7 ~$ X# y/ I% tLPSTR lpszVariableName,- x6 k; b p* h9 o% K
LPVOID lpvBuffer,' U8 U; Q9 ~, w# O" x- x4 B8 E9 o
LPDWORD lpdwSize
2 S' N3 p; H6 M$ s' {( p/ Y1 v$ G); 0 H* z X. X9 f6 m' r* Q, f" g
/ K. L8 c1 h0 c5 \
BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头4 L, D7 }5 _3 c* Y6 L( A5 C4 S
struct _HTTP_FILTER_CONTEXT * pfc,0 _+ B7 u }9 X8 r' m
LPSTR lpszHeaders,, J- y' q! n8 |0 Z7 J
DWORD dwReserved" H; o) q7 B; P, G% ~2 o/ }: Q
); # R% q8 z: l+ s+ N9 [
2 m) y7 j; B6 H+ L9 @. H
BOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端# q) h `+ G0 s0 j% Q9 l2 D
struct _HTTP_FILTER_CONTEXT * pfc,
7 O/ `% b1 }7 b9 a! V9 d. E& U1 \. tLPVOID Buffer,; W" D, F* f$ i, z+ C8 o+ Q
LPDWORD lpdwBytes,
2 q/ U- K5 ~& c5 D7 e* ODWORD dwReserved+ N4 k/ \# n. y: v: o8 H, w
);
; P# |( n# h+ ^3 X5 K$ t, w- X, c2 h3 C8 x) t$ p* e# q- L0 @
VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。7 k1 V- C7 o9 X; `6 p
struct _HTTP_FILTER_CONTEXT * pfc,. W/ f- J' F- i
DWORD cbSize,% A' R3 o r7 E3 U
DWORD dwReserved
; l* H/ Z! v: M; q8 _! d); 5 x2 e3 y% I. V9 b
4 c- {$ V2 T7 x. U; Y
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能, J. o. `, c9 b2 C
struct _HTTP_FILTER_CONTEXT * pfc," I( n; _. g0 H& _2 g
enum SF_REQ_TYPE sfReq,
# x* d6 T$ C3 o+ [$ L( D0 z3 b! zPVOID pData,9 t1 Z% r7 b# E- ^: Q* @; t
DWORD ul1,( h& }# }7 D( a2 ]. ^
DWORD ul2
' R9 D* y7 b, ^5 Z); 1 Z) L# y2 g" v4 v4 J) {# ~+ v. s
1 z6 {" s/ t. z( F} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;# G$ V" @; T+ I$ ^4 N
2 |% q1 o0 i N' ^) j
四、VC++ 6.0中对ISAPI的支持
) F9 l: ?8 `+ p
1 @% \ {9 f( R& F! R- o0 xVC++ 6.0中定义了5个相关的类以简化ISAPI的编程工作:CHttpServer、CHttpServerContext、CHttpFilter、CHttpFilterContext、CHtmlStream,这5个类都没有父类。其中CHttpServer和CHttpServerContext主要用来编写ISA,CHttpFilter和CHttpFilterContext则用来编写ISAPI Filter,而CHtmlStream则用来操作内存中的HTML文件,为其它的4个类提供服务。CHttpServer在每个ISA中只能有一个实例,一个CHttpServer可以对应多个CHttpServerContext实例,每个
4 X: r/ C$ y* H. C9 c1 M% B. }CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。
+ O2 G$ T' O; n1 O$ L
) W) N+ P% Y# k一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。
9 ` V" e2 h8 [2 b, h! S I" g, E" P; d0 f* R9 f G
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为例,该例中有下面这样一个表单:
9 A; w0 P" A$ x1 \) U% W5 A6 i( l: ]( B' [
<form method=get action="pinball.dll?"> ]/ U6 t3 s1 M9 f2 j1 D/ Z
<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">0 |4 B! v9 B8 H/ t
<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>
2 J( z. q9 B. a<input type="radio" name="Favorite" value="2"> Twilight Zone<br>
9 H, S5 I! Q) J$ E h0 a<input type="radio" name="Favorite" value="3"> The Addams Family<br>9 Q) N" |- i$ l; `6 v6 `% B
<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>( E! {' R& ^ v x/ n) W/ s
<input type="radio" name="Favorite" value="0"> I don't see it here<br>3 \5 i& ]# S: I: v
<br>
0 ]' D' e4 ~+ J! M2 T5 b9 i( D {0 o<input type="submit" value="Show Me!">" \" r# o3 {- z0 p+ o. x r
</form>0 V3 n/ I8 n+ L6 `9 b
/ l: [8 t, f) f5 O4 D, c8 L- P
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端3 |/ g: t& j1 i8 A8 y0 [
最终将得到如下的URL串:$ V/ K" h4 ~" n; c4 O
4 u/ D) S+ j% a. D
http://www.abc.com/pinball.dll?M ... GetImage&Favorite=1
' X' q! h& U2 b6 C7 j. _ G! Y) U7 l2 J3 P8 E+ W9 H
在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数
8 Z) D7 ?0 D5 s将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite:5 Q) q- A( H% N2 p
3 [0 s& x# b) i7 W4 B( T# tvoid CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);& P; N9 f( @" n
& `/ Z% [* c0 O2 A6 y, U) r9 g% V而parse map需要按照下面的形式定义:$ J. C- T( I) x" G
1 W: k! m! ?- ?! W+ y; V* i! P2 e//CPinballExtension从CHttpServer派生而来
0 Z8 e5 D) B( G& K. p; R/ w' bBEGIN_PARSE_MAP(CPinballExtension, CHttpServer) - T' T7 b ]' Y
/ i; h$ \; w6 D
//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice8 }8 G% L, b- H5 S3 i
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4)
) U& z. q+ d- m1 [6 q) u: E/ T4 n- U4 g* q9 D
//该参数在URL中的名字为Favorite7 e, o: S1 ^# A r
ON_PARSE_COMMAND_PARAMS("Favorite")
' V, p. ~. {8 \# x; x5 p) }* _) d- V" b
1 D3 {5 j6 C6 t: T" f5 ^END_PARSE_MAP(CPinballExtension)' N' c; s/ v+ g: U0 c( R4 A) ]
- I: u% D/ h2 R% A- c) n3 N" ^而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:) @5 C7 z1 f& _& U" \
( C; @6 x a2 H! M# K
OnPreprocHeaders
6 h, J h5 A7 L% g9 {OnAuthentication( c7 a9 e. u3 N2 @, o* Q" ^, ^
OnUrlMap, S. @& N+ S* y2 j2 U' ]& Z% t% Q
OnSendRawData
: {1 Y- U* ?' r6 M6 E; J* l% f4 Z: }OnReadRawData
# [+ D7 ^5 q' c. ?; KOnLog2 D: f; q2 q- A5 S/ n
OnEndOfNetSession( x, N$ b5 q: K5 c2 E
! c' i9 C! F; {5 F' gMSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。# P7 u+ B7 m F' z$ d1 H; ^% P8 M+ N
0 M% L+ b6 T6 R z9 ?6 [2 I5 l
参考资料:- S1 H Y/ Q9 F* K6 o; Q) V
P x9 c! Y, n3 D
1、MSDN
- g! B. F/ A( r' U: h2、《精通CGI编程》,丁一强等,清华大学出版社 |
|