redis_test/
utils.rs

1use std::net::{SocketAddr, TcpListener};
2use std::path::PathBuf;
3use std::{fs, process};
4
5use socket2::{Domain, Socket, Type};
6use tempfile::TempDir;
7
8#[derive(Clone, Debug)]
9pub struct TlsFilePaths {
10    pub redis_crt: PathBuf,
11    pub redis_key: PathBuf,
12    pub ca_crt: PathBuf,
13}
14
15pub fn build_keys_and_certs_for_tls(tempdir: &TempDir) -> TlsFilePaths {
16    build_keys_and_certs_for_tls_ext(tempdir, true)
17}
18
19pub fn build_keys_and_certs_for_tls_ext(tempdir: &TempDir, with_ip_alts: bool) -> TlsFilePaths {
20    // Based on shell script in redis's server tests
21    // https://github.com/redis/redis/blob/8c291b97b95f2e011977b522acf77ead23e26f55/utils/gen-test-certs.sh
22    let ca_crt = tempdir.path().join("ca.crt");
23    let ca_key = tempdir.path().join("ca.key");
24    let ca_serial = tempdir.path().join("ca.txt");
25    let redis_crt = tempdir.path().join("redis.crt");
26    let redis_key = tempdir.path().join("redis.key");
27    let ext_file = tempdir.path().join("openssl.cnf");
28
29    fn make_key<S: AsRef<std::ffi::OsStr>>(name: S, size: usize) {
30        process::Command::new("openssl")
31            .arg("genrsa")
32            .arg("-out")
33            .arg(name)
34            .arg(format!("{size}"))
35            .stdout(process::Stdio::piped())
36            .stderr(process::Stdio::piped())
37            .spawn()
38            .expect("failed to spawn openssl")
39            .wait()
40            .expect("failed to create key");
41    }
42
43    // Build CA Key
44    make_key(&ca_key, 4096);
45
46    // Build redis key
47    make_key(&redis_key, 2048);
48
49    // Build CA Cert
50    let status = process::Command::new("openssl")
51        .arg("req")
52        .arg("-x509")
53        .arg("-new")
54        .arg("-nodes")
55        .arg("-sha256")
56        .arg("-key")
57        .arg(&ca_key)
58        .arg("-days")
59        .arg("3650")
60        .arg("-subj")
61        .arg("/O=Redis Test/CN=Certificate Authority")
62        .arg("-out")
63        .arg(&ca_crt)
64        .stdout(process::Stdio::piped())
65        .stderr(process::Stdio::piped())
66        .spawn()
67        .expect("failed to spawn openssl")
68        .wait()
69        .expect("failed to create CA cert");
70    assert!(
71        status.success(),
72        "`openssl req` failed to create CA cert: {status}"
73    );
74
75    // Build x509v3 extensions file
76    let ext = if with_ip_alts {
77        "\
78            keyUsage = digitalSignature, keyEncipherment\n\
79            subjectAltName = @alt_names\n\
80            [alt_names]\n\
81            IP.1 = 127.0.0.1\n\
82            "
83    } else {
84        "\
85            [req]\n\
86            distinguished_name = req_distinguished_name\n\
87            x509_extensions = v3_req\n\
88            prompt = no\n\
89            \n\
90            [req_distinguished_name]\n\
91            CN = localhost.example.com\n\
92            \n\
93            [v3_req]\n\
94            basicConstraints = CA:FALSE\n\
95            keyUsage = nonRepudiation, digitalSignature, keyEncipherment\n\
96            subjectAltName = @alt_names\n\
97            \n\
98            [alt_names]\n\
99            DNS.1 = localhost.example.com\n\
100            "
101    };
102    fs::write(&ext_file, ext).expect("failed to create x509v3 extensions file");
103
104    // Read redis key
105    let mut key_cmd = process::Command::new("openssl")
106        .arg("req")
107        .arg("-new")
108        .arg("-sha256")
109        .arg("-subj")
110        .arg("/O=Redis Test/CN=Generic-cert")
111        .arg("-key")
112        .arg(&redis_key)
113        .stdout(process::Stdio::piped())
114        .stderr(process::Stdio::piped())
115        .spawn()
116        .expect("failed to spawn openssl");
117
118    // build redis cert
119    let mut command2 = process::Command::new("openssl");
120    command2
121        .arg("x509")
122        .arg("-req")
123        .arg("-sha256")
124        .arg("-CA")
125        .arg(&ca_crt)
126        .arg("-CAkey")
127        .arg(&ca_key)
128        .arg("-CAserial")
129        .arg(&ca_serial)
130        .arg("-CAcreateserial")
131        .arg("-days")
132        .arg("365")
133        .arg("-extfile")
134        .arg(&ext_file);
135    if !with_ip_alts {
136        command2.arg("-extensions").arg("v3_req");
137    }
138    let status2 = command2
139        .arg("-out")
140        .arg(&redis_crt)
141        .stdin(key_cmd.stdout.take().expect("should have stdout"))
142        .spawn()
143        .expect("failed to spawn openssl")
144        .wait()
145        .expect("failed to create redis cert");
146
147    let status = key_cmd.wait().expect("failed to create redis key");
148    assert!(
149        status.success(),
150        "`openssl req` failed to create request for Redis cert: {status}"
151    );
152    assert!(
153        status2.success(),
154        "`openssl x509` failed to create Redis cert: {status2}"
155    );
156
157    TlsFilePaths {
158        redis_crt,
159        redis_key,
160        ca_crt,
161    }
162}
163
164pub fn get_listener_on_free_port() -> TcpListener {
165    let addr = &"127.0.0.1:0".parse::<SocketAddr>().unwrap().into();
166    let socket = Socket::new(Domain::IPV4, Type::STREAM, None).unwrap();
167    socket.set_reuse_address(true).unwrap();
168    socket.bind(addr).unwrap();
169    socket.listen(1).unwrap();
170    TcpListener::from(socket)
171}
172
173/// Finds a random open port available for listening at, by spawning a TCP server with
174/// port "zero" (which prompts the OS to just use any available port). Between calling
175/// this function and trying to bind to this port, the port may be given to another
176/// process, so this must be used with care (since here we only use it for tests, it's
177/// mostly okay).
178pub fn get_random_available_port() -> u16 {
179    for _ in 0..10000 {
180        let listener = get_listener_on_free_port();
181        let port = listener.local_addr().unwrap().port();
182        if port < 55535 {
183            return port;
184        }
185    }
186    panic!("Couldn't get a valid port");
187}