//! `memoryd serve` with a `[honcho]` section (M5c spec, section 10): an invalid one stops it at //! start with the reason and the `honcho-config-invalid` pointer, before it takes its lock or //! touches its home; a valid one starts it as before. A key is never printed. Every wait has a //! deadline, and every process is killed and every path removed by a guard (tip T44). Do not //! edit. #[path = "support/tmp.rs"] mod tmp; use std::os::unix::fs::PermissionsExt; use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; use std::time::{Duration, Instant}; use bxcore::{Empty, Envelope, Message, PROTOCOL_VERSION, read_frame, write_frame}; use tmp::TempDir; const POINTER: &str = "see docs/runbook.md#honcho-config-invalid"; const START_FAILED: &str = "see docs/runbook.md#memoryd-start-failed"; /// The values of the two keys the configs name. Built from pieces, so that no file holds one /// whole; neither may appear in anything `memoryd` prints. fn env_key() -> String { ["hk", "env", "7f3a9c"].join("-") } fn file_key() -> String { ["hk", "file", "b81d2e"].join("-") } const SECTION: &str = "[honcho] url = \"http://127.0.0.1:9\" agent = \"boxmaker0\" owner = \"kyle\" trusted = \"boxmaker0-trusted\" untrusted = \"boxmaker0-untrusted\" fleet = \"fleet\" [[honcho.read]] agent = \"boxmaker1\" workspace = \"boxmaker1-trusted\" "; struct Home { dir: TempDir, config: PathBuf, } impl Home { /// A home whose `memoryd.toml` is the usual one and `extra`, with a key file holding /// `file_key()` (mode 0600) at `/keys/honcho.key`. fn new(tag: &str, extra: &str) -> Home { let dir = TempDir::new(tag); let soul = dir.write("etc/soul.md", "You are Boxmaker.\n"); let key = dir.write("keys/honcho.key", &format!("{}\n", file_key())); std::fs::set_permissions(&key, std::fs::Permissions::from_mode(0o600)).unwrap(); let text = format!( "[paths]\nhome = \"{}\"\n[memory]\nsoul_starter = \"{}\"\n{extra}", dir.path().display(), soul.display() ); let text = text.replace("KEY_FILE", &key.display().to_string()); let config = dir.write("memoryd.toml", &text); Home { dir, config } } fn path(&self, relative: &str) -> PathBuf { self.dir.path().join(relative) } fn command(&self) -> Command { let mut command = Command::new(env!("CARGO_BIN_EXE_memoryd")); command .args(["serve", "--config"]) .arg(&self.config) .env("HONCHO_TEST_KEY", env_key()) .stdin(Stdio::null()) .stdout(Stdio::null()); command } /// Runs `memoryd serve` to its end, at most 10 s (then it is killed), and gives its exit code /// (`None` if it had to be killed) and its stderr. fn run_to_end(&self) -> (Option, String) { let mut child = Daemon(self.command().stderr(Stdio::piped()).spawn().unwrap()); let deadline = Instant::now() + Duration::from_secs(10); let mut code = None; while Instant::now() < deadline { if let Ok(Some(status)) = child.0.try_wait() { code = status.code(); break; } std::thread::sleep(Duration::from_millis(20)); } let _ = child.0.kill(); let _ = child.0.wait(); let mut stderr = String::new(); if let Some(mut pipe) = child.0.stderr.take() { let _ = std::io::Read::read_to_string(&mut pipe, &mut stderr); } (code, stderr) } /// `memoryd serve` with its stderr in `/memoryd.err`. fn start(&self) -> Daemon { let err = std::fs::File::create(self.path("memoryd.err")).unwrap(); Daemon(self.command().stderr(Stdio::from(err)).spawn().unwrap()) } fn stderr(&self) -> String { std::fs::read_to_string(self.path("memoryd.err")).unwrap_or_default() } } /// A running `memoryd serve`, killed and waited for when dropped. Each test declares it after its /// `Home`, so it drops first. struct Daemon(Child); impl Drop for Daemon { fn drop(&mut self) { let _ = self.0.kill(); let _ = self.0.wait(); } } /// True once `memoryd` answers `memory_block` on `loopd`'s socket, within 10 s. fn answers(home: &Home) -> bool { let socket = home.path("run/loop-memory/memory.sock"); let deadline = Instant::now() + Duration::from_secs(10); while Instant::now() < deadline { if let Ok(mut stream) = UnixStream::connect(&socket) { let limit = Some(Duration::from_secs(5)); let ask = Envelope { v: PROTOCOL_VERSION, id: 1, r#final: true, msg: Message::MemoryBlock(Empty {}), }; if stream.set_read_timeout(limit).is_ok() && write_frame(&mut stream, &ask).is_ok() && matches!( read_frame(&mut stream), Ok(Envelope { msg: Message::MemoryBlockReply(_), .. }) ) { return true; } } std::thread::sleep(Duration::from_millis(20)); } false } fn assert_no_key(text: &str) { assert!( !text.contains(&env_key()), "the env key was printed: {text}" ); assert!( !text.contains(&file_key()), "the file key was printed: {text}" ); } #[test] fn an_invalid_section_stops_memoryd_with_its_reason_and_the_pointer() { let extra = format!( "{}[secrets.honcho_trusted]\nenv = \"HONCHO_TEST_KEY\"\n[secrets.honcho_fleet]\nfile = \"KEY_FILE\"\n", SECTION.replace("owner = \"kyle\"", "owner = \"boxmaker0\"") ); let home = Home::new("honcho-bad", &extra); let (code, stderr) = home.run_to_end(); assert_eq!(code, Some(1), "{stderr}"); assert_eq!( stderr, format!( "memoryd: {}: [honcho] agent and owner are both \"boxmaker0\": the agent's peer must not be the owner's\n{POINTER}\n", home.config.display() ) ); assert_no_key(&stderr); assert!( !home.path("run").exists(), "memoryd stopped before its lock and sockets" ); assert!(!home.path("memory").exists(), "and before its memory"); } #[test] fn a_key_for_an_agent_not_read_stops_memoryd() { let extra = format!("{SECTION}[secrets.honcho_read_boxmaker2]\nenv = \"HONCHO_TEST_KEY\"\n"); let home = Home::new("honcho-key", &extra); let (code, stderr) = home.run_to_end(); assert_eq!(code, Some(1), "{stderr}"); assert_eq!( stderr, format!( "memoryd: {}: [secrets.honcho_read_boxmaker2] is the key of agent \"boxmaker2\", which is not in [[honcho.read]]\n{POINTER}\n", home.config.display() ) ); assert_no_key(&stderr); } /// The readers are built at start (M5c-2): a read workspace's key that cannot be read stops /// `memoryd`, as a written workspace's does. #[test] fn a_read_key_that_cannot_be_read_stops_memoryd() { let extra = format!("{SECTION}[secrets.honcho_read_boxmaker1]\nenv = \"HONCHO_NOT_SET\"\n"); let home = Home::new("honcho-read-key", &extra); let (code, stderr) = home.run_to_end(); assert_eq!(code, Some(1), "{stderr}"); assert!(stderr.contains("honcho_read_boxmaker1"), "{stderr}"); assert!( stderr.contains("see docs/runbook.md#secret-unavailable"), "{stderr}" ); assert_no_key(&stderr); } #[test] fn an_unknown_key_in_the_section_is_still_a_start_failure() { let extra = SECTION.replace("fleet = \"fleet\"", "fleet = \"fleet\"\nfleat = \"x\""); let home = Home::new("honcho-unknown", &extra); let (code, stderr) = home.run_to_end(); assert_eq!(code, Some(1), "{stderr}"); assert!( stderr.starts_with(&format!("memoryd: {}: ", home.config.display())), "{stderr}" ); assert!(stderr.contains("fleat"), "{stderr}"); assert!(stderr.ends_with(&format!("\n{START_FAILED}\n")), "{stderr}"); assert!(!stderr.contains(POINTER), "{stderr}"); } #[test] fn a_valid_section_with_keys_starts_memoryd_as_before() { let extra = format!( "{SECTION}[secrets.honcho_trusted]\nenv = \"HONCHO_TEST_KEY\"\n[secrets.honcho_fleet]\nfile = \"KEY_FILE\"\n[secrets.honcho_read_boxmaker1]\nenv = \"HONCHO_TEST_KEY\"\n" ); let home = Home::new("honcho-good", &extra); let _daemon = home.start(); assert!( answers(&home), "memoryd never answered; stderr: {}", home.stderr() ); let stderr = home.stderr(); assert!(stderr.contains("memoryd: serving\n"), "{stderr}"); assert_no_key(&stderr); // The writers and the readers both load the fleet's key file; its warning is logged once. assert_eq!( stderr .matches("secret honcho_fleet is read in plaintext") .count(), 1, "{stderr}" ); assert!(!stderr.contains(POINTER), "{stderr}"); }