轉自: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
,並通過設定
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:[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的功能無限延伸。