|
|
作者:netguy (mailto:netguy@nsfocus.com)
9 ` B u1 R3 Q' W! A: p3 N# }6 n9 a# D' p x, S, Z
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。
$ o. p2 K* l& B2 Q
2 F* c2 \' j% _/ L5 r一、ISAPI接口和CGI接口的不同。! } L7 L' r9 D! X$ W7 | G6 W9 \
5 P8 c& X" P" k8 ^5 L. z u. q
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。0 b1 T; k% Y: u% L4 p
f! g% R, U' s) ?' ^% `
1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。
[5 a# q& t" t' M
4 K( E: b+ U3 i2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。
7 r+ z" M3 `) o2 L7 C- ^
/ r0 V; X+ }5 i8 ^! hISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。
; T8 p |: {* N' I; J
$ y2 X7 S! \1 W' a- L
' D: \" m( C0 e6 U二、ISA
7 m' I1 W4 x, I& t6 K
1 c& Z, t- O( L0 c7 b$ xISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):
) m( j2 q& e$ t# @6 N* Y! R5 Ahttp://www.abc.com/Scripts/function.dll?- F& v8 d% B( ^. c9 X. K% j+ K
3 l6 j1 p% {8 U! `8 f) f6 ^& }
ISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。
1 a( U, `& Z& [/ p. d5 _! o9 u. z) h+ A# O
1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:5 G+ \ i# M2 P+ M
$ a1 c' M: O' g! B2 u
BOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);5 l2 e0 N$ L9 _1 i2 b) w5 G
typedef struct _HSE_VERSION_INFO
a7 m9 A4 f$ K7 s- H \{: L$ P3 l# d5 T1 }( K% A
DWORD dwExtensionVersion; //版本号8 q9 h6 ]& j# b+ ?4 C
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串
1 I8 x2 e0 O; p} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
. Z4 L H8 J! Z t9 t; s7 @* c' m9 t# ~" ~9 V/ c, ~
2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:
" A4 P: x* p6 M: D( M
: Q/ E, l. g# G' J7 n4 r) A3 c! LDWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );! U- ~2 N/ s* z5 V( Y8 J) G
: ]2 W& x1 l; h5 T2 p
ECB的结构定义如下(IN表示入口参数,OUT表示出口参数):
4 [, t& U& E" H1 X4 {9 C( Q6 v8 o# F4 o
typedef struct _EXTENSION_CONTROL_BLOCK
: ^9 o# I3 v* {/ k5 n1 Y9 `' D. o) \{2 W/ e d" F9 l d
DWORD cbSize; //IN,本结构的大小,只读* i; E7 W& X1 v9 c6 p
DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号
& P2 _+ J9 k) l. I0 ]( ~HCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值
) x4 c5 k8 E+ `$ j' T5 v3 E, GDWORD dwHttpStatusCode; //OUT,当前完成的事务状态; d- F# A8 P0 P5 K; ] [" K
CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容
- }* t0 o, j5 ~; B* eLPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD
4 G' j4 q2 G8 a3 ?. VLPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING
# d5 u' H: H3 B* {; ~1 i% ?7 j5 ALPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO
2 c( x8 @, M- c& x' ~LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED
8 ^/ a% @1 W& b( W# QDWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH& q0 A: x4 o. k
DWORD cbAvailable; //IN,缓冲区中的可用字节数" H1 v0 U- A; ]: O3 w* `
LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据% Q; }/ q5 U$ X+ N$ p+ X; T% ^5 W
LPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE
1 ^# U* w. V" l ~
& W/ N0 h: Z8 G0 M//回调函数,用于返回服务器的连接信息或特定的服务器详细情况1 }! X+ Z. d5 k; l& H- G& \' f
BOOL ( WINAPI * GetServerVariable ) 9 S# Z- c5 \* [4 p$ _2 s1 Z
( HCONN hConn,# X8 D, Q7 D @/ D. n! X+ x' w
LPSTR lpszVariableName,2 _0 N0 H# x0 @/ p$ K4 [3 J7 [
LPVOID lpvBuffer,$ l _! N6 G% m2 W4 D: L
LPDWORD lpdwSize );- B2 B" L! _ N% `" p
; Y0 X: k4 g% Q" R4 K: v3 ?( J
BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
/ \* [+ c( B# D. L( HCONN ConnID,( I- J' U$ c2 B! A5 ~9 c6 s! S
LPVOID Buffer,
: Q' |2 u" T4 H6 T+ ~' g* e% _LPDWORD lpdwBytes,
8 d/ N. q7 z8 g1 c3 dDWORD dwReserved );# n2 U: }' R6 h/ p
( d8 I; u! i L1 u3 Z7 vBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据. [/ U0 ~% ]! a1 A5 e* u( P
( HCONN ConnID,
. Q( F% v: O" F9 p, i- c- ZLPVOID lpvBuffer,, \ G8 b" {6 ^% m) O& ~- _+ ^1 U! Y* E
LPDWORD lpdwSize );# N& t. r0 M) t& ~& G0 r/ v) i/ {
9 A. h; A1 l4 C6 @) |2 _' G
BOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能
9 t) _3 ?9 F8 J6 t( HCONN hConn,
4 ? v2 f Y9 }8 D6 yDWORD dwHSERRequest,
2 _0 S( s$ t( e' `( ~% RLPVOID lpvBuffer,) e, |* @% _8 t) ]/ f6 L
LPDWORD lpdwSize,
4 M' d8 `! J( jLPDWORD lpdwDataType );; C) h! j4 X6 c% A) Z
& M: y; s8 F& O/ x
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;3 W' {8 F# A0 y4 v {
. E" D0 B& l: g在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。
" _& Z; D6 s4 M3 ]+ G4 H) y! D' _5 i
7 A% e7 r* o( e6 ]5 s9 L' Y三、ISAPI Filter( h6 s& O; K* t3 O- B
" X5 X6 r$ d1 V& I" f0 l
ISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。0 y9 v) d j" @+ X5 q
) q5 u2 |% t5 g: TISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何) V C2 Y+ [" M$ y4 ]6 r+ g
ISAPI Filter都必须引出这两个函数以供服务器调用。: ?' r5 w: o8 q7 a( R
9 n8 J* \, u* O5 B' g, ~
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得 j( o2 \& U# d2 a
Filter的文件名并加载它们。
3 M+ o9 V+ f/ b& g
% x) l6 W$ t9 g/ O) gHKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL
: H& }3 e, C- o# J% F
s" p. |& k: |( }2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:
1 _: m1 U; z* y7 R5 q
7 h! Z: ]" ?$ U1 C jBOOL WINAPI GetFilterVersion(
u8 j8 C# N. S, a8 w# X: A- LDWORD dwServerFilterVersion; //IN,服务器使用的版本规范 : n8 Y3 g' u- C: B
DWORD dwFilterVersion; //OUT,过滤器使用的版本规范( U; q1 H) v8 _2 G6 O4 n9 @! j& m
CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串
$ G$ g/ R' T" S9 v: j$ |DWORD dwFlags //OUT,事件和优先级标志; l' m) w1 s, U5 |8 l
);
0 f+ e* ^9 {0 @1 R9 o) M9 r3 S# U( f
事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
( d8 m" f1 f* z: `) k
0 ^7 ^% b N7 Z$ F+ m' h# F3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。4 c; C6 U/ P# H# o
- T% h5 G+ i; q0 y! v# A, [typedef struct _HTTP_FILTER_CONTEXT
8 ~4 X- L6 m3 ?1 c" o{* _( W' K8 G1 P6 m4 ^. U
% ~% a* C0 |" ~! t( P
DWORD cbSize; //IN,本参数块的大小
: A. x7 T1 m u* i t# B9 J4 Q sDWORD Revision; //IN( _* u1 N0 A6 a& B1 I' I' w" y
PVOID ServerContext; //IN,由server使用本参数! B W6 D2 Q8 N% P0 |% o
DWORD ulReserved; //IN,由server使用本参数
8 z+ s) [/ b4 \* m7 A3 Q1 r/ rBOOL fIsSecurePort; //IN,事件是否发生在安全端口上
2 I" [2 M; ?* b2 s: E1 fPVOID pFilterContext; //IN/OUT,与本次请求相关的上下文" m" O, m" \, t1 [' v, k
5 O0 l5 M m+ Y$ D M/ q$ U//回调函数,取得关于服务器和本次连接的信息
4 W% K& J" b; l. H: q" j! gBOOL (WINAPI * GetServerVariable) ( / `3 o; j. j, F0 M2 D
struct _HTTP_FILTER_CONTEXT * pfc,# Z/ z5 M: x Y
LPSTR lpszVariableName,
6 _$ Y- A: O' h" I3 e0 LLPVOID lpvBuffer,
8 A/ V& M3 n! ZLPDWORD lpdwSize
# Y! g7 p/ n! r' U% o7 k);
! V% a9 q$ K) s$ V; T
' F# L. o3 u u9 q" R& G& `BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头, ^- W+ @+ L* s+ q( \. }
struct _HTTP_FILTER_CONTEXT * pfc,
L( o7 A( U3 B) Q3 DLPSTR lpszHeaders,1 f. y7 i4 _" {% E f5 ^& S
DWORD dwReserved$ a1 U8 Q8 S: \0 x0 [) \; w
);
6 X$ W" ]4 [- c5 | |$ G- V% w- y3 h
BOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端% f$ h+ J+ l5 H! @3 z
struct _HTTP_FILTER_CONTEXT * pfc,5 Q/ F* Z7 M& T# a0 y- s
LPVOID Buffer,7 N: o9 `' Q- W9 {, p
LPDWORD lpdwBytes,- D% a! U K6 j: z/ f
DWORD dwReserved
' z1 d5 J3 w" ]); 5 g- n' s: A/ {2 y. b3 j
3 o" X) M; L; s. H
VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。
D- l J! p5 _9 \- e9 Nstruct _HTTP_FILTER_CONTEXT * pfc,
$ b4 |6 N# v+ }DWORD cbSize,0 V) l# c @* }
DWORD dwReserved
$ [7 ~2 K! Y; r) p$ T6 J);
( q8 g6 g# U( h2 G. Y" [. @: i7 n- d( _9 V* R* v
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
1 @# y5 l( u0 X+ D/ I& v i, dstruct _HTTP_FILTER_CONTEXT * pfc,; i# D+ ?/ M% G- N2 [
enum SF_REQ_TYPE sfReq,
6 k; W a4 E' _+ T' fPVOID pData,
$ s9 `' F- m$ k. ?8 NDWORD ul1,% _3 F! }) b2 k8 s" y
DWORD ul2
9 u5 o/ |7 ~& {' I5 D);
2 X/ E( N1 E" c, ~- p& z8 y* Y7 y. h$ ~% `" r: y
} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;
3 E/ `6 _' Z+ f) j3 I: v! L) d8 F% T& i9 z
四、VC++ 6.0中对ISAPI的支持
4 o3 O5 ~) O6 n' R! X) b) _3 r" J0 b* e7 H
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实例,每个
2 }# X; ?2 f1 i/ j k7 A4 {& [: iCHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。% _$ L. q' D& y- x+ a, M
. G" ]* U9 f: ]# y* c/ g一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。
3 `$ v2 ]- e4 X2 u; l% n
( @! |* g+ ]* Z) j! Z1 dParse 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为例,该例中有下面这样一个表单:
: E2 ~/ p9 u, e# T$ t4 @- }" `% `, P3 }
<form method=get action="pinball.dll?">2 J N2 C5 i d( ~/ ^6 M' e" ^& a8 {
<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">
6 K" I/ T3 d% A V/ R( ^: p<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>/ ?- T/ k; s+ |* P! y, l# T
<input type="radio" name="Favorite" value="2"> Twilight Zone<br>
6 ^0 E0 q" N- |/ w<input type="radio" name="Favorite" value="3"> The Addams Family<br>
! N5 p! P, N) N6 K2 V4 S1 x<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>
j* `) a; s# m<input type="radio" name="Favorite" value="0"> I don't see it here<br>7 B2 i0 n0 k$ Y$ i9 I0 o
<br>
* p1 L2 w+ l: s) Q* D$ T* m; x! q+ N7 D<input type="submit" value="Show Me!">+ q, N, g2 l7 O5 {
</form>
5 Z+ \; N: Z& X! q/ m7 ~5 f
% m: Q% X0 j( Z1 U$ x当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端
* H( ~+ o0 B8 R- g) q% S3 d最终将得到如下的URL串:8 _0 D, i; j, c# n4 f8 B. N4 J
6 e. K0 I. E6 r! N5 U
http://www.abc.com/pinball.dll?M ... GetImage&Favorite=1
' _" I, \4 P4 ^9 J* x% ~4 y/ ?5 Z
4 M5 d% |. P* g- T% g! v在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数3 M/ Y* ]3 O3 `% k/ J4 A
将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite:
$ t! V- i. l. t7 q* ^
' a! f) w9 o% J1 wvoid CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);
( Z* r7 Q3 n7 R+ `4 a2 f1 e, y7 T7 b1 j
而parse map需要按照下面的形式定义:& W1 W; Z: T, [% _
% o5 T& Y& s& \8 o" p//CPinballExtension从CHttpServer派生而来
7 N" Q- l8 g) Z+ a' ?BEGIN_PARSE_MAP(CPinballExtension, CHttpServer)
" v" E, L2 Z* l. }- U7 P* V, W4 m0 s: [1 ~& T, |* S
//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice, T" G- I2 g( `. G% ?
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4)
4 H: h" y! S1 `$ E1 o9 c
5 Y# Q7 M Q8 M9 g% J: Z//该参数在URL中的名字为Favorite
+ U/ l$ H* p7 o# y } k5 Q4 BON_PARSE_COMMAND_PARAMS("Favorite") # s% q) i" Z8 B% H: K
2 ]% j; l7 O$ E+ [
END_PARSE_MAP(CPinballExtension)6 m1 M% W* c9 W# ?) E6 C2 c ?4 q& f9 |
' r* D+ j/ W1 m- ^/ U- l5 W8 [
而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:7 ]6 t* V: L4 R. _
4 }& X" a! m& S# T# ^4 ]; m
OnPreprocHeaders
7 F5 s2 [3 i0 r; }: lOnAuthentication
, k h# p W# w' @0 [4 KOnUrlMap
+ ?! r; b! f$ G2 _% M' W+ t+ S: C) f+ UOnSendRawData
" q: B) n% g' ]* R: |OnReadRawData
- o' v* v8 Q; X% N$ l9 ^; AOnLog
/ W- i9 b: R' aOnEndOfNetSession
, K: i- j) [4 |9 R9 |! W; q# `7 }2 K# d+ f' e! E" l1 y$ C
MSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。$ G1 ]3 V* y2 x+ N% T
2 v$ e4 {# ]" p% I参考资料:! {6 s' _4 e/ c: Y/ Q R
5 {5 R: @# ?2 e7 X" X8 T- g! ?8 H
1、MSDN
- q1 ^6 o+ r; s2、《精通CGI编程》,丁一强等,清华大学出版社 |
|