1#!./python 2"""Run Python tests against multiple installations of OpenSSL and LibreSSL 3 4The 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 14The script must be run with Python's build directory as current working 15directory. 16 17The script uses LD_RUN_PATH, LD_LIBRARY_PATH, CPPFLAGS and LDFLAGS to bend 18search paths for header files and shared libraries. It's known to work on 19Linux with GCC and clang. 20 21Please 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""" 25from __future__ import print_function 26 27import argparse 28from datetime import datetime 29import logging 30import os 31try: 32 from urllib.request import urlopen 33except ImportError: 34 from urllib2 import urlopen 35import subprocess 36import shutil 37import sys 38import tarfile 39 40 41log = logging.getLogger("multissl") 42 43OPENSSL_OLD_VERSIONS = [ 44 "1.0.2", 45] 46 47OPENSSL_RECENT_VERSIONS = [ 48 "1.0.2t", 49 "1.1.0l", 50 "1.1.1d", 51] 52 53LIBRESSL_OLD_VERSIONS = [ 54] 55 56LIBRESSL_RECENT_VERSIONS = [ 57 "2.9.2", 58] 59 60# store files in ../multissl 61HERE = os.path.dirname(os.path.abspath(__file__)) 62PYTHONROOT = os.path.abspath(os.path.join(HERE, '..', '..')) 63MULTISSL_DIR = os.path.abspath(os.path.join(PYTHONROOT, '..', 'multissl')) 64 65 66parser = argparse.ArgumentParser( 67 prog='multissl', 68 description=( 69 "Run CPython tests with multiple OpenSSL and LibreSSL " 70 "versions." 71 ) 72) 73parser.add_argument( 74 '--debug', 75 action='store_true', 76 help="Enable debug logging", 77) 78parser.add_argument( 79 '--disable-ancient', 80 action='store_true', 81 help="Don't test OpenSSL < 1.0.2 and LibreSSL < 2.5.3.", 82) 83parser.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) 92parser.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) 101parser.add_argument( 102 '--tests', 103 nargs='*', 104 default=(), 105 help="Python tests to run, defaults to all SSL related tests.", 106) 107parser.add_argument( 108 '--base-directory', 109 default=MULTISSL_DIR, 110 help="Base directory for OpenSSL / LibreSSL sources and builds." 111) 112parser.add_argument( 113 '--no-network', 114 action='store_false', 115 dest='network', 116 help="Disable network tests." 117) 118parser.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) 128parser.add_argument( 129 '--system', 130 default='', 131 help="Override the automatic system type detection." 132) 133parser.add_argument( 134 '--force', 135 action='store_true', 136 dest='force', 137 help="Force build and installation." 138) 139parser.add_argument( 140 '--keep-sources', 141 action='store_true', 142 dest='keep_sources', 143 help="Keep original sources for debugging." 144) 145 146 147class AbstractBuilder(object): 148 library = None 149 url_template = None 150 src_template = None 151 build_template = None 152 install_target = 'install' 153 154 module_files = ("Modules/_ssl.c", 155 "Modules/_hashopenssl.c") 156 module_libs = ("_ssl", "_hashlib") 157 158 def __init__(self, version, args): 159 self.version = version 160 self.args = args 161 # installation directory 162 self.install_dir = os.path.join( 163 os.path.join(args.base_directory, self.library.lower()), version 164 ) 165 # source file 166 self.src_dir = os.path.join(args.base_directory, 'src') 167 self.src_file = os.path.join( 168 self.src_dir, self.src_template.format(version)) 169 # build directory (removed after install) 170 self.build_dir = os.path.join( 171 self.src_dir, self.build_template.format(version)) 172 self.system = args.system 173 174 def __str__(self): 175 return "<{0.__class__.__name__} for {0.version}>".format(self) 176 177 def __eq__(self, other): 178 if not isinstance(other, AbstractBuilder): 179 return NotImplemented 180 return ( 181 self.library == other.library 182 and self.version == other.version 183 ) 184 185 def __hash__(self): 186 return hash((self.library, self.version)) 187 188 @property 189 def openssl_cli(self): 190 """openssl CLI binary""" 191 return os.path.join(self.install_dir, "bin", "openssl") 192 193 @property 194 def openssl_version(self): 195 """output of 'bin/openssl version'""" 196 cmd = [self.openssl_cli, "version"] 197 return self._subprocess_output(cmd) 198 199 @property 200 def pyssl_version(self): 201 """Value of ssl.OPENSSL_VERSION""" 202 cmd = [ 203 sys.executable, 204 '-c', 'import ssl; print(ssl.OPENSSL_VERSION)' 205 ] 206 return self._subprocess_output(cmd) 207 208 @property 209 def include_dir(self): 210 return os.path.join(self.install_dir, "include") 211 212 @property 213 def lib_dir(self): 214 return os.path.join(self.install_dir, "lib") 215 216 @property 217 def has_openssl(self): 218 return os.path.isfile(self.openssl_cli) 219 220 @property 221 def has_src(self): 222 return os.path.isfile(self.src_file) 223 224 def _subprocess_call(self, cmd, env=None, **kwargs): 225 log.debug("Call '{}'".format(" ".join(cmd))) 226 return subprocess.check_call(cmd, env=env, **kwargs) 227 228 def _subprocess_output(self, cmd, env=None, **kwargs): 229 log.debug("Call '{}'".format(" ".join(cmd))) 230 if env is None: 231 env = os.environ.copy() 232 env["LD_LIBRARY_PATH"] = self.lib_dir 233 out = subprocess.check_output(cmd, env=env, **kwargs) 234 return out.strip().decode("utf-8") 235 236 def _download_src(self): 237 """Download sources""" 238 src_dir = os.path.dirname(self.src_file) 239 if not os.path.isdir(src_dir): 240 os.makedirs(src_dir) 241 url = self.url_template.format(self.version) 242 log.info("Downloading from {}".format(url)) 243 req = urlopen(url) 244 # KISS, read all, write all 245 data = req.read() 246 log.info("Storing {}".format(self.src_file)) 247 with open(self.src_file, "wb") as f: 248 f.write(data) 249 250 def _unpack_src(self): 251 """Unpack tar.gz bundle""" 252 # cleanup 253 if os.path.isdir(self.build_dir): 254 shutil.rmtree(self.build_dir) 255 os.makedirs(self.build_dir) 256 257 tf = tarfile.open(self.src_file) 258 name = self.build_template.format(self.version) 259 base = name + '/' 260 # force extraction into build dir 261 members = tf.getmembers() 262 for member in list(members): 263 if member.name == name: 264 members.remove(member) 265 elif not member.name.startswith(base): 266 raise ValueError(member.name, base) 267 member.name = member.name[len(base):].lstrip('/') 268 log.info("Unpacking files to {}".format(self.build_dir)) 269 tf.extractall(self.build_dir, members) 270 271 def _build_src(self): 272 """Now build openssl""" 273 log.info("Running build in {}".format(self.build_dir)) 274 cwd = self.build_dir 275 cmd = [ 276 "./config", 277 "shared", "--debug", 278 "--prefix={}".format(self.install_dir) 279 ] 280 env = os.environ.copy() 281 # set rpath 282 env["LD_RUN_PATH"] = self.lib_dir 283 if self.system: 284 env['SYSTEM'] = self.system 285 self._subprocess_call(cmd, cwd=cwd, env=env) 286 # Old OpenSSL versions do not support parallel builds. 287 self._subprocess_call(["make", "-j1"], cwd=cwd, env=env) 288 289 def _make_install(self): 290 self._subprocess_call( 291 ["make", "-j1", self.install_target], 292 cwd=self.build_dir 293 ) 294 if not self.args.keep_sources: 295 shutil.rmtree(self.build_dir) 296 297 def install(self): 298 log.info(self.openssl_cli) 299 if not self.has_openssl or self.args.force: 300 if not self.has_src: 301 self._download_src() 302 else: 303 log.debug("Already has src {}".format(self.src_file)) 304 self._unpack_src() 305 self._build_src() 306 self._make_install() 307 else: 308 log.info("Already has installation {}".format(self.install_dir)) 309 # validate installation 310 version = self.openssl_version 311 if self.version not in version: 312 raise ValueError(version) 313 314 def recompile_pymods(self): 315 log.warning("Using build from {}".format(self.build_dir)) 316 # force a rebuild of all modules that use OpenSSL APIs 317 for fname in self.module_files: 318 os.utime(fname, None) 319 # remove all build artefacts 320 for root, dirs, files in os.walk('build'): 321 for filename in files: 322 if filename.startswith(self.module_libs): 323 os.unlink(os.path.join(root, filename)) 324 325 # overwrite header and library search paths 326 env = os.environ.copy() 327 env["CPPFLAGS"] = "-I{}".format(self.include_dir) 328 env["LDFLAGS"] = "-L{}".format(self.lib_dir) 329 # set rpath 330 env["LD_RUN_PATH"] = self.lib_dir 331 332 log.info("Rebuilding Python modules") 333 cmd = [sys.executable, "setup.py", "build"] 334 self._subprocess_call(cmd, env=env) 335 self.check_imports() 336 337 def check_imports(self): 338 cmd = [sys.executable, "-c", "import _ssl; import _hashlib"] 339 self._subprocess_call(cmd) 340 341 def check_pyssl(self): 342 version = self.pyssl_version 343 if self.version not in version: 344 raise ValueError(version) 345 346 def run_python_tests(self, tests, network=True): 347 if not tests: 348 cmd = [sys.executable, 'Lib/test/ssltests.py', '-j0'] 349 elif sys.version_info < (3, 3): 350 cmd = [sys.executable, '-m', 'test.regrtest'] 351 else: 352 cmd = [sys.executable, '-m', 'test', '-j0'] 353 if network: 354 cmd.extend(['-u', 'network', '-u', 'urlfetch']) 355 cmd.extend(['-w', '-r']) 356 cmd.extend(tests) 357 self._subprocess_call(cmd, stdout=None) 358 359 360class BuildOpenSSL(AbstractBuilder): 361 library = "OpenSSL" 362 url_template = "https://www.openssl.org/source/openssl-{}.tar.gz" 363 src_template = "openssl-{}.tar.gz" 364 build_template = "openssl-{}" 365 # only install software, skip docs 366 install_target = 'install_sw' 367 368 369class BuildLibreSSL(AbstractBuilder): 370 library = "LibreSSL" 371 url_template = ( 372 "https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-{}.tar.gz") 373 src_template = "libressl-{}.tar.gz" 374 build_template = "libressl-{}" 375 376 377def configure_make(): 378 if not os.path.isfile('Makefile'): 379 log.info('Running ./configure') 380 subprocess.check_call([ 381 './configure', '--config-cache', '--quiet', 382 '--with-pydebug' 383 ]) 384 385 log.info('Running make') 386 subprocess.check_call(['make', '--quiet']) 387 388 389def main(): 390 args = parser.parse_args() 391 if not args.openssl and not args.libressl: 392 args.openssl = list(OPENSSL_RECENT_VERSIONS) 393 args.libressl = list(LIBRESSL_RECENT_VERSIONS) 394 if not args.disable_ancient: 395 args.openssl.extend(OPENSSL_OLD_VERSIONS) 396 args.libressl.extend(LIBRESSL_OLD_VERSIONS) 397 398 logging.basicConfig( 399 level=logging.DEBUG if args.debug else logging.INFO, 400 format="*** %(levelname)s %(message)s" 401 ) 402 403 start = datetime.now() 404 405 if args.steps in {'modules', 'tests'}: 406 for name in ['setup.py', 'Modules/_ssl.c']: 407 if not os.path.isfile(os.path.join(PYTHONROOT, name)): 408 parser.error( 409 "Must be executed from CPython build dir" 410 ) 411 if not os.path.samefile('python', sys.executable): 412 parser.error( 413 "Must be executed with ./python from CPython build dir" 414 ) 415 # check for configure and run make 416 configure_make() 417 418 # download and register builder 419 builds = [] 420 421 for version in args.openssl: 422 build = BuildOpenSSL( 423 version, 424 args 425 ) 426 build.install() 427 builds.append(build) 428 429 for version in args.libressl: 430 build = BuildLibreSSL( 431 version, 432 args 433 ) 434 build.install() 435 builds.append(build) 436 437 if args.steps in {'modules', 'tests'}: 438 for build in builds: 439 try: 440 build.recompile_pymods() 441 build.check_pyssl() 442 if args.steps == 'tests': 443 build.run_python_tests( 444 tests=args.tests, 445 network=args.network, 446 ) 447 except Exception as e: 448 log.exception("%s failed", build) 449 print("{} failed: {}".format(build, e), file=sys.stderr) 450 sys.exit(2) 451 452 log.info("\n{} finished in {}".format( 453 args.steps.capitalize(), 454 datetime.now() - start 455 )) 456 print('Python: ', sys.version) 457 if args.steps == 'tests': 458 if args.tests: 459 print('Executed Tests:', ' '.join(args.tests)) 460 else: 461 print('Executed all SSL tests.') 462 463 print('OpenSSL / LibreSSL versions:') 464 for build in builds: 465 print(" * {0.library} {0.version}".format(build)) 466 467 468if __name__ == "__main__": 469 main() 470