|
13 | 13 | import pickle
|
14 | 14 | import zipfile, tarfile
|
15 | 15 | import sys
|
| 16 | +import socket |
| 17 | +import stat |
| 18 | +import hashlib |
16 | 19 | from unittest import mock, SkipTest, skipIf, skipUnless, expectedFailure
|
17 | 20 | from contextlib import contextmanager
|
18 | 21 | from glob import glob
|
19 |
| -from pathlib import (PurePath, Path) |
| 22 | +from pathlib import (PurePath, Path, PureWindowsPath) |
20 | 23 | import typing as T
|
21 | 24 |
|
22 | 25 | import mesonbuild.mlog
|
@@ -5373,3 +5376,128 @@ def test_fortran_new_module_in_dep(self) -> None:
|
5373 | 5376 | output = entry['output']
|
5374 | 5377 |
|
5375 | 5378 | self.build(output, extra_args=['-j1'])
|
| 5379 | + |
| 5380 | + @skipIfNoExecutable('sshd') |
| 5381 | + @skipIfNoExecutable('sftp') |
| 5382 | + # Not tested on Windows, since there is not yet an OpenSSH server available |
| 5383 | + def test_wrap_file_sftp(self) -> None: |
| 5384 | + testdir = Path(self.unit_test_dir) / '130 wrap file sftp' |
| 5385 | + |
| 5386 | + def write_file(path, contents): |
| 5387 | + with open(path, 'w', encoding='utf-8') as f: |
| 5388 | + f.write(contents) |
| 5389 | + |
| 5390 | + def read_file(path): |
| 5391 | + with open(path, 'r', encoding='utf-8') as f: |
| 5392 | + return f.read() |
| 5393 | + |
| 5394 | + def generate_key(path): |
| 5395 | + subprocess.run(['ssh-keygen', '-f', path, '-N', ''], check=True) |
| 5396 | + os.chmod(path, stat.S_IREAD | stat.S_IWRITE) |
| 5397 | + with open(path.with_suffix('.pub'), 'r', encoding='utf-8') as f: |
| 5398 | + return f.read() |
| 5399 | + |
| 5400 | + def find_free_port(): |
| 5401 | + with socket.socket() as sock: |
| 5402 | + sock.bind(('', 0)) |
| 5403 | + return sock.getsockname()[1] |
| 5404 | + |
| 5405 | + def generate_wrap_file(tmpdir, ssh_server_port, user_key_path, host_public_key, source_hash): |
| 5406 | + if mesonbuild.environment.detect_msys2_arch(): |
| 5407 | + user_key_path = to_msys_path(user_key_path) |
| 5408 | + (tmpdir / 'subprojects').mkdir() |
| 5409 | + write_file(tmpdir / 'subprojects' / 'foo.wrap', |
| 5410 | + textwrap.dedent(f'''\ |
| 5411 | + [wrap-file] |
| 5412 | + directory = foo |
| 5413 | +
|
| 5414 | + source_url = sftp://localhost:{ssh_server_port}/foo.tar.gz |
| 5415 | + source_filename = foo.tar.gz |
| 5416 | + source_hash = {source_hash} |
| 5417 | + source_hostkey = {host_public_key} |
| 5418 | + source_identityfile = {user_key_path} |
| 5419 | + ''')) |
| 5420 | + |
| 5421 | + def generate_sshd_config(sshdir, user_public_key, ssh_server_port, sftpdir): |
| 5422 | + authorized_keys_path = sshdir / 'authorized_keys' |
| 5423 | + write_file(authorized_keys_path, user_public_key) |
| 5424 | + if mesonbuild.environment.detect_msys2_arch(): |
| 5425 | + authorized_keys_path = to_msys_path(authorized_keys_path) |
| 5426 | + sftpdir = to_msys_path(sftpdir) |
| 5427 | + sshd_config_path = sshdir / 'sshd_config' |
| 5428 | + write_file(sshd_config_path, |
| 5429 | + textwrap.dedent(f'''\ |
| 5430 | + ListenAddress localhost:{ssh_server_port} |
| 5431 | + PidFile "{sshdir / 'sshd_pid'}" |
| 5432 | + AuthorizedKeysFile "{authorized_keys_path}" |
| 5433 | + Subsystem sftp internal-sftp -d {sftpdir} |
| 5434 | + PasswordAuthentication no |
| 5435 | + StrictModes no |
| 5436 | + ''')) |
| 5437 | + return sshd_config_path |
| 5438 | + |
| 5439 | + def to_msys_path(path): |
| 5440 | + # Convert A:\b\c to /a/b/c |
| 5441 | + path = PureWindowsPath(str(path)) |
| 5442 | + return f'/{path.drive[:1].lower()}/{path.relative_to(path.drive + "/").as_posix()}' |
| 5443 | + |
| 5444 | + def start_sshd(config_path, host_key_path): |
| 5445 | + if mesonbuild.environment.detect_msys2_arch(): |
| 5446 | + config_path = to_msys_path(config_path) |
| 5447 | + host_key_path = to_msys_path(host_key_path) |
| 5448 | + sshd_path = shutil.which('sshd') |
| 5449 | + sshd = subprocess.Popen([sshd_path, '-f', config_path, '-h', host_key_path, '-D']) |
| 5450 | + try: |
| 5451 | + sshd.wait(1) |
| 5452 | + return None |
| 5453 | + except subprocess.TimeoutExpired: |
| 5454 | + # It seems sshd started successfully |
| 5455 | + return sshd |
| 5456 | + |
| 5457 | + def hash_file(path): |
| 5458 | + h = hashlib.sha256() |
| 5459 | + with open(path, 'rb') as f: |
| 5460 | + h.update(f.read()) |
| 5461 | + return h.hexdigest() |
| 5462 | + |
| 5463 | + # In Ubuntu Bionic, the privilege separation directory /run/sshd is |
| 5464 | + # created when the service is started. Since this has never happened in |
| 5465 | + # the docker images in the CI environment, create it manually. |
| 5466 | + if is_linux() and 'CI' in os.environ: |
| 5467 | + os.makedirs('/run/sshd', mode=0o755, exist_ok=True) |
| 5468 | + builddir = Path(self.builddir) |
| 5469 | + workdir = builddir / 'work' |
| 5470 | + sftpdir = builddir / 'sftp' |
| 5471 | + sshdir = builddir / 'ssh' |
| 5472 | + sftpdir.mkdir() |
| 5473 | + sshdir.mkdir() |
| 5474 | + shutil.copytree(testdir / 'top', workdir) |
| 5475 | + shutil.copy(testdir / 'foo.tar.gz', sftpdir) |
| 5476 | + source_hash = hash_file(testdir / 'foo.tar.gz') |
| 5477 | + host_key_path = sshdir / 'host_key' |
| 5478 | + user_key_path = sshdir / 'user_key' |
| 5479 | + host_public_key = generate_key(host_key_path) |
| 5480 | + user_public_key = generate_key(user_key_path) |
| 5481 | + |
| 5482 | + # As there is no reliable way to avoid the port being taken between |
| 5483 | + # us finding a free port and starting the server, support a number |
| 5484 | + # of retries. |
| 5485 | + attempts = 0 |
| 5486 | + while attempts < 3: |
| 5487 | + port = find_free_port() |
| 5488 | + sshd_config_path = generate_sshd_config(sshdir, user_public_key, port, sftpdir) |
| 5489 | + sshd = start_sshd(sshd_config_path, host_key_path) |
| 5490 | + if sshd is None: |
| 5491 | + print(f'Failed to start sshd, probably due to port being taken. Trying again.') |
| 5492 | + attempts += 1 |
| 5493 | + continue |
| 5494 | + generate_wrap_file(workdir, port, user_key_path, host_public_key, source_hash) |
| 5495 | + try: |
| 5496 | + self.new_builddir() # Ensure builddir is not parent or workdir |
| 5497 | + self.init(str(workdir)) |
| 5498 | + self.build() |
| 5499 | + return |
| 5500 | + finally: |
| 5501 | + sshd.terminate() |
| 5502 | + sshd.wait() |
| 5503 | + raise self.fail(f'Failed to start sshd after {attempts} attempts.') |
0 commit comments