Rename to hkt.sh

This commit is contained in:
mango
2026-03-21 01:10:53 +08:00
parent 76a263d0f9
commit 8f1171fe99
6676 changed files with 1724268 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
{
"mimeTypes": [
{
"type": "application/pdf",
"suffixes": "pdf",
"description": "",
"__pluginName": "Chrome PDF Viewer"
},
{
"type": "application/x-google-chrome-pdf",
"suffixes": "pdf",
"description": "Portable Document Format",
"__pluginName": "Chrome PDF Plugin"
},
{
"type": "application/x-nacl",
"suffixes": "",
"description": "Native Client Executable",
"__pluginName": "Native Client"
},
{
"type": "application/x-pnacl",
"suffixes": "",
"description": "Portable Native Client Executable",
"__pluginName": "Native Client"
}
],
"plugins": [
{
"name": "Chrome PDF Plugin",
"filename": "internal-pdf-viewer",
"description": "Portable Document Format",
"__mimeTypes": ["application/x-google-chrome-pdf"]
},
{
"name": "Chrome PDF Viewer",
"filename": "mhjfbmdgcfjbbpaeojofohoefgiehjai",
"description": "",
"__mimeTypes": ["application/pdf"]
},
{
"name": "Native Client",
"filename": "internal-nacl-plugin",
"description": "",
"__mimeTypes": ["application/x-nacl", "application/x-pnacl"]
}
]
}

View File

@@ -0,0 +1,50 @@
/**
* `navigator.{plugins,mimeTypes}` share similar custom functions to look up properties
*
* Note: This is meant to be run in the context of the page.
*/
module.exports.generateFunctionMocks = utils => (
proto,
itemMainProp,
dataArray
) => ({
/** Returns the MimeType object with the specified index. */
item: utils.createProxy(proto.item, {
apply(target, ctx, args) {
if (!args.length) {
throw new TypeError(
`Failed to execute 'item' on '${
proto[Symbol.toStringTag]
}': 1 argument required, but only 0 present.`
)
}
// Special behavior alert:
// - Vanilla tries to cast strings to Numbers (only integers!) and use them as property index lookup
// - If anything else than an integer (including as string) is provided it will return the first entry
const isInteger = args[0] && Number.isInteger(Number(args[0])) // Cast potential string to number first, then check for integer
// Note: Vanilla never returns `undefined`
return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null
}
}),
/** Returns the MimeType object with the specified name. */
namedItem: utils.createProxy(proto.namedItem, {
apply(target, ctx, args) {
if (!args.length) {
throw new TypeError(
`Failed to execute 'namedItem' on '${
proto[Symbol.toStringTag]
}': 1 argument required, but only 0 present.`
)
}
return dataArray.find(mt => mt[itemMainProp] === args[0]) || null // Not `undefined`!
}
}),
/** Does nothing and shall return nothing */
refresh: proto.refresh
? utils.createProxy(proto.refresh, {
apply(target, ctx, args) {
return undefined
}
})
: undefined
})

View File

