puppeteer-extra-plugin-stealth
Advanced tools
Comparing version 2.6.6 to 2.6.7
@@ -14,2 +14,6 @@ /** | ||
utils.init = () => { | ||
utils.preloadCache() | ||
} | ||
/** | ||
@@ -27,3 +31,3 @@ * Wraps a JS Proxy Handler and strips it's presence from error stacks, in case the traps throw. | ||
traps.forEach(trap => { | ||
newHandler[trap] = function() { | ||
newHandler[trap] = function () { | ||
try { | ||
@@ -137,2 +141,5 @@ // Forward the call to the defined proxy handler | ||
* | ||
* Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before, | ||
* by executing `utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups). | ||
* | ||
* This is evaluated once per execution context (e.g. window) | ||
@@ -164,5 +171,2 @@ */ | ||
* | ||
* Note: Whenever we add a `Function.prototype.toString` proxy we should preload the cache before, | ||
* by executing `utils.preloadCache()` before the proxy is applied (so we don't cause recursive lookups). | ||
* | ||
* @example | ||
@@ -174,4 +178,2 @@ * makeNativeString('foobar') // => `function foobar() { [native code] }` | ||
utils.makeNativeString = (name = '') => { | ||
// Cache (per-window) the original native toString or use that if available | ||
utils.preloadCache() | ||
return utils.cache.nativeToStringStr.replace('toString', name || '') | ||
@@ -195,6 +197,4 @@ } | ||
utils.patchToString = (obj, str = '') => { | ||
utils.preloadCache() | ||
const toStringProxy = new Proxy(Function.prototype.toString, { | ||
apply: function(target, ctx) { | ||
const handler = { | ||
apply: function (target, ctx) { | ||
// This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""` | ||
@@ -220,3 +220,8 @@ if (ctx === Function.prototype.toString) { | ||
} | ||
}) | ||
} | ||
const toStringProxy = new Proxy( | ||
Function.prototype.toString, | ||
utils.stripProxyFromErrors(handler) | ||
) | ||
utils.replaceProperty(Function.prototype, 'toString', { | ||
@@ -243,6 +248,4 @@ value: toStringProxy | ||
utils.redirectToString = (proxyObj, originalObj) => { | ||
utils.preloadCache() | ||
const toStringProxy = new Proxy(Function.prototype.toString, { | ||
apply: function(target, ctx) { | ||
const handler = { | ||
apply: function (target, ctx) { | ||
// This fixes e.g. `HTMLMediaElement.prototype.canPlayType.toString + ""` | ||
@@ -276,3 +279,8 @@ if (ctx === Function.prototype.toString) { | ||
} | ||
}) | ||
} | ||
const toStringProxy = new Proxy( | ||
Function.prototype.toString, | ||
utils.stripProxyFromErrors(handler) | ||
) | ||
utils.replaceProperty(Function.prototype, 'toString', { | ||
@@ -297,3 +305,2 @@ value: toStringProxy | ||
utils.replaceWithProxy = (obj, propName, handler) => { | ||
utils.preloadCache() | ||
const originalObj = obj[propName] | ||
@@ -307,3 +314,23 @@ const proxyObj = new Proxy(obj[propName], utils.stripProxyFromErrors(handler)) | ||
} | ||
/** | ||
* All-in-one method to replace a getter with a JS Proxy using the provided Proxy handler with traps. | ||
* | ||
* @example | ||
* replaceGetterWithProxy(Object.getPrototypeOf(navigator), 'vendor', proxyHandler) | ||
* | ||
* @param {object} obj - The object which has the property to replace | ||
* @param {string} propName - The name of the property to replace | ||
* @param {object} handler - The JS Proxy handler to use | ||
*/ | ||
utils.replaceGetterWithProxy = (obj, propName, handler) => { | ||
const fn = Object.getOwnPropertyDescriptor(obj, propName).get | ||
const fnStr = fn.toString() // special getter function string | ||
const proxyObj = new Proxy(fn, utils.stripProxyFromErrors(handler)) | ||
utils.replaceProperty(obj, propName, { get: proxyObj }) | ||
utils.patchToString(proxyObj, fnStr) | ||
return true | ||
} | ||
/** | ||
@@ -323,3 +350,2 @@ * All-in-one method to mock a non-existing property with a JS Proxy using the provided Proxy handler with traps. | ||
utils.mockWithProxy = (obj, propName, pseudoTarget, handler) => { | ||
utils.preloadCache() | ||
const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler)) | ||
@@ -347,3 +373,2 @@ | ||
utils.createProxy = (pseudoTarget, handler) => { | ||
utils.preloadCache() | ||
const proxyObj = new Proxy(pseudoTarget, utils.stripProxyFromErrors(handler)) | ||
@@ -366,6 +391,3 @@ utils.patchToString(proxyObj) | ||
// Remove last dot entry (property) ==> `HTMLMediaElement.prototype` | ||
objName: objPath | ||
.split('.') | ||
.slice(0, -1) | ||
.join('.'), | ||
objName: objPath.split('.').slice(0, -1).join('.'), | ||
// Extract last dot entry ==> `canPlayType` | ||
@@ -467,2 +489,18 @@ propName: objPath.split('.').slice(-1)[0] | ||
// Proxy handler templates for re-usability | ||
utils.makeHandler = () => ({ | ||
// Used by simple `navigator` getter evasions | ||
getterValue: value => ({ | ||
apply(target, ctx, args) { | ||
// Let's fetch the value first, to trigger and escalate potential errors | ||
// Illegal invocations like `navigator.__proto__.vendor` will throw here | ||
const ret = utils.cache.Reflect.apply(...arguments) | ||
if (args && args.length === 0) { | ||
return value | ||
} | ||
return ret | ||
} | ||
}) | ||
}) | ||
// -- | ||
@@ -469,0 +507,0 @@ // Stuff starting below this line is NodeJS specific. |
@@ -19,2 +19,3 @@ const test = require('ava') | ||
test('makeNativeString: will do what it says', async t => { | ||
utils.init() | ||
t.is(utils.makeNativeString('bob'), 'function bob() { [native code] }') | ||
@@ -227,7 +228,8 @@ t.is( | ||
)} | ||
- Function.prototype.valueOf.call(obj) + "": ${Function.prototype.valueOf.call( | ||
obj | ||
) + ''} | ||
- obj.toString === Function.prototype.toString: ${obj.toString === | ||
Function.prototype.toString} | ||
- Function.prototype.valueOf.call(obj) + "": ${ | ||
Function.prototype.valueOf.call(obj) + '' | ||
} | ||
- obj.toString === Function.prototype.toString: ${ | ||
obj.toString === Function.prototype.toString | ||
} | ||
`.trim() | ||
@@ -237,3 +239,3 @@ } | ||
test('patchToString: passes all toString tests', async t => { | ||
const toStringVanilla = await (async function() { | ||
const toStringVanilla = await (async function () { | ||
const browser = await vanillaPuppeteer.launch({ headless: true }) | ||
@@ -243,3 +245,3 @@ const page = await browser.newPage() | ||
})() | ||
const toStringStealth = await (async function() { | ||
const toStringStealth = await (async function () { | ||
const browser = await vanillaPuppeteer.launch({ headless: true }) | ||
@@ -246,0 +248,0 @@ const page = await browser.newPage() |
@@ -12,3 +12,3 @@ const utils = require('./index') | ||
*/ | ||
evaluate: async function(mainFunction, ...args) { | ||
evaluate: async function (mainFunction, ...args) { | ||
return page.evaluate( | ||
@@ -20,3 +20,3 @@ ({ _utilsFns, _mainFunction, _args }) => { | ||
) | ||
utils.preloadCache() | ||
utils.init() | ||
return eval(_mainFunction)(utils, ..._args) // eslint-disable-line no-eval | ||
@@ -34,3 +34,3 @@ }, | ||
*/ | ||
evaluateOnNewDocument: async function(mainFunction, ...args) { | ||
evaluateOnNewDocument: async function (mainFunction, ...args) { | ||
return page.evaluateOnNewDocument( | ||
@@ -42,3 +42,3 @@ ({ _utilsFns, _mainFunction, _args }) => { | ||
) | ||
utils.preloadCache() | ||
utils.init() | ||
return eval(_mainFunction)(utils, ..._args) // eslint-disable-line no-eval | ||
@@ -45,0 +45,0 @@ }, |
@@ -5,2 +5,4 @@ 'use strict' | ||
const withUtils = require('../_utils/withUtils') | ||
/** | ||
@@ -24,18 +26,26 @@ * Set the hardwareConcurrency to 4 (optionally configurable with `hardwareConcurrency`) | ||
get defaults() { | ||
return { | ||
hardwareConcurrency: 4 | ||
} | ||
} | ||
async onPageCreated(page) { | ||
await page.evaluateOnNewDocument(opts => { | ||
Object.defineProperty( | ||
Object.getPrototypeOf(navigator), | ||
'hardwareConcurrency', | ||
{ | ||
value: opts.hardwareConcurrency || 4, | ||
writable: false | ||
} | ||
) | ||
}, this.opts) | ||
await withUtils(page).evaluateOnNewDocument( | ||
(utils, { opts }) => { | ||
utils.replaceGetterWithProxy( | ||
Object.getPrototypeOf(navigator), | ||
'hardwareConcurrency', | ||
utils.makeHandler().getterValue(opts.hardwareConcurrency) | ||
) | ||
}, | ||
{ | ||
opts: this.opts | ||
} | ||
) | ||
} | ||
} | ||
module.exports = function(pluginConfig) { | ||
module.exports = function (pluginConfig) { | ||
return new Plugin(pluginConfig) | ||
} |
const test = require('ava') | ||
const os = require('os') | ||
const { vanillaPuppeteer, addExtra } = require('../../test/util') | ||
const { | ||
@@ -12,3 +14,3 @@ getVanillaFingerPrint, | ||
test('vanilla: navigator.hardwareConcurrency matches real core count', async t => { | ||
test('vanilla: matches real core count', async t => { | ||
const { pageFnResult } = await getVanillaFingerPrint(fingerprintFn) | ||
@@ -18,3 +20,3 @@ t.is(pageFnResult, os.cpus().length) | ||
test('stealth: navigator.hardwareConcurrency is set to 4', async t => { | ||
test('stealth: default is set to 4', async t => { | ||
const { pageFnResult } = await getStealthFingerPrint(Plugin, fingerprintFn) | ||
@@ -24,3 +26,3 @@ t.is(pageFnResult, 4) | ||
test('stealth: navigator.hardwareConcurrency customized value', async t => { | ||
test('stealth: will override value correctly', async t => { | ||
const { pageFnResult } = await getStealthFingerPrint(Plugin, fingerprintFn, { | ||
@@ -31,1 +33,31 @@ hardwareConcurrency: 8 | ||
}) | ||
test('stealth: does patch getters properly', 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(() => { | ||
const hasInvocationError = (() => { | ||
try { | ||
// eslint-disable-next-line dot-notation | ||
Object['seal'](Object.getPrototypeOf(navigator)['hardwareConcurrency']) | ||
return false | ||
} catch (err) { | ||
return true | ||
} | ||
})() | ||
return { | ||
hasInvocationError, | ||
toString: Object.getOwnPropertyDescriptor( | ||
Object.getPrototypeOf(navigator), | ||
'hardwareConcurrency' | ||
).get.toString() | ||
} | ||
}) | ||
t.deepEqual(results, { | ||
hasInvocationError: true, | ||
toString: 'function get hardwareConcurrency() { [native code] }' | ||
}) | ||
}) |
@@ -21,14 +21,29 @@ 'use strict' | ||
// Overwrite the `languages` property to use a custom getter. | ||
get defaults() { | ||
return { | ||
languages: [] // Empty default, otherwise this would be merged with user defined array override | ||
} | ||
} | ||
async onPageCreated(page) { | ||
await withUtils(page).evaluateOnNewDocument((utils, opts) => { | ||
utils.replaceProperty(Object.getPrototypeOf(navigator), 'languages', { | ||
get: () => opts.languages || ['en-US', 'en'] | ||
}) | ||
}, this.opts) | ||
await withUtils(page).evaluateOnNewDocument( | ||
(utils, { opts }) => { | ||
const languages = opts.languages.length | ||
? opts.languages | ||
: ['en-US', 'en'] | ||
utils.replaceGetterWithProxy( | ||
Object.getPrototypeOf(navigator), | ||
'languages', | ||
utils.makeHandler().getterValue(Object.freeze([...languages])) | ||
) | ||
}, | ||
{ | ||
opts: this.opts | ||
} | ||
) | ||
} | ||
} | ||
module.exports = function(pluginConfig) { | ||
module.exports = function (pluginConfig) { | ||
return new Plugin(pluginConfig) | ||
} |
@@ -62,1 +62,42 @@ const test = require('ava') | ||
}) | ||
test('stealth: does patch getters properly', 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(() => { | ||
const hasInvocationError = (() => { | ||
try { | ||
// eslint-disable-next-line dot-notation | ||
Object['seal'](Object.getPrototypeOf(navigator)['languages']) | ||
return false | ||
} catch (err) { | ||
return true | ||
} | ||
})() | ||
const hasPushError = (() => { | ||
try { | ||
// eslint-disable-next-line dot-notation | ||
navigator.languages.push(null) | ||
return false | ||
} catch (err) { | ||
return true | ||
} | ||
})() | ||
return { | ||
hasInvocationError, | ||
hasPushError, | ||
toString: Object.getOwnPropertyDescriptor( | ||
Object.getPrototypeOf(navigator), | ||
'languages' | ||
).get.toString() | ||
} | ||
}) | ||
t.deepEqual(results, { | ||
hasInvocationError: true, | ||
hasPushError: true, | ||
toString: 'function get languages() { [native code] }' | ||
}) | ||
}) |
@@ -5,2 +5,4 @@ 'use strict' | ||
const withUtils = require('../_utils/withUtils') | ||
/** | ||
@@ -45,12 +47,18 @@ * By default puppeteer will have a fixed `navigator.vendor` property. | ||
async onPageCreated(page) { | ||
this.debug('onPageCreated - Will set these user agent options', { | ||
this.debug('onPageCreated', { | ||
opts: this.opts | ||
}) | ||
await page.evaluateOnNewDocument(v => { | ||
// Overwrite the `vendor` property to use a custom getter. | ||
Object.defineProperty(Object.getPrototypeOf(navigator), 'vendor', { | ||
get: () => v | ||
}) | ||
}, this.opts.vendor || 'Google Inc.') | ||
await withUtils(page).evaluateOnNewDocument( | ||
(utils, { opts }) => { | ||
utils.replaceGetterWithProxy( | ||
Object.getPrototypeOf(navigator), | ||
'vendor', | ||
utils.makeHandler().getterValue(opts.vendor) | ||
) | ||
}, | ||
{ | ||
opts: this.opts | ||
} | ||
) | ||
} // onPageCreated | ||
@@ -57,0 +65,0 @@ } |
@@ -11,3 +11,3 @@ const test = require('ava') | ||
const vendor = await page.evaluate(() => navigator.vendor) | ||
t.true(vendor === 'Google Inc.') | ||
t.is(vendor, 'Google Inc.') | ||
}) | ||
@@ -23,3 +23,3 @@ | ||
const vendor = await page.evaluate(() => navigator.vendor) | ||
t.true(vendor === 'Apple Computer, Inc.') | ||
t.is(vendor, 'Apple Computer, Inc.') | ||
}) | ||
@@ -42,1 +42,31 @@ | ||
}) | ||
test('stealth: does patch getters properly', 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(() => { | ||
const hasInvocationError = (() => { | ||
try { | ||
// eslint-disable-next-line dot-notation | ||
Object['seal'](Object.getPrototypeOf(navigator)['vendor']) | ||
return false | ||
} catch (err) { | ||
return true | ||
} | ||
})() | ||
return { | ||
hasInvocationError, | ||
toString: Object.getOwnPropertyDescriptor( | ||
Object.getPrototypeOf(navigator), | ||
'vendor' | ||
).get.toString() | ||
} | ||
}) | ||
t.deepEqual(results, { | ||
hasInvocationError: true, | ||
toString: 'function get vendor() { [native code] }' | ||
}) | ||
}) |
{ | ||
"name": "puppeteer-extra-plugin-stealth", | ||
"version": "2.6.6", | ||
"version": "2.6.7", | ||
"description": "Stealth mode: Applies various techniques to make detection of headless puppeteer harder.", | ||
@@ -20,3 +20,3 @@ "main": "index.js", | ||
"test": "run-p test:js lint", | ||
"test-ci": "run-s test", | ||
"test-ci": "run-s test:js", | ||
"types": "npx --package typescript@3.7 tsc --emitDeclarationOnly --declaration --allowJs index.js" | ||
@@ -58,3 +58,3 @@ }, | ||
}, | ||
"gitHead": "0abcb1e7084d02e499dd979dc6f07b78940e301c" | ||
"gitHead": "f184a797a4de14ec2383fc0cb2957b1ac451e4da" | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
710924
4978