(root)/
Python-3.12.0/
Tools/
ssl/
multissltests.py
       1  #!./python
       2  """Run Python tests against multiple installations of OpenSSL and LibreSSL
       3  
       4  The script
       5  
       6    (1) downloads OpenSSL / LibreSSL tar bundle
       7    (2) extracts it to ./src
       8    (3) compiles OpenSSL / LibreSSL
       9    (4) installs OpenSSL / LibreSSL into ../multissl/$LIB/$VERSION/
      10    (5) forces a recompilation of Python modules using the
      11        header and library files from ../multissl/$LIB/$VERSION/
      12    (6) runs Python's test suite
      13  
      14  The script must be run with Python's build directory as current working
      15  directory.
      16  
      17  The script uses LD_RUN_PATH, LD_LIBRARY_PATH, CPPFLAGS and LDFLAGS to bend
      18  search paths for header files and shared libraries. It's known to work on
      19  Linux with GCC and clang.
      20  
      21  Please keep this script compatible with Python 2.7, and 3.4 to 3.7.
      22  
      23  (c) 2013-2017 Christian Heimes <christian@python.org>
      24  """
      25  from __future__ import print_function
      26  
      27  import argparse
      28  from datetime import datetime
      29  import logging
      30  import os
      31  try:
      32      from urllib.request import urlopen
      33      from urllib.error import HTTPError
      34  except ImportError:
      35      from urllib2 import urlopen, HTTPError
      36  import re
      37  import shutil
      38  import subprocess
      39  import sys
      40  import tarfile
      41  
      42  
      43  log = logging.getLogger("multissl")
      44  
      45  OPENSSL_OLD_VERSIONS = [
      46  ]
      47  
      48  OPENSSL_RECENT_VERSIONS = [
      49      "1.1.1w",
      50      "3.0.11",
      51      "3.1.3",
      52  ]
      53  
      54  LIBRESSL_OLD_VERSIONS = [
      55  ]
      56  
      57  LIBRESSL_RECENT_VERSIONS = [
      58  ]
      59  
      60  # store files in ../multissl
      61  HERE = os.path.dirname(os.path.abspath(__file__))
      62  PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
      63  MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl'))
      64  
      65  
      66  parser = argparse.ArgumentParser(
      67      prog='multissl',
      68      description=(
      69          "Run CPython tests with multiple OpenSSL and LibreSSL "
      70          "versions."
      71      )
      72  )
      73  parser.add_argument(
      74      '--debug',
      75      action='store_true',
      76      help="Enable debug logging",
      77  )
      78  parser.add_argument(
      79      '--disable-ancient',
      80      action='store_true',
      81      help="Don't test OpenSSL and LibreSSL versions without upstream support",
      82  )
      83  parser.add_argument(
      84      '--openssl',
      85      nargs='+',
      86      default=(),
      87      help=(
      88          "OpenSSL versions, defaults to '{}' (ancient: '{}') if no "
      89          "OpenSSL and LibreSSL versions are given."
      90      ).format(OPENSSL_RECENT_VERSIONS, OPENSSL_OLD_VERSIONS)
      91  )
      92  parser.add_argument(
      93      '--libressl',
      94      nargs='+',
      95      default=(),
      96      help=(
      97          "LibreSSL versions, defaults to '{}' (ancient: '{}') if no "
      98          "OpenSSL and LibreSSL versions are given."
      99      ).format(LIBRESSL_RECENT_VERSIONS, LIBRESSL_OLD_VERSIONS)
     100  )
     101  parser.add_argument(
     102      '--tests',
     103      nargs='*',
     104      default=(),
     105      help="Python tests to run, defaults to all SSL related tests.",
     106  )
     107  parser.add_argument(
     108      '--base-directory',
     109      default=MULTISSL_DIR,
     110      help="Base directory for OpenSSL / LibreSSL sources and builds."
     111  )
     112  parser.add_argument(
     113      '--no-network',
     114      action='store_false',
     115      dest='network',
     116      help="Disable network tests."
     117  )
     118  parser.add_argument(
     119      '--steps',
     120      choices=['library', 'modules', 'tests'],
     121      default='tests',
     122      help=(
     123          "Which steps to perform. 'library' downloads and compiles OpenSSL "
     124          "or LibreSSL. 'module' also compiles Python modules. 'tests' builds "
     125          "all and runs the test suite."
     126      )
     127  )
     128  parser.add_argument(
     129      '--system',
     130      default='',
     131      help="Override the automatic system type detection."
     132  )
     133  parser.add_argument(
     134      '--force',
     135      action='store_true',
     136      dest='force',
     137      help="Force build and installation."
     138  )
     139  parser.add_argument(
     140      '--keep-sources',
     141      action='store_true',
     142      dest='keep_sources',
     143      help="Keep original sources for debugging."
     144  )
     145  
     146  
     147  class ESC[4;38;5;81mAbstractBuilder(ESC[4;38;5;149mobject):
     148      library = None
     149      url_templates = None
     150      src_template = None
     151      build_template = None
     152      depend_target = None
     153      install_target = 'install'
     154      jobs = os.cpu_count()
     155  
     156      module_files = (
     157          os.path.join(PYTHONROOT, "Modules/_ssl.c"),
     158          os.path.join(PYTHONROOT, "Modules/_hashopenssl.c"),
     159      )
     160      module_libs = ("_ssl", "_hashlib")
     161  
     162      def __init__(self, version, args):
     163          self.version = version
     164          self.args = args
     165          # installation directory
     166          self.install_dir = os.path.join(
     167              os.path.join(args.base_directory, self.library.lower()), version
     168          )
     169          # source file
     170          self.src_dir = os.path.join(args.base_directory, 'src')
     171          self.src_file = os.path.join(
     172              self.src_dir, self.src_template.format(version))
     173          # build directory (removed after install)
     174          self.build_dir = os.path.join(
     175              self.src_dir, self.build_template.format(version))
     176          self.system = args.system
     177  
     178      def __str__(self):
     179          return "<{0.__class__.__name__} for {0.version}>".format(self)
     180  
     181      def __eq__(self, other):
     182          if not isinstance(other, AbstractBuilder):
     183              return NotImplemented
     184          return (
     185              self.library == other.library
     186              and self.version == other.version
     187          )
     188  
     189      def __hash__(self):
     190          return hash((self.library, self.version))
     191  
     192      @property
     193      def short_version(self):
     194          """Short version for OpenSSL download URL"""
     195          return None
     196  
     197      @property
     198      def openssl_cli(self):
     199          """openssl CLI binary"""
     200          return os.path.join(self.install_dir, "bin", "openssl")
     201  
     202      @property
     203      def openssl_version(self):
     204          """output of 'bin/openssl version'"""
     205          cmd = [self.openssl_cli, "version"]
     206          return self._subprocess_output(cmd)
     207  
     208      @property
     209      def pyssl_version(self):
     210          """Value of ssl.OPENSSL_VERSION"""
     211          cmd = [
     212              sys.executable,
     213              '-c', 'import ssl; print(ssl.OPENSSL_VERSION)'
     214          ]
     215          return self._subprocess_output(cmd)
     216  
     217      @property
     218      def include_dir(self):
     219          return os.path.join(self.install_dir, "include")
     220  
     221      @property
     222      def lib_dir(self):
     223          return os.path.join(self.install_dir, "lib")
     224  
     225      @property
     226      def has_openssl(self):
     227          return os.path.isfile(self.openssl_cli)
     228  
     229      @property
     230      def has_src(self):
     231          return os.path.isfile(self.src_file)
     232  
     233      def _subprocess_call(self, cmd, env=None, **kwargs):
     234          log.debug("Call '{}'".format(" ".join(cmd)))
     235          return subprocess.check_call(cmd, env=env, **kwargs)
     236  
     237      def _subprocess_output(self, cmd, env=None, **kwargs):
     238          log.debug("Call '{}'".format(" ".join(cmd)))
     239          if env is None:
     240              env = os.environ.copy()
     241              env["LD_LIBRARY_PATH"] = self.lib_dir
     242          out = subprocess.check_output(cmd, env=env, **kwargs)
     243          return out.strip().decode("utf-8")
     244  
     245      def _download_src(self):
     246          """Download sources"""
     247          src_dir = os.path.dirname(self.src_file)
     248          if not os.path.isdir(src_dir):
     249              os.makedirs(src_dir)
     250          data = None
     251          for url_template in self.url_templates:
     252              url = url_template.format(v=self.version, s=self.short_version)
     253              log.info("Downloading from {}".format(url))
     254              try:
     255                  req = urlopen(url)
     256                  # KISS, read all, write all
     257                  data = req.read()
     258              except HTTPError as e:
     259                  log.error(
     260                      "Download from {} has from failed: {}".format(url, e)
     261                  )
     262              else:
     263                  log.info("Successfully downloaded from {}".format(url))
     264                  break
     265          if data is None:
     266              raise ValueError("All download URLs have failed")
     267          log.info("Storing {}".format(self.src_file))
     268          with open(self.src_file, "wb") as f:
     269              f.write(data)
     270  
     271      def _unpack_src(self):
     272          """Unpack tar.gz bundle"""
     273          # cleanup
     274          if os.path.isdir(self.build_dir):
     275              shutil.rmtree(self.build_dir)
     276          os.makedirs(self.build_dir)
     277  
     278          tf = tarfile.open(self.src_file)
     279          name = self.build_template.format(self.version)
     280          base = name + '/'
     281          # force extraction into build dir
     282          members = tf.getmembers()
     283          for member in list(members):
     284              if member.name == name:
     285                  members.remove(member)
     286              elif not member.name.startswith(base):
     287                  raise ValueError(member.name, base)
     288              member.name = member.name[len(base):].lstrip('/')
     289          log.info("Unpacking files to {}".format(self.build_dir))
     290          tf.extractall(self.build_dir, members)
     291  
     292      def _build_src(self, config_args=()):
     293          """Now build openssl"""
     294          log.info("Running build in {}".format(self.build_dir))
     295          cwd = self.build_dir
     296          cmd = [
     297              "./config", *config_args,
     298              "shared", "--debug",
     299              "--prefix={}".format(self.install_dir)
     300          ]
     301          # cmd.extend(["no-deprecated", "--api=1.1.0"])
     302          env = os.environ.copy()
     303          # set rpath
     304          env["LD_RUN_PATH"] = self.lib_dir
     305          if self.system:
     306              env['SYSTEM'] = self.system
     307          self._subprocess_call(cmd, cwd=cwd, env=env)
     308          if self.depend_target:
     309              self._subprocess_call(
     310                  ["make", "-j1", self.depend_target], cwd=cwd, env=env
     311              )
     312          self._subprocess_call(["make", f"-j{self.jobs}"], cwd=cwd, env=env)
     313  
     314      def _make_install(self):
     315          self._subprocess_call(
     316              ["make", "-j1", self.install_target],
     317              cwd=self.build_dir
     318          )
     319          self._post_install()
     320          if not self.args.keep_sources:
     321              shutil.rmtree(self.build_dir)
     322  
     323      def _post_install(self):
     324          pass
     325  
     326      def install(self):
     327          log.info(self.openssl_cli)
     328          if not self.has_openssl or self.args.force:
     329              if not self.has_src:
     330                  self._download_src()
     331              else:
     332                  log.debug("Already has src {}".format(self.src_file))
     333              self._unpack_src()
     334              self._build_src()
     335              self._make_install()
     336          else:
     337              log.info("Already has installation {}".format(self.install_dir))
     338          # validate installation
     339          version = self.openssl_version
     340          if self.version not in version:
     341              raise ValueError(version)
     342  
     343      def recompile_pymods(self):
     344          log.warning("Using build from {}".format(self.build_dir))
     345          # force a rebuild of all modules that use OpenSSL APIs
     346          for fname in self.module_files:
     347              os.utime(fname, None)
     348          # remove all build artefacts
     349          for root, dirs, files in os.walk('build'):
     350              for filename in files:
     351                  if filename.startswith(self.module_libs):
     352                      os.unlink(os.path.join(root, filename))
     353  
     354          # overwrite header and library search paths
     355          env = os.environ.copy()
     356          env["CPPFLAGS"] = "-I{}".format(self.include_dir)
     357          env["LDFLAGS"] = "-L{}".format(self.lib_dir)
     358          # set rpath
     359          env["LD_RUN_PATH"] = self.lib_dir
     360  
     361          log.info("Rebuilding Python modules")
     362          cmd = ["make", "sharedmods", "checksharedmods"]
     363          self._subprocess_call(cmd, env=env)
     364          self.check_imports()
     365  
     366      def check_imports(self):
     367          cmd = [sys.executable, "-c", "import _ssl; import _hashlib"]
     368          self._subprocess_call(cmd)
     369  
     370      def check_pyssl(self):
     371          version = self.pyssl_version
     372          if self.version not in version:
     373              raise ValueError(version)
     374  
     375      def run_python_tests(self, tests, network=True):
     376          if not tests:
     377              cmd = [
     378                  sys.executable,
     379                  os.path.join(PYTHONROOT, 'Lib/test/ssltests.py'),
     380                  '-j0'
     381              ]
     382          elif sys.version_info < (3, 3):
     383              cmd = [sys.executable, '-m', 'test.regrtest']
     384          else:
     385              cmd = [sys.executable, '-m', 'test', '-j0']
     386          if network:
     387              cmd.extend(['-u', 'network', '-u', 'urlfetch'])
     388          cmd.extend(['-w', '-r'])
     389          cmd.extend(tests)
     390          self._subprocess_call(cmd, stdout=None)
     391  
     392  
     393  class ESC[4;38;5;81mBuildOpenSSL(ESC[4;38;5;149mAbstractBuilder):
     394      library = "OpenSSL"
     395      url_templates = (
     396          "https://www.openssl.org/source/openssl-{v}.tar.gz",
     397          "https://www.openssl.org/source/old/{s}/openssl-{v}.tar.gz"
     398      )
     399      src_template = "openssl-{}.tar.gz"
     400      build_template = "openssl-{}"
     401      # only install software, skip docs
     402      install_target = 'install_sw'
     403      depend_target = 'depend'
     404  
     405      def _post_install(self):
     406          if self.version.startswith("3."):
     407              self._post_install_3xx()
     408  
     409      def _build_src(self, config_args=()):
     410          if self.version.startswith("3."):
     411              config_args += ("enable-fips",)
     412          super()._build_src(config_args)
     413  
     414      def _post_install_3xx(self):
     415          # create ssl/ subdir with example configs
     416          # Install FIPS module
     417          self._subprocess_call(
     418              ["make", "-j1", "install_ssldirs", "install_fips"],
     419              cwd=self.build_dir
     420          )
     421          if not os.path.isdir(self.lib_dir):
     422              # 3.0.0-beta2 uses lib64 on 64 bit platforms
     423              lib64 = self.lib_dir + "64"
     424              os.symlink(lib64, self.lib_dir)
     425  
     426      @property
     427      def short_version(self):
     428          """Short version for OpenSSL download URL"""
     429          mo = re.search(r"^(\d+)\.(\d+)\.(\d+)", self.version)
     430          parsed = tuple(int(m) for m in mo.groups())
     431          if parsed < (1, 0, 0):
     432              return "0.9.x"
     433          if parsed >= (3, 0, 0):
     434              # OpenSSL 3.0.0 -> /old/3.0/
     435              parsed = parsed[:2]
     436          return ".".join(str(i) for i in parsed)
     437  
     438  class ESC[4;38;5;81mBuildLibreSSL(ESC[4;38;5;149mAbstractBuilder):
     439      library = "LibreSSL"
     440      url_templates = (
     441          "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{v}.tar.gz",
     442      )
     443      src_template = "libressl-{}.tar.gz"
     444      build_template = "libressl-{}"
     445  
     446  
     447  def configure_make():
     448      if not os.path.isfile('Makefile'):
     449          log.info('Running ./configure')
     450          subprocess.check_call([
     451              './configure', '--config-cache', '--quiet',
     452              '--with-pydebug'
     453          ])
     454  
     455      log.info('Running make')
     456      subprocess.check_call(['make', '--quiet'])
     457  
     458  
     459  def main():
     460      args = parser.parse_args()
     461      if not args.openssl and not args.libressl:
     462          args.openssl = list(OPENSSL_RECENT_VERSIONS)
     463          args.libressl = list(LIBRESSL_RECENT_VERSIONS)
     464          if not args.disable_ancient:
     465              args.openssl.extend(OPENSSL_OLD_VERSIONS)
     466              args.libressl.extend(LIBRESSL_OLD_VERSIONS)
     467  
     468      logging.basicConfig(
     469          level=logging.DEBUG if args.debug else logging.INFO,
     470          format="*** %(levelname)s %(message)s"
     471      )
     472  
     473      start = datetime.now()
     474  
     475      if args.steps in {'modules', 'tests'}:
     476          for name in ['Makefile.pre.in', 'Modules/_ssl.c']:
     477              if not os.path.isfile(os.path.join(PYTHONROOT, name)):
     478                  parser.error(
     479                      "Must be executed from CPython build dir"
     480                  )
     481          if not os.path.samefile('python', sys.executable):
     482              parser.error(
     483                  "Must be executed with ./python from CPython build dir"
     484              )
     485          # check for configure and run make
     486          configure_make()
     487  
     488      # download and register builder
     489      builds = []
     490  
     491      for version in args.openssl:
     492          build = BuildOpenSSL(
     493              version,
     494              args
     495          )
     496          build.install()
     497          builds.append(build)
     498  
     499      for version in args.libressl:
     500          build = BuildLibreSSL(
     501              version,
     502              args
     503          )
     504          build.install()
     505          builds.append(build)
     506  
     507      if args.steps in {'modules', 'tests'}:
     508          for build in builds:
     509              try:
     510                  build.recompile_pymods()
     511                  build.check_pyssl()
     512                  if args.steps == 'tests':
     513                      build.run_python_tests(
     514                          tests=args.tests,
     515                          network=args.network,
     516                      )
     517              except Exception as e:
     518                  log.exception("%s failed", build)
     519                  print("{} failed: {}".format(build, e), file=sys.stderr)
     520                  sys.exit(2)
     521  
     522      log.info("\n{} finished in {}".format(
     523              args.steps.capitalize(),
     524              datetime.now() - start
     525          ))
     526      print('Python: ', sys.version)
     527      if args.steps == 'tests':
     528          if args.tests:
     529              print('Executed Tests:', ' '.join(args.tests))
     530          else:
     531              print('Executed all SSL tests.')
     532  
     533      print('OpenSSL / LibreSSL versions:')
     534      for build in builds:
     535          print("    * {0.library} {0.version}".format(build))
     536  
     537  
     538  if __name__ == "__main__":
     539      main()