|
| 1 | +// This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +// License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +// file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | + |
| 5 | +//! Runs the QUIC example pair (`server_quic` / `client_quic`) against each |
| 6 | +//! other end to end, as separate processes over loopback — the examples are |
| 7 | +//! themselves part of the crate's contract, and this pins them working. |
| 8 | +//! |
| 9 | +//! Cargo builds examples as part of `cargo test`, but provides no |
| 10 | +//! `CARGO_BIN_EXE_<name>` for them (that exists only for `[[bin]]` targets), |
| 11 | +//! so the binaries are located relative to the test executable: |
| 12 | +//! `target/<profile>/examples/`. The test PKI comes from `OUT_DIR`, where |
| 13 | +//! `build.rs` generates it (the `unittest` feature is always enabled for test |
| 14 | +//! builds via the crate's self-dev-dependency). |
| 15 | +
|
| 16 | +#![cfg(feature = "quic")] |
| 17 | + |
| 18 | +use std::io::{BufRead, BufReader, Read, Write}; |
| 19 | +use std::net::SocketAddrV6; |
| 20 | +use std::path::{Path, PathBuf}; |
| 21 | +use std::process::{Child, Command, ExitStatus, Stdio}; |
| 22 | +use std::time::{Duration, Instant}; |
| 23 | + |
| 24 | +/// Bound on every wait in this test, so a wedged process fails the test |
| 25 | +/// instead of hanging the suite. |
| 26 | +const TIMEOUT: Duration = Duration::from_secs(60); |
| 27 | + |
| 28 | +fn example_bin(name: &str) -> PathBuf { |
| 29 | + // The test executable lives in target/<profile>/deps/; examples are |
| 30 | + // siblings of deps/ in target/<profile>/examples/. |
| 31 | + let mut path = std::env::current_exe().unwrap(); |
| 32 | + path.pop(); |
| 33 | + path.pop(); |
| 34 | + path.push("examples"); |
| 35 | + path.push(name); |
| 36 | + assert!( |
| 37 | + path.exists(), |
| 38 | + "example binary {path:?} not found; \ |
| 39 | + it is built by `cargo test --features quic`" |
| 40 | + ); |
| 41 | + path |
| 42 | +} |
| 43 | + |
| 44 | +fn pki(file: &str) -> PathBuf { |
| 45 | + PathBuf::from(env!("OUT_DIR")).join(file) |
| 46 | +} |
| 47 | + |
| 48 | +/// The five positional paths of the examples' `local` subcommand, for test |
| 49 | +/// identity `n`. |
| 50 | +fn identity_args(n: usize) -> Vec<PathBuf> { |
| 51 | + vec![ |
| 52 | + pki(&format!("test-sprockets-auth-{n}.key.pem")), |
| 53 | + pki(&format!("test-sprockets-auth-{n}.certlist.pem")), |
| 54 | + pki(&format!("test-alias-{n}.key.pem")), |
| 55 | + pki(&format!("test-alias-{n}.certlist.pem")), |
| 56 | + pki("log.bin"), |
| 57 | + ] |
| 58 | +} |
| 59 | + |
| 60 | +fn base_cmd(bin: &Path) -> Command { |
| 61 | + let mut cmd = Command::new(bin); |
| 62 | + cmd.arg("--roots") |
| 63 | + .arg(pki("test-root-a.cert.pem")) |
| 64 | + .arg("--corpus") |
| 65 | + .arg(pki("corim-rot.cbor")) |
| 66 | + .arg("--corpus") |
| 67 | + .arg(pki("corim-sp.cbor")) |
| 68 | + .arg("--enforce"); |
| 69 | + cmd |
| 70 | +} |
| 71 | + |
| 72 | +/// Kills the wrapped child on scope exit, so a failing assertion never leaks |
| 73 | +/// a listening server process. |
| 74 | +struct KillOnDrop(Child); |
| 75 | + |
| 76 | +impl Drop for KillOnDrop { |
| 77 | + fn drop(&mut self) { |
| 78 | + let _ = self.0.kill(); |
| 79 | + let _ = self.0.wait(); |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +fn wait_bounded(child: &mut Child, what: &str) -> ExitStatus { |
| 84 | + let deadline = Instant::now() + TIMEOUT; |
| 85 | + loop { |
| 86 | + if let Some(status) = child.try_wait().unwrap() { |
| 87 | + return status; |
| 88 | + } |
| 89 | + assert!( |
| 90 | + Instant::now() < deadline, |
| 91 | + "{what} did not exit within {TIMEOUT:?}" |
| 92 | + ); |
| 93 | + std::thread::sleep(Duration::from_millis(20)); |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +/// Runs `client_quic` against `addr`, feeding `msg` on stdin, and returns |
| 98 | +/// what the client wrote to stdout (the echo; the client logs to stderr). |
| 99 | +fn run_client(bin: &Path, addr: &str, msg: &str) -> String { |
| 100 | + let mut child = base_cmd(bin) |
| 101 | + .arg("--addr") |
| 102 | + .arg(addr) |
| 103 | + .arg("local") |
| 104 | + .args(identity_args(2)) |
| 105 | + .stdin(Stdio::piped()) |
| 106 | + .stdout(Stdio::piped()) |
| 107 | + .stderr(Stdio::null()) |
| 108 | + .spawn() |
| 109 | + .unwrap(); |
| 110 | + |
| 111 | + // Write the message, then drop the handle: the resulting stdin EOF is |
| 112 | + // what moves the client to shutdown-and-drain. |
| 113 | + child |
| 114 | + .stdin |
| 115 | + .take() |
| 116 | + .unwrap() |
| 117 | + .write_all(msg.as_bytes()) |
| 118 | + .unwrap(); |
| 119 | + |
| 120 | + let status = wait_bounded(&mut child, "client_quic"); |
| 121 | + assert!(status.success(), "client_quic exited with {status}"); |
| 122 | + |
| 123 | + // The echo is far smaller than the pipe buffer, so reading after exit |
| 124 | + // cannot have blocked the child. |
| 125 | + let mut echoed = String::new(); |
| 126 | + child |
| 127 | + .stdout |
| 128 | + .take() |
| 129 | + .unwrap() |
| 130 | + .read_to_string(&mut echoed) |
| 131 | + .unwrap(); |
| 132 | + echoed |
| 133 | +} |
| 134 | + |
| 135 | +/// The example pair round-trips messages over an attested QUIC connection: |
| 136 | +/// the server started on port 0 announces its real address, each client's |
| 137 | +/// stdin comes back complete on its stdout, and the server survives clients |
| 138 | +/// departing (it used to panic when a finished client closed its |
| 139 | +/// connection). |
| 140 | +#[test] |
| 141 | +fn example_pair_round_trips() { |
| 142 | + let mut server = KillOnDrop( |
| 143 | + base_cmd(&example_bin("server_quic")) |
| 144 | + .arg("--addr") |
| 145 | + .arg("[::1]:0") |
| 146 | + .arg("local") |
| 147 | + .args(identity_args(1)) |
| 148 | + .stdin(Stdio::null()) |
| 149 | + .stdout(Stdio::piped()) |
| 150 | + .stderr(Stdio::null()) |
| 151 | + .spawn() |
| 152 | + .unwrap(), |
| 153 | + ); |
| 154 | + |
| 155 | + // The server's first stdout line announces the OS-assigned address. Read |
| 156 | + // it via a thread + channel so a silent server trips TIMEOUT rather than |
| 157 | + // blocking the test forever; the thread then keeps draining stdout so |
| 158 | + // the server can never block on a full pipe. |
| 159 | + let stdout = server.0.stdout.take().unwrap(); |
| 160 | + let (tx, rx) = std::sync::mpsc::channel(); |
| 161 | + std::thread::spawn(move || { |
| 162 | + let mut lines = BufReader::new(stdout).lines(); |
| 163 | + if let Some(Ok(line)) = lines.next() { |
| 164 | + let _ = tx.send(line); |
| 165 | + } |
| 166 | + for _ in lines {} |
| 167 | + }); |
| 168 | + let line = rx |
| 169 | + .recv_timeout(TIMEOUT) |
| 170 | + .expect("server_quic announced its listen address"); |
| 171 | + let addr = line |
| 172 | + .strip_prefix("listening on ") |
| 173 | + .unwrap_or_else(|| panic!("unexpected announce line: {line:?}")); |
| 174 | + let addr: SocketAddrV6 = addr.parse().unwrap(); |
| 175 | + let addr = addr.to_string(); |
| 176 | + |
| 177 | + let client_bin = example_bin("client_quic"); |
| 178 | + |
| 179 | + const FIRST: &str = "hello over quic\n"; |
| 180 | + assert_eq!(run_client(&client_bin, &addr, FIRST), FIRST); |
| 181 | + |
| 182 | + const SECOND: &str = "second client\n"; |
| 183 | + assert_eq!(run_client(&client_bin, &addr, SECOND), SECOND); |
| 184 | + |
| 185 | + assert!( |
| 186 | + server.0.try_wait().unwrap().is_none(), |
| 187 | + "server_quic must outlive its clients" |
| 188 | + ); |
| 189 | +} |
0 commit comments