puppeteer-extra-plugin-stealth
Advanced tools
Comparing version 2.1.1 to 2.1.2
@@ -9,3 +9,3 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/_template/index.js#L10-L20) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/_template/index.js#L10-L20) | ||
@@ -12,0 +12,0 @@ **Extends: PuppeteerExtraPlugin** |
@@ -9,3 +9,3 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/index.js#L10-L22) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/index.js#L10-L22) | ||
@@ -12,0 +12,0 @@ **Extends: PuppeteerExtraPlugin** |
@@ -9,3 +9,3 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/console.debug/index.js#L8-L18) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/console.debug/index.js#L8-L18) | ||
@@ -12,0 +12,0 @@ **Extends: PuppeteerExtraPlugin** |
@@ -9,3 +9,3 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/iframe.contentWindow/index.js#L8-L23) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/iframe.contentWindow/index.js#L8-L23) | ||
@@ -12,0 +12,0 @@ **Extends: PuppeteerExtraPlugin** |
@@ -9,3 +9,3 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/navigator.languages/index.js#L8-L21) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/navigator.languages/index.js#L8-L21) | ||
@@ -12,0 +12,0 @@ **Extends: PuppeteerExtraPlugin** |
@@ -9,3 +9,3 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/navigator.permissions/index.js#L8-L46) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/navigator.permissions/index.js#L8-L46) | ||
@@ -12,0 +12,0 @@ **Extends: PuppeteerExtraPlugin** |
@@ -6,17 +6,210 @@ 'use strict' | ||
/** | ||
* Pass the Plugins Length Test. | ||
* In headless mode `navigator.mimeTypes` and `navigator.plugins` are empty. | ||
* This plugin quite emulates both of these to match regular headful Chrome. | ||
* We even go so far as to mock functional methods, instance types and `.toString` properties. :D | ||
*/ | ||
class Plugin extends PuppeteerExtraPlugin { | ||
constructor (opts = { }) { super(opts) } | ||
constructor (opts = {}) { | ||
super(opts) | ||
} | ||
get name () { return 'stealth/evasions/navigator.plugins' } | ||
get name () { | ||
return 'stealth/evasions/navigator.plugins' | ||
} | ||
async onPageCreated (page) { | ||
await page.evaluateOnNewDocument(() => { | ||
// Overwrite the `plugins` property to use a custom getter. | ||
Object.defineProperty(navigator, 'plugins', { | ||
// This just needs to have `length > 0` for the current test, | ||
// but we could mock the plugins too if necessary. | ||
get: () => [1, 2, 3, 4, 5] | ||
}) | ||
function mockPluginsAndMimeTypes () { | ||
/* global MimeType MimeTypeArray PluginArray */ | ||
// Disguise custom functions as being native | ||
const makeFnsNative = (fns = []) => { | ||
const oldCall = Function.prototype.call | ||
function call () { | ||
return oldCall.apply(this, arguments) | ||
} | ||
// eslint-disable-next-line | ||
Function.prototype.call = call | ||
const nativeToStringFunctionString = Error.toString().replace( | ||
/Error/g, | ||
'toString' | ||
) | ||
const oldToString = Function.prototype.toString | ||
function functionToString () { | ||
for (const fn of fns) { | ||
if (this === fn.ref) { | ||
return `function ${fn.name}() { [native code] }` | ||
} | ||
} | ||
if (this === functionToString) { | ||
return nativeToStringFunctionString | ||
} | ||
return oldCall.call(oldToString, this) | ||
} | ||
// eslint-disable-next-line | ||
Function.prototype.toString = functionToString | ||
} | ||
const mockedFns = [] | ||
const fakeData = { | ||
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', | ||
enabledPlugin: Plugin, | ||
__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' | ||
}, | ||
{ | ||
name: 'Chrome PDF Viewer', | ||
filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', | ||
description: '' | ||
}, | ||
{ | ||
name: 'Native Client', | ||
filename: 'internal-nacl-plugin', | ||
description: '' | ||
} | ||
], | ||
fns: { | ||
namedItem: instanceName => { | ||
// Returns the Plugin/MimeType with the specified name. | ||
const fn = function (name) { | ||
if (!arguments.length) { | ||
throw new TypeError( | ||
`Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.` | ||
) | ||
} | ||
return this[name] || null | ||
} | ||
mockedFns.push({ ref: fn, name: 'namedItem' }) | ||
return fn | ||
}, | ||
item: instanceName => { | ||
// Returns the Plugin/MimeType at the specified index into the array. | ||
const fn = function (index) { | ||
if (!arguments.length) { | ||
throw new TypeError( | ||
`Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.` | ||
) | ||
} | ||
return this[index] || null | ||
} | ||
mockedFns.push({ ref: fn, name: 'item' }) | ||
return fn | ||
}, | ||
refresh: instanceName => { | ||
// Refreshes all plugins on the current page, optionally reloading documents. | ||
const fn = function () { | ||
return undefined | ||
} | ||
mockedFns.push({ ref: fn, name: 'refresh' }) | ||
return fn | ||
} | ||
} | ||
} | ||
// Poor mans _.pluck | ||
const getSubset = (keys, obj) => | ||
keys.reduce((a, c) => ({ ...a, [c]: obj[c] }), {}) | ||
function generateMimeTypeArray () { | ||
const arr = fakeData.mimeTypes | ||
.map(obj => getSubset(['type', 'suffixes', 'description'], obj)) | ||
.map(obj => Object.setPrototypeOf(obj, MimeType.prototype)) | ||
arr.forEach(obj => { | ||
arr[obj.type] = obj | ||
}) | ||
// Mock functions | ||
arr.namedItem = fakeData.fns.namedItem('MimeTypeArray') | ||
arr.item = fakeData.fns.item('MimeTypeArray') | ||
return Object.setPrototypeOf(arr, MimeTypeArray.prototype) | ||
} | ||
const mimeTypeArray = generateMimeTypeArray() | ||
Object.defineProperty(navigator, 'mimeTypes', { | ||
get: () => mimeTypeArray | ||
}) | ||
function generatePluginArray () { | ||
const arr = fakeData.plugins | ||
.map(obj => getSubset(['name', 'filename', 'description'], obj)) | ||
.map(obj => { | ||
const mimes = fakeData.mimeTypes.filter( | ||
m => m.__pluginName === obj.name | ||
) | ||
// Add mimetypes | ||
mimes.forEach((mime, index) => { | ||
navigator.mimeTypes[mime.type].enabledPlugin = obj | ||
obj[mime.type] = navigator.mimeTypes[mime.type] | ||
obj[index] = navigator.mimeTypes[mime.type] | ||
}) | ||
obj.length = mimes.length | ||
return obj | ||
}) | ||
.map(obj => { | ||
// Mock functions | ||
obj.namedItem = fakeData.fns.namedItem('Plugin') | ||
obj.item = fakeData.fns.item('Plugin') | ||
return obj | ||
}) | ||
.map(obj => Object.setPrototypeOf(obj, Plugin.prototype)) | ||
arr.forEach(obj => { | ||
arr[obj.name] = obj | ||
}) | ||
// Mock functions | ||
arr.namedItem = fakeData.fns.namedItem('PluginArray') | ||
arr.item = fakeData.fns.item('PluginArray') | ||
arr.refresh = fakeData.fns.refresh('PluginArray') | ||
return Object.setPrototypeOf(arr, PluginArray.prototype) | ||
} | ||
const pluginArray = generatePluginArray() | ||
Object.defineProperty(navigator, 'plugins', { | ||
get: () => pluginArray | ||
}) | ||
// Make mockedFns toString() representation resemble a native function | ||
makeFnsNative(mockedFns) | ||
} | ||
try { | ||
const isPluginArray = navigator.plugins instanceof PluginArray | ||
const hasPlugins = isPluginArray && navigator.plugins.length > 0 | ||
if (isPluginArray && hasPlugins) { | ||
return // nothing to do here | ||
} | ||
mockPluginsAndMimeTypes() | ||
} catch (err) {} | ||
}) | ||
@@ -26,2 +219,4 @@ } | ||
module.exports = function (pluginConfig) { return new Plugin(pluginConfig) } | ||
module.exports = function (pluginConfig) { | ||
return new Plugin(pluginConfig) | ||
} |
@@ -9,7 +9,9 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/index.js#L8-L23) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/navigator.plugins/index.js#L10-L216) | ||
**Extends: PuppeteerExtraPlugin** | ||
Pass the Plugins Length Test. | ||
In headless mode `navigator.mimeTypes` and `navigator.plugins` are empty. | ||
This plugin quite emulates both of these to match regular headful Chrome. | ||
We even go so far as to mock functional methods, instance types and `.toString` properties. :D | ||
@@ -16,0 +18,0 @@ Type: `function (opts)` |
@@ -7,13 +7,21 @@ 'use strict' | ||
* Pass the Webdriver Test. | ||
* Will delete `navigator.webdriver` property. | ||
*/ | ||
class Plugin extends PuppeteerExtraPlugin { | ||
constructor (opts = { }) { super(opts) } | ||
constructor (opts = {}) { | ||
super(opts) | ||
} | ||
get name () { return 'stealth/evasions/navigator.webdriver' } | ||
get name () { | ||
return 'stealth/evasions/navigator.webdriver' | ||
} | ||
async onPageCreated (page) { | ||
// Chrome returns undefined, Firefox false | ||
await page.evaluateOnNewDocument(() => { | ||
Object.defineProperty(navigator, 'webdriver', { | ||
get: () => false | ||
}) | ||
// eslint-disable-next-line | ||
const newProto = navigator.__proto__ | ||
delete newProto.webdriver | ||
// eslint-disable-next-line | ||
navigator.__proto__ = newProto | ||
}) | ||
@@ -23,2 +31,4 @@ } | ||
module.exports = function (pluginConfig) { return new Plugin(pluginConfig) } | ||
module.exports = function (pluginConfig) { | ||
return new Plugin(pluginConfig) | ||
} |
@@ -9,3 +9,3 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/navigator.webdriver/index.js#L8-L20) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/navigator.webdriver/index.js#L9-L28) | ||
@@ -15,2 +15,3 @@ **Extends: PuppeteerExtraPlugin** | ||
Pass the Webdriver Test. | ||
Will delete `navigator.webdriver` property. | ||
@@ -17,0 +18,0 @@ Type: `function (opts)` |
@@ -9,3 +9,3 @@ ## API | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/evasions/user-agent/index.js#L13-L19) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/evasions/user-agent/index.js#L13-L19) | ||
@@ -12,0 +12,0 @@ **Extends: PuppeteerExtraPlugin** |
37
index.js
@@ -35,6 +35,2 @@ 'use strict' | ||
* | ||
* ### Notes | ||
* Word of caution: Due to the intrusive nature of these detection mitigation techniques | ||
* they might break functionality on certain sites. Selectively disable techniques if that happens or submit a PR with a fix. :-) | ||
* | ||
* ### Kudos | ||
@@ -51,5 +47,6 @@ * Thanks to [Evan Sangaline](https://intoli.com/blog/not-possible-to-block-chrome-headless/) and [Paul Irish](https://github.com/paulirish/headless-cat-n-mouse) for kickstarting the discussion! | ||
* const puppeteer = require('puppeteer-extra') | ||
* // Enable stealth plugin | ||
* // Enable stealth plugin with all evasions | ||
* puppeteer.use(require('puppeteer-extra-plugin-stealth')()) | ||
* | ||
* | ||
* ;(async () => { | ||
@@ -78,5 +75,9 @@ * // Launch the browser in headless mode and set up a page. | ||
class Plugin extends PuppeteerExtraPlugin { | ||
constructor (opts = { }) { super(opts) } | ||
constructor (opts = {}) { | ||
super(opts) | ||
} | ||
get name () { return 'stealth' } | ||
get name () { | ||
return 'stealth' | ||
} | ||
@@ -92,2 +93,4 @@ get defaults () { | ||
'iframe.contentWindow', | ||
'window.outerdimensions', | ||
'webgl.vendor', | ||
'user-agent' | ||
@@ -108,4 +111,4 @@ ]) | ||
get dependencies () { | ||
return new Set([...this.opts.enabledEvasions] | ||
.map(e => `${this.name}/evasions/${e}`) | ||
return new Set( | ||
[...this.opts.enabledEvasions].map(e => `${this.name}/evasions/${e}`) | ||
) | ||
@@ -126,3 +129,5 @@ } | ||
*/ | ||
get availableEvasions () { return this.defaults.availableEvasions } | ||
get availableEvasions () { | ||
return this.defaults.availableEvasions | ||
} | ||
@@ -142,3 +147,5 @@ /** | ||
*/ | ||
get enabledEvasions () { return this.opts.enabledEvasions } | ||
get enabledEvasions () { | ||
return this.opts.enabledEvasions | ||
} | ||
@@ -148,5 +155,9 @@ /** | ||
*/ | ||
set enabledEvasions (evasions) { this.opts.enabledEvasions = evasions } | ||
set enabledEvasions (evasions) { | ||
this.opts.enabledEvasions = evasions | ||
} | ||
} | ||
module.exports = function (pluginConfig) { return new Plugin(pluginConfig) } | ||
module.exports = function (pluginConfig) { | ||
return new Plugin(pluginConfig) | ||
} |
{ | ||
"name": "puppeteer-extra-plugin-stealth", | ||
"version": "2.1.1", | ||
"version": "2.1.2", | ||
"description": "Stealth mode: Applies various techniques to make detection of headless puppeteer harder.", | ||
@@ -40,3 +40,3 @@ "main": "index.js", | ||
}, | ||
"gitHead": "2783eda8b71df3eb3e360614302c08007d467628" | ||
"gitHead": "72fe830c158f1e971c8499fdd5232338dd53c220" | ||
} |
# puppeteer-extra-plugin-stealth | ||
> A plugin for [puppeteer-extra](https://github.com/berstend/puppeteer-extra). | ||
> A plugin for [puppeteer-extra](https://github.com/berstend/puppeteer-extra) to prevent detection. | ||
@@ -9,4 +9,53 @@ ### Install | ||
yarn add puppeteer-extra-plugin-stealth | ||
# - or - | ||
npm install puppeteer-extra-plugin-stealth | ||
``` | ||
### Usage | ||
```js | ||
const puppeteer = require("puppeteer-extra") | ||
const pluginStealth = require("puppeteer-extra-plugin-stealth") | ||
puppeteer.use(pluginStealth()) | ||
``` | ||
## Changelog | ||
### `v2.1.2` | ||
- Improved: `navigator.plugins` - we fully emulate plugins/mimetypes in headless now 🎉 | ||
- New: `webgl.vendor` - is otherwise set to "Google" in headless | ||
- New: `window.outerdimensions` - fix missing window.outerWidth/outerHeight and viewport | ||
- Fixed: `navigator.webdriver` now returns undefined instead of false | ||
## Test results (red is bad) | ||
#### Vanilla puppeteer <strong>without stealth 😢</strong> | ||
<table class="image"> | ||
<tr> | ||
<td><figure class="image"><a href="./stealthtests/_results/headless-chromium-vanilla.js.png"><img src="./stealthtests/_results/_thumbs/headless-chromium-vanilla.js.png"></a><figcaption>Chromium + headless</figcaption></figure></td> | ||
<td><figure class="image"><a href="./stealthtests/_results/headful-chromium-vanilla.js.png"><img src="./stealthtests/_results/_thumbs/headful-chromium-vanilla.js.png"></a><figcaption>Chromium + headful</figcaption></figure></td> | ||
<td><figure class="image"><a href="./stealthtests/_results/headless-chrome-vanilla.js.png"><img src="./stealthtests/_results/_thumbs/headless-chrome-vanilla.js.png"></a><figcaption>Chrome + headless</figcaption></figure></td> | ||
<td><figure class="image"><a href="./stealthtests/_results/headful-chrome-vanilla.js.png"><img src="./stealthtests/_results/_thumbs/headful-chrome-vanilla.js.png"></a><figcaption>Chrome + headful</figcaption></figure></td> | ||
</tr> | ||
</table> | ||
#### Puppeteer <strong>with stealth plugin 💯</strong> | ||
<table class="image"> | ||
<tr> | ||
<td><figure class="image"><a href="./stealthtests/_results/headless-chromium-stealth.js.png"><img src="./stealthtests/_results/_thumbs/headless-chromium-stealth.js.png"></a><figcaption>Chromium + headless</figcaption></figure></td> | ||
<td><figure class="image"><a href="./stealthtests/_results/headful-chromium-stealth.js.png"><img src="./stealthtests/_results/_thumbs/headful-chromium-stealth.js.png"></a><figcaption>Chromium + headful</figcaption></figure></td> | ||
<td><figure class="image"><a href="./stealthtests/_results/headless-chrome-stealth.js.png"><img src="./stealthtests/_results/_thumbs/headless-chrome-stealth.js.png"></a><figcaption>Chrome + headless</figcaption></figure></td> | ||
<td><figure class="image"><a href="./stealthtests/_results/headful-chrome-stealth.js.png"><img src="./stealthtests/_results/_thumbs/headful-chrome-stealth.js.png"></a><figcaption>Chrome + headful</figcaption></figure></td> | ||
</tr> | ||
</table> | ||
Tests have been done using [this test site](https://bot.sannysoft.com/) and [these scripts](./stealthtests/). | ||
## API | ||
@@ -22,3 +71,2 @@ | ||
- [Contributing](#contributing) | ||
- [Notes](#notes) | ||
- [Kudos](#kudos) | ||
@@ -28,3 +76,3 @@ - [availableEvasions](#availableevasions) | ||
### [Plugin](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/index.js#L75-L142) | ||
### [Plugin](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/index.js#L72-L151) | ||
@@ -65,7 +113,2 @@ **Extends: PuppeteerExtraPlugin** | ||
#### Notes | ||
Word of caution: Due to the intrusive nature of these detection mitigation techniques | ||
they might break functionality on certain sites. Selectively disable techniques if that happens or submit a PR with a fix. :-) | ||
#### Kudos | ||
@@ -86,5 +129,6 @@ | ||
const puppeteer = require('puppeteer-extra') | ||
// Enable stealth plugin | ||
// Enable stealth plugin with all evasions | ||
puppeteer.use(require('puppeteer-extra-plugin-stealth')()) | ||
;(async () => { | ||
@@ -111,3 +155,3 @@ // Launch the browser in headless mode and set up a page. | ||
#### [availableEvasions](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/index.js#L121-L121) | ||
#### [availableEvasions](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/index.js#L124-L126) | ||
@@ -130,3 +174,3 @@ Get all available evasions. | ||
#### [enabledEvasions](https://github.com/berstend/puppeteer-extra/blob/db57ea66cf10d407cf63af387892492e495a84f2/packages/puppeteer-extra-plugin-stealth/index.js#L136-L136) | ||
#### [enabledEvasions](https://git@github.com/:berstend/puppeteer-extra/blob/ff112879545e8e68d6500d731ceeafc22d187dd3/packages/puppeteer-extra-plugin-stealth/index.js#L141-L143) | ||
@@ -133,0 +177,0 @@ Get all enabled evasions. |
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
50975
42
737
186