@@ -0,0 +1,101 @@
'use strict'
const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin')
const utils = require('../_utils')
const withUtils = require('../_utils/withUtils')
const { generateMimeTypeArray } = require('./mimeTypes')
const { generatePluginArray } = require('./plugins')
const { generateMagicArray } = require('./magicArray')
const { generateFunctionMocks } = require('./functionMocks')
const data = require('./data.json')
/**
* In headless mode `navigator.mimeTypes` and `navigator.plugins` are empty.
* This plugin emulates both of these with functional mocks to match regular headful Chrome.
*
* Note: mimeTypes and plugins cross-reference each other, so it makes sense to do them at the same time.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/mimeTypes
* @see https://developer.mozilla.org/en-US/docs/Web/API/MimeTypeArray
* @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/plugins
* @see https://developer.mozilla.org/en-US/docs/Web/API/PluginArray
*/
class Plugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts)
}
get name() {
return 'stealth/evasions/navigator.plugins'
}
async onPageCreated(page) {
await withUtils(page).evaluateOnNewDocument(
(utils, { fns, data }) => {
fns = utils.materializeFns(fns)
// That means we're running headful
const hasPlugins = 'plugins' in navigator && navigator.plugins.length
if (hasPlugins) {
return // nothing to do here
}
const mimeTypes = fns.generateMimeTypeArray(utils, fns)(data.mimeTypes)
const plugins = fns.generatePluginArray(utils, fns)(data.plugins)
// Plugin and MimeType cross-reference each other, let's do that now
// Note: We're looping through `data.plugins` here, not the generated `plugins`
for (const pluginData of data.plugins) {
pluginData.__mimeTypes.forEach((type, index) => {
plugins[pluginData.name][index] = mimeTypes[type]
Object.defineProperty(plugins[pluginData.name], type, {
value: mimeTypes[type],
writable: false,
enumerable: false, // Not enumerable
configurable: true
})
Object.defineProperty(mimeTypes[type], 'enabledPlugin', {
value:
type === 'application/x-pnacl'
? mimeTypes['application/x-nacl'].enabledPlugin // these reference the same plugin, so we need to re-use the Proxy in order to avoid leaks
: new Proxy(plugins[pluginData.name], {}), // Prevent circular references
writable: false,
enumerable: false, // Important: `JSON.stringify(navigator.plugins)`
configurable: true
})
})
}
const patchNavigator = (name, value) =>
utils.replaceProperty(Object.getPrototypeOf(navigator), name, {
get() {
return value
}
})
patchNavigator('mimeTypes', mimeTypes)
patchNavigator('plugins', plugins)
// All done
},
{
// We pass some functions to evaluate to structure the code more nicely
fns: utils.stringifyFns({
generateMimeTypeArray,
generatePluginArray,
generateMagicArray,
generateFunctionMocks
}),
data
}
)
}
}
module.exports = function(pluginConfig) {
return new Plugin(pluginConfig)
}

View File

@@ -0,0 +1,56 @@
const test = require('ava')
const {
getVanillaFingerPrint,
getStealthFingerPrint
} = require('../../test/util')
const { vanillaPuppeteer, addExtra } = require('../../test/util')
const Plugin = require('.')
test('vanilla: empty plugins, empty mimetypes', async t => {
const { plugins, mimeTypes } = await getVanillaFingerPrint()
t.is(plugins.length, 0)
t.is(mimeTypes.length, 0)
})
test('vanilla: will not have modifications', async t => {
const browser = await vanillaPuppeteer.launch({ headless: true })
const page = await browser.newPage()
const test1 = await page.evaluate(() => ({
mimeTypes: Object.getOwnPropertyDescriptor(navigator, 'mimeTypes'), // Must be undefined if native
plugins: Object.getOwnPropertyDescriptor(navigator, 'plugins') // Must be undefined if native
}))
t.is(test1.mimeTypes, undefined)
t.is(test1.plugins, undefined)
const test2 = await page.evaluate(
() => Object.getOwnPropertyNames(navigator) // Must be an empty array if native
)
t.false(test2.includes('plugins'))
})
test('stealth: has plugin, has mimetypes', async t => {
const { plugins, mimeTypes } = await getStealthFingerPrint(Plugin)
t.is(plugins.length, 3)
t.is(mimeTypes.length, 4)
})
test('stealth: will not leak modifications', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(Plugin())
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const test1 = await page.evaluate(() => ({
mimeTypes: Object.getOwnPropertyDescriptor(navigator, 'mimeTypes'), // Must be undefined if native
plugins: Object.getOwnPropertyDescriptor(navigator, 'plugins') // Must be undefined if native
}))
t.is(test1.mimeTypes, undefined)
t.is(test1.plugins, undefined)
const test2 = await page.evaluate(
() => Object.getOwnPropertyNames(navigator) // Must be an empty array if native
)
t.false(test2.includes('plugins'))
})

View File

@@ -0,0 +1,144 @@
/* 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
}

View File

@@ -0,0 +1,18 @@
/* global MimeType MimeTypeArray */
/**
* Generate a convincing and functional MimeTypeArray (with mime types) from scratch.
*
* Note: This is meant to be run in the context of the page.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/mimeTypes
* @see https://developer.mozilla.org/en-US/docs/Web/API/MimeTypeArray
*/
module.exports.generateMimeTypeArray = (utils, fns) => mimeTypesData => {
return fns.generateMagicArray(utils, fns)(
mimeTypesData,
MimeTypeArray.prototype,
MimeType.prototype,
'type'
)
}

