1import os
2import shutil
3import subprocess
4from unittest import SkipTest
5
6# subprocess does not kill the child daemon when a test case fails by raising
7# an exception. So use pexpect instead.
8import pexpect
9
10import infra
11
12
13SSHD_PORT_INITIAL = 2222
14SSHD_PORT_LAST = SSHD_PORT_INITIAL + 99
15SSHD_PATH = "/usr/sbin/sshd"
16SSHD_HOST_DIR = "host"
17
18# SSHD_KEY_DIR is where the /etc/ssh/ssh_host_*_key files go
19SSHD_KEY_DIR = os.path.join(SSHD_HOST_DIR, "etc/ssh")
20SSHD_KEY = os.path.join(SSHD_KEY_DIR, "ssh_host_ed25519_key")
21
22# SSH_CLIENT_KEY_DIR is where the client id_rsa key and authorized_keys files go
23SSH_CLIENT_KEY_DIR = os.path.join(SSHD_HOST_DIR, "home/br-user/ssh")
24SSH_CLIENT_KEY = os.path.join(SSH_CLIENT_KEY_DIR, "id_rsa")
25SSH_AUTH_KEYS_FILE = os.path.join(SSH_CLIENT_KEY_DIR, "authorized_keys")
26
27
28class OpenSSHDaemon():
29
30    def __init__(self, builddir, logtofile):
31        """
32        Start an OpenSSH SSH Daemon
33
34        In order to support test cases in parallel, select the port the
35        server will listen to in runtime. Since there is no reliable way
36        to allocate the port prior to starting the server (another
37        process in the host machine can use the port between it is
38        selected from a list and it is really allocated to the server)
39        try to start the server in a port and in the case it is already
40        in use, try the next one in the allowed range.
41        """
42        self.daemon = None
43        self.port = None
44
45        self.logfile = infra.open_log_file(builddir, "sshd", logtofile)
46
47        server_keyfile = os.path.join(builddir, SSHD_KEY)
48        auth_keys_file = os.path.join(builddir, SSH_AUTH_KEYS_FILE)
49        daemon_cmd = [SSHD_PATH,
50                      "-D",  # or use -ddd to debug
51                      "-e",
52                      "-h", server_keyfile,
53                      "-f", "/dev/null",
54                      "-o", "ListenAddress=localhost",
55                      "-o", "PidFile=none",
56                      "-o", "AuthenticationMethods=publickey",
57                      "-o", "StrictModes=no",
58                      "-o", "Subsystem=sftp internal-sftp",
59                      "-o", "AuthorizedKeysFile={}".format(auth_keys_file)]
60        for port in range(SSHD_PORT_INITIAL, SSHD_PORT_LAST + 1):
61            cmd = daemon_cmd + ["-p", "{}".format(port)]
62            self.logfile.write(
63                "> starting sshd with '{}'\n".format(" ".join(cmd)))
64            try:
65                self.daemon = pexpect.spawn(cmd[0], cmd[1:], logfile=self.logfile,
66                                            encoding='utf-8')
67            except pexpect.exceptions.ExceptionPexpect as e:
68                self.logfile.write("> {} - skipping\n".format(e))
69                raise SkipTest(str(e))
70
71            ret = self.daemon.expect([
72                # Success
73                "Server listening on .* port {}.".format(port),
74                # Failure
75                "Cannot bind any address."])
76            if ret == 0:
77                self.port = port
78                return
79        raise SystemError("Could not find a free port to run sshd")
80
81    def stop(self):
82        if self.daemon is None:
83            return
84        self.daemon.terminate(force=True)
85
86
87def generate_keys_server(builddir, logfile):
88    """Generate keys required to run an OpenSSH Daemon."""
89    keyfile = os.path.join(builddir, SSHD_KEY)
90    if os.path.exists(keyfile):
91        logfile.write("> SSH server key already exists '{}'".format(keyfile))
92        return
93
94    hostdir = os.path.join(builddir, SSHD_HOST_DIR)
95    keydir = os.path.join(builddir, SSHD_KEY_DIR)
96    os.makedirs(hostdir, exist_ok=True)
97    os.makedirs(keydir, exist_ok=True)
98
99    cmd = ["ssh-keygen", "-A", "-f", hostdir]
100    logfile.write(
101        "> generating SSH server keys with '{}'\n".format(" ".join(cmd)))
102    # When ssh-keygen fails to create an SSH server key it doesn't return a
103    # useful error code. So use check for an error message in the output
104    # instead.
105    try:
106        out = subprocess.check_output(cmd, encoding='utf-8')
107    except FileNotFoundError:
108        logfile.write("> ssh-keygen not found - skipping\n")
109        raise SkipTest("ssh-keygen not found")
110
111    logfile.write(out)
112    if "Could not save your public key" in out:
113        raise SystemError("Could not generate SSH server keys")
114
115
116def generate_keys_client(builddir, logfile):
117    """Generate keys required to log into an OpenSSH Daemon via SCP or SFTP."""
118    keyfile = os.path.join(builddir, SSH_CLIENT_KEY)
119    if os.path.exists(keyfile):
120        logfile.write("> SSH client key already exists '{}'".format(keyfile))
121        return
122
123    keydir = os.path.join(builddir, SSH_CLIENT_KEY_DIR)
124    os.makedirs(keydir, exist_ok=True)
125
126    cmd = ["ssh-keygen",
127           "-f", keyfile,
128           "-b", "2048",
129           "-t", "rsa",
130           "-N", "",
131           "-q"]
132    logfile.write(
133        "> generating SSH client keys with '{}'\n".format(" ".join(cmd)))
134    try:
135        subprocess.check_call(cmd, stdout=logfile, stderr=logfile)
136    except FileNotFoundError:
137        logfile.write("> ssh-keygen not found - skipping\n")
138        raise SkipTest("ssh-keygen not found")
139
140    # Allow key-based login for this user (so that we can fetch from localhost)
141    pubkeyfile = os.path.join(keydir, "{}.pub".format(keyfile))
142    authfile = os.path.join(keydir, "authorized_keys")
143    shutil.copy(pubkeyfile, authfile)
144
145
146def generate_keys(builddir, logtofile):
147    logfile = infra.open_log_file(builddir, "ssh-keygen", logtofile)
148    generate_keys_server(builddir, logfile)
149    generate_keys_client(builddir, logfile)
150