UNPKG

puppeteer-extra-plugin-stealth

Version:

Stealth mode: Applies various techniques to make detection of headless puppeteer harder.

137 lines (123 loc) 4.54 kB
'use strict' const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin') const withUtils = require('../_utils/withUtils') /** * Fix for the HEADCHR_IFRAME detection (iframe.contentWindow.chrome), hopefully this time without breaking iframes. * Note: Only `srcdoc` powered iframes cause issues due to a chromium bug: * * https://github.com/puppeteer/puppeteer/issues/1106 */ class Plugin extends PuppeteerExtraPlugin { constructor(opts = {}) { super(opts) } get name() { return 'stealth/evasions/iframe.contentWindow' } get requirements() { // Make sure `chrome.runtime` has ran, we use data defined by it (e.g. `window.chrome`) return new Set(['runLast']) } async onPageCreated(page) { await withUtils(page).evaluateOnNewDocument((utils, opts) => { try { // Adds a contentWindow proxy to the provided iframe element const addContentWindowProxy = iframe => { const contentWindowProxy = { get(target, key) { // Now to the interesting part: // We actually make this thing behave like a regular iframe window, // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :) // That makes it possible for these assertions to be correct: // iframe.contentWindow.self === window.top // must be false if (key === 'self') { return this } // iframe.contentWindow.frameElement === iframe // must be true if (key === 'frameElement') { return iframe } // Intercept iframe.contentWindow[0] to hide the property 0 added by the proxy. if (key === '0') { return undefined } return Reflect.get(target, key) } } if (!iframe.contentWindow) { const proxy = new Proxy(window, contentWindowProxy) Object.defineProperty(iframe, 'contentWindow', { get() { return proxy }, set(newValue) { return newValue // contentWindow is immutable }, enumerable: true, configurable: false }) } } // Handles iframe element creation, augments `srcdoc` property so we can intercept further const handleIframeCreation = (target, thisArg, args) => { const iframe = target.apply(thisArg, args) // We need to keep the originals around const _iframe = iframe const _srcdoc = _iframe.srcdoc // Add hook for the srcdoc property // We need to be very surgical here to not break other iframes by accident Object.defineProperty(iframe, 'srcdoc', { configurable: true, // Important, so we can reset this later get: function() { return _srcdoc }, set: function(newValue) { addContentWindowProxy(this) // Reset property, the hook is only needed once Object.defineProperty(iframe, 'srcdoc', { configurable: false, writable: false, value: _srcdoc }) _iframe.srcdoc = newValue } }) return iframe } // Adds a hook to intercept iframe creation events const addIframeCreationSniffer = () => { /* global document */ const createElementHandler = { // Make toString() native get(target, key) { return Reflect.get(target, key) }, apply: function(target, thisArg, args) { const isIframe = args && args.length && `${args[0]}`.toLowerCase() === 'iframe' if (!isIframe) { // Everything as usual return target.apply(thisArg, args) } else { return handleIframeCreation(target, thisArg, args) } } } // All this just due to iframes with srcdoc bug utils.replaceWithProxy( document, 'createElement', createElementHandler ) } // Let's go addIframeCreationSniffer() } catch (err) { // console.warn(err) } }) } } module.exports = function(pluginConfig) { return new Plugin(pluginConfig) }