|
|
作者:netguy (mailto:netguy@nsfocus.com) L1 E1 R2 ^# r+ e; p+ a; v
. I4 P% ~# U% U% F
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。2 I4 b2 _ q/ v9 \! n& w2 g
! D4 k+ g2 X$ y7 q' a一、ISAPI接口和CGI接口的不同。
3 N P' ]) W, m+ f# @. y# k% j2 @) Y
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。! [$ F& c8 @% [% l1 g
" o% g; N9 O/ m! Z4 h u6 R) r" q
1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。6 \) ~% g+ X' g( C; o# M5 T
2 U& K: E% r' v7 g7 Q
2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。; J$ ^4 U( G8 T
0 J8 X8 f; k( c1 j, T, D+ {' d
ISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。7 j) j( u# p. N: R6 _* T
9 T2 c7 o" v! |3 `0 Q# W. A( y+ S; B* j6 E' c
二、ISA
0 I5 t y6 I f
1 u) L& |$ s! JISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):- V1 l3 [0 |: u1 u
http://www.abc.com/Scripts/function.dll?
# S0 N7 p# {0 `% ]
S7 B( I3 u% c) R4 IISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。& G. U/ Y7 i! @/ m
; ?& G/ t& ` A* g/ A
1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下: c1 w+ p" W/ O2 v" b7 ^/ N
' N' R, y) o% Q
BOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
0 u9 e9 S: T% K. b e7 h2 _' E6 Jtypedef struct _HSE_VERSION_INFO
# }7 n; r6 \# W$ X{# ? ^; w1 m: P/ `8 Z( p+ P) ^
DWORD dwExtensionVersion; //版本号$ p8 ]4 b% I' L& S$ D$ a. p& j( i
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串( m2 h# Q; w- E9 H+ X$ V
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
) r& {% H4 X! o ^! t
4 m# A* ^+ { d% @. }2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:
8 a. g2 W- N4 ]& X) L
0 \- Y6 r8 O- ]DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );
& e% V2 c- f A7 {5 o. N% _
4 G [6 O0 I8 W. H+ m, x, jECB的结构定义如下(IN表示入口参数,OUT表示出口参数):
1 b5 e0 m0 R5 G
" G, C! t9 i$ ~; V2 b( O5 M/ Q: }1 @typedef struct _EXTENSION_CONTROL_BLOCK
+ u- I2 @. _7 p{
% B3 P4 @7 L( kDWORD cbSize; //IN,本结构的大小,只读5 i2 v4 K8 w7 x# T% J- k4 _
DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号7 @! V( j, s2 f0 T9 _% Y" j/ E
HCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值& |" R" B5 O' k$ R/ N+ U
DWORD dwHttpStatusCode; //OUT,当前完成的事务状态
" C: M/ r' F4 w0 WCHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容
1 d; R2 o' D3 z( K. v" Q3 \LPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD
5 V. M7 O+ L: U( S1 [& `# d( @LPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING
& R& K+ |. Z0 |LPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO
' h5 P& x( H4 oLPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED
; U+ ]" N! w& N5 TDWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH
' j/ N1 x5 K: `. L; A9 w3 ]! vDWORD cbAvailable; //IN,缓冲区中的可用字节数" f% B+ Q6 F& P2 u
LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据
" m$ |' X: E- ?LPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE
$ D0 i q: v" }3 I% u) t ~1 M$ j' W1 R& W# ]) j
//回调函数,用于返回服务器的连接信息或特定的服务器详细情况
# ^6 R( ~$ b' k; h r5 V; | RBOOL ( WINAPI * GetServerVariable )
6 q t8 R2 C x$ u! j( w( HCONN hConn,
: i1 ^6 \# F1 X* c+ O/ l) XLPSTR lpszVariableName,
. g4 |+ W' _' _0 |LPVOID lpvBuffer,1 h8 R/ k: F* n, k2 _1 C
LPDWORD lpdwSize );2 y- ^$ K# ~% M# U- H+ L
2 t a- r' ^- g- TBOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
) }7 |* a2 C! |, }* R( HCONN ConnID,
0 h7 V: ], X3 TLPVOID Buffer,
C; q% C1 Y1 ~' G( ZLPDWORD lpdwBytes,
. W) G; ~: k. f$ r/ C& D9 ?DWORD dwReserved );
' m- V& V8 @7 j; u) u9 J; {5 t* U* }, i; n) z8 r! V# o
BOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据. B$ \# Z/ J: n0 [& x
( HCONN ConnID,- G* D; K$ J: m
LPVOID lpvBuffer,: v* N9 S" q$ m* c! K0 Y5 w
LPDWORD lpdwSize );
' S- B; R+ K( d. {" ~ \9 U# U/ T- m6 S$ b& q; |: B6 c& y
BOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能0 ~$ U) h6 q1 Y) W( b8 U0 K
( HCONN hConn,
4 z9 f( E8 R9 z; X/ CDWORD dwHSERRequest,
. d( n' t; i! k+ T' i1 aLPVOID lpvBuffer,
; f: ?% C D$ G5 B- r( B5 I' sLPDWORD lpdwSize,& |- {/ i. Z4 |! C) W4 j" ]$ t7 E: e
LPDWORD lpdwDataType );. a; P0 Y/ Z% {# q4 ?8 ]
' ]) @. h! s: Y5 p5 e1 L, ?} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
. k U2 j- l) i1 v( r( V ?: V, g. ]9 s' \' P
在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。* Y+ I0 ]- [, C* \5 _* m4 T8 j) `! Z
6 f, r6 L' s( Z4 N/ ?三、ISAPI Filter
5 k- K5 V& c" u$ a! [4 J+ }2 M4 B" x* l' |& S9 S9 w/ s
ISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。6 h+ y. g' A1 `
- B3 p3 l7 K9 Q# V+ t9 _3 p* H
ISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何
- H8 K( y0 L4 KISAPI Filter都必须引出这两个函数以供服务器调用。' ^/ \" O! J1 C$ S1 c
* ~, N6 r' I+ K1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得
4 s9 G- L5 e' S* m8 J S# HFilter的文件名并加载它们。
. T% K+ ]2 f" M; W/ U. f. e7 {0 a8 E
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL
) ^# @; H x. W: ^! j; O# @* ?! Q
+ M2 F8 N6 E( C6 W" O2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:5 @: B! t, w& M; @
7 g9 U% G3 }5 hBOOL WINAPI GetFilterVersion( , I& r' U, ^7 p2 h8 y
DWORD dwServerFilterVersion; //IN,服务器使用的版本规范
. k- V5 R( F; ?$ DDWORD dwFilterVersion; //OUT,过滤器使用的版本规范' B0 u1 G2 C% _! ^% Q
CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串
( z4 Q) \ P6 u) Q N( zDWORD dwFlags //OUT,事件和优先级标志4 K9 U% x, W& C: e3 L$ l& z
);. x5 n: I2 y: h; d" `& ]
, v, R! X6 w5 J' j% Q2 t! J
事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
9 C* u3 }$ q( }4 a, b* k; o; b0 H( U2 h! H
3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。6 Y& a" p& v0 u: V- L
3 G4 n. T7 R D9 i, M
typedef struct _HTTP_FILTER_CONTEXT( D' U5 E o- U
{
7 q; ]& d) b' t) X( M) X- @4 t+ R5 h, z! B
DWORD cbSize; //IN,本参数块的大小
; Z; v4 ]# V# W/ v4 M0 w- r uDWORD Revision; //IN
$ M. I, c" }7 J0 D& o8 LPVOID ServerContext; //IN,由server使用本参数
* K# Y- F# J; W! i, u( K% tDWORD ulReserved; //IN,由server使用本参数2 V R( w2 A* S' x
BOOL fIsSecurePort; //IN,事件是否发生在安全端口上! w' d6 L j, D& A
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文
w2 b% |9 w+ p$ S7 Z% m. s
/ {- A! k/ E6 q, s" x0 l2 ~//回调函数,取得关于服务器和本次连接的信息
4 M3 S, m. K5 s9 UBOOL (WINAPI * GetServerVariable) (
' {& \3 ~5 d- d( Z+ R4 K) Cstruct _HTTP_FILTER_CONTEXT * pfc,
6 K% Z+ k S5 O. O; aLPSTR lpszVariableName,
$ q: J; \7 C/ _1 |; M. S; TLPVOID lpvBuffer,, x; \. A+ o9 Q% w
LPDWORD lpdwSize! q) @: V9 B% b6 E
);
7 ~, l5 q- e) l
* z* ^9 L4 ~1 q6 cBOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头
2 S) p; j' K9 g& J! u( ~3 `struct _HTTP_FILTER_CONTEXT * pfc,
, p6 w8 |+ m+ {. a& l/ n0 |- [LPSTR lpszHeaders,/ y5 B& s8 y- x3 i: r% P
DWORD dwReserved8 r6 O* E+ r, _& A3 y
); $ _0 Q' S! u8 ]( ^' \5 B
6 j; a1 Q" R6 S: j u, IBOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端2 c# k, j4 w" V
struct _HTTP_FILTER_CONTEXT * pfc,6 l, K5 k w+ |* y; Q- O) ^/ G+ c
LPVOID Buffer,) l& h; }; G; q0 w
LPDWORD lpdwBytes," T K3 L# N! }" P$ N
DWORD dwReserved8 d+ F! c9 R! r
);
/ g) m D, C+ Q" n) Y! x* Z7 w
1 y& W# C8 p' a- ^% m3 FVOID * (WINAPI * AllocMem) ( //回调函数,分配内存。
( W+ T0 B: H* F8 v+ k' pstruct _HTTP_FILTER_CONTEXT * pfc,, g6 r5 T( T% @: R& G( r7 |; `4 i
DWORD cbSize,% l( t' }% K- a* G
DWORD dwReserved- G* @" x" I* c L5 j6 [, m9 [
); : l9 A3 G% J1 h7 @+ F! ~* v
( h. X. u* V, ] e( H" g6 o* Q
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
- s% e1 M! m# ]) C5 Ustruct _HTTP_FILTER_CONTEXT * pfc,
8 B6 p$ q9 Q: Yenum SF_REQ_TYPE sfReq,- y- x& A9 \, U& C* r
PVOID pData,! C! ^5 d! ^5 ^* j$ @/ B& [8 b+ h
DWORD ul1,
" z: _- V, u1 k9 A/ [8 GDWORD ul2
- {' L- X% r! L& S7 L1 c L);
2 _ E, c* P( o+ _. z: P! ~) L
( o# R( p2 M. A* B/ ^} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;+ b/ U9 ~2 J) [: W0 ]: _0 t$ X
: z" s7 s2 k& z/ ?四、VC++ 6.0中对ISAPI的支持8 [, v+ L9 |$ L9 I/ K3 B
0 I! B- T( e; g% K6 J6 _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实例,每个
c* T7 B# A+ m" X% N1 VCHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。
* @. K0 @. X0 y: v8 N: @5 f0 y! D0 c0 u3 X7 ]
一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。: E) Q, }7 d/ S- Y1 _2 M5 b2 _
7 W1 B* U$ R$ n- m8 oParse 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为例,该例中有下面这样一个表单:8 d* Y5 s# m7 G
+ V i0 v0 @; F8 T1 E+ o+ d0 x2 j<form method=get action="pinball.dll?">
0 R. N! z5 q/ c% v! `<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">
- { g7 ~/ l2 I7 o$ D<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>9 R! n4 h7 z* M1 \
<input type="radio" name="Favorite" value="2"> Twilight Zone<br>+ ?/ M6 @" U) I8 U
<input type="radio" name="Favorite" value="3"> The Addams Family<br>
" | U, B2 j; o$ J8 ~<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>
, D$ Y" Y. c2 Q1 p% P3 g0 r0 |<input type="radio" name="Favorite" value="0"> I don't see it here<br>
: s* u4 r4 V4 s6 Y5 R/ s4 F<br>
3 |0 F% U; i1 B4 _+ @! Q I) c<input type="submit" value="Show Me!">
' K; F* X1 L# m$ U b, B0 L</form> w" `5 X* B0 G) C+ q$ W' ? Y6 P: P
4 [2 e% G; N* o& }0 S0 d4 p7 O( S3 i当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端
* W. r2 J; k& i: G/ D8 s最终将得到如下的URL串:8 c- E A; s- Z2 m! X
5 @& P4 P+ ?3 `% K' u9 ~6 p" k; F5 Q
http://www.abc.com/pinball.dll?M ... GetImage&Favorite=1" S. O: q$ N9 t/ s
. H7 H* K4 h" t. K在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数( L% j4 s# W; C) O i
将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite:
7 c0 V7 b. V0 C( V4 N8 h3 T6 u0 ^6 Q% H) p0 `, ?
void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);; t: M2 M% h. f- w6 a
. J1 Q! G2 ~% I6 a, N; b而parse map需要按照下面的形式定义:
- l7 G/ `) C; w( v0 F
: S/ z, |' T g0 |! b//CPinballExtension从CHttpServer派生而来4 V0 ]( W5 U. V6 w2 a% r3 ~0 u4 p( N
BEGIN_PARSE_MAP(CPinballExtension, CHttpServer)
$ K; `; h" o% q. j. u% H
+ B% o8 R6 L- A1 `1 Z* n' ?//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice
3 O: Q9 P; c- m# CON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) 7 p) ~. i# j( K
, f3 ~8 H$ ^/ v# b4 l [+ J
//该参数在URL中的名字为Favorite% ?+ m# f0 J: O# ]/ t
ON_PARSE_COMMAND_PARAMS("Favorite")
/ q2 ?$ b3 `/ ^! J1 u9 `* n1 r! x) z) K9 j" A6 @5 e+ v
END_PARSE_MAP(CPinballExtension)2 e5 p z" Z L8 o
6 \( p w4 m% o0 b7 {
而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:
/ d6 m( x# s3 q d) V/ `9 c' D. J
( j% ^- H5 w) DOnPreprocHeaders6 N O4 C$ I' ~* S2 D Z7 z
OnAuthentication
# u: h N8 J/ Q9 KOnUrlMap
3 l6 F& L h, G% [# @. ^1 YOnSendRawData0 Z& c/ R7 K$ u% I( h, A
OnReadRawData
9 c( |- z2 k8 w( `, t& UOnLog# \' P$ E( |. @
OnEndOfNetSession# |) o! a+ R4 P' E
. C! }8 I( l4 q* @. l/ ~MSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。
' |2 H/ Z3 q: u5 m. f6 q/ t* [" d
/ U2 |: h* H7 g/ [参考资料:
, u) m5 E/ p- R6 \: f* l8 @4 Y, ^2 `- c: m
1、MSDN
- x4 A9 b7 u. z+ x! j2、《精通CGI编程》,丁一强等,清华大学出版社 |
|