前言
爲了模擬實現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