IE異步可插入協議擴展應用

轉自:http://friton.blog.sohu.com/35969473.html

 

介紹

對於每天都要使用的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參數爲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已經沒有要傳的數據了,同時,調用事件接口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:[email protected]的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的功能無限延伸。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章