View File

@@ -0,0 +1,208 @@
const test = require('ava')
const { vanillaPuppeteer, addExtra } = require('../../test/util')
const Plugin = require('.')
test('stealth: will have convincing mimeTypes', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(Plugin())
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const results = await page.evaluate(() => {
// We need to help serializing the error or it won't survive being sent back from `page.evaluate`
const catchErr = function(fn, ...args) {
try {
return fn.apply(this, args)
} catch ({ name, message, stack }) {
return { name, message, stack, str: stack.split('\n')[0] }
}
}
return {
mimeTypes: {
exists: 'mimeTypes' in navigator,
isArray: Array.isArray(navigator.mimeTypes),
length: navigator.mimeTypes.length,
// value: navigator.mimeTypes,
toString: navigator.mimeTypes.toString(),
toStringProto: navigator.mimeTypes.__proto__.toString(), // eslint-disable-line no-proto
protoSymbol: navigator.mimeTypes.__proto__[Symbol.toStringTag], // eslint-disable-line no-proto
// valueOf: navigator.mimeTypes.valueOf(),
valueOfSame: navigator.mimeTypes.valueOf() === navigator.mimeTypes,
json: JSON.stringify(navigator.mimeTypes),
hasPropPush: 'push' in navigator.mimeTypes,
hasPropLength: 'length' in navigator.mimeTypes,
hasLengthDescriptor: !!Object.getOwnPropertyDescriptor(
navigator.mimeTypes,
'length'
),
propertyNames: JSON.stringify(
Object.getOwnPropertyNames(navigator.mimeTypes)
),
lengthInProps: Object.getOwnPropertyNames(navigator.mimeTypes).includes(
'length'
),
keys: JSON.stringify(Object.keys(navigator.mimeTypes)),
namedPropsAuthentic: (function() {
navigator.mimeTypes.alice = 'bob'
return navigator.mimeTypes.namedItem('alice') === null // true on chrome
})(),
loopResult: (function() {
let res = ''
for (var bK = 0; bK < window.navigator.mimeTypes.length; bK++)
bK === window.navigator.mimeTypes.length - 1
? (res += window.navigator.mimeTypes[bK].type)
: (res += window.navigator.mimeTypes[bK].type + ',')
return res
})()
},
namedItem: {
exists: 'namedItem' in navigator.mimeTypes,
toString: navigator.mimeTypes.namedItem.toString(),
resultNotFound: navigator.mimeTypes.namedItem('foo'),
resultFound: navigator.mimeTypes // eslint-disable-line no-proto
.namedItem('application/pdf')
.__proto__.toString(),
errors: {
// For whatever weird reason the normal context doesn't suffice, we need to bind this to `navigator.mimeTypes`
noArgs: catchErr.bind(navigator.mimeTypes)(
navigator.mimeTypes.namedItem
).str,
noStackLeaks: !catchErr
.bind(navigator.mimeTypes)(navigator.mimeTypes.namedItem)
.stack.includes(`.apply`),
protoCall: catchErr.bind(navigator.mimeTypes)(
navigator.mimeTypes.__proto__.namedItem // eslint-disable-line no-proto
).str
}
},
item: {
exists: 'item' in navigator.mimeTypes,
toString: navigator.mimeTypes.item.toString(),
resultNotFound: navigator.mimeTypes.item('madness').type,
resultNotFoundNumberString: navigator.mimeTypes.item('777'),
resultEmptyString: navigator.mimeTypes.item('').type,
resultByNumberString: navigator.mimeTypes.item('2').type,
resultByNumberStringZero: navigator.mimeTypes.item('0').type,
resultByNumber: navigator.mimeTypes.item(2).type,
resultNull: navigator.mimeTypes.item(null).type,
resultFound: navigator.mimeTypes.item('application/x-nacl').type,
resultBrackets: navigator.mimeTypes['application/x-pnacl'].type,
errors: {
// For whatever weird reason the normal context doesn't suffice, we need to bind this to `navigator.mimeTypes`
noArgs: catchErr.bind(navigator.mimeTypes)(navigator.mimeTypes.item)
.str,
noStackLeaks: !catchErr
.bind(navigator.mimeTypes)(navigator.mimeTypes.item)
.stack.includes(`.apply`),
protoCall: catchErr.bind(navigator.mimeTypes)(
navigator.mimeTypes.__proto__.item // eslint-disable-line no-proto
).str
}
}
}
})
t.deepEqual(results.mimeTypes, {
exists: true,
hasPropPush: false,
hasPropLength: true,
hasLengthDescriptor: false,
isArray: false,
json: `{"0":{},"1":{},"2":{},"3":{}}`,
keys: `["0","1","2","3"]`,
length: 4,
lengthInProps: false,
loopResult:
'application/pdf,application/x-google-chrome-pdf,application/x-nacl,application/x-pnacl',
namedPropsAuthentic: true,
propertyNames: `["0","1","2","3","application/pdf","application/x-google-chrome-pdf","application/x-nacl","application/x-pnacl"]`,
protoSymbol: 'MimeTypeArray',
toString: '[object MimeTypeArray]',
toStringProto: '[object MimeTypeArray]',
valueOfSame: true
})
t.deepEqual(results.namedItem, {
exists: true,
toString: 'function namedItem() { [native code] }',
resultFound: '[object MimeType]',
resultNotFound: null,
errors: {
noArgs:
"TypeError: Failed to execute 'namedItem' on 'MimeTypeArray': 1 argument required, but only 0 present.",
noStackLeaks: true,
protoCall: 'TypeError: Illegal invocation'
}
})
t.deepEqual(results.item, {
exists: true,
resultBrackets: 'application/x-pnacl',
resultByNumber: 'application/x-nacl',
resultByNumberString: 'application/x-nacl',
resultByNumberStringZero: 'application/pdf',
resultEmptyString: 'application/pdf',
resultFound: 'application/pdf',
resultNotFound: 'application/pdf',
resultNotFoundNumberString: null,
resultNull: 'application/pdf',
toString: 'function item() { [native code] }',
errors: {
noArgs:
"TypeError: Failed to execute 'item' on 'MimeTypeArray': 1 argument required, but only 0 present.",
noStackLeaks: true,
protoCall: 'TypeError: Illegal invocation'
}
})
})
test('stealth: will have convincing mimeType entry', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(Plugin())
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const results = await page.evaluate(() => ({
mimeType: {
exists: !!navigator.mimeTypes[0],
toString: navigator.mimeTypes[0].toString(),
toStringProto: navigator.mimeTypes[0].__proto__.toString(), // eslint-disable-line no-proto
protoSymbol: navigator.mimeTypes[0].__proto__[Symbol.toStringTag], // eslint-disable-line no-proto
enabledPlugin: !!navigator.mimeTypes[0].enabledPlugin, // should not throw
enabledPlugin2: !!navigator.mimeTypes['application/pdf'].enabledPlugin, // should not throw
enabledPlugins: !!navigator.mimeTypes[0].enabledPlugins, // regression: should not exist (anymore)
pdfPlugin: JSON.stringify(
navigator.mimeTypes['application/pdf'].enabledPlugin
),
length: !!navigator.mimeTypes[0].length, // should not throw and return mimeTypes length
lengthDescriptor: !!Object.getOwnPropertyDescriptor(
navigator.mimeTypes[0],
'length'
),
json: JSON.stringify(navigator.mimeTypes[0]),
propertyNames: JSON.stringify(
Object.getOwnPropertyNames(navigator.mimeTypes[0])
),
nested:
navigator.mimeTypes['application/pdf'].enabledPlugin[0].enabledPlugin[0]
.enabledPlugin[0].enabledPlugin[0].enabledPlugin[0].suffixes
}
}))
t.deepEqual(results.mimeType, {
exists: true,
protoSymbol: 'MimeType',
toString: '[object MimeType]',
toStringProto: '[object MimeType]',
enabledPlugin: true,
enabledPlugin2: true,
enabledPlugins: false,
pdfPlugin: '{"0":{}}',
length: false,
lengthDescriptor: false,
json: '{}',
propertyNames: '[]',
nested: 'pdf'
})
})

