//! Reading the `as_token` out of the appservice registration the matrix //! container already has. //! //! No new credential is delivered for this. `nix/host-modules/hive-matrix.nix` //! binds the registration directory into the container read-only so tuwunel can //! load it, and the registration **is** the `as_token` — so the file this //! module opens is one this process could already read, and one the homeserver //! beside it reads too. //! //! Scanned line-by-line rather than parsed as YAML. The file has exactly one //! renderer (`appserviceRegistrationScript` in that same module, a `printf` of //! `as_token: `), so a parser would be a second, looser reading of a shape //! this repo writes itself — and it would pull a YAML crate into a binary whose //! only other input is JSON. use anyhow::{Context, Result, bail}; /// The key the token is stored under, and the whole of the agreement with the /// renderer. const KEY: &str = "as_token:"; /// Read the registration at `path` and return its `as_token`. /// /// # Errors /// When the file cannot be read, or holds no `as_token` with a value — which is /// what a registration rendered by something other than this repo looks like /// from here. pub fn as_token(path: &str) -> Result { let text = std::fs::read_to_string(path) .with_context(|| format!("reading the appservice registration at {path}"))?; // The path, not the file's contents: every line of it is either a secret or // a shape this module already knows. extract(&text).with_context(|| format!("no `as_token` in the registration at {path}")) } /// [`as_token`] over text already in hand, so the agreement with the renderer /// can be tested without a file. fn extract(text: &str) -> Result { for line in text.lines() { if let Some(rest) = line.strip_prefix(KEY) { let token = rest.trim(); if token.is_empty() { bail!("the registration's `as_token` is empty"); } return Ok(token.to_owned()); } } bail!("the registration carries no `as_token` line") } #[cfg(test)] mod tests { use super::*; /// The registration exactly as `appserviceRegistrationScript` renders it — /// the quoted heredoc, then the `printf` of the two tokens. Reproduced /// verbatim because that script is the other end of this agreement and /// lives in a file no Rust test can reach. const RENDERED: &str = "id: hyperhive\n\ url: null\n\ sender_localpart: hive\n\ rate_limited: false\n\ namespaces:\n \ users:\n \ - exclusive: false\n \ regex: '@[a-z0-9._=/+-]+:example\\.test$'\n \ aliases: []\n \ rooms: []\n\ as_token: deadbeef\n\ hs_token: cafebabe\n"; #[test] fn the_token_is_taken_from_the_registration_this_repo_renders() { assert_eq!( extract(RENDERED).expect("the rendered shape parses"), "deadbeef" ); } #[test] fn the_homeservers_own_token_is_not_mistaken_for_the_appservices() { // `hs_token` authenticates the homeserver TO the appservice and is a // different secret with a confusingly similar name; a substring search // would find it inside neither, but a `contains("s_token")`-shaped one // would. The control is that the line order in `RENDERED` puts // `as_token` first, so this arm needs the reverse to mean anything. let reversed = "hs_token: cafebabe\nas_token: deadbeef\n"; assert_eq!( extract(reversed).expect("order does not matter"), "deadbeef" ); } #[test] fn a_registration_with_no_token_is_an_error_rather_than_an_empty_string() { // An empty token authenticates nothing, and a homeserver answers a // request carrying one with a 403 that names the account rather than // the credential — so failing here is the only report an operator can // act on. for bad in ["id: hyperhive\n", "as_token:\n", "as_token: \n"] { assert!(extract(bad).is_err(), "{bad:?} must not yield a token"); } } }