用 Chrome 擴展實現修改

用 Chrome 擴展實現修改 ajax 請求的響應

用 Chrome 擴展實現修改 ajax 請求的響應

wincss wincss
4 個月前

背景

Fiddler 和 Charles 是常見的 HTTP 調試器,它們會在本地運行一個代理服務器,能夠查看瀏覽器或其它客戶端軟件經過這個代理髮起的請求和服務器的響應,也能夠在請求提交到服務器以前和服務器返回響應以後設置斷點,手工修改請求、響應的內容。另外,兩個軟件均可以以「中間人攻擊」的形式,解密 HTTPS 通訊。css

某天,我在調試某個網站的過程當中,但願把「修改 ajax 請求的響應」這一功能持久化下來方便使用。Fiddler 提供了自定義腳本的功能,但它的 macOS 版本和我用 Homebrew 安裝的 Mono 不兼容,並且我也不但願個人全部網絡流量都通過 Fiddler 代理。另外考慮到未來可能把這個功能小範圍發佈出來供其它人使用,那麼 Fiddler 或者 Charles 這種「重量級」解決方案就被否認了,因此我很天然地想到了使用 Chrome 擴展來完成此任務。web

目標

實現一個 Chrome 擴展,自動修改特定網站的 ajax 請求的響應。網站是一個重度使用 ajax 的網站,而且帶有比較嚴格的「反做弊」限制,對 Cookie、HTTP Header(Referer等)甚至多個 ajax 請求的順序都有要求,若是出錯就沒法繼續。ajax

方案1:WebRequest API 修改響應

從 Fiddler 這種方案天然而然地想到,若是 Chrome 也提供相似的「斷點」機制,外加 JavaScript 腳本,上述問題就解決了。因而搜了一下 「chrome extension hook ajax」,找到了Is there a hook for when an AJAX call returns? 第一個回答即是使用 WebRequest API 。chrome

因而開始學習 WebRequest API 的文檔,它能監聽的事件如上圖所示,最有可能知足條件的就是 onResponseStarted 了,但仔細一看發現它是一個異步事件,並不能實現「斷點」,也不能作任何改動(This event is informational and handled asynchronously. It does not allow modifying or cancelling the request)json

換個關鍵詞 「webrequest api modify response」 再次搜索,找到了和個人需求十分吻合的問題和答案:chrome extension - modifying HTTP response ,其中有三個有用的信息:api

  1. Chrome 的 issue:allow extension to edit response body - chromium - Monorail 意味着上述想法並不能實現
  2. 能夠替換頁面上的 XMLHttpRequest
  3. 用 WebRequest API 將請求重定向到 data:-URL
初看上去,第三個方案彷佛比較簡單,因此先按此執行。

方案2:WebRequest API 重定向

我想的方案是:在背景頁中以 blocking 模式註冊監聽 onBeforeRequest 事件,在事件處理函數中,從新發起這個 ajax 請求。此處要注意兩點:1.要在原頁面的環境中請求,大體思路是在頁面中注入腳本,背景頁使用消息機制和頁面腳本通訊,2.「從新發起」的 ajax 請求依然會被 onBeforeRequest 事件攔截,須要作額外的處理。「從新發起」的 ajax 請求拿到數據後,進行修改,最後編碼爲 data:-URL 做爲 blockingResponse 返回。瀏覽器

背景頁的代碼大體以下:服務器

