
在开发浏览器插件,或者写油猴脚本的时候,需要拦截一些 ws/http 请求。
本文来简单总结一下用过的实践。
在 Chrome Extensions 中拦截请求
在插件中,可以通过 chrome.webRequest API 非常便捷地实现拦截。
webRequest - MDN
在 Manifest V3 平台下,屏蔽网络请求/重定向多个网址等需求无法再用 webRequest API。
See developer.chrome.com - 替换屏蔽型 Web 请求监听器
1 2 3 4 5 6
| "permissions": [ "webRequest", ],
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| chrome.webRequest.onBeforeRequest.addListener( details => { console.log(details.url) }, { urls: ["<all_urls>"] } )
chrome.webRequest.onCompleted.addListener( details => { console.log(details) }, { urls: ["<all_urls>"] } )
|
有关 details 对象,see onResponseStarted#details_2 - MDN
这一 API 可以轻松的获取请求头/请求体/响应头等信息。但是其无法获取到 response body,所以无法获得或修改响应结果。这就对需要 hook 到详细内容的功能不友好。
(这里不讨论用 devtools API 的方法,感觉不太通用)
考虑脚本注入
脚本注入即是将一段 JS 插入到页面某一位置中执行。可以通过事件系统 + 修改 window.XMLHttpRequest/fetch/WebSocket 对象的方式实现 hooks。
拦截 XHR 请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| ;(function () { function ajaxEventTrigger (event) { const ajaxEvent = new CustomEvent(event, { detail: this }) window.dispatchEvent(ajaxEvent) }
const OldXHR = window.XMLHttpRequest
function NewXHR () { const realXHR = new OldXHR()
realXHR.addEventListener('readystatechange', function () { ajaxEventTrigger.call(this, 'ajaxReadyStateChange') }, false) const setRequestHeader = realXHR.setRequestHeader realXHR.requestHeader = {} realXHR.setRequestHeader = function (name, value) { realXHR.requestHeader[name] = value setRequestHeader.call(realXHR, name, value) } return realXHR }
window.XMLHttpRequest = NewXHR })()
|
1 2 3 4 5 6 7 8
| window.addEventListener('ajaxReadyStateChange', e => { const xhr = e.detail if (xhr.readyState === 4 && xhr.status === 200) { handleResponse(xhr) } })
|
拦截 fetch 请求
fetch API 基于 Promise,因此拦截请求不必引入事件系统。这里给出一种方案:
1 2 3 4 5 6 7 8 9 10 11 12 13
| fetch = (...args) => { return Promise.resolve(args) .then(args => beforeHook(...args)) .then(args => { const request = new Request(...args) return oldFetch(request) }) .then(resp => afterHook(resp)) }
const beforeHook = () => { } const afterHook = () => { }
|
See github.com/mlegenhausen/fetch-intercept
拦截 ws 请求
这里给出一个简化版的 ws hook。思路很简单,在原生 WS 的事件中插入一个钩子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| function MutableMessageEvent (o) { this.bubbles = o.bubbles || false this.cancelBubble = o.cancelBubble || false this.cancelable = o.cancelable || false this.currentTarget = o.currentTarget || null this.data = o.data || null this.defaultPrevented = o.defaultPrevented || false this.eventPhase = o.eventPhase || 0 this.lastEventId = o.lastEventId || '' this.origin = o.origin || '' this.path = o.path || new Array(0) this.ports = o.parts || new Array(0) this.returnValue = o.returnValue || true this.source = o.source || null this.srcElement = o.srcElement || null this.target = o.target || null this.timeStamp = o.timeStamp || null this.type = o.type || 'message' this.__proto__ = o.__proto__ || MessageEvent.__proto__ }
const wsHook = { before: data => data, after: e => e }
const _WS = WebSocket WebSocket = function (url) { this.url = url const WSObject = new _WS(url)
const _send = WSObject.send WSObject.send = function (data) { arguments[0] = wsHook.before(data) || data _send.apply(this, arguments) }
WSObject._addEventListener = WSObject.addEventListener WSObject.addEventListener = function () { const eventThis = this if (arguments[0] === 'message') { arguments[1] = (function (userFunc) { return function instrumentAddEventListener () { arguments[0] = wsHook.after(new MutableMessageEvent(arguments[0])) if (arguments[0] === null) return userFunc.apply(eventThis, arguments) } })(arguments[1]) } return WSObject._addEventListener.apply(this, arguments) }
return WSObject }
|
1 2 3 4 5 6 7 8
| wsHook.before = (data) => { console.log(data) } wsHook.after = (messageEvent) => { console.log(messageEvents) }
|
在浏览器拓展中注入 hooks
由于在浏览器拓展 content_scripts 环境与真实的页面环境隔离,只能获取 DOM 等,无法直接更改 window.XMLHttpRequest/fetch/WebSocket 对象。所以必须借助 script 标签注入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| // manifest.json { "content_scripts": [ { "matches": ["http://*/*", "https://*/*"], "js": ["content-script.js"] } ], "web_accessible_resources": [ { "resources": ["injected-script.js"], "matches": ["http://*/*", "https://*/*"] } ] }
|
1 2 3 4 5
| const scriptDOM = document.createElement('script') scriptDOM.setAttribute('src', 'injected-script.js') document.head.appendChild(s)
|
1 2 3 4 5 6 7 8
|
window.XMLHttpRequest = newXHR
window.addEventListener('ajaxReadyStateChange', handleFunc)
|
参考
https://github.com/debingfeng/blog/blob/master/docs/javascript/practise/JavaScript%E7%9B%91%E5%90%AC%E6%89%80%E6%9C%89Ajax%E8%AF%B7%E6%B1%82%E7%9A%84%E4%BA%8B%E4%BB%B6.md
https://github.com/mlegenhausen/fetch-intercept
https://github.com/skepticfx/wshook