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