📄 ie异步可插入协议扩展(摘自哈巴狗的小窝).txt
字号:
IE异步可插入协议扩展(摘自哈巴狗的小窝)
IE异步可插入协议扩展
作者 陈省
介绍
对于每天都要使用的IE浏览器的人来说,输入www.google.com 等网址进行网上冲浪就象呼吸一样自然。大多数情况时,我们可能根本想不起来要在网址前面加上http:// 来声明要访问的是一个基于http协议的Web网站。所谓网络协议,其实无非就是一组描述如何获取不同资源并进行通讯的行为规则。IE浏览器除了内置了对 http协议外,还持ftp和gopher等协议。
从IE4开始,IE允许通过插入式异步协议扩展来扩展它处理协议的功能,人们可以通过自定义的扩展来让IE支持更多的协议,比如一些不是普遍支持的流媒体协议等。此外,我们还可以通过插入式协议扩展让IE可以以HTML文件的形式显示一个数据库中的表。
异步可插入协议的原理
可插入式协议是基于异步的URL Moniker技术的。Moniker最早是从OLE2中引入的概念,当时的Moniker就是一个COM绑定和定位对象,人们可以使用Moniker来定位并加载被保存到文件中的COM组件,实现COM的可持续性,一开始Moniker是基于同步方式实现的。随着网络技术的发展,定位并从网络上获取信息的需求逐渐超过了对本地数据的存取需求,因为网络的通讯通常都是不稳定的,因此需要以异步的方式来实现。为此微软设计了URL moniker对象来提供网络信息下载过程的一个统一接口,基于URL来访问网络资源的Moniker演变成了以异步方式实现的Moniker。
IE的URL moniker是在urlmon.dll动态连接库中实现的。当urlmon.dll处理http, ftp, Gopher等内置协议的访问时,它把访问请求转发给内部的一个COM组件来处理,该COM组件使用WinInet函数来完成实际的处理工作。对于非内置的协议,urlmon.dll则把请求转发给特定的可插入协议扩展进行处理,比如说mailto:协议。
一个典型的异步可插入协议(APP)的主要工作的就是接收一个非IE内置的UrlURL协议字符串,对字符串进行解析,分析字符串的元素,并根据协议访问相应的系统或者网络资源,并将网络资源的内容输出到浏览器。
一个自定义的电子书可插入协议的实现
我平时业余时间喜欢上网上找一些娱乐小说和技术书籍来看,其中有一些小说采用的是付费方式才能看既然是付费的小说,自然会提供一些加密的方式,避免盗版书在网上的传播。
接下来,我想写一个程序对一些Html文件进行加密,只有用户在浏览器中键入EBook://c:\abc.htm,然后输入口令后,才能看到解密后的Html页面。接下来,就看如何使用APP来实现这样一个可插入协议。
创建COM组件
首先,新建一个ActiveX Library项目,保存为IEProtocol.dpr,然后新建一个名为TIEEncryptAPP的COM组件,保存为 CIEProtocol.pas文件。一个APP组件至少要实现IInternetProtocol接口(该接口定义在urlmon.pas单元中),又由于IInternetProtocol接口派生自IInternetProtocolRoot,所以我们还需要实现 IInternetProtocolRoot接口。下面是实现了IInternetProtocol接口的TIEEncryptAPP类的定义:
type TIEEncryptAPP = class(TComObject, IInternetProtocol) protected //IInternetProtocolRoot接口定义 function Start(szUrl: LPCWSTR; OIProtSink: IInternetProtocolSink; OIBindInfo: IInternetBindInfo; grfPI, dwReserved: DWORD): HResult; stdcall; function Continue(const ProtocolData: TProtocolData): HResult; stdcall; function Abort(hrReason: HResult; dwOptions: DWORD): HResult; stdcall; function Terminate(dwOptions: DWORD): HResult; stdcall; function Suspend: HResult; stdcall; function Resume: HResult; stdcall; //IInternetProtocol接口定义 function Read(pv: Pointer; cb: ULONG; out cbRead: ULONG): HResult; stdcall; function Seek(dlibMove: LARGE_INTEGER; dwOrigin: DWORD; out libNewPosition: ULARGE_INTEGER): HResult; stdcall; function LockRequest(dwOptions: DWORD): HResult; stdcall; function UnlockRequest: HResult; stdcall; end;
其中IInternetProtocolRoot接口的方法意义如下:
Abort 停止一个正在进行的资源下载过程
Continue 允许协议扩展继续进行进行资源数据下载过程。
Resume 未来扩充需要,暂时未实现。
Start 启动同该协议相关的资源下载过程。
Suspend 未来扩充需要,暂时未实现
Terminate 结束下载过程,释放扩展分配的资源。
而IInternetProtocol协议的方法定义如下:
LockRequest锁定资源下载请求,这时IInternetProtocolRoot接口的Terminate方法将允许被调用,与此同时未下载完的数据仍然可以被读取。
Read浏览器调用这个方法从协议扩展获得相应的数据。
Seek移动读取数据的位置。
UnlockRequest释放请求锁定
对于电子图书这样一个简单的协议扩展来说,我们只需要实现Start方法来启动下载过程,并通过Read方法向浏览器返回解密后的电子图书的数据就可以了。其它的方法只要简单的返回请求结果,而无须做任何的操作:
function TIEEncryptAPP.Abort(hrReason: HResult; dwOptions: DWORD): HResult;begin Result := Inet_E_Invalid_Request;end; function TIEEncryptAPP.Continue( const ProtocolData: TProtocolData): HResult;begin Result := Inet_E_Invalid_Request;end; function TIEEncryptAPP.LockRequest(dwOptions: DWORD): HResult;begin Result := S_OK;end; function TIEEncryptAPP.Resume: HResult;begin Result := Inet_E_Invalid_Request;end; function TIEEncryptAPP.Seek(dlibMove: LARGE_INTEGER; dwOrigin: DWORD; out libNewPosition: ULARGE_INTEGER): HResult;begin Result := E_Fail;end;
function TIEEncryptAPP.Suspend: HResult;begin Result := Inet_E_Invalid_Request;end; function TIEEncryptAPP.Terminate(dwOptions: DWORD): HResult;begin Result := S_OK;end; function TIEEncryptAPP.UnlockRequest: HResult;begin Result := S_OK;end;
启动协议处理
首先来看如何启动协议处理,当我们在浏览器中输入EBook://c:\ebook.htm字符串想要浏览加密的页面文件时,IE会找到EBook的扩展协议,然后调用协议的Start方法来启动协议处理过程:
threadvar ResultHTML: array[0..64 * 1024 - 1] of Char; { 64 kB } CurrPos: Integer; BytesLeft: Integer; ProtSink: IInternetProtocolSink;
function TIEEncryptAPP.Start(szUrl: LPCWSTR; OIProtSink: IInternetProtocolSink; OIBindInfo: IInternetBindInfo; grfPI, dwReserved: DWORD): HResult;Const ErrorHTML = '<HTML><BODY BGCOLOR="#FFFFFF">'#13+ '<H2>浏览电子书%s时发生错误</H2>'#13+ '<P><I>%s</I></P>'#13+ '</BODY></HTML>';var S: string;begin S := WideCharToString(szURL); { EBook:// } Delete(S, 1, 8); //去掉后面/符号 SetLength(S, Length(S) - 1); S := HTTPDecode(S); if FileExists(S) then begin //显示密码提示框 if InputBox('密码','请输入密码', '')<>'hubdog' then S:=Format(ErrorHTML, [S, '无效的密码']) else S := Decrypt(S); end else S := Format(ErrorHTML, [S, '没有找到文件']); CurrPos := 0; BytesLeft := Length(S); FillChar(ResultHTML, SizeOf(ResultHTML), 0); StrPCopy(ResultHTML, S); ProtSink := OIProtSink; //数据通知 OIProtSink.ReportData(bscf_LastDataNotification, 0, BytesLeft); //数据可完全获得的通知 OIProtSink.ReportData(bscf_DataFullyAvailable, 0, BytesLeft); Result := S_OK;end;
Start方法中有一个szUrl的参数,对应着我们在浏览器中输入的url字符串(注意:IE会在输入的字符串末尾自动加上一个斜杠),为了获得要处理的被加了密的html文件,使用Delete函数先从字符串中删除EBook://8个字符,然后在用SetLength去掉IE添加的斜杠,同时要注意IE传过来的字符串参数是进行Http编码的,所以还要调用HttpApp单元中的HttpDecode来进行解码还原为c:\ebook.htm的文件名字符串。
如果输入的文件存在的话,则提示用户输入密码,如果密码匹配的话,则调用Decrypt函数对文件进行解密并,返回解密后的文本串。如果文件不存在,或者密码不匹配,则生成ErrorHtml返回一个错误描述的HTML页面。关于加密和解密过程,比较简单,我会在后面介绍。
获得解密后的文本后,将文本内容复制到ResultHTML字符串缓冲区中(这里的缓冲区处于简单的考虑,写死成64K)。另外要注意的是这里用的参数都使用ThreadVar来声明,这是因为协议处理过程是一个多线程异步的过程,同一时刻,可能有多个EBook的协议请求在处理中,所以变量都要声明为线程安全的,以避免资源冲突。接下来保存IE通过Start方法传过来的OIProtSink协议处理事件接口(稍后还会用到),然后调用接口的ReportData方法通知IE要获取的数据量为BytesLeft,并通过设定ReportData的grfBSCF参数为LastDataNotification 和DataFullyAvailable通知IE,数据已经完全准备好了,这样稍后IE就会调用扩展的Read方法来获得解密后的页面数据。
返回解密数据
function TIEEncryptAPP.Read(pv: Pointer; cb: ULONG; out cbRead: ULONG): HResult;var I: Integer;begin if (BytesLeft > 0) then begin I := CB; if (I > BytesLeft) then I := BytesLeft; Move(ResultHTML[CurrPos], PV^, I); CBRead := I; Dec(BytesLeft, I); Inc(CurrPos, I); Result := S_OK; {通知IE读取更多的数据 } end else begin //数据全部下载完成 Result := S_False; ProtSink.ReportResult(S_OK, 0, nil); end;end;
在Read 方法中,IE会传过来一个内部缓冲区的指针pv,同时cb参数表示缓冲区的大小,电子书的数据有可能会很大,而IE的缓冲区不会无限大,因此IE会分多次来读取电子书的数据,我们每次应该尽可能读取cb大小的数据,将其移动到IE的缓冲区内,读取完成后减少BytesLeft的值,同时增加CurrPos 的值来记录当前以发送给IE的数据位置,并返回cbRead告诉IE传送的数据到底有多少。如果一次没有返回全部的数据,则返回S_OK通知IE还有没传送完的数据,这样IE就会继续调用Read方法来完成数据下载,最后当所有的数据都处理完毕后,则返回S_False通知IE已经没有要传的数据了,同时,调用事件接口ProtSink的ReportData方法通知IE,协议处理完毕。
加密解密
还是为了简单起见,html页面的加密非常简单,我使用XOR加密,这样的好处是,处理简单。因为XOR加密和解密是一个可逆过程,加密和解密使用同一个函数就可以完成了。下面是加密和解密字符串类:
type //加密字符串类 TEncryptStrings = class(TStringList) public procedure SaveToStream(Stream: TStream); override; end; //解密字符串类 TDecryptStrings = class(TStringList) public procedure LoadFromStream(Stream: TStream); override; end; implementation //用xor算法进行加密 procedure EncodeStream(Input, Output: TStream);var InBuf: array[0..1023] of byte; BufPtr: PChar; I, BytesRead: Integer;begin Assert(Assigned(Input), '无效的流指针'); //必须重新设置流指针位置 Input.Position := 0; Output.Position := 0; repeat BytesRead := Input.Read(InBuf, SizeOf(InBuf)); I := 0; while I < BytesRead do begin InBuf[I] := InBuf[I] xor 8; Inc(I); end; OutPut.Write(InBuf, BytesRead); until BytesRead = 0; Input.Position := 0; Output.Position := 0;end; { TDecryptStrings } procedure TDecryptStrings.LoadFromStream(Stream: TStream);var OutStream:TMemoryStream;begin //解密 OutStream:=TMemoryStream.Create; try EncodeStream(Stream, OutStream); inherited LoadFromStream(OutStream); finally OutStream.Free; end;end; { TEncryptStrings } procedure TEncryptStrings.SaveToStream(Stream: TStream);var OutStream: TMemoryStream;begin inherited; //加密 OutStream := TMemoryStream.Create; try EncodeStream(Stream, OutStream); Stream.CopyFrom(OutStream, 0); finally OutStream.Free; end;end;
为了减少编码工作量,我直接从TStringList类派生了两个字符串列表处理类,并重载了LoadFromStream和SaveToStream方法来对流进行加解密处理。加解密处理都是调用的EncodeStream方法来对字符串流进行加密,加密使用每个字符同8进行xor运算。
下面我写了一个程序,可以对html文件进行处理点击Button1,则将文件进行加密处理,点击Button2可以对察看解密后文件的原有内容:
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -