Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Tools/ssl/multissltests.py
12 views
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 <[email protected]>
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.1u",
50
"3.0.9",
51
"3.1.1",
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 AbstractBuilder(object):
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 BuildOpenSSL(AbstractBuilder):
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 BuildLibreSSL(AbstractBuilder):
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()
540
541