|
|
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。 1 g" x" K9 Z, E5 `" V% b2 l- [
2 O! Z/ j" v' g* v% h* Q+ |/ G, n
一、ISAPI接口和CGI接口的不同。 K% C! `% h1 D8 M+ g
# ]7 r: u1 Y7 y1 j0 EISAPI程序和CGI程序完成类似的功能,但是实现方法不同。 \. d0 d, h6 \* q0 Q
& D& Z! ]' l3 ~ |1 m6 m
1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。 " C9 }# |' i; T5 Y
1 Q3 C- ?& M$ P- E$ G
2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。 A$ _) G: W6 v0 d! j" }
. n1 a, Y# B* I+ _6 l
ISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。 8 P n0 \4 [6 ^+ U9 z+ Y
* w: E$ b$ `: [* x0 \' ?/ p9 e
$ e* }' t( n0 v4 {* n8 Z1 a' D! {
二、ISA
6 W, L" V2 j8 A6 U1 v4 _5 i1 T' {" ~/ {; N' @: X1 r! ]
ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下): 1 u# r3 Q, I4 k* f: k6 A
http://www.abc.com/Scripts/function.dll? : G- j6 z* n+ E q+ u. p
: d2 U. S1 v2 R- ]
ISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。 4 X) E, `5 E G5 n( [
, L7 X' d" c0 J, R1 c1 Q
1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:
7 {1 z$ P7 D: Y% |
2 k2 H* ^7 B8 W* qBOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
1 B+ E2 Y# V# Ntypedef struct _HSE_VERSION_INFO 6 a$ {) |9 x* A
{
' F7 Z3 R1 Z9 x5 b- G sDWORD dwExtensionVersion; //版本号
# ^, n# E, F% t2 t: A& W( ?CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串
$ S/ `! h8 y" M6 c: y} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
9 G9 i9 Y3 l# M
' q+ M$ H8 \2 r2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下: 1 I+ }8 }; a9 O4 z/ V1 c+ F
$ F% K+ i' ?9 h9 R! mDWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB ); - s( j& w& ^ Z4 e+ ?: I% m, i
+ j2 L5 v$ u( k( H$ s' j) V4 r' D9 ?
ECB的结构定义如下(IN表示入口参数,OUT表示出口参数): / x4 a( \8 T- ]; L6 h
+ k3 |, y: {* x6 I2 ~typedef struct _EXTENSION_CONTROL_BLOCK
3 h( [& e. O& z6 Y, ~7 D$ G{
' x6 S8 d0 E, `- ^% iDWORD cbSize; //IN,本结构的大小,只读 $ I6 v8 v0 E( z% c5 M
DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号
0 T" L4 [9 P$ D( z$ O& b. BHCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值 3 ?+ e# a, h9 V
DWORD dwHttpStatusCode; //OUT,当前完成的事务状态
$ O* u$ y! b" H7 b+ s3 j. fCHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容
4 m8 M% }9 L* G$ T# rLPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD
9 @( l7 z& a6 ~+ V# r: o9 ?* QLPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING
$ r- C+ {) S i4 M7 BLPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO : W7 D9 {0 E+ e; \) C3 b! p1 n& |
LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED ( n! _8 i) ]( ]/ U3 w: I' B
DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH ; g1 v" [0 `# h4 Q3 e. E; Q, G' ?
DWORD cbAvailable; //IN,缓冲区中的可用字节数
( H& v! q, [6 A) w# _LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据 3 z" J& D8 P9 h' S0 ~) X
LPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE ; I8 S! q9 {: \1 D' G# h
/ ^7 j/ b* T! r& | ? U" D//回调函数,用于返回服务器的连接信息或特定的服务器详细情况
; i. d. ?+ Q! l/ [: y, R$ GBOOL ( WINAPI * GetServerVariable )
7 p/ w* Y# W0 {/ q( HCONN hConn, 1 J w) V9 x& }/ n
LPSTR lpszVariableName,
; Q" R% F5 l4 u0 S) \0 ULPVOID lpvBuffer,
' u$ z8 M) S Y1 T3 O" qLPDWORD lpdwSize );
6 t, l$ V2 n8 A( O6 O* b+ d: |; _( N: N# x
BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
3 }+ Q2 e$ b4 o, `9 E8 b9 @! h( HCONN ConnID,
1 O, @+ [5 O4 QLPVOID Buffer,
( Y+ ^0 t. L, c5 S b4 h" n+ R3 iLPDWORD lpdwBytes,
' E" \, J9 _# ~DWORD dwReserved );
& |* F3 Q* w; k i
$ C; Q- M/ |0 N8 C$ f2 `2 }( _7 HBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据 # m* j( q7 _1 P+ v7 ?. J4 {5 v. I) F
( HCONN ConnID,
, p |! f, u: z& I5 C* jLPVOID lpvBuffer, 6 q% ~' Q5 u1 N, |7 I
LPDWORD lpdwSize );
6 O' M% _4 }) H) e2 M) r0 Y9 @+ I" c2 @% O
BOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能 # P' K7 ?: D$ q0 S: D( S
( HCONN hConn, 4 e# ?+ Y$ t' @' Q' W" ^
DWORD dwHSERRequest, : ^0 f. d6 x$ D2 R# A, }; v2 V
LPVOID lpvBuffer, 3 ^, B6 O/ {7 U' y2 O) }+ v
LPDWORD lpdwSize,
/ i0 Q& C- n4 G3 g1 J- ELPDWORD lpdwDataType ); ; m( Z# \, P6 P$ k& `. H, B. g
" M+ j& N: {/ u- L
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
: o2 k; G. J6 K, b2 \5 `+ U' c4 l3 y7 p: R, Z
在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。
3 R7 M$ g) b; _& r1 p' N5 \) @
* N9 C" M; w0 Z; c/ w8 |& S w' k' D三、ISAPI Filter
2 O4 r1 e. ?/ r9 K& w
1 d; `* | m6 i! t, Q! l; J6 aISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。 - r! E! s) r# l; X D1 M" l
: R" C& I) S4 s8 a3 GISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何 % o% [5 m; t6 L
ISAPI Filter都必须引出这两个函数以供服务器调用。
! O; M, H1 S3 H7 ~+ C( v7 |$ n( }1 f4 A
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得 9 n6 p5 c5 v( {# f5 z. B
Filter的文件名并加载它们。
# {) z+ ]4 t7 c' G' j6 n4 x' V; c. k" Y% \# B
HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL 0 B9 D$ t! @* ^
, c$ F" o; D, u2 @
2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:
; E' a' f5 R# N( X7 x, L; n) j( N4 U7 `" U1 D; }. o6 h
BOOL WINAPI GetFilterVersion(
' B. H( X; ]2 ?9 G) p( n' xDWORD dwServerFilterVersion; //IN,服务器使用的版本规范
- B9 d/ i# L8 eDWORD dwFilterVersion; //OUT,过滤器使用的版本规范
" M8 ^' X* [8 ]CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串
1 F% x7 y, _6 J! [2 W3 O, a" y9 sDWORD dwFlags //OUT,事件和优先级标志
3 p2 T! t( ?! Y- J e. N; |) h); 7 R2 Y1 L) {' I% N9 G/ I
7 K, v4 C- e5 H, S/ {. L
事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
2 I, b, n6 J, ^4 `; [
0 z2 \8 `1 P+ D ^3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。
* x" j! h# @* n2 l7 Z, v+ i9 t1 ?: E3 p! K
typedef struct _HTTP_FILTER_CONTEXT 6 a7 r- S2 `7 V* z% K
{ , _- H( |/ e, f0 J" X/ G2 C
4 J4 a1 x1 e; x- D2 D& l- W; Q0 WDWORD cbSize; //IN,本参数块的大小
9 `6 Q8 i+ A d `5 _$ ]DWORD Revision; //IN
. @6 r$ P7 ?4 l, e2 APVOID ServerContext; //IN,由server使用本参数
' @( G4 e3 ?& t' f7 G, d( e5 eDWORD ulReserved; //IN,由server使用本参数
6 L2 S& ]3 K. q* S* UBOOL fIsSecurePort; //IN,事件是否发生在安全端口上 , T! r8 k5 d( i3 ^+ ~$ f
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文 2 z' I5 s$ b- h1 @/ o/ v, X
" T, [2 I6 L2 c1 l x; d//回调函数,取得关于服务器和本次连接的信息
: q, x+ C4 S* L; F; L" A7 YBOOL (WINAPI * GetServerVariable) (
# p+ I" I8 G9 r" H" X; `struct _HTTP_FILTER_CONTEXT * pfc,
|0 Y0 y/ O% Z4 R$ R* g- l( GLPSTR lpszVariableName,
! ]' y5 Z4 Z$ [2 y9 ~- ILPVOID lpvBuffer, w) y( z; S( I
LPDWORD lpdwSize
' H- L8 ~( {+ Z$ C); g- ^% I5 s/ u0 b1 q
9 R" X' f( |1 m; K6 X" W
BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头
5 o2 ]/ C7 T, H* J9 Ustruct _HTTP_FILTER_CONTEXT * pfc,
1 {3 t+ b6 @9 w* T0 F" T- l* eLPSTR lpszHeaders,
* i9 a9 k( T2 t) nDWORD dwReserved
! j4 s p- W$ N- D A);
* N: j7 E2 @. T- \- k* M- @! w$ _4 \! p) O- t7 g
BOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端 ; k6 p! A! ]+ l' r( u+ G q
struct _HTTP_FILTER_CONTEXT * pfc,
, U ^( \/ ~6 @LPVOID Buffer,
. q! k% v8 Z9 b# N" x# eLPDWORD lpdwBytes, 9 I% y% U% ]( N5 [
DWORD dwReserved + O5 [" {6 E0 C! U. z
); 3 f2 {9 \, j% n7 Y
& x, h, |1 u3 t$ y/ Q4 l
VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。
% g5 o4 o8 c7 t" Jstruct _HTTP_FILTER_CONTEXT * pfc,
6 S+ l3 t& P, ]8 A; d/ ~DWORD cbSize,
& M9 w2 j. @, \5 X g9 R3 a* l8 }DWORD dwReserved
/ H0 u# ~5 Z+ f- G7 Q);
8 a# k; v* Y+ C- B5 s g$ W) x2 h* }% M, }8 \% }: W5 t$ b) C5 m# R
BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能 # z: Z( p a& h5 V$ D; W
struct _HTTP_FILTER_CONTEXT * pfc,
]% S9 P( T, E7 K. `5 ?8 F1 c' m1 @enum SF_REQ_TYPE sfReq,
0 @6 G Z+ {# |2 z7 YPVOID pData, - j, k2 B) ^- ^/ O+ V. `+ W/ M5 }
DWORD ul1,
8 r0 `6 @' o; }DWORD ul2
9 i2 q% ^0 [: k, i. ~! D( y);
) D' G3 |- r7 F- \& t
: v. y6 m) C" ^ E/ r8 ]7 S} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;
9 q" W0 @6 d' _8 @8 |: \
# h# `& R5 Q% d6 m) `四、VC++ 6.0中对ISAPI的支持 8 O( Z* d; R5 n
' M6 n5 y' ?3 ?5 u+ l9 F. f
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实例,每个 7 u$ ~' Q0 x: b+ y; d" D$ e# O0 J
CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。 ! P( x* y6 O! ?6 D4 G# b1 z
( }) T3 t. {+ i* T8 A( o一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。
1 b3 \1 V3 y; J' T1 j3 z$ S$ i* P" i3 j% K/ i, W" A' k( T
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为例,该例中有下面这样一个表单: # O2 A# V6 f" |4 u4 @
2 F/ x* |0 Y( R3 A6 i<form method=get action="pinball.dll?"> $ D" r" C) ^' F5 T; i+ h
<input type="hidden" name="MfcISAPICommand" VALUE="GetImage"> 4 {0 [6 S9 Z9 k: y
<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br> % Z! F* \. ?& ^& L
<input type="radio" name="Favorite" value="2"> Twilight Zone<br>
- E# R( M; }# Q, C<input type="radio" name="Favorite" value="3"> The Addams Family<br>
- x" X7 F* s( J& z9 c" ~8 J<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br> - [3 S) D6 t% c/ ~; a& M
<input type="radio" name="Favorite" value="0"> I don't see it here<br>
# c4 p* n0 S, F! I4 U. ^<br>
# Q6 o( x0 I2 N) E; Q: C/ a<input type="submit" value="Show Me!"> ' u/ g# D4 ]/ ]7 h, g
</form> 6 O$ d6 O4 [$ y6 k
' N2 |3 H/ ~) d0 f- E2 @% V$ y$ v当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端 + S, y* B, s% Y! u) g
最终将得到如下的URL串:
: ?) I+ i- @% I( N) h9 n: N9 {' h
http://www.abc.com/pinball.dll?M ... mage&Favorite=1
3 W5 m. J, X \" j, S& w2 M$ I- ~4 T* r- Z" \" f
在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数
) i3 U! k" X4 `, k$ d9 y: W将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite:
7 e5 I: K$ M; K& L7 g
' H; I7 x g; S! b8 I. A0 e; y& vvoid CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice); " J3 u- }5 X/ w( u0 {
+ w3 v% Q9 X! N' b5 y: o
而parse map需要按照下面的形式定义:
& N0 e) ?5 t5 {' m- K4 q7 P5 `8 I0 R# h2 \
//CPinballExtension从CHttpServer派生而来 5 }, v' m \- d% E |
BEGIN_PARSE_MAP(CPinballExtension, CHttpServer) 5 V- }5 {' _6 W3 S; G
! Y/ ?6 D* @ {! y+ p0 |//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice " j( L2 e5 M6 i3 k% R) @
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) % c* z) _9 T0 S" w
1 H) t6 z/ l6 G1 M//该参数在URL中的名字为Favorite
7 G c- k: Z* x9 wON_PARSE_COMMAND_PARAMS("Favorite") ' e0 N+ _3 d, \; ?( @6 {* M2 m
) L, c3 j' }+ {& D& s6 H. FEND_PARSE_MAP(CPinballExtension)
" q& P0 ~ T' j0 H6 H. W
* F9 L: F( _+ f( [) Z& ]3 T8 F而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件: ' C3 J' y% d2 I7 R( R+ b4 s9 ^0 Q
. x+ {# l) c1 {3 k
OnPreprocHeaders 3 I8 `. o6 X8 d* W( l
OnAuthentication ; Q% B8 h x* A' G3 R x2 G- u
OnUrlMap ' c' k' n8 G4 p6 Q
OnSendRawData $ n$ J0 D) {. w( t
OnReadRawData
4 `4 ^2 \. A3 \# |- B. h! FOnLog 0 z* l! _7 c' t2 V8 u, Y
OnEndOfNetSession
, u V0 O$ B/ |& \* B6 x6 f( o7 E' @9 {3 g: f% c e9 i0 G. D+ j
MSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。
. X4 \- _# B: y8 U+ K* y
( c1 i% s4 B+ _参考资料:
/ N7 }$ ]6 `6 R: L4 [" E7 ^
% b G! a( ?% Z: i1、MSDN + h& o# |& N0 ~
2、《精通CGI编程》,丁一强等,清华大学出版社 |
|