|
作者:netguy (mailto:netguy@nsfocus.com) ; l; ? p( S/ ? |1 G2 w: }+ V! V
8 R$ N9 w7 U h q2 h
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。
6 }8 K# E- A% u. Q- H, Q( W2 Q, `: d3 x
一、ISAPI接口和CGI接口的不同。
. H, u* `' |) ]" N. ` u! S3 y; d) y5 F' R) z7 \$ b- l! L7 k) X
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。
: N8 Z- ^) V% T) S/ f2 y. X8 p" s2 x6 c
1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。$ G; {9 _" \5 Z! ~) o X7 ^+ e% a
* b6 `2 c% h5 E" s# i
2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。# e" `* ^6 M& @4 c, c" X
; I+ t4 i! B& U6 S+ }
ISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。
; G. X' L4 l9 R* }6 h( i0 b. ]+ o) \, c9 d- R: E3 ]/ F
* t7 D" w* C2 m$ t: K; k+ ?& w
二、ISA
: L3 k5 l; J8 R5 w% U; Q
7 ^4 ?2 Y) p! V. h& ?ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):* E" z& {) |6 F, M" n9 L8 `9 v
http://www.abc.com/Scripts/function.dll?
- k3 Z5 _4 I v9 s2 [' U8 i j2 R# r0 V; S" K: u
ISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。
# p. F: G Z# X9 E+ `$ W8 e; g) s. S8 L/ @% E* n8 T8 X
1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:
6 }# Y# p' ~* v W9 b2 x/ ^ A2 r5 f2 V+ n, X' Q
BOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);% h( \. _& G& z* d3 w6 v
typedef struct _HSE_VERSION_INFO M4 U3 g( c9 Q
{
- V3 [+ F3 ^& sDWORD dwExtensionVersion; //版本号% p9 S0 Y3 Y) M+ n
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串( W4 W4 E, O& V4 n7 P* E
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
! z3 E7 w- L ?8 V8 ~# X. d2 j" e5 ^# k" H+ c
2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:
% t$ p5 K& l1 H/ C) A
/ [* m8 {. U0 rDWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );
) T8 h% p/ j0 \8 @+ c H. r' H. g. X4 Q9 w* g3 Y# o2 t0 I
ECB的结构定义如下(IN表示入口参数,OUT表示出口参数):9 Y8 t; W1 N9 X$ Y
- i, @/ w b( e1 Utypedef struct _EXTENSION_CONTROL_BLOCK
% E6 M8 ]. s' T{
t: n# e+ o0 D9 wDWORD cbSize; //IN,本结构的大小,只读
+ |7 O9 T2 k. m4 Q, |/ cDWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号
6 X3 V" p8 ^3 ?# Q, FHCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值, _# s5 S1 ]- W5 i: R# o/ S# D
DWORD dwHttpStatusCode; //OUT,当前完成的事务状态
4 S; c2 B2 R0 j# L8 t/ A# [CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容, q4 T, _5 o+ \: N0 r4 A: |( b
LPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD
8 \* R" X+ r& S8 r7 W3 h6 G) oLPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING: y3 Q* F/ |+ s* N# I: q1 z$ w
LPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO
; S- l- P+ k% w1 k# v6 }( cLPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED+ K" ~- e& j* h W3 b4 I8 ~" _) ^
DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH
4 S; q7 h. Q1 g4 {1 m5 UDWORD cbAvailable; //IN,缓冲区中的可用字节数. Z0 D- g, m5 _ A( C+ B6 e
LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据
' t2 t2 k' j K5 uLPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE
( x- d: `* E/ t( H& R/ A9 ~% i. a+ E8 `6 a2 q& x6 q* g' {
//回调函数,用于返回服务器的连接信息或特定的服务器详细情况
" {5 G5 u1 n/ t# U* O. W0 yBOOL ( WINAPI * GetServerVariable )
6 N3 o4 T, V' E6 r0 E& L& F( HCONN hConn,3 ^- M9 ^9 t7 U- a! P
LPSTR lpszVariableName,
# e- I/ D" T' K, ^( l$ GLPVOID lpvBuffer,
' ^: Y" c( A6 F$ g% b1 Q/ L, H0 e: sLPDWORD lpdwSize );
( j6 H6 ]. B. G' r- ?$ i8 ~' T, B0 [% j: }+ c% A
BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
: y) a% W8 @9 x: B& u+ H1 k \. S( HCONN ConnID,. ?- F. n+ `" k; k
LPVOID Buffer,
1 L7 f4 E' ]7 O, ULPDWORD lpdwBytes,
1 Y" r4 D( j+ P) }9 JDWORD dwReserved );# W) P7 `" l% H$ f7 U0 h
4 B% S1 h2 H: d8 K" j* PBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据9 ^! g* r7 I4 x* w9 l
( HCONN ConnID,
/ @7 Y1 t( B, S- B/ g7 m1 ELPVOID lpvBuffer,& t6 G' \% l9 y" R: g% M( I
LPDWORD lpdwSize );
) R2 ?. h' G+ a b: r5 ]" f% I U) D: S" m6 ]
BOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能' W: A- T; O. ?2 @5 Z _: b
( HCONN hConn,
- V" I0 F. n$ h7 v5 w3 rDWORD dwHSERRequest,* Y" W, r0 Q! s2 h8 {; |% |3 a
LPVOID lpvBuffer,
1 a# F. l B' o: E$ A1 C8 QLPDWORD lpdwSize,
- A7 }5 O8 w+ o' wLPDWORD lpdwDataType );
) W n' x$ H5 ?# t% ~; f7 K3 m! P! c I9 {( t _
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;1 b, V9 ?' C! @/ K
6 `+ D. x& G! n& [在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。# s# F5 y7 f" ]% G
% x3 Z! l9 h2 T0 M! ]9 Y三、ISAPI Filter$ H( H% l/ `6 R6 v7 V7 t2 M' y
+ x" j% j3 w# w: U: CISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。7 `* c8 N4 L" W& J6 X6 {5 V
& X! D7 r0 c# e' c- j+ k
ISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何, `! G* Q3 i3 A) o3 t- `
ISAPI Filter都必须引出这两个函数以供服务器调用。
# ] { j4 {6 g- j2 |
) ?" N7 v! P+ B! `6 W5 L1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得
5 C! \7 G8 l. T) YFilter的文件名并加载它们。& x$ }! f2 z/ k. J. ^
+ M! B5 Y9 e. F# `! d! b
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL0 I7 G/ ?# P' E$ Z& }5 E7 C/ V
+ Z% |7 O/ W, q4 w' i2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:
; W/ g6 L) X& @* D2 p7 g, e0 f1 h' k C' h
BOOL WINAPI GetFilterVersion(
. g3 V. i2 a5 {5 u1 O$ s/ u$ kDWORD dwServerFilterVersion; //IN,服务器使用的版本规范
% \$ @- S, i3 {' @- vDWORD dwFilterVersion; //OUT,过滤器使用的版本规范
/ T+ N" J/ B9 aCHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串
# N/ V+ z6 o c! j6 j+ iDWORD dwFlags //OUT,事件和优先级标志+ Y) O J% \4 L
);2 r" X/ B U# l9 c6 N1 F y- o
3 D$ I/ F7 f) ~" a事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。2 T5 _9 a) U) o- R/ F( }
! R) |' K) E! k% y5 B
3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。# u* G! l8 `' N9 Z( l* R
* @ J, O, ]# `! a' P0 e% _typedef struct _HTTP_FILTER_CONTEXT
4 |, l) }# P0 L* l" ~. G{
8 B+ l. I; w/ w1 s. E5 }+ ?6 X0 j$ L# _4 p5 P! n0 Y
DWORD cbSize; //IN,本参数块的大小) e* C4 i; ^, S) E1 a
DWORD Revision; //IN
8 U& q; K: K9 X& Y7 g" yPVOID ServerContext; //IN,由server使用本参数1 W4 l7 H: N1 `6 I
DWORD ulReserved; //IN,由server使用本参数
- P. ]/ U! p( B6 y, w% y1 w7 aBOOL fIsSecurePort; //IN,事件是否发生在安全端口上' X) Y5 j0 G% l/ v. D7 {1 l3 E
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文' c0 N ?* |: o+ U% o' i2 d- k) M5 { V
- R2 q6 S$ { J% \- [7 n. r
//回调函数,取得关于服务器和本次连接的信息+ _5 Z' A; `7 p% o
BOOL (WINAPI * GetServerVariable) (
! @. T7 g( ]* {$ Bstruct _HTTP_FILTER_CONTEXT * pfc,
' M8 z- ?! }& F; e& ^ y9 b VLPSTR lpszVariableName,
8 P' ~) ^ S3 u( Z( _7 Z8 yLPVOID lpvBuffer, z ?# N" J2 [1 m* i
LPDWORD lpdwSize+ U* d- q6 I4 m; i4 X, k
);
3 B$ N- C ~% `& l1 X5 H0 T. T3 t& ~ `2 `) y
BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头
9 t3 f0 @, @! D: } Zstruct _HTTP_FILTER_CONTEXT * pfc, a: E+ t5 D6 ?/ @
LPSTR lpszHeaders,
3 t: E: _* R ]. L2 aDWORD dwReserved
* S1 H9 z/ k: Z# z6 r* k( G0 P- l: x, {);
! a1 t H$ v8 E9 }) O
8 L( d3 k& S6 x2 Y# i: o' v' VBOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端
1 Z9 ^& |( I" W" {& e6 ustruct _HTTP_FILTER_CONTEXT * pfc,
, \2 H0 X3 P0 Z; }7 M* B/ `LPVOID Buffer,/ b" ~$ F. z3 U% J. k
LPDWORD lpdwBytes,
- ?% U$ F/ d& w9 D9 Z- z( rDWORD dwReserved
1 r" L( b$ u! h0 J);
8 p# ?9 S$ B4 V4 p+ ^) ]% e- {* M5 H* L( g
VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。4 m6 _; T3 R. @* t/ p1 |8 K8 R( U
struct _HTTP_FILTER_CONTEXT * pfc,- h$ H' N, e0 e8 a) z" ~
DWORD cbSize,( Y, r) t9 j& i
DWORD dwReserved/ D1 W _- A- V" [
);
' b- l8 h( J/ ^9 }) D: @% J" j
* k3 C5 {8 Y4 S9 P0 @* ~" O8 ABOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
' @% }; w+ J: e4 n4 Zstruct _HTTP_FILTER_CONTEXT * pfc,
' v. D I H- K4 M; @# Menum SF_REQ_TYPE sfReq,
+ O8 e! @. A! E% {3 U% }! IPVOID pData,
* @$ K% a1 U4 w0 p( S' bDWORD ul1," E# j: O) j3 f" w; h
DWORD ul2+ U5 B, @" o& t. [
);
- T/ v5 g8 `/ U8 M6 g: L" ~: |' D/ g- T
} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;. c; L5 J% ^1 ^. ]
6 @# V$ _) i" `, w; T# o四、VC++ 6.0中对ISAPI的支持2 S* L8 o) g# E
- x, T B" T2 M$ F; S! }
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实例,每个
. N M& q) f3 @6 i2 V FCHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。' z; Z9 J0 K, {7 a, E0 V# }7 w
% X; e' |& X/ G; v一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。' m! v" o; B( z; ?( F P
- V" E9 [+ j! F2 GParse 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为例,该例中有下面这样一个表单:
% w2 S5 P3 D8 [$ T b3 q: W) K0 \2 @# c, z _ [2 A' ?/ c* S
<form method=get action="pinball.dll?">
& s1 p* g% c; V2 O6 b2 N8 L, o<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">
1 b1 M; `5 G8 n<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>; h6 m' \, @; g+ W
<input type="radio" name="Favorite" value="2"> Twilight Zone<br>' @( |9 G( l6 a+ @* e9 d
<input type="radio" name="Favorite" value="3"> The Addams Family<br>* h; d0 h, c9 [& M* E3 D3 \3 T
<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br># y, U! k0 g: ?5 ?; Z* i T
<input type="radio" name="Favorite" value="0"> I don't see it here<br>
; p. J+ Z# V3 g<br>) g- ^9 U2 v- B+ q8 q
<input type="submit" value="Show Me!">
C6 Y( n( V% }! ~</form>8 C9 x" [( K' |& u6 d+ M
# Z" K# \. a* d0 P. s' h1 y
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端
( p$ c0 P- l/ r7 d/ |最终将得到如下的URL串:" z( E s J$ o% `/ `0 ~5 B2 w. c
8 f2 n3 C' M$ A6 k3 n' c/ Ehttp://www.abc.com/pinball.dll?M ... GetImage&Favorite=10 B. g6 S7 Q" D: H
( y# `3 O4 {, K1 r* C2 j在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数
' K9 y, I1 ]- r& K' z6 K# u; C3 [将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite:
& T8 v9 i! j( U6 y' V) j: I+ l" h& C A5 C% m; P
void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);
8 d2 S3 p/ t( ~) T5 S4 L
" ]6 J6 }& v9 u# O4 F) o0 Q. n而parse map需要按照下面的形式定义:- I" q, D. T0 y% ` S. q$ N, [
- R" _; X& K s* f, W8 {9 H; x" c//CPinballExtension从CHttpServer派生而来" f$ K1 u% Y2 H
BEGIN_PARSE_MAP(CPinballExtension, CHttpServer) 9 ?* w `% C' K: z# I
% L& W, X2 S" K3 s0 C//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice1 Q3 u1 s) {. }" A
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) ) H5 e( Z! z' P% \" C
, v2 `# g8 T$ d
//该参数在URL中的名字为Favorite
) T3 }7 s# i6 xON_PARSE_COMMAND_PARAMS("Favorite") 5 }, I) A6 w# Z& {' j
2 d2 s" ^5 i9 c; WEND_PARSE_MAP(CPinballExtension): S, Y I7 ^$ u9 q, o
' B$ X$ i: c( d& Z
而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:
; S" j4 t* _6 g# x! W& M/ X! g. P+ p" M2 z3 R7 X
OnPreprocHeaders5 ]) i @$ v8 {# {
OnAuthentication2 ^; c9 I3 [/ l& O
OnUrlMap6 M2 N9 w- `- L* z4 p
OnSendRawData N/ `( Y. g+ z" d+ F
OnReadRawData3 _' U( c; D; m# l; c
OnLog/ O0 M* D S' C3 F4 B
OnEndOfNetSession& S/ K/ x' V9 d- z/ W
9 ~( Y8 b0 A/ t0 L7 G& g
MSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。
! E! d8 }9 P( d( P8 s, _' R6 K3 ?, M/ u4 h
% l5 I+ i- g1 p4 T/ ^5 n: @8 N参考资料:
4 A7 v7 ?) X5 P; O
, E9 c9 j, u* V/ d$ J7 \3 ?3 Q1、MSDN* m, [. `& @" T) v+ M9 ~& g* |
2、《精通CGI编程》,丁一强等,清华大学出版社 |
|