|
|
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。 0 B* u+ i6 T; j/ n
+ H6 u7 |( X$ A9 N3 y( O6 t
一、ISAPI接口和CGI接口的不同。 : c1 S; ~ l9 u2 z/ @* c7 W b" {
. l' [# L+ ~& R+ p2 D4 Z
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。
" b+ u7 H0 i' h D3 `3 k
& F; ?; m) c% e D2 {; r9 A1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。
3 B* @* o6 U) Y0 d
1 m; J2 W& H0 o2 t5 S2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。
* T" S" ?. f* }2 ]1 j
6 M7 g! { @4 o: U( X5 y# wISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。
) W* I0 T6 ]! M* l9 h9 ]" Y9 m5 _
9 k9 N& _, J: b8 W L
+ N3 y& a6 ?9 _3 }% J二、ISA + j/ N& ^9 f- @. N/ u7 n
7 N8 d5 y6 m. m" n2 r8 g1 T$ e3 ?ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):
, U, Z& j7 c+ H. |# Ihttp://www.abc.com/Scripts/function.dll?
- h; N# M5 Q1 t/ ^
& |2 V9 F& |8 J# q" }8 G4 j7 bISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。 $ I& K6 `4 w+ V f
1 C4 l- Y' X5 s+ X6 V
1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下: $ b7 c5 u* v+ L; |
( s# E' z& ]. w' Y" ]) u6 F
BOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
" \; E1 C* z% Q0 O7 f$ E5 Jtypedef struct _HSE_VERSION_INFO & v, @3 U. U) t9 a4 L
{
U+ _ @: |, B3 a2 PDWORD dwExtensionVersion; //版本号 : e' v- `' |% O- o9 r' O
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串
# X s4 K4 G+ C0 [} HSE_VERSION_INFO, *LPHSE_VERSION_INFO; # d* P" w$ G2 s4 s, S
& Q7 W3 z, P! T5 J+ Z. j2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下: ) c0 h) O/ l p) g5 x# h
2 } j" v$ x+ k( Q
DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );
9 A$ w8 `: H0 k0 {9 {$ U2 {3 l4 h" o; D1 D% ]! k
ECB的结构定义如下(IN表示入口参数,OUT表示出口参数): 1 E0 ?- }7 C" t3 F# w" w
4 {7 a |( G. z N$ A+ ^
typedef struct _EXTENSION_CONTROL_BLOCK , P1 q4 [" w9 ^7 n" b
{ * @" T; p3 S) X) q
DWORD cbSize; //IN,本结构的大小,只读
: e4 @8 A; \" W) ^' PDWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号
- ^. B) i% G2 {; N4 cHCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值 0 o1 S9 k( H! g3 i0 W
DWORD dwHttpStatusCode; //OUT,当前完成的事务状态 ' H5 J' k* e- N3 U9 n/ J! E: B1 `
CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容
/ `& n3 S& ~ ?8 G/ hLPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD " G. e1 c& G2 D" n+ g
LPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING
& s/ G2 z3 w9 G% s# D% RLPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO
; f9 C$ M! W! t3 }LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED
! d! B! x7 z- PDWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH
: B* F0 t3 ]% C' r3 g4 GDWORD cbAvailable; //IN,缓冲区中的可用字节数
% x' n/ Y4 t3 b' I$ iLPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据 0 u) ]! p* L* ~! ~9 T; n
LPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE ' q9 g. H. ^( K) ~# b2 {
?1 X O0 B0 i//回调函数,用于返回服务器的连接信息或特定的服务器详细情况 * Q" }( ^! g# c
BOOL ( WINAPI * GetServerVariable ) ) Y. v/ o1 T) p/ ?
( HCONN hConn,
; R; a8 H% |/ B- J) q& d+ TLPSTR lpszVariableName,
3 |/ P, {, W w$ VLPVOID lpvBuffer,
9 |# R3 i* @1 @LPDWORD lpdwSize );
3 ]" B' h: X4 }6 Q$ h. v
2 t/ F' {7 y: w8 X& B' c; eBOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据 & d7 ]8 A1 R# t; _6 m2 W' ^. |
( HCONN ConnID,
, {6 Q9 W- L" t7 ~* FLPVOID Buffer,
2 t* v' J, x% yLPDWORD lpdwBytes, 8 C# a- i! K+ W7 ]1 E; h
DWORD dwReserved );
" f8 o2 Y% U+ q1 @5 A- J/ g9 {8 `) V3 n$ u- o! D; v, z, \
BOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据 * C/ }/ U1 I/ X/ U
( HCONN ConnID, 8 Z6 P! P% G) u. {% d$ s
LPVOID lpvBuffer, 5 t' F( I! T9 }; N2 d% b B
LPDWORD lpdwSize );
( T5 ?& [" l- _) \
" H- q% m/ O- l# I- H: OBOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能 " r$ O9 [: ^; |! c6 }
( HCONN hConn,
6 `6 M, K& I. m% z0 KDWORD dwHSERRequest, 6 W0 w/ i- s6 ]* Z
LPVOID lpvBuffer, ' h% m B' U# `( _7 c
LPDWORD lpdwSize, " L% F$ \$ S; N& M) r. |7 t" K7 x
LPDWORD lpdwDataType );
% e0 N, S8 b4 [7 D) F9 T
# M$ C1 O4 a5 [} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK; / e. ^; ~ N6 i' \. B7 i+ H
# c) }0 I+ L) M6 `6 x8 n Q- q
在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。
; A0 [- S+ L8 Q$ m# e" ~2 I
/ \4 f z/ G( C. `, s: f) ]三、ISAPI Filter . k& p! p1 a% `4 k
! ?, J% Q& J9 DISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。
4 r5 K' S6 w* {6 H" j* w- J: M8 Y3 D
' d8 @1 l0 P( \4 @/ u2 P3 d6 D nISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何
8 D4 o6 w$ R" V' `/ W) A( {ISAPI Filter都必须引出这两个函数以供服务器调用。 . N2 j* ~* u* b
& S3 ]5 p! i$ T V( i1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得
; M4 }9 ^" M5 z. B/ @2 e Q+ |- h( [Filter的文件名并加载它们。
& B# _; v# i3 c# I+ t! k) R" I8 f, G5 b
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL
6 [ ` Z& T+ z9 H7 k+ S
/ o. A7 z/ k( B" |; V. s. S2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:
( g1 V8 C: [8 W1 \# u
3 ]. q( F9 A& q9 S; DBOOL WINAPI GetFilterVersion( ) V1 Z8 |4 ~* K" ~3 ]- g' Y
DWORD dwServerFilterVersion; //IN,服务器使用的版本规范 2 i" S0 ?& e+ y
DWORD dwFilterVersion; //OUT,过滤器使用的版本规范
, S$ L& d; Y- y7 t9 {3 }CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串
/ r# U ~9 I6 yDWORD dwFlags //OUT,事件和优先级标志 . ]. \: l5 u/ \' Z9 F& ?( F
); , D* u- x s# y$ f5 M9 g/ d% P
* h, G, _) P4 ~- i M: m
事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。 ' K* Z v9 I) L% F. g5 e. C2 w
/ ?2 B5 c( p. J2 I r3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。 - _) H$ o$ A* I* d' Y; {! O6 ?" M
1 x7 ?! S: T# O9 M6 V1 ^; Ntypedef struct _HTTP_FILTER_CONTEXT 3 O" y1 f; }9 P- A, O0 x
{ / S5 u6 \7 k' R" z6 S
; y0 M2 O! h2 j( s" J
DWORD cbSize; //IN,本参数块的大小
7 k0 s/ {& a. B# J& CDWORD Revision; //IN 8 K8 v8 C; ^ b, A2 _: c+ v6 ]
PVOID ServerContext; //IN,由server使用本参数 / I( |' c. v' z5 b* W5 {0 i$ Q! X
DWORD ulReserved; //IN,由server使用本参数
5 w. b! w" Y% V- _BOOL fIsSecurePort; //IN,事件是否发生在安全端口上 . v' q9 _5 ~$ l2 b6 Q+ A
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文 6 Y; T4 H4 j5 n9 R2 q
n: T* c3 x3 l- H//回调函数,取得关于服务器和本次连接的信息 5 @4 M1 m3 r+ F- s3 }' r
BOOL (WINAPI * GetServerVariable) (
q' Z5 g- [1 o& I8 Ostruct _HTTP_FILTER_CONTEXT * pfc, * r* H% { v5 E$ d% M, K
LPSTR lpszVariableName, / S5 p% r/ [8 T, g( V: g
LPVOID lpvBuffer, " y, a$ N3 I4 k, C1 d
LPDWORD lpdwSize 8 G6 K! f4 l- t7 u7 z4 Z
);
+ P( I, b8 N1 `- A& \, g/ g! O8 {; ]
BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头
: c4 F R$ q l; M! L( bstruct _HTTP_FILTER_CONTEXT * pfc, o2 e6 m0 H5 k5 T8 M6 i" l( b
LPSTR lpszHeaders, 7 w9 G+ p2 J3 y. o
DWORD dwReserved
2 L# T0 L7 o. u+ I# V- c8 X" d); - F- O8 j3 C. ?8 N" S
; p, {% u' \* j- c/ DBOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端 , O! C3 |( r; j8 G/ u
struct _HTTP_FILTER_CONTEXT * pfc, ( h/ N% M; X: h. B |% u
LPVOID Buffer,
1 P& U6 P8 Z- o4 ~. c" ~LPDWORD lpdwBytes,
. c& N' q4 v0 x5 f9 yDWORD dwReserved
' U0 m7 n% m& r% [);
5 c; ^9 _0 C6 h% Y" [! I* W
: ]( w$ C6 Y, m6 d" fVOID * (WINAPI * AllocMem) ( //回调函数,分配内存。 ' v& n }/ h" z; U9 N. g
struct _HTTP_FILTER_CONTEXT * pfc, * p: W2 T" A$ z+ N& h9 T
DWORD cbSize, ! n+ x) T Y' D2 u
DWORD dwReserved 7 m8 v; t6 z8 i" s5 o& a* a' ~
);
8 N/ V, Z+ _ K1 V; N+ l* b2 `8 e' F# y( l4 K
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
: @% k4 }$ ^: J. Z3 n/ z$ M, ?6 c$ C( fstruct _HTTP_FILTER_CONTEXT * pfc,
" ^/ d7 d' I3 q$ }/ |enum SF_REQ_TYPE sfReq, % w, g3 W: C# `2 y( X8 c
PVOID pData, & g L) i, z! _* I3 f# m( u$ H: k
DWORD ul1,
) f: }5 G* `5 y- T6 M/ n; MDWORD ul2 4 K: Z$ R; u1 |. W( H3 k. V9 O
); 9 {4 m. E. _; u, o7 w! z* V3 \% w0 |
1 ]/ q# i% ?6 z6 H4 r
} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT; % F- g! @, ]& \* f& k
' B7 D, B# ^3 {. p& R% `
四、VC++ 6.0中对ISAPI的支持
2 {5 K4 {: F) B$ [
& i: [) g3 `7 g' aVC++ 6.0中定义了5个相关的类以简化ISAPI的编程工作:CHttpServer、CHttpServerContext、CHttpFilter、CHttpFilterContext、CHtmlStream,这5个类都没有父类。其中CHttpServer和CHttpServerContext主要用来编写ISA,CHttpFilter和CHttpFilterContext则用来编写ISAPI Filter,而CHtmlStream则用来操作内存中的HTML文件,为其它的4个类提供服务。CHttpServer在每个ISA中只能有一个实例,一个CHttpServer可以对应多个CHttpServerContext实例,每个
3 f- I9 [2 M+ @1 vCHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。 8 {. m8 B; a3 ]5 f+ L
3 x9 }- a7 v5 W, S, l一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。 ) [5 c" J0 |# }2 s: o) P" B& T) Y
5 P2 i: \, w9 ]9 k1 V6 R5 s! |
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为例,该例中有下面这样一个表单:
) ?2 _0 g1 k% ~. M& b: ^% |- f) L6 E# m: u6 y9 L7 x* C$ i5 w# Q
<form method=get action="pinball.dll?">
& c9 w- e5 c* E# |$ w1 D0 p<input type="hidden" name="MfcISAPICommand" VALUE="GetImage"> . _% m R! {! n5 g6 z3 I
<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br> 3 d) o/ p& s1 [% F( J4 |3 |# k
<input type="radio" name="Favorite" value="2"> Twilight Zone<br>
. k& f. o7 T& x$ g4 G<input type="radio" name="Favorite" value="3"> The Addams Family<br>
2 n1 M$ i5 J; @+ ]<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br> + v" j. c U! e
<input type="radio" name="Favorite" value="0"> I don't see it here<br>
: [) }" O, @0 n9 i7 L0 [9 A<br> ) t l" C5 f! Z% R. H$ N$ z
<input type="submit" value="Show Me!"> ' b& x5 }1 N6 E% v. Y! h) a I0 \
</form>
4 s- u$ F6 [- t( S% D' v- l1 }+ d% @/ G- j
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端
, Z& L1 [1 |4 Z1 E, ?最终将得到如下的URL串: % g7 Q$ w, K: U" d. }" Q
2 G1 P3 c& d- khttp://www.abc.com/pinball.dll?M ... mage&Favorite=1 ! A6 E, D' Z+ ~0 m. v
, Q, X- c+ G2 U; M. ?
在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数 + P# u3 H) v. G* j \. S
将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite: # L9 L; z( m% ?6 ]; g1 S
l9 M5 h" S- {- {# S. v6 Mvoid CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice); 4 v+ Z3 a6 \2 P9 g
, L; ~0 Y: X. i% _而parse map需要按照下面的形式定义:
. C; K( J% R$ Z! B2 C1 W7 w9 ]
) {+ t' {+ T: S% `3 _//CPinballExtension从CHttpServer派生而来 2 H8 T: X% l7 H7 S, S& L
BEGIN_PARSE_MAP(CPinballExtension, CHttpServer) 7 [' `) P5 d/ f! h, N
5 i; I0 c# R {//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice + ^+ G+ M! o% n" o8 u' n) C
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) , s; D/ I$ \7 H5 x E
) e% g/ \/ n! V* W& w) g4 T& c) ]
//该参数在URL中的名字为Favorite
* [% m4 _7 D6 J* {! ?' J& ~ ~' }ON_PARSE_COMMAND_PARAMS("Favorite")
6 q7 b) ?" ?& [4 k1 [ H& d
0 E6 H2 ]9 Y0 w o4 rEND_PARSE_MAP(CPinballExtension) ( f7 l" A; F! F& ?( O- i
! D- W8 l$ X( {2 |, X; j7 b而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件: ! j. y9 {- S5 p0 y+ ?
7 [5 q3 [$ c/ yOnPreprocHeaders % f9 R1 \5 t7 \: E, k. W7 L: i7 M
OnAuthentication
% R( V6 V6 G9 r+ yOnUrlMap 3 \9 |* ]4 B& K: W+ o! z
OnSendRawData
3 B8 o I# L* \: |& mOnReadRawData , k1 f3 i" v! m$ W
OnLog 8 A9 U% ]3 i, n6 L/ [
OnEndOfNetSession 0 i' z! ?* Q+ n5 u9 h4 x
% j Y7 x# S# ]) k' DMSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。
3 x; e% [7 ^/ m- J, a. a
8 g( `# M; Y% }! B3 H参考资料: # {2 T/ L" E, v8 i( u
6 L& G3 u) P% E1、MSDN
5 U5 p6 M0 h/ `: u) e, Y2、《精通CGI编程》,丁一强等,清华大学出版社 |
|