使用 JavaScript 攔截和跟蹤瀏覽器中的 HTTP 請求

http://www.ibm.com/developerworks/cn/web/wa-lo-jshttp/index.html

HTTP 請求的攔截功能的應用

隨着互聯網應用及 B/S 架構的軟件系統的風行,越來越多的基於瀏覽器的應用得以開發和部署。對已經上線的應用系統進行攔截和跟蹤可以方便、快捷地實現很多功能。例如,

  1. IBM 的 Tivoli Access Manager 單點登錄(SSO,關於更多 TAM 的信息,請參見本文後附的資源列表)就是基於逆向代理服務器的原理實現的,逆向代理服務器的實現必須要對既有頁面中的一些 URL 進行攔截和改寫;
  2. 現今一些有趣的匿名代理服務器也是根據逆向代理服務器的原理實現的(如 http://www.youhide.com/),這類網站也有 URL 改寫的需求;
  3. 另外,IBM 提供的在線翻譯網頁的服務中,爲了能夠讓被翻譯頁面的鏈接所指向的頁面在用戶交互時繼續得到翻譯,我們同樣需要使用 URL 的攔截和改寫功能。

不僅僅是 URL 改寫,通過攔截和跟蹤技術可以在極小修改或者使用反向代理不修改原網頁的前提下爲基於 BS 的 Web 應用提供更復雜的頁面改寫,腳本改寫等功能。

基本原理

如圖 1 所示,傳統的 Web 訪問模型通過瀏覽器向 HTTP 服務器獲取 Web 數據,瀏覽器將獲取的數據進行解釋渲染,得到用戶的客戶端界面。

圖 1. 傳統 Web 訪問模型
圖 1. 傳統 Web 訪問模型

而在帶有服務器端和瀏覽器端跟蹤和攔截的訪問模型中,傳統的訪問方式發生了變化。服務器端和瀏覽器端的跟蹤和攔截將頁面中的攔截對象,在適當的攔截時機進行了攔截變化,導致用戶面前的瀏覽器的解釋渲染結果和 HTTP 服務器上的內容存在了邏輯上的差異,而這種差異恰巧是 HTTP Server 所需要的結果,而不方便將差異部署在 HTTP Server 上。 Server trace 可以作爲反向代理服務器的一項邏輯功能存在,Browser trace 是通過腳本完成在客戶端攔截和跟蹤行爲。

服務器端實現和客戶端瀏覽器實現的比較

攔截根據位置可以分爲服務器端和客戶端兩大類,客戶端攔截藉助 JavaScript 腳本技術可以方便地和瀏覽器的解釋器和用戶的操作進行交互,能夠實現一些服務器端攔截不容易實現的功能。如果將服務器端和客戶端攔截融合在一起,可以很好地處理攔截和跟蹤問題。

表 1. 服務器端攔截和客戶端攔截的功能比較
功能比較 服務器端攔截 客戶端攔截
向頁面頭部插入代碼 強。簡潔,無需逐個加入代碼 麻煩。需要逐個爲頁面加代碼
訪問資源的權限 強。可以訪問跨域資源。 受限。瀏覽器間有差異
會話的控制和訪問 強。可以使用相應 API 。 受限。需要使用 cookie
頁面 HTML 的攔截和跟蹤 一般。通過正則跟蹤。 強。可以使用 DOM 操作
頁面腳本的攔截和跟蹤 一般。通過正則跟蹤 強。可以利用解釋器環境
跟蹤用戶的交互行爲 一般。通過正則跟蹤 強。可以利用瀏覽器事件

從表 1 可以看出服務器端攔截和客戶端攔截具有很強的互補性,服務器端攔截擅長服務器 - 瀏覽器的通信控制、資源的訪問以及批量頁面的操作,瀏覽器端攔截可以依靠 Script 解釋器的環境,擅長個體頁面的邏輯攔截和跟蹤。本文因篇幅限制,主要針對使用 JavaScript 的 瀏覽器端的攔截和跟蹤進行介紹。不過在進入具體的實現之前,先介紹幾個術語。

攔截對象

攔截對象指的是需要被處理的元素,可能包括 [HTML 標籤 ],[HTML 標籤的屬性 ],[ 腳本變量 ],[ 對象 ],[ 函數 ] 等。攔截對象 和 攔截時機 就構成了一個個非常實際的功能或用途。

功能:攔截頁面中的所有鏈接,將其 href 屬性的值 xxx 改爲 http://proxyhost/agent?url=xxx

攔截對象 : <A> 標籤

攔截時機:根據需要適當選擇

功能:攔截頁面中的 document.write 的功能,在調用此函數前修改函數參數

攔截對象 : 對象方法

攔截時機:根據需要適當選擇

功能 : 攔截頁面中用戶自定義的函數 functionA 的調用

攔截對象: 用戶自定義函數

攔截時機 : 根據需要適當選擇

插入點和攔截時機

插入點: 插入點指的是攔截腳本 (JavaScript) 植入頁面文件的位置。

攔截時機: 攔截時機從頁面的載入過程可以分爲 [ 頁面加載前 ],[ 頁面加載中 ],[ 頁面加載後 ] 。

通常來說,在頁面加載後進行處理代碼相對簡單,但是功能也相對受限。頁面加載前處理代碼實現相對複雜,但是功能相對豐富。用戶可以根據需要混合使用。

從用戶的角度出發,自然希望插入點簡單,攔截時機和攔截點豐富。 因此本文的討論的攔截和跟蹤的技術的出發點也就明瞭了,即 : 在儘可能少的對原有系統的改變的前提下,通過 JavaScript 代碼跟蹤並改變系統的行爲與內容。

攔截時機的實現

攔截時機中講到的 [ 頁面加載前 ] 是指在用戶的 page 內容被瀏覽器解釋前進行攔截的時機。這種攔截可以在用戶沒有看到渲染結果的時候完成攔截修改,這種技術將在第五種方法中詳細介紹。

[ 頁面加載中 ] 攔截通常是將代碼放在頁面中合適的位置,實現代碼簡單,但是代碼的插入點 ( 也就是頁面的合適位置 ) 可能阻礙了這種時機的使用頻度,也違背了本文的初衷。因此 [ 頁面加載中 ] 的攔截時機通常使用 [ 頁面加載前 ] 的攔截時機進行替代。

例如 : 某 Web 頁面的第 100 行有如下內容:

<script>function userFunction() { //do some actions }</script>

在頁面的第 200 行調用了上面定義的函數,如:

<script>userFunction(); <script>

第 200 行的腳本會在頁面的渲染過程中發生作用,如果希望攔截的話,就需要在第 100 行後,200 行前對 userFunction 進行覆蓋,這樣就會在頁面的加載過程中完成攔截。這種做法嚴重依賴代碼的插入點,因此使用效果不好。

[ 頁面加載後 ] 攔截,首先在這裏介紹,實現簡單,能實現很多功能。

方法 : 通常在頁面的尾部 </body> 標籤前,加入:

<script type= "text/javascript" src="lib.js" ></script>

在 lib.js 文件中,通過改變 window.onload 的定義來構建我們的代碼入口。爲了保持功能,建議首先保存原來的函數的指針。典型的實現代碼可以參考如下 :

var oldOnload = window.onload; 
window.onload = function() {
    //code to insert
    if(oldOnload != null) { 
        oldOnload(); 
    } 
}

在文件末尾 </body> 前植入代碼,是爲了儘量防止其他加載代碼的覆蓋,如果不考慮加載過程中的代碼覆蓋的話,也可以考慮將代碼放在頁面的相對靠前的位置。

只要不是在加載過程中執行的代碼和效果都可以考慮通過 [ 頁面加載後 ] 這種攔截時機實現。這種代碼的結構簡單,可以很好的執行攔截和跟蹤任務。特別是針對函數覆蓋這種需求,通常情況下,onload 事件之前系統已經加載好所需要的 script 定義的函數和類,因此 onload 事件中的代碼覆蓋可以起到很好的作用。下面介紹的幾種方法都可以首先考慮這種攔截時機。

常見用法 : 在代碼的插入位置,可以植入一些 DOM 的操作,來實現頁面的變化。

例如 : 遍歷 DOM Tree, 修改特定 Node 的特定屬性,或者在特定位置插入新的 Node 等等。

攔截和跟蹤的主要方法及其實現

主要方法

在瀏覽器端的攔截和跟蹤主要是利用 JavaScript 的腳本環境完成,根據筆者的經驗,主要尋找並總結了如下的方法。這些方法的使用效果和支持平臺可以互相彌補。

表 2
名稱 特點 優點 缺點
利用瀏覽器的 Event 通過對 [ 鼠標事件 ],[ 鍵盤事件 ],[HTML 事件 ],[Mutation 事件 ] 的監聽,可以對用戶的交互,頁面的變化,特別是標籤節點的變化做出響應 瀏覽器自身支持,代碼量小,幾乎可以用來控制所有的 HTML 內容 此方法中的 Mutation Event,Firefox2.0 平臺已支持,IE6.0 尚未支持
通過 AOP 技術攔截 可以攔截大部分對象的方法調用。 很多 JS 代碼庫和框架已經支持 AOP 技術,代碼簡單 ActiveX 對象無法有效攔截。無法攔截普通的函數。另外單獨使用此項技術會造成插入點複雜。
覆蓋函數進行攔截 通過編寫同名方法覆蓋系統定義,用戶自定義的函數 ( 構造函數 ),達到攔截目的,對普通函數的攔截是對 AOP 的補充。 不依賴其他的代碼庫和 JS 框架,對系統函數的覆蓋有很好的效果,可以攔截構造函數用來控制對象的生成。 攔截自定義函數會造成插入點複雜
通過動態代理進行攔截 主要用來解決 ActiveX 對象的攔截問題,通過構造 ActiveX 對象的代理對象,實現攔截和跟蹤。 典型的例子如 IE 平臺下 AJAX 通信的攔截 代碼複雜,屬性更新的同步機制可能導致某些應用異常。
通過自代理和 HTML 解析進行攔截 此方法主要解決的是攔截時機的問題,配合上面的方法,就可以實現很多功能,而不必要等待頁面的 onload 事件。 實現瀏覽器端頁面加載前攔截的好方法 代碼複雜

主要方法的實現

利用瀏覽器的 Event

瀏覽器的事件也可以很好地用來攔截和跟蹤頁面。 鼠標和鍵盤的交互事件這裏暫不介紹。比較常用的是 onload,onunload 事件和 Mutation Events 事件。 onload 和 onunload 事件可以作用在 window,frame,img 和 object 等對象上,利用 onload 可以在對象載入前執行一些操作 ,onunload 事件可以跟蹤瀏覽器關閉前執行操作。

在瀏覽器的 Event 中,Mutation Eventsii 是更加重要的跟蹤工具之一。 Mutation Events 是 DOM2.0 標準的一部分,目前 Firefox2.x 已經開始支持 Mutation Events, IE6.0 目前尚不支持。 在 IE6.0 中可以是通過使用 onpropertychange 事件及覆蓋節點的方法彌補部分的不足。這裏重點介紹一下 Mutation Events 。

Mutation Event 主要包括了七種事件,如下所示。

  • DOMAttrModified:跟蹤 DOM 節點屬性的變化;
  • DOMCharacterDataModified:DOM 節點字符數據的變化;
  • DOMNodeInserted:DOM 新節點被插入到給定的父節點;
  • DOMNodeInsertedIntoDocument:DOM 節點被直接或隨着祖先節點而插入;
  • DOMNodeRemoved:DOM 節點被從父節點刪除;
  • DOMNodeRemovedFromDocument:DOM 節點被直接或跟隨祖先節點被刪除;
  • DOMSubtreeModified:DOM 元素或文檔變化。

可以說利用 Onload 事件的攔截,我們基本上解決了靜態 HTML 內容的攔截,而對於腳本操作的 HTML 變化,我們就可以通過 Mutation Event 來進行解決。

下面類似的實現框架可以用來跟蹤 src、action、href 等屬性的變化。

document.addEventListener("DOMAttrModified", AttributeNodeModified, false); 
function AttributeNodeModified(evt) 
{ 
    if(evt.attrName == "href") { } 
    if(evt.attrName == "src") { } 
    if(evt.attrName == "action") { } 
 }

通過 DOMAttrModified、DOMNodeInserted 等事件可以很好地跟蹤和攔截 HTML 的變化。只可惜在 IE6.0 平臺上還不能支持 Mutation Event,導致在 IE6.0 上進行攔截和跟蹤的效果大打折扣。針對此平臺,通過覆蓋 document.write/DomNode.appendChild/DomNode.insertBefore 等方法和利用 onpropertychange 事件可以有限度地支持攔截和跟蹤 HTML。

通過 AOP 技術攔截

針對對象方法調用的攔截,比較成熟的方案是使用 JavaScript 平臺下的 AOP 技術。

目前,JavaScript 平臺上的 AOP 方案主要有 Ajaxpectiii、jQuery AOPiv、Dojo AOPv 等。這些代碼庫主要功能是給指定對象的指定方法添加 Before, After,Around 等通知,從而達到攔截對象方法調用的目的 , 並且支持正則搜索方法名稱。

Ajaxpect 的示例代碼如下 :

var thing = {
    makeGreeting: function(text) {
        return 'Hello ' + text + '!'; 
    }
}

function aopizeAdvice(args) { 
    args[0] = 'AOP ' + args[0];return args;
}

function shoutAdvice(result) {
    return result.toUpperCase();
} 

Ajaxpect.addBefore(thing, 'makeGreeting', aopizeAdvice); 
Ajaxpect.addAfter(thing, /make*/, shoutAdvice);

當然,在不方便使用上述代碼庫並且需求簡單的時候,我們同樣可以通過對象的方法覆蓋的方式達到同樣的效果。但是無論 AOP 還是方法覆蓋, 都存在一個問題, 就是攔截代碼的插入點不能做到很簡捷,因爲攔截代碼的存在位置直接影響了代碼的執行效果,因此在使用上還有一定的不方便。另外,針對 IE 平臺的 ActiveX 對象,代碼庫不能很好的發揮功效,這是一些不足的地方。

覆蓋系統類 / 方法進行攔截

覆蓋已定義的函數是一種比 AOP 更直接的攔截和跟蹤腳本調用的方式。

其原理是在原函數定義後,調用前通過定義同名函數,達到攔截和跟蹤的目的。其一般形式多如下面 :

1: var oriFunction = someFunction; 
2: someFunction = function () {
3:     return oriFunction(); //or  oriFunction.call(x,);
4: }

第一步是(第一行代碼)爲了將指向原來函數的指針保存,以便後續使用。

第二步便是定義同名函數,在同名函數裏面的適當位置調用原來的功能。這種方法不但可以跟蹤原來函數,還可以修改和過濾函數的參數,甚至可以修改返回值。當需要操縱參數的時候,只需在新定義的函數中訪問 arguments 對象即可。

例如:針對系統函數 window.open(URL,name,specs,replace) 我們可以通過下面的代碼進行攔截:

var oriWindowOpen = window.open; 
window.open = function(url,names,specs,replace)  {
    url = "http://www.ibm.com"; //or arguments[0]="http://www.ibm.com";
    return oriWindowOpen(url,names,specs,replace); 
 }

上面的攔截會導致所有的 window.open 調用全部打開 http://www.ibm.com 窗口 。

函數覆蓋的適用範圍較廣,不但可以模擬 AOP 的實現,還可以對非對象函數進行操作。函數覆蓋可以根據使用的差異分成若干情況 :

  • 覆蓋系統定義的函數、對象的方法:覆蓋系統定義的函數或方法可以不用顧及代碼插入點的問題,大可以將函數覆蓋的代碼放置在頁面的最前邊,並參照上面的形式進行操作。但是特別注意在 IE 平臺下對 ActiveX 的對象的方法無效。
  • 覆蓋用戶自定義的函數、對象的方法:覆蓋用戶自定義的函數,對象的方法需要考慮代碼插入點的問題。正確的代碼插入點的位置應該是在原函數定義之後,調用之前。
  • 覆蓋構造函數:覆蓋構造函數是滿足上面兩種情況的一種特殊使用形式,跟蹤對象創建之除,可以有效地針對對象的需要作出各種特殊的設置。

    覆蓋構造函數的一般形式 :

    var oriFunction = someFunction;
    someFunction = function () {
        temp = oriFunction(); //oriFunction.call(x,);
        return temp;
    }

下面結合動態代理的方法給出 IE/Firefox 平臺的 Ajax 通信攔截的一種簡單實現。

Ajax 通信的核心是通過 XMLHttpRequest 對象和 HTTP Server 進行通信 ( 同步 / 異步 ),Firefox 和 IE 平臺對 XMLHttpRequest 對象的實現不一樣,因此兩種瀏覽器的攔截方案也大相徑庭。我們通過上面的技術將對 XMLHttpRequest 對象的方法進行跟蹤。

攔截方法調用,我們可以使用 AOP,當然也可以直接覆蓋函數。

在 Firefox 平臺,我們可以通過下面的代碼實現攔截 Ajax 對象的通信:

var oriXOpen = XMLHttpRequest.prototype.open; 
XMLHttpRequest.prototype.open = function(method,url,asncFlag,user,password) {
    //code to trace or intercept
    oriXOpen.call(this,method,url,asncFlag,user,password); 
};

但是在 IE 6.0 平臺,上面的代碼將不會有作用,因爲在 IE 6.0 平臺,Ajax 通信對象是通過 ActiveX 對象完成的,JS 中的函數覆蓋不能起到作用。

通過動態代理進行攔截

當在 IE6.0 平臺遭遇 ActiveX 對象的時候,面對直接的函數覆蓋不能奏效的時候,我們可以考慮通過另外一種辦法,即動態代理 ActiveX 對象的方式實現攔截和跟蹤。

首先我們通過覆蓋構造函數的方法,將創建 XMLHttpRequest 對象的過程進行改造。

var oriActiveXObject = ActiveXObject; 
ActiveXObject = function(param) {
    var obj = new oriActiveXObject(param);
    if(param == "Microsoft.XMLHTTP" || 
        param=="Msxml2.XMLHTTP" || 
        param == "Msxml2.XMLHTTP.4.0") { 
    	    return createActiveXAgent(obj); 
    } 
    return obj; 
 };

我們將構造過程攔截下來後,進行自己的改造,主要操作是創建對象,對象中設置與 ActiveX 對象相同的屬性和方法,並且還需要同步屬性方法。

function createActiveXAgent(ao) {
    var agent = new Object;
    agent.activeXObject = ao; //被包裹的內核,是真正的通信對象
    agent.syncAttribute = function() { //syncAttribute是用來同步屬性的	
        try{
            this.readyState = this.activeXObject.readystate;
            this.responseText = this.activeXObject.responseText;
            this.responseXML = this.activeXObject.responseXML;
            this.status = this.activeXObject.status;
            this.statusText = this.activeXObject.statusText;
        }catch(e) { }
    };
    agent.trigStateChange = function() { //模擬onreadystatechange
        agent.syncAttribute();
        if(agent.onreadystatechange != null) {
            agent.onreadystatechange();
        }
    };
    agent.activeXObject.onreadystatechange = agent.trigStateChange;
    agent.abort = function() { //模擬abort
        this.activeXObject.abort();
        this.syncAttribute();
    };
    agent.getAllResponseHeaders =function() 	{ //模擬內核對應的方法
        var result = this.activeXObject.getAllResponseHeaders();
        this.syncAttribute();  
        return result;
    };
    agent.getResponseHeader = function(headerLabel) { //模擬內核對應的方法
        var result = this.activeXObject.getResponseHeader(headerLabel);
        this.syncAttribute(); 
        return result;
    };
    agent.open = function(method,url,asyncFlag,userName,password) {
        //code to trace and intercept;
        this.activeXObject.open(method,url,asyncFlag,userName,password);
        this.syncAttribute(); 
    };
    agent.send = function(content) { //模擬內核對應的方法
        this.activeXObject.send(content);
        this.syncAttribute(); 
    };
    agent.setRequestHeader = function (label,value) { //模擬內核對應的方法
        this.activeXObject.setRequestHeader(label,value);
        this.syncAttribute(); 
    };
    return agent;
};

從上面的代碼可以看出來,代理對象通過自身的方法模擬了原來 ActiveX 對象的方法。而更關鍵的屬性問題,是通過在函數調用前後的屬性同步函數實現的。即:在調用代理內核方法之前,將屬性從代理對象同步給內核對象;在內核方法調用之後,將屬性從內核對象同步給代理對象。

因爲 AJAX 對象的屬性幾乎不被用戶寫入,故上面的實現只需要單向屬性同步,即將內核屬性同步給代理屬性。對於複雜的應用,可以通過雙向屬性同步函數來解決屬性的代理問題。

這種動態代理的方法將 ActiveX 對象像果核一樣包裹起來,通過代理對象自身的同名屬性和方法提供給外界進行訪問,從而達到跟蹤和攔截的目的。

通過自代理和 HTML 解析進行攔截

當代碼攔截點需要簡單可靠的時候,上面的方法無法很好的滿足需求。於是我們需要新的思路來解決代碼的攔截點問題。

自代理和 HTML 解析是通過攔截原有的 HTTP 通信,通過重新通信獲得內容後,在瀏覽器解析前通過我們自己的代碼進行簡單解析過濾的方式進行代碼處理的方案。

首先是攔截原有的解析,重新加載新的內容:

var s = document.location.href;
var comm = new ActiveXObject("Microsoft.XMLHTTP"); 
comm.open('get',s,false); 
comm.onreadystatechange = function() { 
    if(comm.readyState == 4) { 
        document.execCommand("stop"); 
        var retText = removeMe(comm.responseText);
        retText = processContent(retText);
    } 
} 
comm.send(null);

如果將上面的代碼寫在一個 js 文件裏,然後通過 <script> 標籤插入到頁面的最開始位置(<HTML> 後面)。

在 IE6.0 的瀏覽器下面,上面的代碼因爲使用了 XMLHTTP 的同步通信機制,因此代碼會阻塞在 comm.send(null) 處,當通信結束得到完整的頁面之後,會觸發 stop 導致瀏覽器停止解析,轉而執行我們的 processContent. removeMe 的意義在於重新獲得的片面中去除這段代碼自身,防止無窮迭代。

在 Firefox 下,我們需要使用 window.stop 代替上面的 execCommand.

當我們搶在瀏覽器之前拿到 HTML 內容後,我們下面的任務就是分析 HTML. 目前尚沒有成熟的 JS 分析 HTML 的框架。因此我們可以選擇將 HTML 轉換成 XML, 然後藉助 DOM 進行分析,也可以實現我們自己的 HTML 分析方案 .

我們可以將 HTML 的頁面分析成節點如下的一顆樹:

節點 { 
     父節點 ; 	
     屬性個數 ;  
     屬性集合 ; 
     子節點個數 ;  
     子節點集合  
 }
圖 2. HTML 文本分析狀態圖
圖 2. HTML 文本分析狀態圖

圖 2 是個簡單的 HTML 文本分析狀態圖,不支持 HTML 的 & 字符串拼接功能。可以反覆調用這個模塊用來從 HTML 文檔中提取字符塊生成相應的節點,然後可以利用 JavaScript 的正則表達時提取節點的屬性。

通過 HTML 文本分析狀態圖可以得到 HTML 解析的代碼,然後得到一個根爲 root 的節點。後面對樹進行進一步的處理,就可以實現很多攔截功能。比如 function 覆蓋。前面講到的用戶自定義函數覆蓋會受到代碼插入點複雜的影響。如果在這種方法的攔截下,就可以實現分析出 <script> 節點的內容中含有特定的 function 定義,進而替換或者在其後插入新的函數定義,不會造成插入點複雜的結果。

結論

通過上面的介紹,可以看出發揮 JavaScript 的優勢,我們能夠很好的控制頁面的加載過程,在不需要二進制瀏覽器插件的情況下,僅僅通過腳本對 Web 內面的邏輯攔截是可行的。這種方法在反向代理、網站的改版重構、以及不方便對原有內容進行直接改動的場合是十分有益的。


發佈了20 篇原創文章 · 獲贊 5 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章