详解XMLHttpRequest响应拦截的原型属性覆盖方法
在 JavaScript 中有两种发起 HTTP 请求的 API – 现代的fetch()和传统的 XMLHttpRequest。它们功能完全相同,只是语法不同。XMLHttpRequest 使用回调处理响应,而fetch()返回更方便使用的 Promise。
XMLHttpRequest 是发起 HTTP 请求的主流 API。在新项目中使用传统的 XMLHttpRequest 是没有意义的。
另一方面,将现有可运行的基于 XMLHttpRequest 的代码升级到fetch()并不会带来显著好处。那些经过多年开发、拥有大量代码库的成功网站,没有理由在代码中用fetch()替换 XMLHttpRequest。将他们可运行的代码升级到fetch()只会带来 bug 和风险。
我检查了我所知道的一些流行网站的网络活动。google、youtube、gmail、bing、linkedin、tiktok、instagram、facebook 主要依赖 XMLHttpRequest,也使用一些fetch()。reddit、quora 则不使用 XMLHttpRequest。
为什么要重写 XMLHttpRequest 中的 response
首先,在前端开发和调试过程中,在网页接收到 HTTP 响应之前修改响应是一个有用的技术。通过重写 XMLHttpRequest,可以在不改变后端的情况下记录、伪造或调整响应体。
这种技术也可用于网页爬虫,并且使一些浏览器扩展的功能得以实现。
但是如果网页需要多次重新加载,比如在开发或调试期间,最好不要在控制台执行修改响应数据的脚本,而是作为浏览器扩展的 content script 自动执行。当脚本作为 content script 注入时,可以方便地由小型可重用模块组成。
拦截 HTTP 响应数据的示例
如上所述,许多流行网站都使用 XMLHttpRequest 发起 HTTP 请求。在这个实验中我使用知名且信誉良好的 Facebook。。Facebook 的初始 HTML 是在服务器端渲染的,所以不能通过修改 XMLHttpRequest 响应来修改它。但是之后逐渐加载的内容可以在被网页访问之前进行修改:
-
words mushroom、fungus 或 fungi 被替换为字符串 🍄🍄🍄REPLACED🍄🍄🍄 -
jpg 图片的 URL 被替换为一个蘑菇图片的 URL
HTTP 响应中的文本修改是通过以下函数完成的:
var RE = /"[^"]+\.jpg?[^"]+"/gi;
var REPLACEMENT = '"https://scontent-zrh1-1.xx.fbcdn.net/v/t39.30808-6/272917062_10157959892971991_7437132751388296237_n.jpg?_nc_cat=100&ccb=1-7&_nc_sid=127cfc&_nc_ohc=g7Qun1RfEvgQ7kNvgFVEOv6&_nc_ht=scontent-zrh1-1.xx&_nc_gid=AmiHBSQhbkAppb0buDWHP2N&oh=00_AYAYpDPV90lNRXvX2-bftFkUPHcqQJYVBmsE8BZnyNvqmg&oe=66EB3BAE"'
.replaceAll('/', '\\/');
var RE2 = /mushroom|fungus|fungi/gi;
var REPLACEMENT2 = '🍄🍄🍄REPLACED🍄🍄🍄';
function modifyTextResponse(val) {
if (typeof val === 'string')
return val.replaceAll(RE, REPLACEMENT).replaceAll(RE2, REPLACEMENT2);
return val;
}
下面的示例脚本使用了这个 modifyTextResponse(val)函数。
这个蘑菇图片很好看但 URL 很长且难看。Facebook 页面的内容安全策略(CSP)阻止从其他来源加载图片。我本可以使用反 CSP 浏览器扩展来放宽 CSP,但为了简单起见,我遵守了 CSP 并使用了 Facebook 托管的图片。
在视频中,脚本在页面加载时自动作为 content script 注入。
{
"name": "XMLHttpRequest",
"version": "1.0",
"manifest_version": 3,
"description": "XMLHttpRequest",
"permissions": [
"scripting"
],
"action": {},
"icons": {
"128": "icon.png"
},
"content_scripts": [
{
"matches": [
"https://*.facebook.com/*"
],
"run_at": "document_start",
"js": [
"main.js"
],
"world":"MAIN"
}
]
}
脚本必须注入到页面上下文中,即 MAIN world。重写原生 JavaScript 方法是 MAIN world 的主要用例,否则这个 world 没有扩展 API,用处不大。
访问 XMLHttpRequest 响应数据的唯一方式
XMLHttpRequest 中有几个提供访问响应数据的属性:
-
response -
responseText -
responseXML
这些属性都是 getter 函数。要重写任何类型的响应,只需要重写 response getter 就够了。responseText 和 responseXML 似乎只是通过转换 response 的值来工作。
但是需要了解什么时候进行重写。有两个合理的选择:
-
readystatechange 事件监听器 -
open()方法
XMLHttpRequest 的所有可能事件
我们看看 HTTP 请求期间发生的所有事件。
<script src="api.js" type="module"></script> <button type="button" id="btn">Send</button>
脚本为所有以 on 开头的 XMLHttpRequest 属性添加监听器:
// api.js
const url = "https://data.cdc.gov/api/views/95ax-ymtc/rows.json";
function onEvent(e) {
console.log(e.type.padEnd(16, ' '), this.readyState, this.response.length, e.loaded);
}
function request() {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
for (let k in xhr)
if (k.startsWith('on'))
xhr[k] = onEvent;
}
btn.addEventListener("click", request);
这个脚本请求一些公开可用的数据。
你可以看到,readystatechange 监听器可能被多次调用,甚至可以访问未完全加载的数据。某些网站可能不会等待 readyState===4 就立即使用不完整的数据。
下面的代码通过在 XMLHttpRequest 对象中创建新的 response 属性来重写 prototype 中的 response getter:
if (!oldXHROpen)
var oldXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {
let oldOnreadystatechange = this.onreadystatechange;
this.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE) {
const txt = this.responseText;
if (txt) {
Object.defineProperty(this, 'response', { writable: true });
this.response = modifyTextResponse(txt);
}
}
if (oldOnreadystatechange)
return oldOnreadystatechange.apply(this, arguments);
};
return oldXHROpen.apply(this, arguments);
}
这种方法适用于许多网站,但在 Facebook 上会产生异常效果 – 关键词没有被替换,而且页面很快就会崩溃。这可能是因为 Facebook 页面在数据完全加载之前就使用了数据。因此,创建一个从原生 getter 读取数据的 getter 而不是属性是至关重要的。新的 getter 应该在所有可能的事件回调中都能正常工作。定义 getter 的唯一可能位置是open()方法。
代理 getter 转换继承 getter 返回的值
这段不会出错的代码定义了一个 response getter 函数,它首先通过在 this 对象上调用 prototype 中的 response getter 获取真实的响应值,然后返回用当前响应值调用modifyTextResponse()产生的值:
function defineProxyGetter(obj, property, func) {
Object.defineProperty(obj, property, {
get() {
const val = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, property).get.call(obj);
return func(val);
}
});
}
if (!oldXHROpen)
var oldXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {
defineProxyGetter(this, 'response', modifyTextResponse);
return oldXHROpen.apply(this, arguments);
}
在这篇文章中我修改了文本数据,因为这种修改更常见且结果容易可视化,但同样的方法应该也适用于 blob 或任何类型的响应数据。当然modifyTextResponse()应该替换为合适的函数。
以上关于详解XMLHttpRequest响应拦截的原型属性覆盖方法的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 详解XMLHttpRequest响应拦截的原型属性覆盖方法
微信
支付宝