puppeteer-extra-plugin-stealth
Version:
Stealth mode: Applies various techniques to make detection of headless puppeteer harder.
223 lines (189 loc) • 7.19 kB
JavaScript
const test = require('ava')
const {
getVanillaFingerPrint,
getStealthFingerPrint
} = require('../../test/util')
const { vanillaPuppeteer, addExtra } = require('../../test/util')
const Plugin = require('.')
const { errors } = require('puppeteer')
// FIXME: This changed in more recent chrome versions
// test('vanilla: videoCard is Google Inc', async t => {
// const pageFn = async page => await page.evaluate(() => window.chrome) // eslint-disable-line
// const { videoCard } = await getVanillaFingerPrint(pageFn)
// t.deepEqual(videoCard, ['Google Inc.', 'Google SwiftShader'])
// })
test('stealth: videoCard is Intel Inc', async t => {
const pageFn = async page => await page.evaluate(() => window.chrome) // eslint-disable-line
const { videoCard } = await getStealthFingerPrint(Plugin, pageFn)
t.deepEqual(videoCard, ['Intel Inc.', 'Intel Iris OpenGL Engine'])
})
test('stealth: customized values', async t => {
const pageFn = async page => await page.evaluate(() => window.chrome) // eslint-disable-line
const { videoCard } = await getStealthFingerPrint(Plugin, pageFn, {
vendor: 'foo',
renderer: 'bar'
})
t.deepEqual(videoCard, ['foo', 'bar'])
})
/* global WebGLRenderingContext */
async function extendedTests() {
const results = {}
async function test(name, fn) {
const detectionPassed = await fn()
if (detectionPassed) console.log(`Chrome headless detected via ${name}`)
results[name] = detectionPassed
}
const canvas = document.createElement('canvas')
const context = canvas.getContext('webgl')
await test('descriptorsOK', _ => {
const descriptors = Object.getOwnPropertyDescriptors(
WebGLRenderingContext.prototype
)
const str = descriptors.getParameter.toString()
return str === `[object Object]`
})
await test('toStringOK', _ => {
const str = context.getParameter.toString()
return str === `function getParameter() { [native code] }`
})
await test('toStringOK2', _ => {
const str = WebGLRenderingContext.prototype.getParameter.toString()
return str === `function getParameter() { [native code] }`
})
// Make sure we not reveal our proxy through errors
await test('errorOK', _ => {
try {
return context.getParameter()
} catch (err) {
return !err.stack.includes(`at Object.apply`)
}
})
// Should not throw (that was old stealth behavior)
await test('elementOK', _ => {
try {
return context.getParameter(123) === null
} catch (_) {
return false
}
})
return results
}
test('vanilla: webgl is native', async t => {
const pageFn = async page => {
// page.on('console', msg => {
// console.log('Page console: ', msg.text())
// })
return await page.evaluate(extendedTests) // eslint-disable-line
}
const { pageFnResult: result } = await getVanillaFingerPrint(pageFn)
const wasHeadlessDetected = Object.values(result).some(e => e === false)
if (wasHeadlessDetected) {
console.log(result)
}
t.false(wasHeadlessDetected)
})
test('stealth: webgl is native', async t => {
const pageFn = async page => await page.evaluate(extendedTests) // eslint-disable-line
const { pageFnResult: result } = await getStealthFingerPrint(Plugin, pageFn)
const wasHeadlessDetected = Object.values(result).some(e => e === false)
if (wasHeadlessDetected) {
console.log(result)
}
t.false(wasHeadlessDetected)
})
/**
* A very simple method to retrieve the name of the default videocard of the system
* using webgl.
*
* Example (Apple Retina MBP 13): {vendor: "Intel Inc.", renderer: "Intel(R) Iris(TM) Graphics 6100"}
*
* @see https://stackoverflow.com/questions/49267764/how-to-get-the-video-card-driver-name-using-javascript-browser-side
* @returns {Object}
*/
function getVideoCardInfo(context = 'webgl') {
const gl = document.createElement('canvas').getContext(context)
if (!gl) {
return {
error: 'no webgl'
}
}
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info')
if (debugInfo) {
return {
vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
}
}
return {
error: 'no WEBGL_debug_renderer_info'
}
}
test('stealth: handles WebGLRenderingContext', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(Plugin())
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const videoCardInfo = await page.evaluate(getVideoCardInfo, 'webgl')
t.is(videoCardInfo.error, undefined)
t.is(videoCardInfo.vendor, 'Intel Inc.')
t.is(videoCardInfo.renderer, 'Intel Iris OpenGL Engine')
})
test('stealth: handles WebGL2RenderingContext', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(Plugin())
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const videoCardInfo = await page.evaluate(getVideoCardInfo, 'webgl2')
t.is(videoCardInfo.error, undefined)
t.is(videoCardInfo.vendor, 'Intel Inc.')
t.is(videoCardInfo.renderer, 'Intel Iris OpenGL Engine')
})
test('vanilla: normal toString stuff', async t => {
const browser = await vanillaPuppeteer.launch({ headless: true })
const page = await browser.newPage()
const test1 = await page.evaluate(() => {
return WebGLRenderingContext.prototype.getParameter.toString + ''
})
t.is(test1, 'function toString() { [native code] }')
const test2 = await page.evaluate(() => {
return WebGLRenderingContext.prototype.getParameter.toString()
})
t.is(test2, 'function getParameter() { [native code] }')
})
test('stealth: will not leak toString stuff', 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(() => {
return WebGLRenderingContext.prototype.getParameter.toString + ''
})
t.is(test1, 'function toString() { [native code] }') // returns function () { [native code] }
const test2 = await page.evaluate(() => {
return WebGLRenderingContext.prototype.getParameter.toString()
})
t.is(test2, 'function getParameter() { [native code] }')
})
test('stealth: sets user opts correctly', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(
Plugin({ vendor: 'alice', renderer: 'bob' })
)
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const videoCardInfo = await page.evaluate(getVideoCardInfo, 'webgl')
t.is(videoCardInfo.error, undefined)
t.is(videoCardInfo.vendor, 'alice')
t.is(videoCardInfo.renderer, 'bob')
})
test('stealth: does not affect protoype', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(
Plugin({ vendor: 'alice', renderer: 'bob' })
)
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const result = await page.evaluate(() => {
try {
return WebGLRenderingContext.prototype.getParameter(37445)
} catch (err) {
return err.message
}
})
t.is(result, 'Illegal invocation')
})