diff options
Diffstat (limited to 'testing/mozbase/mozproxy/mozproxy/backends/mitm/android.py')
-rw-r--r-- | testing/mozbase/mozproxy/mozproxy/backends/mitm/android.py | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/testing/mozbase/mozproxy/mozproxy/backends/mitm/android.py b/testing/mozbase/mozproxy/mozproxy/backends/mitm/android.py new file mode 100644 index 0000000000..bdd2921728 --- /dev/null +++ b/testing/mozbase/mozproxy/mozproxy/backends/mitm/android.py @@ -0,0 +1,228 @@ +"""Functions to download, install, setup, and use the mitmproxy playback tool""" +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +from __future__ import absolute_import + +import glob +import os +import subprocess +import sys +from subprocess import PIPE + +import mozinfo +from mozproxy.backends.mitm.mitm import Mitmproxy +from mozproxy.utils import download_file_from_url, tooltool_download, LOG + +# path for mitmproxy certificate, generated auto after mitmdump is started +# on local machine it is 'HOME', however it is different on production machines +try: + DEFAULT_CERT_PATH = os.path.join( + os.getenv("HOME"), ".mitmproxy", "mitmproxy-ca-cert.cer" + ) +except Exception: + DEFAULT_CERT_PATH = os.path.join( + os.getenv("HOMEDRIVE"), + os.getenv("HOMEPATH"), + ".mitmproxy", + "mitmproxy-ca-cert.cer", + ) + +# On Windows, deal with mozilla-build having forward slashes in $HOME: +if os.name == "nt" and "/" in DEFAULT_CERT_PATH: + DEFAULT_CERT_PATH = DEFAULT_CERT_PATH.replace("/", "\\") + + +class MitmproxyAndroid(Mitmproxy): + def setup(self): + self.download_and_install_host_utils() + self.install_mitmproxy_cert(self.browser_path) + + def download_and_install_host_utils(self): + """ + If running locally: + 1. Will use the `certutil` tool from the local Firefox desktop build + + If running in production: + 1. Get the tooltools manifest file for downloading hostutils (contains certutil) + 2. Get the `certutil` tool by downloading hostutils using the tooltool manifest + """ + if self.config["run_local"]: + # when running locally, it is found in the Firefox desktop build (..obj../dist/bin) + self.certutil_path = os.path.join(os.environ["MOZ_HOST_BIN"], "certutil") + if not ( + os.path.isfile(self.certutil_path) + and os.access(self.certutil_path, os.X_OK) + ): + raise Exception( + "Abort: unable to execute certutil: {}".format(self.certutil_path) + ) + self.certutil_path = os.environ["MOZ_HOST_BIN"] + os.environ["LD_LIBRARY_PATH"] = self.certutil_path + else: + # must download certutil inside hostutils via tooltool; use this manifest: + # mozilla-central/testing/config/tooltool-manifests/linux64/hostutils.manifest + # after it will be found here inside the worker/bitbar container: + # /builds/worker/workspace/build/hostutils/host-utils-66.0a1.en-US.linux-x86_64 + LOG.info("downloading certutil binary (hostutils)") + + # get path to the hostutils tooltool manifest; was set earlier in + # mozharness/configs/raptor/android_hw_config.py, to the path i.e. + # mozilla-central/testing/config/tooltool-manifests/linux64/hostutils.manifest + # the bitbar container is always linux64 + if os.environ.get("GECKO_HEAD_REPOSITORY", None) is None: + raise Exception("Abort: unable to get GECKO_HEAD_REPOSITORY") + + if os.environ.get("GECKO_HEAD_REV", None) is None: + raise Exception("Abort: unable to get GECKO_HEAD_REV") + + if os.environ.get("HOSTUTILS_MANIFEST_PATH", None) is not None: + manifest_url = os.path.join( + os.environ["GECKO_HEAD_REPOSITORY"], + "raw-file", + os.environ["GECKO_HEAD_REV"], + os.environ["HOSTUTILS_MANIFEST_PATH"], + ) + else: + raise Exception("Abort: unable to get HOSTUTILS_MANIFEST_PATH!") + + # first need to download the hostutils tooltool manifest file itself + _dest = os.path.join(self.mozproxy_dir, "hostutils.manifest") + have_manifest = download_file_from_url(manifest_url, _dest) + if not have_manifest: + raise Exception("failed to download the hostutils tooltool manifest") + + # now use the manifest to download hostutils so we can get certutil + tooltool_download(_dest, self.config["run_local"], self.mozproxy_dir) + + # the production bitbar container host is always linux + self.certutil_path = glob.glob( + os.path.join(self.mozproxy_dir, "host-utils*[!z|checksum]") + )[0] + + # must add hostutils/certutil to the path + os.environ["LD_LIBRARY_PATH"] = self.certutil_path + + bin_suffix = mozinfo.info.get("bin_suffix", "") + self.certutil_path = os.path.join(self.certutil_path, "certutil" + bin_suffix) + if os.path.isfile(self.certutil_path): + LOG.info("certutil is found at: %s" % self.certutil_path) + else: + raise Exception("unable to find certutil at %s" % self.certutil_path) + + def install_mitmproxy_cert(self, browser_path): + """Install the CA certificate generated by mitmproxy, into geckoview android + + 1. Create an NSS certificate database in the geckoview browser profile dir, only + if it doesn't already exist. Use this certutil command: + `certutil -N -d sql:<path to profile> --empty-password` + 2. Import the mitmproxy certificate into the database, i.e.: + `certutil -A -d sql:<path to profile> -n "some nickname" -t TC,, -a -i <path to CA.pem>` + """ + + cert_db_location = "sql:%s/" % self.config["local_profile_dir"] + + if not self.cert_db_exists(cert_db_location): + self.create_cert_db(cert_db_location) + + # DEFAULT_CERT_PATH has local path and name of mitmproxy cert i.e. + # /home/cltbld/.mitmproxy/mitmproxy-ca-cert.cer + self.import_certificate_in_cert_db(cert_db_location, DEFAULT_CERT_PATH) + + # cannot continue if failed to add CA cert to Firefox, need to check + if not self.is_mitmproxy_cert_installed(cert_db_location): + LOG.error("Aborting: failed to install mitmproxy CA cert into Firefox") + self.stop_mitmproxy_playback() + sys.exit() + + def import_certificate_in_cert_db(self, cert_db_location, local_cert_path): + # import mitmproxy cert into the db + args = [ + "-A", + "-d", + cert_db_location, + "-n", + "mitmproxy-cert", + "-t", + "TC,,", + "-a", + "-i", + local_cert_path, + ] + LOG.info("importing mitmproxy cert into db using command") + self.certutil(args) + + def create_cert_db(self, cert_db_location): + # create cert db if it doesn't already exist; it may exist already + # if a previous pageload test ran in the same test suite + args = ["-d", cert_db_location, "-N", "--empty-password"] + + LOG.info("creating nss cert database") + + self.certutil(args) + + if not self.cert_db_exists(cert_db_location): + raise Exception("nss cert db creation command failed. Cert db not created.") + + def cert_db_exists(self, cert_db_location): + # check if the nss ca cert db already exists in the device profile + LOG.info( + "checking if the nss cert db already exists in the android browser profile" + ) + + args = ["-d", cert_db_location, "-L"] + cert_db_exists = self.certutil(args, raise_exception=False) + + if cert_db_exists: + LOG.info("the nss cert db exists") + return True + else: + LOG.info("nss cert db doesn't exist yet.") + return False + + def is_mitmproxy_cert_installed(self, cert_db_location): + """Verify mitmxproy CA cert was added to Firefox on android""" + LOG.info("verifying that the mitmproxy ca cert is installed on android") + + # list the certifcates that are in the nss cert db (inside the browser profile dir) + LOG.info( + "getting the list of certs in the nss cert db in the android browser profile" + ) + args = ["-d", cert_db_location, "-L"] + + cmd_output = self.certutil(args) + + if "mitmproxy-cert" in cmd_output: + LOG.info( + "verfied the mitmproxy-cert is installed in the nss cert db on android" + ) + return True + return False + + def certutil(self, args, raise_exception=True): + cmd = [self.certutil_path] + list(args) + LOG.info("Certutil: Running command: %s" % " ".join(cmd)) + try: + cmd_proc = subprocess.Popen( + cmd, stdout=PIPE, stderr=PIPE, env=os.environ.copy() + ) + + cmd_output, errs = cmd_proc.communicate() + except subprocess.SubprocessError: + LOG.critical("could not run the certutil command") + raise + + if cmd_proc.returncode == 0: + # Debug purpose only remove if stable + LOG.info("Certutil returncode: %s" % cmd_proc.returncode) + LOG.info("Certutil output: %s" % cmd_output) + return cmd_output + else: + if raise_exception: + LOG.critical("Certutil command failed!!") + LOG.info("Certutil returncode: %s" % cmd_proc.returncode) + LOG.info("Certutil output: %s" % cmd_output) + LOG.info("Certutil error: %s" % errs) + raise Exception("Certutil command failed!!") + else: + return False |