|
|
ISAPI(Internet Server Application Programming Interface)作为一种可用来替代CGI的方法,是由微软和Process软件公司联合提出的Web服务器上的API标准。ISAPI与Web服务器结合紧密,功能强大,能够获得大量的信息,因此利用ISAPI可以开发出灵活高效的Web服务器增强程序。由于ISAPI程序与Web服务器的关系,使得ISAPI接口在安全方面有一定的研究价值。本文主要讨论ISAPI在IIS和VC++ 6.0中的实现。
~2 u- w, i! U5 ]" A: Z& X7 D$ J! f- o. ~" u( Z5 v
一、ISAPI接口和CGI接口的不同。
2 n: {; w: K/ c6 k/ g" X/ w0 F U* A6 w" [
ISAPI程序和CGI程序完成类似的功能,但是实现方法不同。 * C/ C- B6 @) M& t0 ]8 R8 x$ ]
1 l8 K" h# P+ X% I9 U2 b, W1、ISAPI程序以DLL形式被Web服务器加载到自己的进程空间中,因此和服务器共用同一个地址空间,且在没有客户请求时可以将其从内存中卸载;而对客户端发来的每个对CGI程序的请求则需要服务器为它单独启动一个进程,这需要耗费大量的时间和内存。当并发的请求数目很大时,使用CGI在效率上不如ISAPI。
& S/ n0 L' O7 s# T" G7 g
" n n6 j! o4 L" e5 j2 M# C2、CGI程序通过环境块和标准输入输出与Web服务器进行通信,而ISAPI程序与服务器结合得更为紧密,与服务器共享同一个进程上下文,主要通过一个参数块与服务器进行交互,可以从服务器那里获得关于当前HTTP连接的大量信息。 " T1 Z; J5 o" E7 u% H1 u
- h" O2 j( U$ L4 p0 L( VISAPI主要分为ISA和ISAPI Filter两部分。ISA方法相对而言要传统一些,利用一些特殊的链接,指向服务器的作业,供程序开发人员设计一些扩展功能;而ISAPI过滤器则倾向于构造服务器直接调用的模块,提供一种无缝链接部件用于监测直接来自于服务器的HTTP请求。 ! {% y$ P+ p) n6 x# y, J
( ]! X0 N) u2 L% B: B; h! ^. d2 b' O
二、ISA
! u; }5 ~$ \( G0 E! y% _( t2 @* B7 u5 h. n5 K
ISA(Internet Server Application)也可称为ISAPI DLL,其功能和CGI程序的功能直接相对应,使用方法和CGI也类似,由客户端在URL中指定其名称而激活。例如下面的请求将调用服务器的虚拟可执行目录Scripts下的function.dll(ISAPI DLL必须放在服务器的虚拟可执行目录下): , @# u1 z: Z8 u
http://www.abc.com/Scripts/function.dll?
9 u3 `8 {$ M, p! {5 ]4 e3 `5 L- U) n
ISA和服务器之间的接口主要有两个:GetExtentionVersion( )和HttpExtentionProc( )。任何ISA都必须在其PE文件头的引出表中定义这两个引出函数,以供Web服务器在适当的时候调用。
6 A3 a& C: @2 C: j# K* }- T W
1 R8 L5 B3 ~' U+ D, ?2 E, q k4 }1、当服务器刚加载ISA时,它会调用ISA提供的GetExtentionVersion( )来获得该ISA所需要的服务器版本,并与自己的版本相比较,以保证版本兼容。函数原型如下: ) U) z" }- o) T: f' d6 J
j: b4 s5 b/ k' I% r/ s7 u0 h. @. H
BOOL WINAPI GetExtentionVersion(HSE_VERSION_INFO *version); 1 x0 X, q7 j$ h1 N& @# a. N
typedef struct _HSE_VERSION_INFO
! M; P# x# R. M' B1 H: | @{ ( Y8 m2 s p3 j. Y. J
DWORD dwExtensionVersion; //版本号
$ o2 G$ U' Q. G% L: MCHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN]; //关于ISA的描述字符串 - R, K9 N% x% }6 {; Y
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO; 5 Y- }1 G4 R8 I" H
6 A0 o( U- S; R' C( w" W3 F, L
2、ISA的真正入口是HttpExtentionProc( ),它相当于普通C程序的main( )函数,在这个函数中根据不同的客户请求作不同的处理。服务器和HttpExtentionProc( )之间是通过扩展控制块(Extention Control Block)来进行通信的,即ECB中存放入口参数和出口参数,包括服务器提供的几个回调函数的入口地址。函数原型如下:
/ w' O1 N% m& k7 q T4 a( K. s% ^# B( _7 `& Y# j' k
DWORD HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB ); " l! Q# l' ?6 l- X9 I, N3 _! z
: R, Y$ V, F: D8 I) tECB的结构定义如下(IN表示入口参数,OUT表示出口参数):
! @/ c& @" o6 D5 w9 x' @% e2 Z/ B" a+ Z1 [" m4 K; @! g8 I
typedef struct _EXTENSION_CONTROL_BLOCK
6 [. v) g4 `. _& P6 _; l{ 5 J& g, Q9 o4 l2 F
DWORD cbSize; //IN,本结构的大小,只读
: }# m7 o& L4 D5 ^$ ^2 ^DWORD dwVersion //IN,版本号,高16位为主版本号,低16位为次版本号
2 }, V, N, O$ ?# }0 X2 xHCONN ConnID; //IN,连接句柄,由服务器分配,ISA只能读取该值 8 o+ f; G: Z7 B2 a/ s
DWORD dwHttpStatusCode; //OUT,当前完成的事务状态 1 r/ S/ n! X* {2 W$ W
CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; //OUT,需要写入到日志文件中的内容 7 q! Q/ j& B4 z: T1 ^- n
LPSTR lpszMethod; //IN,等价于CGI的环境变量REQUEST_METHOD : V' I; E" L1 j- T7 W) q
LPSTR lpszQueryString; //IN,等价于环境变量QUERY_STRING # a# T1 c6 n( I* y, K! ~
LPSTR lpszPathInfo; //IN,等价于环境变量PATH_INFO 5 ^9 E" O3 ~4 z: G. c- N6 f. F- M
LPSTR lpszPathTranslated; //IN,等价于环境变量PATH_TRANSLATED / h% F4 k8 ]; j0 S% E- K/ S5 H D
DWORD cbTotalBytes; //IN,等价于环境变量CONTENT_LENGTH
5 r7 E2 _* n& X! CDWORD cbAvailable; //IN,缓冲区中的可用字节数 " j! ]8 P- T! E
LPBYTE lpbData; //IN,缓冲区指针,指向客户端发来的数据 . I# d; A# z) d# J
LPSTR lpszContentType; //IN,等价于环境变量CONTENT_TYPE 2 z. L$ X1 l* S7 c0 H" A
2 ?7 q/ y+ W7 E1 k& M
//回调函数,用于返回服务器的连接信息或特定的服务器详细情况 - a/ c* r$ Z5 ^! d& e
BOOL ( WINAPI * GetServerVariable ) + Z Q( t$ a7 b1 ^) T" x
( HCONN hConn, 8 o7 z/ \- N: m9 d8 V
LPSTR lpszVariableName, 9 g# o0 ]- ?2 v0 s! W
LPVOID lpvBuffer,
; q" d1 P: \% [" g6 {) R5 F; HLPDWORD lpdwSize ); + f+ U3 r$ i& G# R# P
+ i% W' p1 B1 k3 yBOOL ( WINAPI * WriteClient ) //回调函数,从客户端的HTTP请求中读取数据
: D" @7 E ?! c# O( HCONN ConnID, 3 m7 s( e t% n1 z
LPVOID Buffer,
' G: O/ X$ I% ^5 i4 v- C% ILPDWORD lpdwBytes,
9 V* j% p2 E9 t, {DWORD dwReserved ); 9 V5 i% `" y5 l4 l
' b% j2 [! j) q- t; Z/ X9 PBOOL ( WINAPI * ReadClient ) //回调函数,向客户端发送数据 3 X4 S4 d; n! t6 z7 w$ h
( HCONN ConnID,
: g0 F' C0 J" mLPVOID lpvBuffer,
' _& O7 W; n& |+ y9 ZLPDWORD lpdwSize );
7 p8 F. x, R3 v/ d! r s( o8 m5 k8 _) L# p. x; l- k6 Q
BOOL ( WINAPI * ServerSupportFunction ) //回调函数,访问服务器的一般和特定功能 ' x3 _- j0 @# y5 ~" G/ P
( HCONN hConn, ( l/ @5 Q; V+ k$ }* y i
DWORD dwHSERRequest, / a5 S7 ]( h: R- x' f$ m
LPVOID lpvBuffer,
8 A0 @8 u! I; i$ i& c/ s' T, RLPDWORD lpdwSize, 2 l o) L& |6 ^8 _" c" X( H
LPDWORD lpdwDataType ); 7 M6 S0 T9 o: U; x5 T6 Q! l
0 ~+ w6 h8 A2 R$ M1 Y0 w2 K( N; z
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK; ; j4 E Y# i) _5 b- p8 y
( ]7 [+ N. Z% }+ z在上述ECB中,服务器不但提供了当前HTTP连接的句柄和一些变量,而且提供了4个回调函数给ISA调用,从而使ISA可以获得更详尽的信息。
7 z1 P) N0 v; z+ ]9 t% V
8 s1 X, H9 g& i( f三、ISAPI Filter \' z6 l& `( G& F$ @% X2 n8 ?6 q
. J4 b: e/ a9 OISAPI Filter位于服务器和客户端之间,能够对服务器和客户端之间的通信进行预处理和后处理,比如对通信进行加密/解密、提供对客户进行身份验证的新方法、提供自定义的日志记录等,在CGI中没有与ISAPI Filter直接相对应的部分。
, }, F2 h; V' J: O: W- j& D1 B" O, H1 C5 j7 E' k$ ~
ISAPI Filter与服务器之间的接口有两个:GetFilterVersion( )和HttpFilterProc( )。任何 + l' E! K, t! t& @1 U; l1 T j% A2 C
ISAPI Filter都必须引出这两个函数以供服务器调用。 0 V! H9 y0 o! K, Y" y- V
* E b! G$ Q& K" V$ ?
1、在注册表的如下键值中存放着所有ISAPI Filter的文件名,IIS服务器启动时从该键值中获得
& U& J2 l, N+ ^* i+ E4 ]Filter的文件名并加载它们。
" ?* P& u- i& X2 }) Q
4 Z0 I4 _" z1 y7 oHKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/FilterDLL
( ?( @: K$ s( [- ]
6 _: J' Q, f* P% I H2、然后服务器调用每个Filter提供的GetFilterVersion( )函数,获得版本号以及该Filter希望处理的事件,即ISAPI Filter通过引出GetFilterVersion( )函数来告知服务器自己希望处理什么类型的事件,因为ISAPI Filter是通过事件来激活的,当满足条件的事件到达时,服务器就会调用Filter引出的主函数HttpFilterProc( )对该事件进行处理。GetFilterVersion( )的原型如下:
8 _" @1 P1 k7 m. I( K# _# w4 O
' Q6 X, H% T8 b! G! @BOOL WINAPI GetFilterVersion(
1 T' e3 i' ], c$ O8 \+ NDWORD dwServerFilterVersion; //IN,服务器使用的版本规范 3 o, f- a5 y' x$ z+ i! E7 ]
DWORD dwFilterVersion; //OUT,过滤器使用的版本规范
2 }$ J! s5 c8 fCHAR lpszFilterDesc[SF_MAX_FILTER_DESC_LEN+1]; //OUT,对该过滤器的描述字符串 * Q6 k4 F) A9 f4 u; n+ o& }
DWORD dwFlags //OUT,事件和优先级标志 8 d7 ^6 G7 w( f5 U# {0 ^5 Q, m
);
2 n9 a3 T/ m1 _$ X6 l
) |" B# W* A! m" T s事件和优先级标志dwFlasg的取值在MSDN中有详细解释,其中包括该Filter被调用的优先级,一般应使用默认的低优先级,否则可能会对系统的性能造成很大影响。
- A {. N X' r: o' k* S; ^+ n- f3 g& Y9 F# U( J- m: F
3、HttpFilterProc( )是ISAPI Filter主要的入口函数,它根据当前的事件的不同作出不同的处理。服务器通过如下的参数块和Filter进行交互,这个参数块的作用和ISA中的ECB类似。
3 ~" V" L( M1 @/ [2 E
% [' ]! C- r9 d0 H6 P# ]& z+ g" J& etypedef struct _HTTP_FILTER_CONTEXT " ]! h! A, f( ]7 f x- x+ |" m
{
6 R# F7 p2 X' y0 F0 E; m! v
- R, ?- ^9 s' NDWORD cbSize; //IN,本参数块的大小
5 k5 { B1 C1 V, K; q7 V8 {DWORD Revision; //IN ( A9 \8 ] r$ H% ~0 r
PVOID ServerContext; //IN,由server使用本参数 2 M* {# @: F7 b( Z: h) L
DWORD ulReserved; //IN,由server使用本参数 # c- P" r, ? V. u
BOOL fIsSecurePort; //IN,事件是否发生在安全端口上 : N8 ^9 R# e. c/ Q: ]
PVOID pFilterContext; //IN/OUT,与本次请求相关的上下文
7 E: s( d* }) [9 |' y# S: O
/ x+ B5 Z) j$ D) O//回调函数,取得关于服务器和本次连接的信息
- d; |- g4 X4 e: B. G; I7 [BOOL (WINAPI * GetServerVariable) (
8 o& }/ F% h/ j* q H0 {7 tstruct _HTTP_FILTER_CONTEXT * pfc,
& j: G8 j% o5 ZLPSTR lpszVariableName,
: l( ^1 T, {# B9 gLPVOID lpvBuffer,
! Y% o! h# I# }' @9 ALPDWORD lpdwSize
7 R( M# _1 z2 |3 l( K P/ ?8 N);
9 |: s U4 f4 H, `. F, @/ S) K" [. Y% R+ z
BOOL (WINAPI * AddResponseHeaders) ( //回调函数,给HTTP响应添加一个标头
5 C) ~1 Q0 m% vstruct _HTTP_FILTER_CONTEXT * pfc, " x* u6 F, j$ w z6 l
LPSTR lpszHeaders,
) U8 o- k: e D6 S8 C5 V7 c: LDWORD dwReserved 3 K1 V5 T7 H6 s1 A2 i; b
); + H2 ^) j# k8 S$ T. d; Q
* a9 P. N" [8 ~5 mBOOL (WINAPI * WriteClient) ( //回调函数,将原始数据发送给客户端 # t. b, I" @0 k0 M
struct _HTTP_FILTER_CONTEXT * pfc,
" b; W' Y* M6 oLPVOID Buffer,
. [2 [7 D* O7 F) E4 s6 F f' mLPDWORD lpdwBytes, + ~; F* N' I- R) q M3 |
DWORD dwReserved
2 l: Q7 @# K4 E: R. y+ W. a+ n7 z); " `* W0 ^' K& b) a
4 J" G6 Z/ w+ `# }VOID * (WINAPI * AllocMem) ( //回调函数,分配内存。 a8 p0 J; p# `0 m- W8 p
struct _HTTP_FILTER_CONTEXT * pfc, 1 E) `. v3 m4 G: O- _
DWORD cbSize, 3 q+ Z1 L: @; ^; V& P
DWORD dwReserved
6 E1 n" L% d. |) ~6 t/ R$ {; l);
3 b: m+ \, r5 J
) R% H, [1 J; w% \BOOL (WINAPI * ServerSupportFunction) ( //回调函数,访问服务器的一般和特定功能
' K# K: L& X5 y, S" @struct _HTTP_FILTER_CONTEXT * pfc, 0 N1 w! `3 Z0 L! O) |" l0 Y) X( o8 Y
enum SF_REQ_TYPE sfReq, 4 H0 `6 F! P* \+ w9 I
PVOID pData,
8 }2 w% @4 g- x E) U, l, s2 [+ UDWORD ul1,
+ b' q3 b6 x2 n& j/ ^( dDWORD ul2 1 [* R+ W2 ?5 ~; y
); ) e% T( y$ p6 ]. i7 [ `
0 d" h6 j+ u$ c} HTTP_FILTER_CONTEXT, *PHTTP_FILTER_CONTEXT;
% ]5 d0 j5 @$ e: l7 N7 u
- s) F" C# V! F# d, h1 Z四、VC++ 6.0中对ISAPI的支持
" P3 n- R' e, e$ }' j9 _% @0 ^& R+ k; J
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实例,每个 # y- n7 q" E2 U3 `
CHttpServerContext处理一个客户请求,这样可以处理并发的HTTP请求;CttpFilter和CHttpFilterContext之间的关系与此类似,在每个ISAPI Filter中只能有一个CHttpFilter实例,但是可以有多个CHttpFilterContext来处理并发的事件。CHttpServer和CHttpFilter是独立的类,它们可以共存于一个DLL中,也可以分别在不同的DLL中。 ! k1 S* z# v) V
* I( d7 L F4 d+ R, ?% I, K
一个ISA可以提供多个命令,每个命令对应于CHttpServer(或其子类)的一个成员函数,客户端可以在URL中指定命令名及其参数。在VC++ 6.0中是通过parse map来实现这种对应的。
% S8 f$ }( T. q9 \3 F5 N" a: X- E" _) c& S4 L
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为例,该例中有下面这样一个表单:
6 M& y' r$ U! d0 \% o5 [1 X* g3 k! o' u8 v( D
<form method=get action="pinball.dll?"> . X/ E% p N; N
<input type="hidden" name="MfcISAPICommand" VALUE="GetImage">
# Y; M2 s3 v6 u0 J<input type="radio" name="Favorite" value="1" checked> Attack from Mars<br>
3 {% Z+ i# j% R1 @7 K# Z<input type="radio" name="Favorite" value="2"> Twilight Zone<br>
3 G5 X2 C `. Q3 u- B0 _' [<input type="radio" name="Favorite" value="3"> The Addams Family<br>
- v) H: W0 [- v' T6 [+ b) z<input type="radio" name="Favorite" value="4"> Cirqus Voltaire<br>
& d% j8 e( G) C! W$ W& t x! H) t( O<input type="radio" name="Favorite" value="0"> I don't see it here<br> 0 d! v, L( A3 q) ~
<br>
! p A* W# e- J1 U" T<input type="submit" value="Show Me!">
! B2 ^1 F w. C</form> + J6 h; G3 j, f) W
, [% L+ c/ f9 Y+ l
当客户端选中了上面的表单中的“Attack from Mars”这一项并点击了submit按钮后,服务器端
- B& g: i. R4 X5 |' s& U5 d最终将得到如下的URL串:
9 [- @# W7 z- a8 C' S+ n/ t2 F
http://www.abc.com/pinball.dll?M ... mage&Favorite=1 : P, F' g* w6 v, I/ k v
% l' D" {' @9 A& P6 o8 Q在该URL串中,命令名是GetImage,参数Favorite的值是1,因此pinball.dll中的如下成员函数
" v( q! Q! f/ w1 i" e4 r% K! t! R将被调用以处理该请求,其中参数dwChoice对应URL中的参数Favorite: - j; p2 H8 G8 g1 S% U
# J. j% I' g% D0 X1 xvoid CPinballExtension::GetImage(CHttpServerContext* pCtxt, long dwChoice);
% u5 G3 ^# y) l2 u; i: L+ R4 K: a9 Q) s1 Y; Y! {
而parse map需要按照下面的形式定义:
: I( d# h+ i" b, S* v
( Q0 [' X( K* f& _/ N/ `//CPinballExtension从CHttpServer派生而来 + n) M" V) B, x
BEGIN_PARSE_MAP(CPinballExtension, CHttpServer) 7 G/ y( {% }; a0 G% Y
! g+ G J+ P a& J
//GetImage是CPinballExtension的成员函数,且有一个long型的参数即dwChoice
% N0 m. d( t0 ~% D/ m- tON_PARSE_COMMAND(GetImage, CPinballExtension, ITS_I4) 1 `3 N1 j! \5 G. k4 E
9 {" f4 q$ X; S6 b" Q/ r//该参数在URL中的名字为Favorite
; z' x U1 Y/ QON_PARSE_COMMAND_PARAMS("Favorite")
" k" L! u; J s: f9 I @$ ~$ y6 Q9 b# u! }. r* {
END_PARSE_MAP(CPinballExtension)
1 s2 Y C5 L% ?" W
/ b6 M0 r% x- | M) O而对于ISAPI Filter,在VC中可以通过重载CHttpFilter(或其子类)的不同的成员函数来实现对不同事件的处理。可重载的函数如下,每一个成员函数均对应一个或多个事件:
6 `9 D# o* B& O( e
: }; u3 @0 L5 r' }) W% hOnPreprocHeaders
8 A7 |" g. u2 S5 m& i xOnAuthentication
- Q! W& A; l% l- @& B! }OnUrlMap
" \: p" v& ?" c* u' ROnSendRawData 8 J, `9 b, h2 J# d% q$ K( L
OnReadRawData $ I, n/ {; G5 }. B) f
OnLog 4 A# o( o2 Z% b0 a
OnEndOfNetSession 9 S& E8 K0 Z* [+ Q! H& K# f; L
/ D# O/ T7 y4 l9 rMSDN提供了4个关于ISAPI的编程实例:counter、MFCUCASE、pinball、wwwquote,有兴趣的可看看,本文主要不是介绍编程,所以不再赘述。 , K- Z4 ^: ]. |# B. z
$ G$ e; {' ]' X参考资料: " P! Q, T+ w. b: t
+ p' G2 E: [; j: o) o. n3 V1、MSDN ) d& P, o7 O7 b4 {
2、《精通CGI编程》,丁一强等,清华大学出版社 |
|