Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Selenium webdriver: firefox headless inject javascript to modify browser property

I'm trying to figure out how is possible to use selenium webdriver with python or java to inject javascript in order to modify browser property/attribute. My final object is to get something similar to this with selenium and firefox since it is a more open and flexible choice.

Puppeter and chromium file test.js:

const puppeteer = require("puppeteer");

(async () => {
  const browser = await puppeteer.launch({
    args: ["--no-sandbox"],
    headless: true,
  });
  const page = await browser.newPage();
  const fs = require("fs");

  // In your puppeteer script, assuming the javascriptChromium.js file is in same folder of our script
  const preloadFile = fs.readFileSync("./javascriptChromium.js", "utf8");
  await page.evaluateOnNewDocument(preloadFile);

  const testUrl="https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html";

  await page.goto(testUrl);

  // save screenshot
  await page.screenshot({path: "puppeteer-chromium-async-script-test.png"});
  await browser.close()
})();

Javascript file javascriptChromium.js

// overwrite the `languages` property to use a custom getter
Object.defineProperty(navigator, "languages", {
  get: function() {
    return ["en-US", "en", "es"];
  }
});

// Overwrite the `plugins` property to use a custom getter.
Object.defineProperty(navigator, 'plugins', {
  get: () => [1, 2, 3, 4, 5],
});

// Pass the Webdriver test
Object.defineProperty(navigator, 'webdriver', {
  get: () => false,
});

This code works well and I checked that the property are changed via this test Web site.

Now, selenium and firefox:

import os
from selenium import webdriver

def readJSFile(scriptFile):
    with open(scriptFile, 'r') as fileHandle:  
        script=fileHandle.read()
    return script
injectedJavascript=readJSFile("./javascriptFirefox.js")

options=webdriver.FirefoxOptions()
options.set_headless(True)
driver=webdriver.Firefox(options=options)
driver.set_script_timeout(3)

# inject JavaScript
try:
    driver.execute_async_script(injectedJavascript)
except:
    print("Timeout")

# solution found here https://stackoverflow.com/questions/17385779/how-do-i-load-a-javascript-file-into-the-dom-using-selenium
driver.execute_script("var s=window.document.createElement('script'); s.src='javascriptFirefox.js';window.document.head.appendChild(s);")
testUrl="https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html";
driver.get(testUrl)

# example sync script
time=driver.execute_script("return performance.timing.loadEventEnd - performance.timing.navigationStart;")
print(time)
# example async script
time=driver.execute_async_script("var callback = arguments[arguments.length-1]; const time = () => { total=performance.timing.loadEventEnd - performance.timing.navigationStart; callback(total); }; time();")
print(time)

file="selenium-firefox-async-script-test.png"
driver.save_screenshot(file)

driver.quit()

Javascript file javascriptFirefox.js

// overwrite the `languages` property to use a custom getter
const setProperty = () => {
    Object.defineProperty(navigator, "languages", {
        get: function() {
            return ["en-US", "en", "es"];
        }
    });

    // Overwrite the `plugins` property to use a custom getter.
    Object.defineProperty(navigator, 'plugins', {
        get: () => [1, 2, 3, 4, 5],
    });

    // Pass the Webdriver test
    Object.defineProperty(navigator, 'webdriver', {
      get: () => false,
    });
    callback();
};
setProperty();

I'm new of javascript, but what seems different between the two approaches (puppeteer and selenium) is about how they manage the current tab/page. The former via page class and method page.evaluateOnNewDocument while for the latter I did not find and equivalent way. I tried also the use greasemonkey or violentlmonkey to inject javascript without success.

Do you have any suggestions?

Thank you

like image 895
erotavlas Avatar asked Jul 20 '18 09:07

erotavlas


People also ask

How do I initiate Firefox browser in headless mode for script execution?

Headless Execution Firefox Driver It is quite simple to run your tests in the headless mode. You need simply to add the "--headless" argument to the FirefoxOptions object. * However, the current official version of Mozilla Firefox is 56.

How do I run a Selenium script in headless mode?

ChromeOptions options = new ChromeOptions() options. addArgument("headless"); ChromeDriver driver = new ChromeDriver(options); In the above code, the browser is instructed to run in the headless mode using the addArgument() method of the ChromeOptions class provided by the Selenium WebDriver.

Is Selenium headless faster?

Improved speed and performance Selenium tests can take a while to complete, due to elements on a page that the browser needs to load. Headless testing gets rid of this load time, allowing you to cut your testing times significantly. In our tests with headless testing, we've seen a 30% reduction of test execution times.


