puppeteer-extra-plugin-stealth
Version:
Stealth mode: Applies various techniques to make detection of headless puppeteer harder.
287 lines (264 loc) • 9.87 kB
JavaScript
const test = require('ava')
const {
getVanillaFingerPrint,
getStealthFingerPrint
} = require('../../test/util')
const { vanillaPuppeteer, addExtra } = require('../../test/util')
const Plugin = require('.')
const STATIC_DATA = require('./staticData.json')
/* global chrome */
test('vanilla: is chrome false', async t => {
const pageFn = async page => await page.evaluate(() => window.chrome) // eslint-disable-line
const { pageFnResult: chrome, hasChrome } = await getVanillaFingerPrint(
pageFn
)
t.is(hasChrome, false)
t.false(chrome instanceof Object)
t.is(chrome, undefined)
})
test('stealth: is chrome true', async t => {
const pageFn = async page => await page.evaluate(() => window.chrome) // eslint-disable-line
const { pageFnResult: chrome, hasChrome } = await getStealthFingerPrint(
Plugin,
pageFn
)
t.is(hasChrome, true)
t.true(chrome instanceof Object)
})
test('stealth: will add convincing chrome.runtime object', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(
Plugin({
runOnInsecureOrigins: true // for testing
})
)
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
//
const results = await page.evaluate(() => {
const catchErr = (fn, ...args) => {
try {
return fn.apply(this, args)
} catch (err) {
return err.toString()
}
}
return {
runtime: {
exists: window.chrome && 'runtime' in window.chrome,
toString: chrome.runtime.toString()
},
staticData: {
OnInstalledReason: chrome.runtime.OnInstalledReason,
OnRestartRequiredReason: chrome.runtime.OnRestartRequiredReason,
PlatformArch: chrome.runtime.PlatformArch,
PlatformNaclArch: chrome.runtime.PlatformNaclArch,
PlatformOs: chrome.runtime.PlatformOs,
RequestUpdateCheckStatus: chrome.runtime.RequestUpdateCheckStatus
},
id: {
exists: 'id' in chrome.runtime,
undefined: chrome.runtime.id === undefined
},
sendMessage: {
exists: 'sendMessage' in chrome.runtime,
name: chrome.runtime.sendMessage.name,
toString1: chrome.runtime.sendMessage + '',
toString2: chrome.runtime.sendMessage.toString(),
validIdWorks:
chrome.runtime.sendMessage('nckgahadagoaajjgafhacjanaoiihapd', '') ===
undefined
},
sendMessageErrors: {
noArg: catchErr(chrome.runtime.sendMessage),
singleArg: catchErr(chrome.runtime.sendMessage, ''),
tooManyArg: catchErr(
chrome.runtime.sendMessage,
'',
'',
'',
'',
'',
''
),
incorrectArg: catchErr(chrome.runtime.sendMessage, '', '', {}, ''),
noValidID: catchErr(chrome.runtime.sendMessage, 'foo', '')
}
}
})
const bla = `TypeError: Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback)`
t.deepEqual(results, {
runtime: {
exists: true,
toString: '[object Object]'
},
staticData: STATIC_DATA,
id: {
exists: true,
undefined: true
},
sendMessage: {
exists: true,
name: 'sendMessage',
toString1: 'function sendMessage() { [native code] }',
toString2: 'function sendMessage() { [native code] }',
validIdWorks: true
},
sendMessageErrors: {
noArg: `${bla}: No matching signature.`,
singleArg: `${bla}: chrome.runtime.sendMessage() called from a webpage must specify an Extension ID (string) for its first argument.`,
tooManyArg: `${bla}: No matching signature.`,
incorrectArg: `${bla}: No matching signature.`,
noValidID: `${bla}: Invalid extension id: 'foo'`
}
})
})
test('stealth: will add convincing chrome.runtime.connect', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(
Plugin({
runOnInsecureOrigins: true // for testing
})
)
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const results = await page.evaluate(() => {
const catchErr = (fn, ...args) => {
try {
return fn.apply(this, args)
} catch (err) {
return err.toString()
}
}
return {
connect: {
exists: 'connect' in chrome.runtime,
name: chrome.runtime.connect.name,
toString1: chrome.runtime.connect + '',
toString2: chrome.runtime.connect.toString(),
validIdWorks:
chrome.runtime.connect('nckgahadagoaajjgafhacjanaoiihapd') !==
undefined
},
connectErrors: {
noArg: catchErr(chrome.runtime.connect),
singleArg: catchErr(chrome.runtime.connect, ''),
tooManyArg: catchErr(chrome.runtime.connect, '', '', '', '', '', ''),
incorrectArg: catchErr(chrome.runtime.connect, '', '', {}, ''),
noValidID: catchErr(chrome.runtime.connect, 'foo', ''),
connectInfoFirst: {
emptyObject: catchErr(chrome.runtime.connect, {}),
tooManyArg: catchErr(chrome.runtime.connect, {}, {}),
unexpectedProp: catchErr(chrome.runtime.connect, { wtf: true }),
invalidName: catchErr(chrome.runtime.connect, { name: 666 }),
invalidTLS: catchErr(chrome.runtime.connect, {
includeTlsChannelId: 777
}),
invalidBoth: catchErr(chrome.runtime.connect, {
name: 666,
includeTlsChannelId: 777
}),
validName: catchErr(chrome.runtime.connect, { name: 'foo' }),
missingExtensionId: catchErr(chrome.runtime.connect, {
name: 'bob',
includeTlsChannelId: false
})
}
}
}
})
const bla = `TypeError: Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo)`
t.deepEqual(results, {
connect: {
exists: true,
name: 'connect',
toString1: 'function connect() { [native code] }',
toString2: 'function connect() { [native code] }',
validIdWorks: true
},
connectErrors: {
noArg: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`,
singleArg: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`,
tooManyArg: `${bla}: No matching signature.`,
incorrectArg: `${bla}: No matching signature.`,
noValidID: `${bla}: Invalid extension id: 'foo'`,
connectInfoFirst: {
emptyObject: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`,
tooManyArg: `${bla}: No matching signature.`,
unexpectedProp: `${bla}: Unexpected property: 'wtf'.`,
invalidName: `${bla}: Error at property 'name': Invalid type: expected string, found number.`,
invalidTLS: `${bla}: Error at property 'includeTlsChannelId': Invalid type: expected boolean, found number.`,
invalidBoth: `${bla}: Error at property 'name': Invalid type: expected string, found number.`,
validName: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`,
missingExtensionId: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`
}
}
})
})
test('stealth: will add convincing chrome.runtime.connect response', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(
Plugin({
runOnInsecureOrigins: true // for testing
})
)
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const results = await page.evaluate(() => {
const connectResponse = chrome.runtime.connect(
'nckgahadagoaajjgafhacjanaoiihapd'
)
return {
connectResponse: {
exists: !!connectResponse,
toString1: connectResponse + '',
toString2: connectResponse.toString(),
nestedToString: connectResponse.onDisconnect.addListener + ''
},
disconnect: {
toString: connectResponse.disconnect + '',
noReturn: connectResponse.disconnect() === undefined
}
}
})
t.deepEqual(results, {
connectResponse: {
exists: true,
toString1: '[object Object]',
toString2: '[object Object]',
nestedToString: `function addListener() { [native code] }`
},
disconnect: {
toString: `function disconnect() { [native code] }`,
noReturn: true
}
})
})
// FIXME: This changed in more recent chrome versions
// test('stealth: error stack is fine', async t => {
// const puppeteer = addExtra(vanillaPuppeteer).use(
// Plugin({
// runOnInsecureOrigins: true // for testing
// })
// )
// const browser = await puppeteer.launch({ headless: true })
// const page = await browser.newPage()
// const result = await page.evaluate(() => {
// const catchErr = (fn, ...args) => {
// try {
// return fn.apply(this, args)
// } catch ({ name, message, stack }) {
// return {
// name,
// message,
// stack
// }
// }
// }
// return catchErr(chrome.runtime.connect, '').stack
// })
// /**
// * OK:
// TypeError: Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.␊
// - at catchErr (__puppeteer_evaluation_script__:4:19)␊
// - at __puppeteer_evaluation_script__:18:12
// */
// t.is(result.split('\n').length, 3)
// })