|
|
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。 * k7 q- [. c8 j Z
! r( a0 g1 g( V) ?6 h* x6 a! g一、ISAPI接口和CGI接口的不同。
4 Q% g; q+ U! N. g# _6 ? u5 Y) B( W& J% v
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。 ) I0 J4 }( P- Y& ]9 v( v0 B
" H" a7 D/ r" T5 J8 d
1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。 # o+ g' f+ D# Q+ ]
% ?6 \4 d( T" z# A6 h
2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。
! P6 {% n& s* D# M& }, ^( [9 _$ c3 v, l+ c: _1 P6 u4 `) S7 [
ISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。 4 L1 } \' e% r9 `
5 ?# l9 ]4 t8 \
7 h0 K& {1 T; |3 s: S0 _二、ISA
6 L/ R2 z, g$ w8 M+ U* N3 `; q" F. w. ~( E9 j& V) \
ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下): 5 Y# [1 o5 f: [7 c( i# w! _
http://www.abc.com/Scripts/function.dll?
$ B" Q9 i$ e2 O1 M: R" U
j! y# v- [. f4 L; i0 V4 rISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。 / u( `5 ^ s: j2 `# {: T5 q: c
# @4 p& X5 B; U5 i8 b1 K4 ~- }* i' S1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下: & e% `' W z0 n/ h( e) N
9 a% X6 F, |/ iBOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
, m- v7 y% Y$ [7 y% v% `6 F; ?typedef struct _HSE_VERSION_INFO
* |( j4 S7 F$ s. Q2 a6 Q% @{
+ d6 s5 C' i9 N7 k' VDWORD dwExtensionVersion; //版本号 2 u8 N' {7 l0 o
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串 9 a3 j: L, _# }# `* x2 ~
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO; - T0 V, ^4 _8 i4 q, m
7 L, j& d) [4 S$ L/ k4 u
2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下: % Q/ [' Y6 I" j( k1 F7 B v
! x; F' L8 H1 Q) pDWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB ); & P; E% L% M3 o: f- a
' f9 s; b! _+ D3 I1 E: R) P) oECB的结构定义如下(IN表示入口参数,OUT表示出口参数): 8 A) E7 s5 m% |) N q: h
+ t. Z/ f2 ?. l# L
typedef struct _EXTENSION_CONTROL_BLOCK / x Y! m' F! {# l3 U
{ ( M) X* j7 |) x0 o, r' W3 y( C
DWORD cbSize; //IN,本结构的大小,只读 7 M. r0 \) n- e1 G" R
DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号
6 C. H2 V$ Q wHCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值 + W/ ?5 z* M5 p1 x
DWORD dwHttpStatusCode; //OUT,当前完成的事务状态
6 J$ J/ f/ h; s: O- I$ j& SCHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容 ! T1 {7 T" Z! B! X6 F3 G b3 V' ]) }
LPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD 6 {' F) o/ q0 I- o$ I# U
LPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING
! q3 }9 o7 }! rLPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO 3 q; g' N9 M G* e0 k1 ^1 \# @
LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED
8 n8 h7 X$ X& R4 U$ S' a3 vDWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH + a3 B" [2 U9 P) O6 ~) E
DWORD cbAvailable; //IN,缓冲区中的可用字节数 ) ~* R* _' A% l/ h3 T
LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据
3 ^! i* y* g' }/ m# KLPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE + V, w0 z, d. a5 ~# z
% E2 v; j1 C, u3 g, B# b, v//回调函数,用于返回服务器的连接信息或特定的服务器详细情况 ' Z1 g. C$ l5 [; v. L
BOOL ( WINAPI * GetServerVariable )
& n! D# D7 w! J! [% J( HCONN hConn, 9 E) |5 o4 }$ N) Z! B
LPSTR lpszVariableName,
' Q' n" U K; [% {1 wLPVOID lpvBuffer, Y6 l' x& Y) g% h/ W+ w
LPDWORD lpdwSize );
- O5 ]2 Z! B% z% N/ Y" d' m2 \, j5 `3 e+ e9 ^- @0 i: a
BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
( y9 t( i* q9 I) ^* ^( HCONN ConnID,
- @ b W3 @) I, @8 Q3 e1 RLPVOID Buffer,
; X5 @& C4 P. a8 BLPDWORD lpdwBytes,
0 d; [* ^& N. F8 s3 k& UDWORD dwReserved );
1 D& N5 L4 Z2 r2 V, B* n
- r; b+ k$ S5 E% C/ \+ YBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据 * y F ]3 K8 t9 V. |) E0 P) Y
( HCONN ConnID,
4 i* c5 ^% ]5 c: q5 I2 N d: ^LPVOID lpvBuffer, 8 N' {- H; _8 k% P
LPDWORD lpdwSize );
* A2 u/ K* s! c& x" h; G) y% d8 i2 p9 V
BOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能 6 j" E y9 ]1 s, i* P. e- E
( HCONN hConn,
. D* K. y# X. H1 v! a& s$ y6 bDWORD dwHSERRequest,
5 x. q1 }4 v, {. I) `LPVOID lpvBuffer, : ^9 O5 @2 X! l9 g7 [' P& t
LPDWORD lpdwSize, - D/ [( @# x& l/ ]; z
LPDWORD lpdwDataType ); / K/ B W( G$ Y
- J, Z4 p1 T1 ?4 k; `; S& Y} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
0 {! ?# L/ s( ?
! t) @. P9 z0 h2 l: X4 t/ W在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。 9 @( D* c* S9 C' ]7 j
& Y; l4 @1 D6 k: L8 C
三、ISAPI Filter 7 v8 A2 i- P% k% `0 Q, Y' ?3 c
9 l; Q0 z5 A) p; L; ?
ISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。
$ y9 V8 N, e, K" D2 a9 ^ x# J9 F
( n! m0 w3 _' L) lISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何 ) {3 v* T: `: v2 k( h4 X
ISAPI Filter都必须引出这两个函数以供服务器调用。
5 F: D+ C1 N% F1 U- n
; v8 Q( F' g- O/ ~6 ]7 T/ H1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得 2 I* ^ P0 |, H* v" V' c
Filter的文件名并加载它们。
8 c4 n: r- |5 F3 y4 Z: v5 c
$ l. j8 Y' t0 X0 {7 hHKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL 5 ^) ~$ g* _5 D- b: T
0 k& J2 g }4 v1 P
2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:
/ ?( e5 t8 q& Z
% @( z8 @: E/ U( G- R' o/ p: o% W; E7 XBOOL WINAPI GetFilterVersion(
, f" \2 I% I& ?+ f: O8 vDWORD dwServerFilterVersion; //IN,服务器使用的版本规范
5 ]+ J; K, h9 T5 r; R# pDWORD dwFilterVersion; //OUT,过滤器使用的版本规范
5 l' W& h' x9 V' iCHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串 0 u; [0 ^) S! Q$ }; c" ^
DWORD dwFlags //OUT,事件和优先级标志
u1 E0 q- \ f3 P3 d);
+ X9 e& @3 N9 n7 y4 X! Y* Q& e
; z) V+ D$ \5 n' {* b7 F, `事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
- z r5 n1 v- G" ~
# P' X" \2 j' y3 M/ G2 l7 H, z# a3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。
$ |) G* [1 d; l1 I# j7 e
$ q" c# g; n, ?6 M+ jtypedef struct _HTTP_FILTER_CONTEXT 2 d# g* ?2 F5 p l1 |6 B
{ ! N* v" m% p# {
/ T0 \+ e+ o3 a/ N' s+ u4 ^DWORD cbSize; //IN,本参数块的大小
6 l3 s$ q8 {: Y' Z1 IDWORD Revision; //IN ) A" y1 D2 X/ t" @
PVOID ServerContext; //IN,由server使用本参数
2 U. }- q' W6 EDWORD ulReserved; //IN,由server使用本参数
* ~4 v m; ~3 N" I( ^. E5 MBOOL fIsSecurePort; //IN,事件是否发生在安全端口上 8 Y) r. a) ?4 s; ]7 f+ I& t: E! `
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文 2 o) ]8 O& D$ d0 Q# W
) R( s4 o! z4 ?3 X3 N E
//回调函数,取得关于服务器和本次连接的信息
7 O$ ?+ o1 j) n [1 ^- _BOOL (WINAPI * GetServerVariable) ( # E) `- e* |0 q4 F9 ], z) M
struct _HTTP_FILTER_CONTEXT * pfc, 3 r2 R' F& g/ w. g
LPSTR lpszVariableName, , f3 D0 m2 H) _
LPVOID lpvBuffer, ( {: r! l# d" Z; x, Q; B
LPDWORD lpdwSize - `6 p4 L1 e' A, G
);
# W& L1 y! H$ w y+ T8 n) w- g0 p/ ]$ w- ^" j
BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头
* ]5 a) o. s9 M: i/ f+ Fstruct _HTTP_FILTER_CONTEXT * pfc, . b( i8 d+ K9 c; S
LPSTR lpszHeaders,
9 a) x n2 j. _ N, hDWORD dwReserved
) \: o- J5 g; r, O0 B); . r* ^! U! L: E0 e
3 X7 R" @! F6 ?$ q9 I& c1 XBOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端 + I& q) Q$ j3 Q. z. K7 {
struct _HTTP_FILTER_CONTEXT * pfc, % I/ J# r/ t$ C6 a
LPVOID Buffer,
8 `& h3 n$ ~8 P1 R# @1 MLPDWORD lpdwBytes, : [6 g, q! ^, {% C# `0 q
DWORD dwReserved
8 Y0 j0 D8 T% h% f); $ Z$ Z9 d: d! R: K; P+ C' W
% O( `0 O9 U7 a) z8 y& c
VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。
2 S3 ?4 v& N$ {8 Y) x! }7 mstruct _HTTP_FILTER_CONTEXT * pfc, 8 _# P; r$ ? |
DWORD cbSize, 7 p+ K' k' J, S
DWORD dwReserved
i+ I; h6 V$ V1 A; [); ( m- r5 a) G! R, I$ r
0 P4 m! Y7 v3 w5 i# c7 K
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能 " `3 J0 r1 t) f( m# n9 Q
struct _HTTP_FILTER_CONTEXT * pfc,
$ N7 M0 R& B: x; Fenum SF_REQ_TYPE sfReq,
: B1 K+ H6 T( |2 BPVOID pData, + _& w& v' D( a5 U K
DWORD ul1, " W( E% |' H: L$ `; F
DWORD ul2 " f Q* N: B8 \& B* @
); ) W* G/ ^& k9 Y3 [$ R
/ M# B* [- ?1 I+ b! t} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;
) V" A- v0 U' L
- m( M6 k5 c4 @0 V* A6 l9 I* N四、VC++ 6.0中对ISAPI的支持
: @. ~2 J6 N6 h' E/ o
# m" o1 O5 c. S& F: \& EVC++ 6.0中定义了5个相关的类以简化ISAPI的编程工作:CHttpServer、CHttpServerContext、CHttpFilter、CHttpFilterContext、CHtmlStream,这5个类都没有父类。其中CHttpServer和CHttpServerContext主要用来编写ISA,CHttpFilter和CHttpFilterContext则用来编写ISAPI Filter,而CHtmlStream则用来操作内存中的HTML文件,为其它的4个类提供服务。CHttpServer在每个ISA中只能有一个实例,一个CHttpServer可以对应多个CHttpServerContext实例,每个
9 H. _, h% u" T. c+ \2 c* ~CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。
2 @# n8 N; i4 Y' {* [ r1 ^! P& l1 _3 |. S' F) R. z, ]
一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。
2 N4 Z; c% N: |& P
$ R+ y) W* }0 G9 j$ iParse 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为例,该例中有下面这样一个表单: 7 T5 o: t. i4 s! Q: R: z
2 N% ~9 g% u( |
<form method=get action="pinball.dll?">
" H, u7 p( b9 d. v& t; J<input type="hidden" name="MfcISAPICommand" VALUE="GetImage"> / b$ y% B5 l5 c
<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br> & w# v% E+ a. f, K
<input type="radio" name="Favorite" value="2"> Twilight Zone<br> " h5 ~" T- m m( ?
<input type="radio" name="Favorite" value="3"> The Addams Family<br> - P+ B8 B9 Q5 y3 }* r
<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br> x w; H0 z; Z# w+ W" m2 v
<input type="radio" name="Favorite" value="0"> I don't see it here<br> ; c' ?% x8 L9 `# F9 U! X
<br> ; G( p" M0 m& v$ F
<input type="submit" value="Show Me!"> 3 o* n! N9 s9 P/ U- V0 ?
</form> / { G, T# ` m5 k0 k
+ S9 ^( Y4 J. O+ F- \3 C8 [$ W
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端 6 o& Q( a) [% f/ {9 U( o
最终将得到如下的URL串:
# K# j9 d4 P q! a4 y4 {4 p. i0 z2 C+ ^7 b; d: t3 e
http://www.abc.com/pinball.dll?M ... mage&Favorite=1 8 b' N' E* q9 u4 C$ o3 i
% B' F3 C( v$ |
在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数
8 K" r# J! o; S0 D将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite: q6 U+ q; m2 [4 R3 d$ j+ w. c
4 x e2 }( `5 s* M% J! G
void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);
; @) y2 c& B1 v( x
" N `' F& Y, ]' g$ I( C5 m而parse map需要按照下面的形式定义: . K/ t v. E+ c2 ], t& ], |
* s* Y' w$ |- O$ w) `
//CPinballExtension从CHttpServer派生而来 % m* p1 J: ^# |9 P
BEGIN_PARSE_MAP(CPinballExtension, CHttpServer) * c: O7 W# K9 P
& O( ?4 Q' D* x( V//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice
8 H& W+ G( z$ X! FON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4)
, I7 A" j0 Y5 R* [, n4 U( t4 D% f& D9 t; _3 n0 k* }
//该参数在URL中的名字为Favorite B: b" @ v9 `6 K: g$ N
ON_PARSE_COMMAND_PARAMS("Favorite")
0 c# Z5 a7 e$ @9 p6 Y, R
* N1 e' o* Q+ N9 v* \* bEND_PARSE_MAP(CPinballExtension)
, ]7 K7 L' C! X5 `
3 R; E9 m9 P, H5 }而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件: , I z: h/ D [# d
" o- }: M7 D; o# U
OnPreprocHeaders
. I7 \2 I" d/ y7 z$ j1 P2 U4 rOnAuthentication
0 B' }& U2 q- Y4 A( {, ?* m3 r! }OnUrlMap
& q0 g- N* ~; ]& AOnSendRawData 8 l3 ?* z3 O g1 a8 W
OnReadRawData 0 P' I8 O7 \+ u) P6 `0 {
OnLog 7 N" X, j1 D0 G* J7 Y+ B4 q
OnEndOfNetSession B, }" E7 ], W
3 \2 U( p5 o& ^# P6 l: u, [7 [8 WMSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。
0 I) m3 y( _7 |' C
! h5 j( m; V; l) x9 x- \ E参考资料:
5 ]# H6 T4 Q+ q; I6 a0 E) {
; |! q( F) S9 l3 @6 Z1、MSDN 2 c* I0 q: o, v8 D" \1 X
2、《精通CGI编程》,丁一强等,清华大学出版社 |
|