|
|
作者:netguy (mailto:netguy@nsfocus.com) 0 T; w. y# q* J1 _$ n, O% m
: {2 e) X0 K1 _0 Y# _) vISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。5 z" t6 n5 M7 _9 o7 f/ t$ T
* ?4 U) `" \, ]) F一、ISAPI接口和CGI接口的不同。
* C; |, H+ S- [. Q$ ^$ s# U( U/ j" ?5 K
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。, d5 O" `5 _* n, q
' [- R5 {+ X% z, j( a& W- A H
1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。7 g0 L2 \: L' ^2 b4 z; ]
/ u2 t7 w" @; t" x& G
2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。: z5 N! z0 N7 d' W0 Z/ W) N: j8 {
* }/ |& \. P1 Q |
ISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。
( p8 \& P, H7 J" F
& i% ]. P$ w9 [# R0 z$ e+ L. ^
" q) q( u- T9 K# E二、ISA
7 x- b0 d% B& d0 Y' M2 [ Y" M* p8 N( P2 G& a" i5 Y( R: w- c
ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):
5 v, M9 W4 V' }) mhttp://www.abc.com/Scripts/function.dll?
: B: f$ ?( v/ R J- m' x: T+ n7 j7 k3 c9 c+ F; b
ISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。" h8 Q1 A# L- ^
) B" U2 H8 F# ^' q, d2 X1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:
% \* E- K2 y( y( \7 ^2 a6 x% G& o2 b4 g; @" L7 E. M
BOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
+ t5 B, l5 i6 l/ ?2 htypedef struct _HSE_VERSION_INFO. m6 x: |5 P7 a. p+ ]; i
{
M/ Q" c1 O1 q! |- }/ j) h4 S( KDWORD dwExtensionVersion; //版本号2 R3 q# \$ ~$ T
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串- f8 x3 h6 C6 \ U3 s {
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
: ^3 ?5 @4 W# ^6 t" U
: q! q* f- h2 x* K; }3 v, F2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:6 z M) x( H8 R/ n6 m1 `6 l
8 d+ I! m$ Z! j$ m; O
DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );' s( F# h9 T; B0 i- _, ~, g% f
5 a+ D) B% J& _) ^& t3 t
ECB的结构定义如下(IN表示入口参数,OUT表示出口参数):1 p; i. r' @% ?* [
8 V8 F, j; E& J
typedef struct _EXTENSION_CONTROL_BLOCK 3 p3 B! f" B2 \" l& a
{6 S: X8 \- y/ |! l8 L' Y
DWORD cbSize; //IN,本结构的大小,只读/ ^( f" R% o0 U7 [6 a9 O
DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号5 ]! b( D3 q1 A
HCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值4 [& i7 O4 U O
DWORD dwHttpStatusCode; //OUT,当前完成的事务状态
O! M# D/ |( ICHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容- L$ m1 K7 E, k4 ^; j3 Q
LPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD; E3 ]+ O5 O. Z# G, C# G1 q
LPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING2 y+ c7 c5 `; h( [9 I
LPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO, u- ]. ?) B1 W$ {- v% s
LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED$ ?: @: C% R7 {7 o! D3 _
DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH. O3 D1 \$ y( ?5 e0 y" U
DWORD cbAvailable; //IN,缓冲区中的可用字节数9 c5 [0 L V, t, h. Y- S6 ^
LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据
, M* I o0 _" ]+ XLPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE
: n' q5 Z# x4 r1 [4 W
0 B0 F9 {& l# z, K6 ?; b" X8 S8 }//回调函数,用于返回服务器的连接信息或特定的服务器详细情况5 a8 n* c7 G) u
BOOL ( WINAPI * GetServerVariable ) & c& f8 T1 Z' b, _' `
( HCONN hConn,
7 A4 b$ }8 c- ^1 {3 K! j- hLPSTR lpszVariableName,
' [ Q% \0 L; `! Z1 ]LPVOID lpvBuffer," [7 r" g9 J W/ q
LPDWORD lpdwSize );
( A3 n- j- P$ V b( a; g3 k% s& C: R( u4 {
BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据! M% U( N9 C1 M" i8 j0 V: q
( HCONN ConnID,
6 @( T" ]' v$ q# x& |8 K& L7 QLPVOID Buffer,
1 @7 c" e# u) _6 z5 pLPDWORD lpdwBytes,
) o* z2 |2 s) n" _! MDWORD dwReserved );2 N h& W. `" U" G) L9 f6 l; |
# }4 W e( ^2 B6 Z& p+ |! WBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据
- W$ E+ y4 |; J& w" ?( HCONN ConnID,
' B1 N$ N% E0 a% L$ L1 r' p9 y/ oLPVOID lpvBuffer,
' v% ?; r+ j/ e- ~LPDWORD lpdwSize );
# a: A6 o1 x/ }4 h. j
9 z' l8 N7 }7 R: |8 A- L, i9 p! G+ W- fBOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能
' m; i& }2 v1 F/ U& r( HCONN hConn,3 k# [4 h$ K) t9 {
DWORD dwHSERRequest,! P. L5 u- j8 x: X8 |# R
LPVOID lpvBuffer,
5 b1 l+ Q7 o) j# JLPDWORD lpdwSize," v& Y) M# M+ j" r
LPDWORD lpdwDataType );" `+ h/ k! m& Q6 H. B4 M' e
# ~. Q+ ~. E: G5 g1 w} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
/ ?. k7 s2 ^7 l) G+ A6 k1 x- g) s/ l6 t9 I% }, {4 q% _* G5 {
在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。7 f' ^- ^2 }0 M$ f5 a% U1 i+ I" F
- b7 v8 K4 A7 {6 S j
三、ISAPI Filter
% S1 p9 O2 Z* i7 b+ r
2 h& ?$ _: r' wISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。
. Y# G _2 w# h5 r' H
m! i9 ~6 m$ Q" d: YISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何" z. D. p% ^. F( h) N! @* f
ISAPI Filter都必须引出这两个函数以供服务器调用。' H2 i s4 {. m
* R R* s1 f% M1 l( ~
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得5 w! ?6 f7 i0 U4 q
Filter的文件名并加载它们。
* \# Q/ b! G' m& R: u9 c0 y& X+ j
8 j+ t6 I3 ~. Z$ yHKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL
9 |7 D! g7 h* v6 M: ]3 m; M# W5 {+ t1 |; z0 p0 g
2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:5 a( o6 S ?" m+ m5 G/ A
$ Y1 {5 E. o& u1 M4 ~) z
BOOL WINAPI GetFilterVersion(
$ j, ~8 c# Q* G5 V0 H5 S: kDWORD dwServerFilterVersion; //IN,服务器使用的版本规范
6 f" ]" b( ?+ T4 G2 H5 k1 lDWORD dwFilterVersion; //OUT,过滤器使用的版本规范' U- {# a, F' w6 H* s6 _' v5 `0 l
CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串9 f: G; O6 u6 l- r7 z
DWORD dwFlags //OUT,事件和优先级标志
0 Z7 o: Z/ i5 K( x4 \' [. R);% x# I% A2 l: j
9 J5 D; e8 R7 Q; E5 _6 u( c6 T+ k5 o事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
T E/ G E, K0 Z% G6 R" N+ R/ Q7 C" ~8 L) V/ k
3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。: M: C9 A( ~9 V. L3 ]$ T$ j
+ N: a \0 f& G! v
typedef struct _HTTP_FILTER_CONTEXT: N/ X9 y9 k6 j9 E ]" g4 k
{
" {5 f) {$ A; Z8 l: F `( M
& \5 G+ A5 E1 ] `2 v+ j% I8 KDWORD cbSize; //IN,本参数块的大小. u8 q9 j; X2 L5 `* O3 e0 W
DWORD Revision; //IN% s/ U' v6 w( N6 d" ^; p3 B7 @
PVOID ServerContext; //IN,由server使用本参数 Y" h# p y- L* L+ Q
DWORD ulReserved; //IN,由server使用本参数
7 I, O( q$ B7 J" L# WBOOL fIsSecurePort; //IN,事件是否发生在安全端口上3 O7 i% Q- N, V
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文( ~7 l' q- B0 B
+ H8 ~, n. z9 Y
//回调函数,取得关于服务器和本次连接的信息0 c8 V8 n; j8 C# b ^
BOOL (WINAPI * GetServerVariable) ( " W5 W. Z3 t h/ F+ f
struct _HTTP_FILTER_CONTEXT * pfc,
; Q4 p/ d/ l5 iLPSTR lpszVariableName,
) F- x; B, `* @. s3 ]; [LPVOID lpvBuffer,0 }/ g3 \$ h) n% \; X5 ?4 q
LPDWORD lpdwSize
( ^3 w1 b; t7 q2 t1 V0 b# z/ N); " _) V- s; v4 l, f, H
- o/ A( _; \: d2 @* M7 H( w" w
BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头- T# S& N+ u; Z, d1 w' h k5 X
struct _HTTP_FILTER_CONTEXT * pfc,
0 `4 k! e; M" w% v/ A( HLPSTR lpszHeaders,
5 F$ ^% V% Q" N; S l$ IDWORD dwReserved
; q* d) Q3 x: Z* O6 M- N6 });
2 C( z1 M& S- Y$ ^
0 T. \' o' L1 x7 R7 GBOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端, N9 M8 K: l8 n/ U5 \
struct _HTTP_FILTER_CONTEXT * pfc,' h: U0 N' {/ q4 Q7 K* p
LPVOID Buffer,/ G' O( \8 l2 c1 q! O
LPDWORD lpdwBytes,
3 N* _1 e7 d7 T T, ^8 r# WDWORD dwReserved
( {* L- F; L. k- G); ( ~. B" [$ P* \5 [2 Z( U
5 U$ o! {) B/ c, w% Z, M; Z
VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。
1 K! y; u$ `5 h: ?7 h4 Zstruct _HTTP_FILTER_CONTEXT * pfc,
1 g# J. G4 e; ?: i! `+ R# YDWORD cbSize,
" J3 A' I6 ~2 `; S/ X% vDWORD dwReserved2 `. J2 q% D+ T; b6 G9 p# i
); ) `! L" a. ^% u* \
1 k# Q* h& d& f* n+ u7 OBOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能9 C0 Z& k! F' `- W# |# R9 X- L
struct _HTTP_FILTER_CONTEXT * pfc, t4 G3 b; e% b+ b/ @
enum SF_REQ_TYPE sfReq,
: ?& S, Z& x5 F; [1 b' L, o! P0 lPVOID pData,
# r4 @8 N& Y. c5 _DWORD ul1,
$ s* V- b! B$ X. s+ X7 b' X9 {DWORD ul20 r+ x" X1 l. r5 S9 [7 c* k/ G
); $ K) G8 }$ ~5 q" S- W8 N) I
! t9 ` P6 ^7 w" b& K' {} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;
; n8 `/ G' k( E2 n3 A" o9 d) y) L
0 ^2 n L7 Z& Z7 w5 s四、VC++ 6.0中对ISAPI的支持
1 h( w* }& ?9 k( a' o, E$ T5 k9 \$ o8 y! B* J/ Y- a x) [( ~
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实例,每个/ [6 p5 f5 T- N, H) @5 s
CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。# ]8 p4 \! L6 b7 B7 V
8 C* g, V% m6 g' r: }2 H1 ~
一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。
6 T' O9 B4 w& O( T, G& h, K2 L" w
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为例,该例中有下面这样一个表单:
Q7 [# G- A; v/ \$ G3 o! M/ X, m, Z' a# V9 W- b H( ~' p
<form method=get action="pinball.dll?"># y) |/ C g' D+ `2 a: X
<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">
' A6 S2 x' Q9 t<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>
8 r$ t9 G$ g9 C: o, h2 l<input type="radio" name="Favorite" value="2"> Twilight Zone<br>! o, R( }- h9 o- q+ }
<input type="radio" name="Favorite" value="3"> The Addams Family<br>
! q3 w3 Z4 A% Z7 s# f6 Q<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>, i* _& m- E1 |) _: r7 B
<input type="radio" name="Favorite" value="0"> I don't see it here<br>; J% N* R& w# o- K0 Q
<br>
9 I) ?$ p! V( u# [ b<input type="submit" value="Show Me!">
% [5 Y. _/ [9 {</form>
' ?( q1 t: s* U, X$ M, L
2 l! b8 t+ ?: W6 ~7 H5 a当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端
9 A q* w0 b% i9 b% Z5 B G最终将得到如下的URL串:
" d% V8 i( x0 ], }! q; ]
" q& d+ S1 z8 [http://www.abc.com/pinball.dll?M ... GetImage&Favorite=1& h6 e, Z O/ \3 ` K+ S2 {
3 o8 c+ e7 O0 V! i8 N Q在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数
; {. x% \( d4 n3 ]# D将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite:
9 [8 u, I& t3 B5 @4 s2 j$ x* ?8 [! e# b3 z
void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);' K1 ^9 I% t; Z
4 ?6 V2 L5 t. J4 }" T% }( w# k而parse map需要按照下面的形式定义:( P3 d8 V6 G+ C( n7 M
9 W/ `' M M( j+ _, s. M& o
//CPinballExtension从CHttpServer派生而来
2 y" B9 x5 O4 d: j/ P3 @BEGIN_PARSE_MAP(CPinballExtension, CHttpServer)
) T+ T8 k, O* d" d# T" S+ h( U3 L2 I- e3 b! v( `9 X2 g
//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice1 D8 Q5 I3 ]4 V- e& i* w
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) 5 G# U8 a$ @* L
- N& a. p9 K% A/ q9 h; y- Z
//该参数在URL中的名字为Favorite- F0 q$ q: ^. _2 N5 q% e
ON_PARSE_COMMAND_PARAMS("Favorite") G9 R# a1 C3 f
# W/ u& O5 C5 A P% e
END_PARSE_MAP(CPinballExtension)
8 v- w) ]; r: B+ x, l5 a1 h7 U% ~9 Y$ Q. V" _. k- w, D/ I5 C
而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:# Y4 R* Q4 U' V& t3 s. g- Z& y
3 D: f- v( X o, ?OnPreprocHeaders
/ k8 H+ g3 C) _5 L1 X$ M3 k7 |OnAuthentication
2 a `$ [: z4 l6 EOnUrlMap- D o6 D1 T$ V0 u0 y. Z. J" u
OnSendRawData$ ~# } | |7 Z2 F7 P
OnReadRawData& q& {) \4 @, ?; s- Z/ b7 ]* L
OnLog4 l5 e$ C$ ~# F j
OnEndOfNetSession
* ?' u" B0 J1 y6 W# d9 K5 e( P' }' `2 s A; @/ c* e0 w" t' {
MSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。3 }- H" A# R. ~: ^3 ?" m$ Z
M" [; t, m* G: }, N3 |+ ^' R! d参考资料:# w M" m) n" ^
2 m; l3 q! \1 h& A, X# S) F+ X' o
1、MSDN+ y" V o5 x) _# l9 D
2、《精通CGI编程》,丁一强等,清华大学出版社 |
|