View File

@@ -0,0 +1,4 @@
{
"private": true,
"main": "index.js"
}

View File

@@ -0,0 +1,18 @@
/* global Plugin PluginArray */
/**
* Generate a convincing and functional PluginArray (with plugins) from scratch.
*
* Note: This is meant to be run in the context of the page.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/plugins
* @see https://developer.mozilla.org/en-US/docs/Web/API/PluginArray
*/
module.exports.generatePluginArray = (utils, fns) => pluginsData => {
return fns.generateMagicArray(utils, fns)(
pluginsData,
PluginArray.prototype,
Plugin.prototype,
'name'
)
}

View File

@@ -0,0 +1,184 @@
const test = require('ava')
const { vanillaPuppeteer, addExtra } = require('../../test/util')
const Plugin = require('.')
test('stealth: will have convincing plugins', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(Plugin())
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const results = await page.evaluate(() => {
// We need to help serializing the error or it won't survive being sent back from `page.evaluate`
const catchErr = function(fn, ...args) {
try {
return fn.apply(this, args)
} catch ({ name, message, stack }) {
return { name, message, stack, str: stack.split('\n')[0] }
}
}
return {
plugins: {
exists: 'plugins' in navigator,
isArray: Array.isArray(navigator.plugins),
length: navigator.plugins.length,
// value: navigator.plugins,
toString: navigator.plugins.toString(),
toStringProto: navigator.plugins.__proto__.toString(), // eslint-disable-line no-proto
protoSymbol: navigator.plugins.__proto__[Symbol.toStringTag], // eslint-disable-line no-proto
// valueOf: navigator.plugins.valueOf(),
valueOfSame: navigator.plugins.valueOf() === navigator.plugins,
json: JSON.stringify(navigator.plugins),
hasPropPush: 'push' in navigator.plugins,
hasPropLength: 'length' in navigator.plugins,
hasLengthDescriptor: !!Object.getOwnPropertyDescriptor(
navigator.plugins,
'length'
),
propertyNames: JSON.stringify(
Object.getOwnPropertyNames(navigator.plugins)
),
lengthInProps: Object.getOwnPropertyNames(navigator.plugins).includes(
'length'
),
keys: JSON.stringify(Object.keys(navigator.plugins)),
loopResult: [...navigator.plugins].map(p => p.name).join(',')
},
namedItem: {
exists: 'namedItem' in navigator.plugins,
toString: navigator.plugins.namedItem.toString(),
resultNotFound: navigator.plugins.namedItem('foo'),
resultFound: navigator.plugins // eslint-disable-line no-proto
.namedItem('Chrome PDF Viewer')
.__proto__.toString(),
errors: {
// For whatever weird reason the normal context doesn't suffice, we need to bind this to `navigator.plugins`
noArgs: catchErr.bind(navigator.plugins)(navigator.plugins.namedItem)
.str,
noStackLeaks: !catchErr
.bind(navigator.plugins)(navigator.plugins.namedItem)
.stack.includes(`.apply`),
protoCall: catchErr.bind(navigator.plugins)(
navigator.plugins.__proto__.namedItem // eslint-disable-line no-proto
).str
}
},
item: {
exists: 'item' in navigator.plugins,
toString: navigator.plugins.item.toString(),
resultNotFound: navigator.plugins.item('madness').name,
resultNotFoundNumberString: navigator.plugins.item('777'),
resultEmptyString: navigator.plugins.item('').name,
resultByNumberString: navigator.plugins.item('2').name,
resultByNumberStringZero: navigator.plugins.item('0').name,
resultByNumber: navigator.plugins.item(2).name,
resultNull: navigator.plugins.item(null).name,
resultFound: navigator.plugins.item('application/x-nacl').name,
errors: {
// For whatever weird reason the normal context doesn't suffice, we need to bind this to `navigator.plugins`
noArgs: catchErr.bind(navigator.plugins)(navigator.plugins.item).str,
noStackLeaks: !catchErr
.bind(navigator.plugins)(navigator.plugins.item)
.stack.includes(`.apply`),
protoCall: catchErr.bind(navigator.plugins)(
navigator.plugins.__proto__.item // eslint-disable-line no-proto
).str
}
}
}
})
t.deepEqual(results.plugins, {
exists: true,
hasPropLength: true,
hasLengthDescriptor: false,
hasPropPush: false,
isArray: false,
json: `{"0":{"0":{}},"1":{"0":{}},"2":{"0":{},"1":{}}}`,
keys: `["0","1","2"]`,
length: 3,
lengthInProps: false,
loopResult: 'Chrome PDF Plugin,Chrome PDF Viewer,Native Client',
propertyNames: `["0","1","2","Chrome PDF Plugin","Chrome PDF Viewer","Native Client"]`,
protoSymbol: 'PluginArray',
toString: '[object PluginArray]',
toStringProto: '[object PluginArray]',
valueOfSame: true
})
t.deepEqual(results.namedItem, {
exists: true,
toString: 'function namedItem() { [native code] }',
resultFound: '[object Plugin]',
resultNotFound: null,
errors: {
noArgs:
"TypeError: Failed to execute 'namedItem' on 'PluginArray': 1 argument required, but only 0 present.",
noStackLeaks: true,
protoCall: 'TypeError: Illegal invocation'
}
})
t.deepEqual(results.item, {
exists: true,
resultByNumber: 'Native Client',
resultByNumberString: 'Native Client',
resultByNumberStringZero: 'Chrome PDF Plugin',
resultEmptyString: 'Chrome PDF Plugin',
resultFound: 'Chrome PDF Plugin',
resultNotFound: 'Chrome PDF Plugin',
resultNotFoundNumberString: null,
resultNull: 'Chrome PDF Plugin',
toString: 'function item() { [native code] }',
errors: {
noArgs:
"TypeError: Failed to execute 'item' on 'PluginArray': 1 argument required, but only 0 present.",
noStackLeaks: true,
protoCall: 'TypeError: Illegal invocation'
}
})
})
test('stealth: will have convincing plugin entry', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(Plugin())
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const results = await page.evaluate(() => ({
plugins: {
exists: !!navigator.plugins[0],
toString: navigator.plugins[0].toString(),
toStringProto: navigator.plugins[0].__proto__.toString(), // eslint-disable-line no-proto
protoSymbol: navigator.plugins[0].__proto__[Symbol.toStringTag], // eslint-disable-line no-proto
length: navigator.plugins[0].length, // should not throw and return mimeTypes length
lengthDescriptor: Object.getOwnPropertyDescriptor(
navigator.plugins[0],
'length'
)
},
plugin: {
mtIndex: !!navigator.plugins[0][0], // mimeType should be accessible through index
mtNamed: !!navigator.plugins[0]['application/x-google-chrome-pdf'], // mimeType should be accessible through name
json: JSON.stringify(navigator.plugins[0]),
propertyNames: JSON.stringify(
Object.getOwnPropertyNames(navigator.plugins[0])
)
}
}))
t.deepEqual(results.plugins, {
exists: true,
protoSymbol: 'Plugin',
toString: '[object Plugin]',
toStringProto: '[object Plugin]',
length: 1
})
t.deepEqual(results.plugin, {
mtIndex: true,
mtNamed: true,
json: '{"0":{}}',
propertyNames: '["0","application/x-google-chrome-pdf"]'
})
})

View File

@@ -0,0 +1,25 @@
## API
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
#### Table of Contents
- [class: Plugin](#class-plugin)
### class: [Plugin](https://github.com/berstend/puppeteer-extra/blob/e6133619b051febed630ada35241664eba59b9fa/packages/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/index.js#L26-L88)
- `opts` (optional, default `{}`)
**Extends: PuppeteerExtraPlugin**
In headless mode `navigator.mimeTypes` and `navigator.plugins` are empty.
This plugin emulates both of these with functional mocks to match regular headful Chrome.
Note: mimeTypes and plugins cross-reference each other, so it makes sense to do them at the same time.
- **See: <https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/mimeTypes>**
- **See: <https://developer.mozilla.org/en-US/docs/Web/API/MimeTypeArray>**
- **See: <https://developer.mozilla.org/en-US/docs/Web/API/NavigatorPlugins/plugins>**
- **See: <https://developer.mozilla.org/en-US/docs/Web/API/PluginArray>**
---