|
|
作者:netguy (mailto:netguy@nsfocus.com) 3 c3 N. Q/ K7 f
/ r0 _0 ?1 O/ g& z; NISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。
9 Q- X0 f( f( m8 ]# `* x3 g* W% M* |6 z# h& X7 l5 f* J+ d5 e
一、ISAPI接口和CGI接口的不同。) ]* T0 ^' \& J1 U8 H" P
5 ^$ ]% C4 A0 D+ {& \" ]ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。
s r0 z, _/ c8 P* M: {
5 Y9 L6 _. H$ P& n1 y1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。6 B. N7 m, k: ^9 y" ^
+ `3 c5 o ^% M. U8 {
2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。) B% h( f/ I+ K C
k/ f: s8 }4 x+ S! W4 ?
ISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。
: g- P- I3 ^0 Y$ z6 D+ v4 O4 ]
; }6 K6 ^& m- o7 p" f- J' ~ W2 K, I8 C: d6 N" F8 n! _: S6 V
二、ISA% D% Y+ x! A, j( i9 C% |- ~
3 p! a, k8 J0 t% B
ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):
6 c+ x& K4 W2 V& vhttp://www.abc.com/Scripts/function.dll?
% `' \6 {0 ~, s8 j
t5 @( P( e8 }4 P8 j9 ~, Z) wISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。
& D( F$ U$ T' I& \6 V3 I/ J3 c! z( o( ?
1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:
) x$ q0 U: n9 w1 I9 v5 r j/ `
9 u% V, H. b$ S+ RBOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
4 \: ] B5 ~: U Z7 I0 Jtypedef struct _HSE_VERSION_INFO, Q1 a' m5 {$ R3 Z$ W S+ E% S5 g9 Q
{* h7 ^; p: U* ^- Z. e0 m$ g1 x3 ]
DWORD dwExtensionVersion; //版本号5 G V1 J a$ t5 v7 n! U0 g
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串$ | u+ Z6 z4 ^/ K; Z) D6 G
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;+ ^1 g/ l% Q Q9 a, V8 J/ T
" a) [ z7 C5 n) Z& Z1 f" }( ]
2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:, _* K" K0 o+ N
8 h6 ]& Q* q) IDWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );$ d c& m. z8 `* j P/ V
. J. Q5 q9 E8 a' m) B% l, r0 sECB的结构定义如下(IN表示入口参数,OUT表示出口参数):% S( {) r. U* O* }; p1 G" k
. D2 F9 l7 k2 ^typedef struct _EXTENSION_CONTROL_BLOCK
# F) N H, T0 _# D: z! t+ ]{2 V& N, S. Z, f& _% C# @
DWORD cbSize; //IN,本结构的大小,只读
* C; i) D) c" g' r6 C# a4 p. {DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号. V8 Z( H8 s0 }/ W3 B
HCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值
9 \8 n2 t' K7 ]& I8 n4 J1 R; j9 iDWORD dwHttpStatusCode; //OUT,当前完成的事务状态
% f9 w4 o, K7 N! j- ]+ KCHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容
, L9 w8 R' J4 }LPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD v# v4 h/ n' C0 U: t
LPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING9 Y$ O6 F3 U9 k% {( m
LPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO
; g! E% s( w* gLPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED: L% ]3 h2 @3 r9 q# x+ k8 M
DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH$ A& b9 p/ T, ]* j+ I
DWORD cbAvailable; //IN,缓冲区中的可用字节数" j' o7 ], j$ d
LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据
8 P3 A' ^0 [7 Z3 w' dLPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE
# j: U( e( z8 z7 f0 v" [7 l; v' {0 S( d) ]+ y
//回调函数,用于返回服务器的连接信息或特定的服务器详细情况
0 Z% v0 Q' G% }5 D8 ]7 `BOOL ( WINAPI * GetServerVariable )
. _# J, T* c- Z6 b% A7 M( HCONN hConn,, m) w( Y' J' w- S( k
LPSTR lpszVariableName,1 D4 K( E, Z8 ^
LPVOID lpvBuffer,
Y$ D; p- p" j3 `5 i5 `* T4 vLPDWORD lpdwSize );' o4 f5 A# d4 O B! \
1 b. x) Z$ ?! k- k1 _1 F9 \; b
BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
" B9 | n. V& g3 Z( HCONN ConnID,
) c. c" e! I- P: a0 cLPVOID Buffer,
9 {! @* K0 J2 C# DLPDWORD lpdwBytes,6 {) L, ^) M+ m, x% h, R# f
DWORD dwReserved );
1 p7 n% Z' K1 B- S0 b7 L' B& Z& j* E! }1 P+ i$ ]: c
BOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据* |3 I! ~# f" h2 ?( U
( HCONN ConnID,0 b4 Y, o4 d7 @) k/ c& e4 L
LPVOID lpvBuffer,4 q" S+ P: ]1 [$ p
LPDWORD lpdwSize );! x4 u/ t6 A' k5 Y: Y
+ |3 j5 u1 i" j7 h/ L8 SBOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能
7 U4 B( ^" E7 _- e! G; y6 r" ]/ A( HCONN hConn,! i8 K) m. E" ~8 p/ R2 D; L
DWORD dwHSERRequest,, }% K- U1 Z K7 L& ?, ]! ^
LPVOID lpvBuffer,
; R: l/ J0 C. i# l- [' \LPDWORD lpdwSize,
" g+ N( i9 |& Q8 N1 U2 pLPDWORD lpdwDataType );
( r& X/ B' h5 |. x
4 @/ E, n: t* ?- }. A, i) d} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
4 Z' e7 `9 M* a
' |) O" [5 @) w+ H5 R- j6 P( E在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。
/ l+ j A0 |5 |2 @) o& V8 u
t+ T' y- E9 ]4 c3 j% s三、ISAPI Filter6 M" {9 \+ L4 m- ~
" N: `4 O- j, i! G5 Z) G- I1 U( _ISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。9 ~% H. W; k- d! v4 k' I2 ~& P: R
! L3 J5 J7 | E4 k/ AISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何
7 K0 H1 M ]0 E3 v$ \1 vISAPI Filter都必须引出这两个函数以供服务器调用。
4 g7 _/ O: B4 P8 V4 C, f. [& C! N+ x$ X0 H0 F3 f
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得
/ i& j* c( J# A5 N. C1 `/ UFilter的文件名并加载它们。
. V3 h' L! v2 p" v/ H
& A5 M# i. i2 C8 `% FHKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL5 x. C: }4 u( D) @
1 j5 _0 P1 o" i9 r7 M I5 Q' B7 x
2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:. b! V8 k" r: S
, K: Z$ V9 d# E1 p$ R' C
BOOL WINAPI GetFilterVersion( + |( \+ E& z7 U9 p4 d
DWORD dwServerFilterVersion; //IN,服务器使用的版本规范
. F8 Y0 Y0 |7 j oDWORD dwFilterVersion; //OUT,过滤器使用的版本规范
+ S6 l- p @$ qCHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串
+ w7 H% D t7 nDWORD dwFlags //OUT,事件和优先级标志 m F% w9 m6 X: Y; \# p6 K
);1 F) D9 z% h, @5 a+ G6 Z
9 P, w4 g3 J. |& i: |. {* s1 i事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。* @1 M0 }4 L) p3 x0 u9 _' P
3 f2 r% k- g2 z- i6 K. b
3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。
0 [- a- b W' p* V6 h" ^- O; q3 v- P% X d! w$ r) y7 g. M
typedef struct _HTTP_FILTER_CONTEXT4 N" N/ y% I) V- C" O
{
5 E) t+ {6 d: S% B
" u7 h" d9 V% g+ R9 EDWORD cbSize; //IN,本参数块的大小
, j' g) m1 L, E; r, TDWORD Revision; //IN- L0 Z4 r; ^/ Q" Z0 n$ r7 V' H
PVOID ServerContext; //IN,由server使用本参数
$ H7 ~' S7 ~2 a9 w0 N \2 p, KDWORD ulReserved; //IN,由server使用本参数4 x$ X8 \) {+ b+ p$ F) P: p8 z
BOOL fIsSecurePort; //IN,事件是否发生在安全端口上% X+ R' n; i2 k8 _+ z' U' ?
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文
7 Q8 I6 s9 g8 S3 N# K- P( o7 ~! k+ n" m( S
//回调函数,取得关于服务器和本次连接的信息4 Y/ x$ Z' Z& t
BOOL (WINAPI * GetServerVariable) (
, Q% R1 z- S2 [" p$ istruct _HTTP_FILTER_CONTEXT * pfc,
8 a4 B2 V j O. aLPSTR lpszVariableName,
; s$ x" u) A: n, C7 [$ f) n3 OLPVOID lpvBuffer,6 M3 E6 b0 l& ]0 p8 V; T, ~ b( v
LPDWORD lpdwSize
7 p5 Q3 C0 Q) |. I5 r U6 @/ @" D3 {); , h+ A6 v6 S' [6 d
. ^1 r* _; U0 lBOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头
. r5 C# p& h/ z- s; P8 mstruct _HTTP_FILTER_CONTEXT * pfc,$ @% G/ T: v8 H* E. Z, F
LPSTR lpszHeaders,# N) y0 G9 I6 E5 z& q. U
DWORD dwReserved; Y1 H7 k( ]! c+ `. f5 l' a
); + n8 @# S% s, J" `
# S1 F( u$ r4 oBOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端
) I: \" z$ |7 Q6 rstruct _HTTP_FILTER_CONTEXT * pfc,% V; U0 N$ Z O7 L! A- k
LPVOID Buffer,1 s, J T$ k% ~! M5 W2 P
LPDWORD lpdwBytes,& ]+ A. k3 z6 R1 E6 h. {5 M% u2 Q
DWORD dwReserved
& m# O$ x/ x- c Y/ @/ X" r); 9 e3 R+ j+ q3 d* ^! A( @
1 ~) L( I5 M o [7 ~( V
VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。) ^6 b% b$ t& w0 a1 e
struct _HTTP_FILTER_CONTEXT * pfc,, s9 g9 |9 c, k5 d
DWORD cbSize,
% u+ v& t3 w. z& tDWORD dwReserved
$ e0 T7 _- m- w8 P" k- S7 h6 n. ^);
V. a. @# V0 s4 i" k- e$ N l) R/ a; g3 \9 `- j
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
# @2 u2 [* E3 g! p7 K. p: }3 Hstruct _HTTP_FILTER_CONTEXT * pfc,: x6 a; T2 z7 L9 ]# g
enum SF_REQ_TYPE sfReq,4 j: `0 m, ^& n* y" t+ C9 T! }
PVOID pData,* D4 b: N" h) b: T
DWORD ul1,
' b: q- V! m) H* YDWORD ul2
/ w4 B# d& x- H% _, o6 [* {2 a/ v);
2 I% Y4 g' B* ~; O. j. l m0 i$ V) x" j' r4 g
} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;0 n, L* D. D' c' e+ R
1 a, a. j" O( |$ Z! f四、VC++ 6.0中对ISAPI的支持
, y2 |6 w1 h* A; m/ m0 S" t( L) `" q8 Z9 H y3 ~( [) T: }& [3 W
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实例,每个* h3 Q/ i1 ^! T/ F5 V" {
CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。
' T0 F& G0 e- m, v' r4 m1 |# P. x- r f( j+ F9 `; v7 b# ^
一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。
6 q2 E' J$ A Z) b0 ^3 n' o% z6 B7 E! c
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为例,该例中有下面这样一个表单:
4 e+ |" g8 s) H0 o J2 }# N; L4 a) z
% _' S. m! U1 D8 G3 ?<form method=get action="pinball.dll?">& S& u8 D a" m6 e
<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">& g) U# P0 D. }. |5 _/ e1 e
<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>
7 J) k) A' u$ Y0 z' Y! }<input type="radio" name="Favorite" value="2"> Twilight Zone<br>. c0 _( p6 x& i) m5 E3 `0 V
<input type="radio" name="Favorite" value="3"> The Addams Family<br>2 K7 @# @9 d* n9 Z8 T" e
<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>
1 _9 G/ T* e- S* a<input type="radio" name="Favorite" value="0"> I don't see it here<br>$ d! ^ W8 R+ _* n+ Z
<br>) I# [8 t7 ^6 {( o
<input type="submit" value="Show Me!">
3 ]( y; N; i. E$ u) \</form>
4 A) k+ p/ F% Y( V6 c. @. \1 L: [
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端: Y9 x) c* S3 o3 b/ z
最终将得到如下的URL串:
7 Q9 F/ \4 }( g* z* R7 h5 P0 k% Z' i' x
http://www.abc.com/pinball.dll?M ... GetImage&Favorite=1
) f$ y! R3 ?( R. M
' n* k. ]! j* O# X. A. M' k) r q在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数5 S, g; N: J* a* n1 j. ~
将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite:
+ s7 `' C- e( |2 k! N4 v, J# X4 c2 i+ y g' m1 z
void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);6 Z+ g+ Q: R; Q
* `& r' b3 |6 C `
而parse map需要按照下面的形式定义:
$ ^- K9 Q; j4 ^6 j8 d& O
( p6 ]' U$ x& ?6 J//CPinballExtension从CHttpServer派生而来
/ m- K6 t) C6 m) u) ]# {# nBEGIN_PARSE_MAP(CPinballExtension, CHttpServer) % C, d2 u9 R: Q$ O" n7 _
\0 s' b# p$ j//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice
+ f5 H( R( b' U. ?& T0 n' W1 C$ J2 tON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4)
& J C; e3 W) i5 E
' t6 _/ A8 A u' ]! _& M% m//该参数在URL中的名字为Favorite" q) ~6 P4 F8 L% z5 l/ Y; M
ON_PARSE_COMMAND_PARAMS("Favorite")
9 f/ A+ z! j' |5 E9 Y( X5 B! r3 t/ V* O$ K P
END_PARSE_MAP(CPinballExtension)+ j) e, m6 I! }; V) u g7 n
' d2 V i( {( @! F0 h L而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:
% `: u1 g1 ~: V6 B# f$ M3 g" P1 _7 O; S1 A; g9 b- `! \
OnPreprocHeaders7 C7 m1 L) v* ?
OnAuthentication
/ ?4 w; V5 c7 i( @( GOnUrlMap
- ?% P% V4 s( v; k P6 M8 ?OnSendRawData) S# C3 d3 y1 f: F5 _' A: L
OnReadRawData) c+ |- X% B8 O
OnLog
4 i0 ]( L5 Z3 K: e8 V6 G* YOnEndOfNetSession: ?* r7 j8 e! l& J
: M" ]) I, t" i+ S3 y# x5 W) ^0 aMSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。1 \4 v$ D. q- U+ R6 D
% @' J% D. O5 X( b: Y7 G) `7 D0 ^3 s参考资料:
9 g6 ^$ ?2 t/ h6 h+ P7 ]4 t$ t
; l0 Y2 }% O: |- F. y, ^% m7 ]1、MSDN
5 C. i- s1 C2 C+ i% O- z2、《精通CGI编程》,丁一强等,清华大学出版社 |
|