|
|
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。 3 O/ K- Y2 E! R9 u
5 i/ U, D, H$ n% ` Y! e. W
一、ISAPI接口和CGI接口的不同。
: `6 Z9 A* r4 k# A+ s7 U% ~6 h- j" C! r/ v; S3 u
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。
4 y) h2 ]: \6 J; d: u" K( a3 [# q3 u$ l. q6 b
1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。
" _' }5 n* P' O! J3 E
* G; y9 \& ~ }- d, r* z* j+ A2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。
+ T- Q' Q( T* W' ?, A, c; y
+ `# |8 Z8 M# C& F/ Y( ]ISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。 8 P' A& Q& I4 _' ~% d
' [% d4 n. D6 m; M6 L$ g* a& a5 v' C( X* N
二、ISA # ^. m8 M b! [0 a' F+ G
) d, U/ [" L+ H( R- UISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下):
1 j2 Q% M4 e9 J0 [http://www.abc.com/Scripts/function.dll?
* A4 i1 y1 s/ ?: D# |" }9 W x8 u' h$ z/ s) t4 \1 d
ISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。
: B$ h7 `9 x& K) O7 [ N! l) t' [/ u
1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下:
M2 c" e/ J7 Z2 \- G
/ W- s, x& ?: V4 j6 tBOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version);
* Q% M, e o7 ttypedef struct _HSE_VERSION_INFO ) ]7 O* W3 D7 e* e) z7 `' D( K& j3 Z: A
{ & u6 i, e7 C- S- ~0 T8 F
DWORD dwExtensionVersion; //版本号 6 g9 h1 l- E* T8 b |
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串 0 q# v, P" G. L$ r4 Q" }7 O
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
) Q w" z1 l' y, s2 e& p& m6 r0 k) T6 z7 \) J( {* G7 ^
2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:
, A% M/ j9 u, J/ _3 s6 f, H. V
/ G/ U& A4 n% H+ s6 PDWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB ); 0 M, S. |+ i3 S' }
" J& e, o7 t: o
ECB的结构定义如下(IN表示入口参数,OUT表示出口参数):
: U7 K2 E3 q1 ?1 F6 A0 p. l8 D$ [7 `. A" K" ?% k
typedef struct _EXTENSION_CONTROL_BLOCK
# ^; t% {& ^- m# r{
+ R9 r4 Q8 q4 p5 M: K fDWORD cbSize; //IN,本结构的大小,只读 6 M ^6 a& Z) y; v- S
DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号 % c) N( J( n" n: c( }
HCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值
( ^' j* d9 N3 f& c2 b+ k/ yDWORD dwHttpStatusCode; //OUT,当前完成的事务状态
3 u! _$ N |$ Z+ r7 {! v. w$ \CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容 $ m9 l, C; t/ V1 S
LPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD + s; {7 C) R/ O
LPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING
, a v9 S8 k8 wLPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO
3 V! W- ~% j+ j( fLPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED
$ R# H5 f2 A6 N S; C! ~0 ~DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH 7 f' e5 F/ s8 `0 p% b7 W
DWORD cbAvailable; //IN,缓冲区中的可用字节数
! t; G; i+ N6 z1 V& m% `LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据 + M, {: Q8 b) ^. o
LPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE ; Y. i2 \& y' j+ Y' n l; ` M
2 |5 N5 y N6 z( i
//回调函数,用于返回服务器的连接信息或特定的服务器详细情况
* U Q- V* b) N$ ]. ?BOOL ( WINAPI * GetServerVariable )
7 b9 r7 U0 d5 l# p( HCONN hConn,
& V/ E* a1 ]3 A& k$ VLPSTR lpszVariableName,
( M" E& T Q8 C3 v6 N9 eLPVOID lpvBuffer,
+ @/ j+ F2 C0 q$ [0 F1 s" d( b* jLPDWORD lpdwSize );
6 E1 {6 `8 l5 ^6 {' {% h t" x* R9 I0 v& Q
BOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
. f3 F! p+ f9 C* f) X( HCONN ConnID, 1 K# i1 R% b! ?) C
LPVOID Buffer, y( E6 s2 u1 s/ O, u7 v+ \/ |
LPDWORD lpdwBytes, , _1 B% E( }( W; i
DWORD dwReserved );
+ j4 f, S) K% @. J0 }, u. x8 p0 T/ K7 L
BOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据 e5 I# a2 u; e
( HCONN ConnID,
5 f; r. f3 Y1 u- N k8 u5 j" gLPVOID lpvBuffer, 3 R* _0 M7 U. w. I8 _* @4 v
LPDWORD lpdwSize ); 1 c2 B9 I& a% Y! _
0 s6 `% ^+ [; w3 lBOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能
6 J$ M, V+ ?% Y$ _5 a7 O( [( HCONN hConn,
; u/ J$ t1 ^$ ^) Y( [4 pDWORD dwHSERRequest, l1 V$ V7 }! T! X% _3 [
LPVOID lpvBuffer,
% V* J& H$ d7 X* T0 K7 L$ D! BLPDWORD lpdwSize, 0 p' |) e, K) \
LPDWORD lpdwDataType );
: A- C* @2 n3 p( y0 I0 g" L1 ?! S( Z* T* }2 n. f: e9 ]
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;
* b9 `8 j# t. d0 W* {" @6 s) l8 M+ J- u2 l- Q. {
在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。 1 J( q$ I8 |( s/ N" M
9 T9 M9 V y3 b( J6 L; V三、ISAPI Filter # G$ B, v, [5 @% f
+ J& r( F6 T! U! w9 `
ISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。
# f+ t9 D2 k3 [ w
' H% i; |: w1 Q5 _( X2 @# |8 MISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何
5 G) n& c6 V% A' kISAPI Filter都必须引出这两个函数以供服务器调用。 & ]# Q- R9 V) J. F
" ]0 H" Q% O+ Y$ g( o- F2 U
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得
# D" J" K0 Z7 wFilter的文件名并加载它们。 9 T* C5 N1 \& D: a z. ^
7 i7 d" p) Y0 gHKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL 9 Q) Z# ] R8 x
6 f& Y1 A1 s! T ^- X( e
2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:
$ [3 |- }4 S. O& t5 {) j) b- Y$ L4 _0 d
BOOL WINAPI GetFilterVersion(
2 C! j2 O3 X) K2 \6 zDWORD dwServerFilterVersion; //IN,服务器使用的版本规范 ! b- n9 @" A/ v- K& }
DWORD dwFilterVersion; //OUT,过滤器使用的版本规范 1 C1 h' n+ a4 k' Z, t! R
CHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串 ' u3 [4 d7 O& o/ X) b$ u
DWORD dwFlags //OUT,事件和优先级标志 & @( m+ {: l' z2 b4 g/ J
); . B Z( r! H& S+ W' G
5 B2 D: L& R3 ~9 D y/ M* _: A5 d! t; A事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。 . z9 t& B" l8 I
2 @0 k, i0 c' w- p* R1 F3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。
& j7 Q3 ^% f6 l" [) h# o7 }0 T, Y+ s' o, E. h5 o; G
typedef struct _HTTP_FILTER_CONTEXT
$ h" \% U$ d7 A9 T( N! @4 S1 _{ 5 C) c: f; e& o) K: E& t' X& D
; O) O5 B" a& c3 q: kDWORD cbSize; //IN,本参数块的大小 9 ?$ C$ Q) P& q! H6 [
DWORD Revision; //IN ( n4 R$ a! t3 a% g
PVOID ServerContext; //IN,由server使用本参数
# q. R. {, w. i& WDWORD ulReserved; //IN,由server使用本参数 . z, d% R) Y& k
BOOL fIsSecurePort; //IN,事件是否发生在安全端口上
- P- d: H# W/ Q% w: l( VPVOID pFilterContext; //IN/OUT,与本次请求相关的上下文
8 d& ]8 X' ?& v! s) y3 h& [; [! {( F
//回调函数,取得关于服务器和本次连接的信息 3 r% A4 c2 g4 V7 U8 x- S
BOOL (WINAPI * GetServerVariable) ( ) w7 Q% I; [, J9 \7 Q+ P# ]9 O
struct _HTTP_FILTER_CONTEXT * pfc, : n* R2 ^0 K+ S3 K! { c
LPSTR lpszVariableName,
3 f& H+ Z( f% {/ _8 M( x) i' FLPVOID lpvBuffer, & y3 C$ p: x( c' S/ V1 W0 c
LPDWORD lpdwSize ( P; n- F! }8 Q0 J O! x7 h
); & ^( B- e+ R4 l+ G: p s
3 R6 A, W- b- b0 c) x+ S3 _" N# |BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头 ' I/ R6 ^" v2 I% F" X
struct _HTTP_FILTER_CONTEXT * pfc, ( F3 g- }3 N3 j5 v
LPSTR lpszHeaders, " ^, c8 ], u( w; Y- Y1 Y# k- Q6 i
DWORD dwReserved
* ]4 Z$ P- x6 r);
: M- W3 I" u/ W$ R7 p, [) D4 S9 V/ T2 L' ]- T
BOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端
6 y4 B, K! _% @3 E+ ?# ]+ }struct _HTTP_FILTER_CONTEXT * pfc, 6 [4 v+ n# {# b4 M5 e* q
LPVOID Buffer,
2 u G0 S9 M8 t# ALPDWORD lpdwBytes,
! m8 l0 }1 ~, R e2 KDWORD dwReserved
" F. c( z, m0 T+ X( K& N); 1 Y$ m4 R* X5 \; ^" s/ y
0 ?7 |" O; n+ F8 Z
VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。
' q) U; A. a' p4 ]struct _HTTP_FILTER_CONTEXT * pfc, 3 ]1 f- N# u3 B, W% i C9 Z% |
DWORD cbSize, 8 L5 X' j3 q$ c4 K( W
DWORD dwReserved 2 u: o; D" q( N
); % s( K5 @1 ?" ^) B
& Y$ t* b7 G8 p& e. [& TBOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
( X, [' x' S6 p- L9 Fstruct _HTTP_FILTER_CONTEXT * pfc,
2 G2 u% R `% k6 F1 k8 c, i! O& Tenum SF_REQ_TYPE sfReq,
- j# V0 u; D. }8 X8 [PVOID pData,
6 ?5 p: U# z0 aDWORD ul1,
% l- R" |5 B6 [DWORD ul2
4 M# ^/ L$ {6 h( e; Y);
+ f$ X/ t+ R+ m# P$ U2 l$ `4 m R4 t$ M6 @$ f+ j* ~( u: `
} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;
) e9 p* B& h( V4 n4 \* @: E: t% z
$ v: S, P" z& [- |9 ~四、VC++ 6.0中对ISAPI的支持
$ e: H0 X$ I. r) Y
$ ^$ _5 h, Z+ N1 h! Q' W) Q' v$ uVC++ 6.0中定义了5个相关的类以简化ISAPI的编程工作:CHttpServer、CHttpServerContext、CHttpFilter、CHttpFilterContext、CHtmlStream,这5个类都没有父类。其中CHttpServer和CHttpServerContext主要用来编写ISA,CHttpFilter和CHttpFilterContext则用来编写ISAPI Filter,而CHtmlStream则用来操作内存中的HTML文件,为其它的4个类提供服务。CHttpServer在每个ISA中只能有一个实例,一个CHttpServer可以对应多个CHttpServerContext实例,每个 8 ?8 u6 G; J0 y7 i" {! K: b( B
CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。
2 O S; }. ?& p: B$ v4 q* Y- ?
" u7 P9 t1 C, B2 W4 S1 Z! P一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。 2 v: N' f- D) d3 Q3 S3 C
0 _: N2 f% u3 L$ C7 _1 O; N
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为例,该例中有下面这样一个表单:
, c2 I: m2 f [; p& A
0 y$ x% y9 j, p" E" `, _5 I( ?<form method=get action="pinball.dll?">
2 A; Q5 b4 {9 F' J3 q/ W<input type="hidden" name="MfcISAPICommand" VALUE="GetImage"> , W) j: b5 [/ l! X! J, h
<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>
; L" |1 D2 N. E( l<input type="radio" name="Favorite" value="2"> Twilight Zone<br> 0 _3 |5 z0 @3 G: Z/ V+ R* v
<input type="radio" name="Favorite" value="3"> The Addams Family<br> % {9 c" g0 ]3 _6 F2 A" S# O9 V( B* X7 s
<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br> ; O% f7 Q( D" ^ j6 U
<input type="radio" name="Favorite" value="0"> I don't see it here<br>
" c9 k$ g6 H+ `0 ^4 a3 ]<br>
2 w' Q: V2 b6 S& d<input type="submit" value="Show Me!">
/ a3 X, w5 u5 I* S5 H! I7 `</form>
2 `3 c0 Z" Z8 ~9 T3 I. \; f7 D. }; v: x
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端 0 X8 s, w) ?$ M5 t7 ^5 T3 O
最终将得到如下的URL串:
. Y$ A' m! _0 U; I1 @; M: S A6 M2 x/ o; D
http://www.abc.com/pinball.dll?M ... mage&Favorite=1
% T5 h) m( s# \& g- H7 c
* q% {: G$ D- J" n3 c4 o/ Z$ Q在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数 5 u) |# q& } r. c" [+ k
将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite:
7 K5 r& ]7 w: m( B& J# Y6 n) v y( e1 e0 M2 x+ s0 ]
void CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice); & D, S, t9 P6 Q; K# {/ Y* s- i6 D7 P
l# H# a( q/ p* G+ _
而parse map需要按照下面的形式定义: ' e8 K9 P7 J$ O% L" p
: z' u6 u v. l3 i* n9 S' i
//CPinballExtension从CHttpServer派生而来
* n& ^8 M% [- H! j: ]BEGIN_PARSE_MAP(CPinballExtension, CHttpServer) . O6 F9 O: v) j0 T1 H) {
. i) c' m5 Y, b6 [//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice ) T% c9 J) ^( l! c
ON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) : V+ {5 m+ W. Z! i
0 W1 q; H# e' n7 _7 S//该参数在URL中的名字为Favorite
1 U, j4 V! k3 `ON_PARSE_COMMAND_PARAMS("Favorite")
' @7 |# F7 m; O, W0 I1 W9 y9 Y+ T! \3 \+ q
END_PARSE_MAP(CPinballExtension)
, J1 ]# _/ f& g
; g e+ N8 S: p6 ]: R$ z$ c+ t而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:
% q* U; u) K6 [! T8 W: Z0 ]
& e! I" W0 P& @9 t- [OnPreprocHeaders + s: |3 b( L3 m' Q- T
OnAuthentication
, F0 |$ J$ T& S+ k+ nOnUrlMap
* J* r7 s5 b, j; QOnSendRawData ) b1 N8 z d1 V% R# q: V; I6 Y
OnReadRawData + Z/ n6 i9 p1 o7 @
OnLog
2 k9 f2 R( X! o- O) d* M6 \, JOnEndOfNetSession , i$ o+ g3 D0 Q S, _/ U) M% P
, B1 p1 p: k' O' @% A: ]MSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。 1 U8 a& u" R1 Z5 D' }
9 o2 d4 W- q+ a' Y' B
参考资料:
, ^+ W$ K: ?' K! s; e) S4 G: d# G; x6 ?( y6 \
1、MSDN
9 L8 \0 K. G" M, z1 S$ p* h2、《精通CGI编程》,丁一强等,清华大学出版社 |
|