chrome.webRequest.onBeforeRequest.addListener( function(details) { // 若是請求帶有特殊標記,則不進行修改 if (details.url.endsWith("#do_not_modify_this_request")) { return {} } // 使用消息機制將請求傳遞給頁面再發起 ajax,而不是在背景頁中發起 chrome.tabs.sendMessage(details.tabId, details, function(response) { // 此處能夠修改response... redirectUrl = "data:application/json;charset=UTF-8;base64," + Base64.encode(newResponse) }); return {redirectUrl: ...}; }, {urls: [...]}, ["blocking", "requestBody"] ); 
頁面腳本的代碼大體以下:
chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) { // 從新發起的請求要作標記,避免無限循環 var settings = { url: request.url + "#do_not_modify_this_request", method: request.method, dataType: "text" }; if (request.requestBody && request.requestBody.formData) { settings.data = request.requestBody.formData; } $.ajax(settings).done(function(data) { sendResponse(data); }); // 因爲 sendResponse 是異步調用的,須要返回 true return true; } ); 

前途是光明的,道路是曲折的。在這裏我就遇到了「暫時的困難」,寫完上面的代碼,我居然不知道怎麼寫下去了。前面說的方案,到這裏彷佛已經完成了 90% 了:背景頁攔截了 ajax 請求,在頁面注入腳本從新發起了請求,拿到告終果,進行了修改,編碼成 data:-URL。但差就差在最後一步上,這個 data:-URL 是在回調函數裏拼出來的,而執行回掉函數的時候,外層的事件處理函數早就應該返回啦。並無任何機制能夠在返回前「等一下」 sendMessage。網絡

繼續上搜索引擎,我看到了 54257 - The absence of synchronous message API make impossible to pass options to scripts that are loaded before the page to block content. - chromium - Monorail 這又是一個 issue ,並且結論仍是 WontFix 。app

其中有人提到,用 storage API 能夠解決,但下一條回覆反駁了他,由於 storage API 自己也是異步的。因而這個想法又一次失敗了。

方案2.5:不完美的嘗試

我把背景頁改了一下,頁面腳本保持不變:

chrome.webRequest.onBeforeRequest.addListener( function(details) { // 發起 ajax 請求的部分不變,再也不處理響應 if (details.url.endsWith("#do_not_modify_this_request")) { return {} } chrome.tabs.sendMessage(details.tabId, details, function(response) {}); // 直接生成新頁面,進行重定向 content = "......" return {redirectUrl: "data:application/json;charset=UTF-8;base64," + Base64.encode(content)}; }, {urls: [...]}, ["blocking", "requestBody"] ); 

方法簡單粗暴,ajax 請求照常發起(由於服務端的限制,若是不發起這個 ajax 請求的話,下一步其它 ajax 必然會返回錯誤,功能就沒法使用了),可是結果直接忽略,用預先準備好的假頁面直接返回。

這個方案實際執行時「時好時壞」,緣由是,事件處理函數返回後,頁面的下一個 ajax 請求就會發起,若是模擬 ajax 請求先於它發生,則結果正常。反之,若是下一個 ajax 請求發起時,個人模擬請求還沒有發送,那服務端就直接拒絕執行,返回「服務器繁忙」的錯誤,其實這「服務器繁忙」就是「發現你在做弊」的意思。

方案3:替換 XMLHttpRequest

最後,只剩下這一個方案了,上面那個答案說得很複雜,但搜一下仍是能找到「成品」方案。How can I modify the XMLHttpRequest responsetext received by another function? 中已經寫好了代碼,略作修改以下:

function modifyResponse(response) { var original_response, modified_response; if (this.readyState === 4) { // 使用在 openBypass 中保存的相關參數判斷是否須要修改 if (this.requestUrl ... && this.requestMethod ...) { original_response = response.target.responseText; Object.defineProperty(this, "responseText", {writable: true}); modified_response = JSON.parse(original_response); // 根據 sendBypass 中保存的數據修改響應內容 this.responseText = JSON.stringify(modified_response); } } } function openBypass(original_function) { return function(method, url, async) { // 保存請求相關參數 this.requestMethod = method; this.requestURL = url; this.addEventListener("readystatechange", modifyResponse); return original_function.apply(this, arguments); }; } function sendBypass(original_function) { return function(data) { // 保存請求相關參數 this.requestData = data; return original_function.apply(this, arguments); }; } XMLHttpRequest.prototype.open = openBypass(XMLHttpRequest.prototype.open); XMLHttpRequest.prototype.send = sendBypass(XMLHttpRequest.prototype.send); 

這段代碼會替換 XMLHttpRequest 中的 open 和 send 函數,在 open 中優先註冊 readystatechange 的事件監聽,以便在原頁面代碼執行前修改 responseText 的內容。

這段代碼不能直接注入頁面,由於 Chrome 擴展的 Content Script 會運行在隔離環境中,直接注入的話,並不能影響到頁面原有的 XMLHttpRequest。想要實現咱們想要的功能,能夠參考Building a Chrome Extension - Inject code in a page using a Content script 的作法。再寫一個文件:

var s = document.createElement("script"); s.src = chrome.extension.getURL("xmlhttp.js"); s.onload = function() { this.remove(); }; (document.head || document.documentElement).appendChild(s); 

這個功能一看就知道,在頁面上增長一個 <script> 標籤,src 屬性指向插件中的 xmlhttp.js。爲了讓這個文件能在頁面內被引用,須要在 manifest.json 里加一行:

"web_accessible_resources": ["xmlhttp.js"] 

須要注意的是:用這種方法插入的腳本,是沒法控制在什麼時候執行的(不像 Content Script,能夠設置 document_start、document_end、document_idle),而在此文件執行前發起的 ajax 是沒法被修改的。幸虧在個人需求中,這些 ajax 請求都是由用戶點擊觸發的,在那以前時間充足,足夠我動手腳了。

綜上所述,一個能修改 ajax 響應的 Chrome 擴展就寫好了。

相關文章
相關標籤/搜索