介绍
对于每天都要使用的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可以对察看解密后文件的原有内容:
procedure TForm1.Button1Click(Sender: TObject);
var
Strings:TEncryptStrings;
begin
if not OpenDialog1.Execute then Exit;
Strings:=TEncryptStrings.Create;
try
Memo1.Lines.LoadFromFile(OpenDialog1.FileName);
Strings.Text:=Memo1.Text;
Strings.SaveToFile(OpenDialog1.FileName);
Memo2.Lines.LoadFromFile(OpenDialog1.FileName);
finally
Strings.Free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
Strings:TDecryptStrings;
begin
if not OpenDialog1.Execute then Exit;
Strings:=TDecryptStrings.Create;
try
Memo1.Lines.LoadFromFile(OpenDialog1.FileName);
Strings.LoadFromFile(OpenDialog1.FileName);
Memo2.Lines.Text:=Strings.Text;
finally
Strings.Free;
end;
end;
界面如下:
注册扩展
完成了扩展协议后,只剩下注册扩展了,要想注册扩展,需要在注册表的HKEY_CLASSES_ROOT\PROTOCOLS\Handler\下添加EBook关键字,然后在该关键字下添加名为CLSID的字段,设定其值为扩展的Guid,下面是用于注册的类工厂:
type
TIEEncryptAPPFactory = class(TComObjectFactory)
public
procedure UpdateRegistry(Register: Boolean); override;
end;
{ TIEEncryptAPPFactory }
procedure TIEEncryptAPPFactory.UpdateRegistry(Register: Boolean);
begin
inherited;
if Register then
CreateRegKeyValue(HKEY_CLASSES_ROOT, 'PROTOCOLS\Handler\EBook', 'CLSID',
GuidToString(ClassID))
else
DeleteRegKeyValue(HKEY_CLASSES_ROOT, 'PROTOCOLS\Handler\EBook', 'CLSID');
end;
initialization
TIEEncryptAPPFactory.Create(ComServer, TIEEncryptAPP, Class_IEEncryptAPP,
'IEEncryptAPP', '', ciMultiInstance, tmApartment);
end.
最后,将本书光盘中的ebook.htm文件放到c:根目录下,注册扩展后,启动IE,输入ebook://c:\ebook.htm,然后在弹出的密码框中输入hubdog,IE就会显示解密后的电子小说,界面示意如下:
临时注册扩展
上面的注册方法可以称为持久注册的方法,一旦注册就总是生效,。IE还提供临时注册的方法,只要编写一个BHO扩展,在BHO加载时,调用TemporyRegister方法进行注册,在IE退出时调用:
var
Factory:IClassFactory;
procedure TemporaryRegister;
begin
CoGetClassObject(Class_IEEncryptAPP
, CLSCTX_SERVER, nil, IClassFactory, Factory);
CoInternetGetSession(0, InternetSession, 0);
InternetSession.RegisterNameSpace(Factory, Class_IEEncryptAPP
, 'EBook', 0, nil, 0);
end;
procedure UnRegister;
begin
InternetSession.UnregisterNameSpace(Factory, 'EBook');
end;
这样的好处是,在程序运行时,可以随时解除对扩展协议的支持,而前面的永久注册法必须在解除注册后,重新启动IE才行。缺点是必须通过一个BHO来实现临时注册。
其它的APP
除了上面的协议扩展外,IE还支持NameSpace Handler以及Mime-Handler两种APP扩展。其中NameSpace扩展是对特定名字空间进行处理的协议扩展,比如如果我们注册一个对名字空间<hubdog>,则当IE处理http://hubdog.csdn.net、mailto:hubdog@263.net的URL时,一旦遇到hubdog名字空间,就会调用我们的NameSpace Handler进行处理,而不管URL是基于http协议的还是ftp等其它协议的都进行处理。从实现的角度来看,NameSpace的实现方法和前面的协议扩展几乎一样,除了注册时要填写的注册表项内容不同而已。
而Mime协议扩展处理的主要是对一些特殊的媒体资源如图片,声音文件进行处理,比如下表是IE默认支持的一些媒体形式。
text/richtext |
text/html |
audio/x-aiff |
audio/basic |
audio/wav |
image/gif |
image/jpeg |
…
如果那天哪天你发明一种新的音乐形式,比如扩展名为.sy,就可以注册一个Mime扩展对 .sy文件处理,让IE播放相应的声音。
Mime扩展除了需要支持IInternetProtocol接口外,还必须实现IInternetProtocolSink接口,接口定义如下:
IInternetProtocolSink = interface
['{79eac9e5-baf9-11ce-8c82-00aa004ba90b}']
function Switch(const ProtocolData: TProtocolData): HResult; stdcall;
function ReportProgress(ulStatusCode: ULONG; szStatusText: LPCWSTR): HResult; stdcall;
function ReportData(grfBSCF: DWORD; ulProgress, ulProgressMax: ULONG): HResult; stdcall;
function ReportResult(hrResult: HResult; dwError: DWORD; szResult: LPCWSTR): HResult; stdcall;
end;
数据通讯方式上来看,Mime扩展同一般的协议扩展差别比较大,通讯的流程是这样的:
1. 首先,IE会在遇到相应资源下载请求时,调用扩展的Start方法来启动下载过程。
2. 然后IE会调用扩展的ReportProgress方法,告知扩展被下载的数据保存的缓存文件名称。
3. 当IE下载完原始数据后,会调用扩展的ReportData方法通知扩展准备对原始数据进行加工处理。
4. 这时,扩展需要调用IE提供的IInternetProtocol接口的Read方法来获得原始数据。
5. 对原始数据处理后,扩展要调用IE的IInternetProtocolSink接口的ReportData方法通知IE数据处理完毕。
6. 最后,IE调用扩展的Read方法获得处理后的数据。
可以看出来同一般协议扩展的纯主动向IE返回数据的方式不同,Mime的数据通讯方式即有被动的接收IE获取的原始数据,也有将处理后的数据返回IE的主动通讯方式。
由于本质上来看,Mime同一般的APP的实现相差不多,所以这里我将不再浪费篇幅来给出Mime扩展的实现实例了。
总结
IE早已经不再是一个单纯意义的Web浏览程序了,通过对IE支持的协议扩充,我们可以将IE变成一个网络开发平台,可以将IE的功能无限延伸。