/* * See here for most recent detection avoidance: https://github.com/paulirish/headless-cat-n-mouse/blob/master/apply-evasions.js */ // We'll use Puppeteer is our browser automation framework. const puppeteer = require('puppeteer'); // This is where we'll put the code to get around the tests. const preparePageForTests = async (page) => { // Pass the User-Agent Test. const userAgent = 'Mozilla/5.0 (X11; Linux x86_64)' + 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.39 Safari/537.36'; await page.setUserAgent(userAgent); // Pass the Webdriver Test. await page.evaluateOnNewDocument(() => { const newProto = navigator.__proto__; delete newProto.webdriver; navigator.__proto__ = newProto; }); // Pass the Chrome Test. await page.evaluateOnNewDocument(() => { // We can mock this in as much depth as we need for the test. const mockObj = { app: { isInstalled: false, }, webstore: { onInstallStageChanged: {}, onDownloadProgress: {}, }, runtime: { PlatformOs: { MAC: 'mac', WIN: 'win', ANDROID: 'android', CROS: 'cros', LINUX: 'linux', OPENBSD: 'openbsd', }, PlatformArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64', }, PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64', }, RequestUpdateCheckStatus: { THROTTLED: 'throttled', NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available', }, OnInstalledReason: { INSTALL: 'install', UPDATE: 'update', CHROME_UPDATE: 'chrome_update', SHARED_MODULE_UPDATE: 'shared_module_update', }, OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic', }, }, }; window.navigator.chrome = mockObj; window.chrome = mockObj; }); // Pass the Permissions Test. await page.evaluateOnNewDocument(() => { const originalQuery = window.navigator.permissions.query; window.navigator.permissions.__proto__.query = parameters => parameters.name === 'notifications' ? Promise.resolve({state: Notification.permission}) : originalQuery(parameters); // Inspired by: https://github.com/ikarienator/phantomjs_hide_and_seek/blob/master/5.spoofFunctionBind.js const oldCall = Function.prototype.call; function call() { return oldCall.apply(this, arguments); } Function.prototype.call = call; const nativeToStringFunctionString = Error.toString().replace(/Error/g, "toString"); const oldToString = Function.prototype.toString; function functionToString() { if (this === window.navigator.permissions.query) { return "function query() { [native code] }"; } if (this === functionToString) { return nativeToStringFunctionString; } return oldCall.call(oldToString, this); } Function.prototype.toString = functionToString; }); // Pass the Plugins Length Test. 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] }); }); // Pass the Languages Test. await page.evaluateOnNewDocument(() => { // Overwrite the `plugins` property to use a custom getter. Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); }); // Pass the iframe Test await page.evaluateOnNewDocument(() => { Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', { get: function() { return window; } }); }); // Pass toString test, though it breaks console.debug() from working await page.evaluateOnNewDocument(() => { window.console.debug = () => { return null; }; }); }; (async () => { // Launch the browser in headless mode and set up a page. const browser = await puppeteer.launch({ args: ['--no-sandbox'], headless: true, }); const page = await browser.newPage(); // Prepare for the tests (not yet implemented). await preparePageForTests(page); // Navigate to the page that will perform the tests. const testUrl = 'https://intoli.com/blog/' + 'not-possible-to-block-chrome-headless/chrome-headless-test.html'; await page.goto(testUrl); // Save a screenshot of the results. await page.screenshot({path: 'headless-test-result.png'}); // Clean up. await browser.close() })();