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