1 Answers

I found the solution to the problem by following this post. In few words, by using an extensions it is possible to inject javascript code into the Web page also with firefox. In order to avoid a waste of time for the other users, the main files are:

Python file: selenium+firefox

import json
import os
import sys

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.firefox.firefox_profile import AddonFormatError

# Patch in support for WebExtensions in Firefox.
# See: https://intoli.com/blog/firefox-extensions-with-selenium/
class FirefoxProfileWithWebExtensionSupport(webdriver.FirefoxProfile):
    def _addon_details(self, addon_path):
        try:
            return super()._addon_details(addon_path)
        except AddonFormatError:
            try:
                with open(os.path.join(addon_path, "manifest.json"), "r") as f:
                    manifest = json.load(f)
                    return {
                        "id": manifest["applications"]["gecko"]["id"],
                        "version": manifest["version"],
                        "name": manifest["name"],
                        "unpack": False,
                    }
            except (IOError, KeyError) as e:
                raise AddonFormatError(str(e), sys.exc_info()[2])

profile_folder="profile_path"
profile=FirefoxProfileWithWebExtensionSupport(profile_folder)
extension_directory="extension"
profile.add_extension(extension_directory)
# firefox dev it is necessary for custom profile, not for standard one
firefox_binary="/usr/bin/firefox-dev"
options=webdriver.FirefoxOptions()
# firefox 56+ headless mode https://developer.mozilla.org/en-US/Firefox/Headless_mode
options.set_headless(True)
driver=webdriver.Firefox(options=options, firefox_profile=profile, firefox_binary=firefox_binary)

test_url="https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html";
driver.get(test_url)

file="selenium-firefox-extension-profile-script-second-test.png"
driver.save_screenshot(file)

test_url="https://intoli.com/blog/making-chrome-headless-undetectable/chrome-headless-test.html";
driver.get(test_url)

file="selenium-firefox-extension-profile-script-first-test.png"
driver.save_screenshot(file)

driver.quit()

Extensions files: manifest.js and content.js

{
  "manifest_version": 2,
  "name": "Smart Extension",
  "version": "1.0.0",
  "applications": {
    "gecko": {
      "id": "[email protected]"
    }
  },
  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["content.js"],
      "run_at": "document_start"
    }
  ]
}

var script=document.createElement("script");
script.src=browser.extension.getURL("myscript.js");
script.async=false;
document.documentElement.appendChild(script);

Javascript file: myscript.js

// overwrite the `languages` property to use a custom getter
Object.defineProperty(navigator, "languages", {
  get: function() {
    return ["en", "es"];
  }
});

// Overwrite the `plugins` property to use a custom getter.
Object.defineProperty(navigator, "plugins", {
  get: () => new Array(Math.floor(Math.random() * 6) + 1),
});

// Pass the Webdriver test
Object.defineProperty(navigator, "webdriver", {
  get: () => false,
});

// hairline: store the existing descriptor
const elementDescriptor=Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");

// redefine the property with a patched descriptor
Object.defineProperty(HTMLDivElement.prototype, "offsetHeight", {
    ...elementDescriptor,
  get: function() {
    if (this.id === "modernizr") {
      return 1;
    }
    return elementDescriptor.get.apply(this);
  },
});

["height", "width"].forEach(property => {
  // store the existing descriptor
  const imageDescriptor=Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, property);

  // redefine the property with a patched descriptor
  Object.defineProperty(HTMLImageElement.prototype, property, {
    ...imageDescriptor,
    get: function() {
      // return an arbitrary non-zero dimension if the image failed to load
      if (this.complete && this.naturalHeight == 0) {
        return 24;
      }
      // otherwise, return the actual dimension
      return imageDescriptor.get.apply(this);
    },
  });
});

const getParameter=WebGLRenderingContext.getParameter;
WebGLRenderingContext.prototype.getParameter=function(parameter) {
  // UNMASKED_VENDOR_WEBGL WebGLRenderingContext.prototype.VENDOR
  if (parameter === 37445) {
    return "Intel Open Source Technology Center";
  }
  // UNMASKED_RENDERER_WEBGL WebGLRenderingContext.prototype.RENDERER
  if (parameter === 37446) { 
    return "Mesa DRI Intel(R) Ivybridge Mobile";
  }
  return getParameter(parameter);
};

This works well for all tests in graphical mode while in headless mode all tests except WebGL test which seems affect to a bug.

like image 63
erotavlas Avatar answered Nov 15 '22 15:11

erotavlas