前言
为了模拟实现navigator plugins与mimetyps,大致需要做四件事,调整数组类型和数组成员类型、补充缺失的函数、修改toString方法及对象替换。如下的总结虽未模拟完全,但可作为一种启发参考。其它操作与此类似。
1 调整数组类型和数组成员类型
正常navigator.plugins对象数组中成员类型是Plugin,数组类型为PluginArray。正常navigator.mimeTypes数组成员类型是MimeType,数组类型mimeTypes。
PluginArray {0: Plugin, 1: Plugin, 2: Plugin, Chrome PDF Plugin: Plugin, Chrome PDF Viewer: Plugin, Native Client: Plugin, length: 3}
MimeTypeArray {0: MimeType, 1: MimeType, 2: MimeType, 3: MimeType, application/pdf: MimeType, application/x-google-chrome-pdf: MimeType, application/x-nacl: MimeType, application/x-pnacl: MimeType, length: 4}
Plugin,MimeType,PluginsArray,MimeTypeArray均是浏览器内置构造函数。为了模拟改造,所创建模拟数据和数组成员也应是对应的类型,首先创建普通对象数组。
let mimeTypes = [{
"description": "",
"enabledPlugin": {},
"suffixes": "pdf",
"type": "application/pdf"
}, {
"description": "Portable Document Format",
"enabledPlugin": {},
"suffixes": "pdf",
"type": "application/x-google-chrome-pdf"
}, {
"description": "Native Client Executable",
"enabledPlugin": {},
"suffixes": "",
"type": "application/x-nacl"
}, {
"description": "Portable Native Client Executable",
"enabledPlugin": {},
"suffixes": "",
"type": "application/x-pnacl"
}];
调整成员类型和数组类型,需要修改原型对象。
mimeTypes.map(o => Object.setPrototypeOf(o,MimeType.prototype));
Object.setPrototypeOf(mimeTypes,MimeTypeArray.prototype);
2 补充缺失的函数
这两个对象在原型链上还可以找到其它辅助函数。
navigator.plugins.__proto__
PluginArray {item: ƒ, namedItem: ƒ, refresh: ƒ, constructor: ƒ, …}
length: (...)
item: ƒ item()
namedItem: ƒ namedItem()
refresh: ƒ refresh()
navigator.mimeTypes.__proto__
MimeTypeArray {Symbol(Symbol.toStringTag): "MimeTypeArray", item: ƒ, namedItem: ƒ, constructor: ƒ, …}
length: (...)
item: ƒ item()
namedItem: ƒ namedItem()
这个功能通过给对象直接赋值即可mimeTypes.namedItem = function() {}
3 修改toString方法
正常的toString方法如下。
navigator.plugins.item.toString();
"function item() { [native code] }"
由于调用一个函数的toString方法,如果没提供覆盖实现,最终会通过原型链找到Function对象的toString方法。因此我们只要对Function的toString加一层代理。如果发现当前调用了我们自定义的方法,那么返回一个含有native的描述。如果调用的不是我们自定义的方法则放行。
- 如果调用了方法代理方法本身的String,那么返回一段toString描述。
- 如果是调用了我们自己实现的自定义方法,则返回一段自定义文本,并改变方法的名称。
- 如果调用的不是我们自定义方法,则放行,调原始方法。
const makeFnsNative = (fns = []) => {
const oldCall = Function.prototype.call
function call () {
return oldCall.apply(this, arguments)
}
// eslint-disable-next-line
Function.prototype.call = call
const nativeToStringFunctionString = Error.toString().replace(
/Error/g,
'toString'
)
const oldToString = Function.prototype.toString
function functionToString () {
for (const fn of fns) {
if (this === fn.ref) {
return `function ${fn.name}() { [native code] }`
}
}
if (this === functionToString) {
return nativeToStringFunctionString
}
return oldCall.call(oldToString, this)
}
// eslint-disable-next-line
Function.prototype.toString = functionToString
}
4 对象替换
最后就是将我们修改的结果进行替换,即重新定时属性描述符的get方法。
Object.defineProperty(navigator, 'plugins', {
get: () => pluginArray
})
5 总结
以上介绍了组略介绍了修改的内容,还未模拟完全。但通过这些作为一个简单的启发,那么其它内容也可以以此方式分析进行修改。
6 参考
[1].plugin的模拟实现,https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth