puppeteer-extra-plugin-stealth
Version:
Stealth mode: Applies various techniques to make detection of headless puppeteer harder.
145 lines (130 loc) • 5.53 kB
JavaScript
/* global MimeType MimeTypeArray Plugin PluginArray */
/**
* Generate a convincing and functional MimeType or Plugin array from scratch.
* They're so similar that it makes sense to use a single generator here.
*
* Note: This is meant to be run in the context of the page.
*/
module.exports.generateMagicArray = (utils, fns) =>
function(
dataArray = [],
proto = MimeTypeArray.prototype,
itemProto = MimeType.prototype,
itemMainProp = 'type'
) {
// Quick helper to set props with the same descriptors vanilla is using
const defineProp = (obj, prop, value) =>
Object.defineProperty(obj, prop, {
value,
writable: false,
enumerable: false, // Important for mimeTypes & plugins: `JSON.stringify(navigator.mimeTypes)`
configurable: true
})
// Loop over our fake data and construct items
const makeItem = data => {
const item = {}
for (const prop of Object.keys(data)) {
if (prop.startsWith('__')) {
continue
}
defineProp(item, prop, data[prop])
}
return patchItem(item, data)
}
const patchItem = (item, data) => {
let descriptor = Object.getOwnPropertyDescriptors(item)
// Special case: Plugins have a magic length property which is not enumerable
// e.g. `navigator.plugins[i].length` should always be the length of the assigned mimeTypes
if (itemProto === Plugin.prototype) {
descriptor = {
...descriptor,
length: {
value: data.__mimeTypes.length,
writable: false,
enumerable: false,
configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`
}
}
}
// We need to spoof a specific `MimeType` or `Plugin` object
const obj = Object.create(itemProto, descriptor)
// Virtually all property keys are not enumerable in vanilla
const blacklist = [...Object.keys(data), 'length', 'enabledPlugin']
return new Proxy(obj, {
ownKeys(target) {
return Reflect.ownKeys(target).filter(k => !blacklist.includes(k))
},
getOwnPropertyDescriptor(target, prop) {
if (blacklist.includes(prop)) {
return undefined
}
return Reflect.getOwnPropertyDescriptor(target, prop)
}
})
}
const magicArray = []
// Loop through our fake data and use that to create convincing entities
dataArray.forEach(data => {
magicArray.push(makeItem(data))
})
// Add direct property access based on types (e.g. `obj['application/pdf']`) afterwards
magicArray.forEach(entry => {
defineProp(magicArray, entry[itemMainProp], entry)
})
// This is the best way to fake the type to make sure this is false: `Array.isArray(navigator.mimeTypes)`
const magicArrayObj = Object.create(proto, {
...Object.getOwnPropertyDescriptors(magicArray),
// There's one ugly quirk we unfortunately need to take care of:
// The `MimeTypeArray` prototype has an enumerable `length` property,
// but headful Chrome will still skip it when running `Object.getOwnPropertyNames(navigator.mimeTypes)`.
// To strip it we need to make it first `configurable` and can then overlay a Proxy with an `ownKeys` trap.
length: {
value: magicArray.length,
writable: false,
enumerable: false,
configurable: true // Important to be able to use the ownKeys trap in a Proxy to strip `length`
}
})
// Generate our functional function mocks :-)
const functionMocks = fns.generateFunctionMocks(utils)(
proto,
itemMainProp,
magicArray
)
// We need to overlay our custom object with a JS Proxy
const magicArrayObjProxy = new Proxy(magicArrayObj, {
get(target, key = '') {
// Redirect function calls to our custom proxied versions mocking the vanilla behavior
if (key === 'item') {
return functionMocks.item
}
if (key === 'namedItem') {
return functionMocks.namedItem
}
if (proto === PluginArray.prototype && key === 'refresh') {
return functionMocks.refresh
}
// Everything else can pass through as normal
return utils.cache.Reflect.get(...arguments)
},
ownKeys(target) {
// There are a couple of quirks where the original property demonstrates "magical" behavior that makes no sense
// This can be witnessed when calling `Object.getOwnPropertyNames(navigator.mimeTypes)` and the absense of `length`
// My guess is that it has to do with the recent change of not allowing data enumeration and this being implemented weirdly
// For that reason we just completely fake the available property names based on our data to match what regular Chrome is doing
// Specific issues when not patching this: `length` property is available, direct `types` props (e.g. `obj['application/pdf']`) are missing
const keys = []
const typeProps = magicArray.map(mt => mt[itemMainProp])
typeProps.forEach((_, i) => keys.push(`${i}`))
typeProps.forEach(propName => keys.push(propName))
return keys
},
getOwnPropertyDescriptor(target, prop) {
if (prop === 'length') {
return undefined
}
return Reflect.getOwnPropertyDescriptor(target, prop)
}
})
return magicArrayObjProxy
}