navigator plugins与mimetyps的模拟实现分析

目录
  1. 1. 前言
  2. 2. 1 调整数组类型和数组成员类型
  3. 3. 2 补充缺失的函数
  4. 4. 3 修改toString方法
  5. 5. 4 对象替换
  6. 6. 5 总结
  7. 7. 6 参考

前言

为了模拟实现navigator plugins与mimetyps,大致需要做四件事,调整数组类型和数组成员类型、补充缺失的函数、修改toString方法及对象替换。如下的总结虽未模拟完全,但可作为一种启发参考。其它操作与此类似。

1 调整数组类型和数组成员类型

正常navigator.plugins对象数组中成员类型是Plugin,数组类型为PluginArray。正常navigator.mimeTypes数组成员类型是MimeType,数组类型mimeTypes。

1
PluginArray {0: Plugin, 1: Plugin, 2: Plugin, Chrome PDF Plugin: Plugin, Chrome PDF Viewer: Plugin, Native Client: Plugin, length: 3}
1
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均是浏览器内置构造函数。为了模拟改造,所创建模拟数据和数组成员也应是对应的类型,首先创建普通对象数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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"
}];

调整成员类型和数组类型,需要修改原型对象。

1
2
mimeTypes.map(o => Object.setPrototypeOf(o,MimeType.prototype));
Object.setPrototypeOf(mimeTypes,MimeTypeArray.prototype);

2 补充缺失的函数

这两个对象在原型链上还可以找到其它辅助函数。

1
2
3
4
5
6
7
navigator.plugins.__proto__

PluginArray {item: ƒ, namedItem: ƒ, refresh: ƒ, constructor: ƒ, …}
length: (...)
item: ƒ item()
namedItem: ƒ namedItem()
refresh: ƒ refresh()
1
2
3
4
5
6
navigator.mimeTypes.__proto__

MimeTypeArray {Symbol(Symbol.toStringTag): "MimeTypeArray", item: ƒ, namedItem: ƒ, constructor: ƒ, …}
length: (...)
item: ƒ item()
namedItem: ƒ namedItem()

这个功能通过给对象直接赋值即可mimeTypes.namedItem = function() {}

3 修改toString方法

正常的toString方法如下。

1
2
navigator.plugins.item.toString();
"function item() { [native code] }"

由于调用一个函数的toString方法,如果没提供覆盖实现,最终会通过原型链找到Function对象的toString方法。因此我们只要对Function的toString加一层代理。如果发现当前调用了我们自定义的方法,那么返回一个含有native的描述。如果调用的不是我们自定义的方法则放行。

  1. 如果调用了方法代理方法本身的String,那么返回一段toString描述。
  2. 如果是调用了我们自己实现的自定义方法,则返回一段自定义文本,并改变方法的名称。
  3. 如果调用的不是我们自定义方法,则放行,调原始方法。
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
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方法。

1
2
3
Object.defineProperty(navigator, 'plugins', {
get: () => pluginArray
})

5 总结

以上介绍了组略介绍了修改的内容,还未模拟完全。但通过这些作为一个简单的启发,那么其它内容也可以以此方式分析进行修改。

6 参考

[1].plugin的模拟实现,https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth