2026-09-12 13:05:20 -07:00
|
|
|
use common::io::read_string;
|
|
|
|
|
use common::request::Request;
|
|
|
|
|
use std::os::unix::fs::FileTypeExt;
|
|
|
|
|
use std::os::unix::net::UnixStream;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use std::process::{Child, Command};
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
2026-09-12 13:19:36 -07:00
|
|
|
/// Idle shutdown takes milliseconds; anything approaching the daemon's 3s
|
|
|
|
|
/// grace period means it is waiting on something it should not be.
|
|
|
|
|
const EXIT_TIMEOUT: Duration = Duration::from_secs(2);
|
|
|
|
|
|
2026-09-12 13:05:20 -07:00
|
|
|
const PASSWORD_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../test_passwords");
|
|
|
|
|
|
|
|
|
|
fn temp_socket_path(tag: &str) -> PathBuf {
|
|
|
|
|
let mut path = std::env::temp_dir();
|
|
|
|
|
path.push(format!("saslauthd-{}-{}.sock", tag, std::process::id()));
|
|
|
|
|
let _ = std::fs::remove_file(&path);
|
|
|
|
|
path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A spawned daemon that is killed if the test panics before stopping it.
|
|
|
|
|
struct Daemon(Child);
|
|
|
|
|
|
|
|
|
|
impl Drop for Daemon {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
if self.0.try_wait().ok().flatten().is_none() {
|
|
|
|
|
let _ = self.0.kill();
|
|
|
|
|
let _ = self.0.wait();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn is_socket(path: &PathBuf) -> bool {
|
|
|
|
|
std::fs::metadata(path)
|
|
|
|
|
.map(|m| m.file_type().is_socket())
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn spawn_daemon(socket: &PathBuf) -> Daemon {
|
|
|
|
|
let child = Command::new(env!("CARGO_BIN_EXE_saslauthd"))
|
|
|
|
|
.arg("--socket-name")
|
|
|
|
|
.arg(socket)
|
|
|
|
|
.arg("--password-dir")
|
|
|
|
|
.arg(PASSWORD_DIR)
|
|
|
|
|
.spawn()
|
|
|
|
|
.expect("failed to spawn saslauthd");
|
2026-09-12 13:19:36 -07:00
|
|
|
// Wrap immediately so a failed startup below still kills the process.
|
|
|
|
|
let daemon = Daemon(child);
|
2026-09-12 13:05:20 -07:00
|
|
|
|
|
|
|
|
let deadline = Instant::now() + Duration::from_secs(5);
|
|
|
|
|
while !is_socket(socket) {
|
|
|
|
|
assert!(Instant::now() < deadline, "daemon never created {socket:?}");
|
|
|
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
|
|
|
}
|
2026-09-12 13:19:36 -07:00
|
|
|
daemon
|
2026-09-12 13:05:20 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn send_signal(daemon: &Daemon, signal: &str) {
|
|
|
|
|
let child = &daemon.0;
|
|
|
|
|
let status = Command::new("kill")
|
|
|
|
|
.arg(format!("-{signal}"))
|
|
|
|
|
.arg(child.id().to_string())
|
|
|
|
|
.status()
|
|
|
|
|
.expect("failed to run kill");
|
|
|
|
|
assert!(status.success(), "kill -{signal} failed");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wait_for_exit(daemon: &mut Daemon, timeout: Duration) -> std::process::ExitStatus {
|
|
|
|
|
let child = &mut daemon.0;
|
|
|
|
|
let deadline = Instant::now() + timeout;
|
|
|
|
|
loop {
|
|
|
|
|
if let Some(status) = child.try_wait().expect("try_wait failed") {
|
|
|
|
|
return status;
|
|
|
|
|
}
|
|
|
|
|
if Instant::now() >= deadline {
|
|
|
|
|
panic!("daemon did not exit within {timeout:?}");
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn authenticate(socket: &PathBuf, password: &str) -> String {
|
|
|
|
|
let mut stream = UnixStream::connect(socket).expect("connect to daemon");
|
|
|
|
|
Request::new("bp", password, "imap", "")
|
|
|
|
|
.write_to_stream(&mut stream)
|
|
|
|
|
.expect("write request");
|
|
|
|
|
read_string(&mut stream).expect("read response")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn assert_clean_shutdown(signal: &str) {
|
|
|
|
|
let socket = temp_socket_path(signal);
|
|
|
|
|
let mut daemon = spawn_daemon(&socket);
|
|
|
|
|
|
|
|
|
|
// Make sure the daemon actually works before we shut it down.
|
|
|
|
|
assert_eq!(authenticate(&socket, "some-rad-password"), "OK");
|
|
|
|
|
assert_eq!(authenticate(&socket, "wrong-password"), "NO");
|
|
|
|
|
|
|
|
|
|
send_signal(&daemon, signal);
|
2026-09-12 13:19:36 -07:00
|
|
|
let status = wait_for_exit(&mut daemon, EXIT_TIMEOUT);
|
2026-09-12 13:05:20 -07:00
|
|
|
|
|
|
|
|
assert!(status.success(), "expected exit status 0 on {signal}, got {status}");
|
|
|
|
|
assert!(!socket.exists(), "socket file {socket:?} was left behind");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_shuts_down_cleanly_on_sigterm() {
|
|
|
|
|
assert_clean_shutdown("TERM");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_shuts_down_cleanly_on_sigint() {
|
|
|
|
|
assert_clean_shutdown("INT");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_recreates_a_stale_socket_on_startup() {
|
|
|
|
|
let socket = temp_socket_path("stale");
|
|
|
|
|
std::fs::write(&socket, b"stale").expect("create stale socket file");
|
|
|
|
|
|
|
|
|
|
let mut daemon = spawn_daemon(&socket);
|
|
|
|
|
assert_eq!(authenticate(&socket, "some-rad-password"), "OK");
|
|
|
|
|
|
|
|
|
|
send_signal(&daemon, "TERM");
|
2026-09-12 13:19:36 -07:00
|
|
|
let status = wait_for_exit(&mut daemon, EXIT_TIMEOUT);
|
|
|
|
|
assert!(status.success());
|
|
|
|
|
assert!(!socket.exists());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn it_does_not_wait_forever_for_a_silent_client() {
|
|
|
|
|
let socket = temp_socket_path("silent");
|
|
|
|
|
let mut daemon = spawn_daemon(&socket);
|
|
|
|
|
|
|
|
|
|
// Connect but never send a request, leaving a handler blocked on read.
|
|
|
|
|
let _silent = UnixStream::connect(&socket).expect("connect to daemon");
|
|
|
|
|
|
|
|
|
|
send_signal(&daemon, "TERM");
|
|
|
|
|
// Must finish once the grace period (3s) expires, whatever the client does.
|
|
|
|
|
let status = wait_for_exit(&mut daemon, Duration::from_secs(5));
|
2026-09-12 13:05:20 -07:00
|
|
|
assert!(status.success());
|
|
|
|
|
assert!(!socket.exists());
|
|
|
|
|
}
|