delphi99的专栏

编程不悔。热爱程序人生

[Delphi] 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,并通过设定ReportDatagrfBSCF参数为LastDataNotificationDataFullyAvailable通知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已经没有要传的数据了,同时,调用事件接口ProtSinkReportData方法通知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.netmailto: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的功能无限延伸。

 

转自Delphi深度探索

阅读更多
个人分类: Internet编程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