Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/src/refeed.rs

//! **The estate keys the ONE box fed this box again, kept** (monetize MAIL-RELAY + NIGHT-PUBREPO,
//! 2026-10-28).
//!
//! A box is fed its estate keys at LAY time, by the ONE box. Keys the ONE box holds LATER — the
//! mains' mail relay (`secret.smtp_url` / `secret.smtp_from` / `secret.smtp_ca`), the site's
//! `public_repositories` — reach a laid box as one signed `refeed` order over the pull channel
//! (`holger_fleet::order::Refeed`; holger-fleet refuses a key Holger does not take, by name). This
//! module keeps them.
//!
//! * kept at [`KEPT_REL`] under the data root, 0600, written whole (temp, fsync, rename) — the
//!   custody the box's own fleet key has; a later refeed goes over an earlier one (a relay as a
//!   whole, the public list as a whole);
//! * read over the feed in force ([`over`]): the kept keys are the newer ones (a lay feeds
//!   first, the operator feeds again after), so they win. The pull channel's postman reads it at
//!   every mail: a relay is in use from the next mail, with no restart. The boot reads the public
//!   repositories through it (server-cli `paired::fed_public_repositories_at`): they are in force
//!   from the next start;
//! * checked BEFORE anything is kept: a relay that does not parse, a public repository this box
//!   does not serve or one that gates itself, are refused by name and nothing is kept; a token is
//!   never logged, never in an answer.

use std::collections::BTreeMap;
use std::path::Path;

use holger_fleet::order::{refeed_key, Refeed};

/// Where the fed keys are kept under the data root.
pub const KEPT_REL: &str = "fleet/refeed.json";
/// The feed keys of the relay (monetize-mail `FEED_SMTP_*`, the lay's own words).
pub const FEED_SMTP_URL: &str = refeed_key::SMTP_URL;
pub const FEED_SMTP_FROM: &str = refeed_key::SMTP_FROM;
/// One more root the relay's certificate is trusted under (PEM; the ONE box's `secret.smtp_ca`).
pub const FEED_SMTP_CA: &str = refeed_key::SMTP_CA;
const RELAY: [&str; 3] = [FEED_SMTP_URL, FEED_SMTP_FROM, FEED_SMTP_CA];

/// Pure: **are `names` repositories this box may make public** — each one it serves
/// (`served`) and none that gates itself (`gated`); refused by name (server-lib `posture`).
pub fn public_check(names: &[String], served: &[String], gated: &[String]) -> Result<(), String> {
    for n in names {
        if !served.iter().any(|s| s == n) {
            let declared: Vec<&str> = served.iter().map(String::as_str).collect();
            return Err(crate::posture::public_unknown(n, &declared));
        }
        if gated.iter().any(|g| g == n) {
            return Err(format!(
                "{} `{n}` gates itself: it is never public",
                crate::posture::ACCESS_REFUSED
            ));
        }
    }
    Ok(())
}

/// **Keep the keys of `r`**, checked first: the relay as the shared sender reads it
/// (monetize-mail: `smtp://` STARTTLS or `smtps://`, a sender, the root a PEM), the public
/// repositories by `public_ok` (what this box serves now). Then written whole at [`KEPT_REL`],
/// 0600, over what was kept before. The answer names the keys and the relay's host and port, never
/// a secret value and never the sender's address (RA5-26: the answer is logged; no addresses in logs). A key that does not pass is refused by name and nothing is kept.
pub fn keep(
    root: &Path,
    r: &Refeed,
    public_ok: &dyn Fn(&[String]) -> Result<(), String>,
) -> Result<String, String> {
    // A key the box keeps as a generic setting (holger-fleet `refeed_key::AS_SETTINGS`) is never
    // kept here: `HolgerBox::keep_setting` keeps it.
    let r = &r.kept_keys();
    let mut said = Vec::new();
    if let Some(url) = r.get(FEED_SMTP_URL) {
        let from = r.get(FEED_SMTP_FROM).unwrap_or_default();
        let ca = r.get(FEED_SMTP_CA);
        let mailer =
            monetize_mail::Mailer::with_root(url, from, ca.map(str::as_bytes)).map_err(|why| {
                format!("the refeed's mail relay cannot mail ({why}): nothing was kept")
            })?;
        said.push(format!(
            "the mail relay {} is in use from the next mail, with the sender it was fed",
            mailer.relay().describe()
        ));
    }
    let public = r
        .keys
        .get(refeed_key::PUBLIC_REPOSITORIES)
        .map(|v| crate::posture::public_list_of(v));
    if let Some(list) = &public {
        public_ok(list).map_err(|why| format!("{why}: nothing was kept"))?;
        said.push(format!(
            "public repositories {} from the next start",
            if list.is_empty() {
                "none".to_string()
            } else {
                list.join(", ")
            }
        ));
    }
    let mut kept = kept(root);
    if r.get(FEED_SMTP_URL).is_some() {
        kept.retain(|k, _| !RELAY.contains(&k.as_str()));
    }
    for (k, v) in &r.keys {
        kept.insert(k.clone(), v.trim().to_string());
    }
    let body = serde_json::to_vec(&Refeed { keys: kept })
        .map_err(|e| format!("the refeed could not be written: {e}"))?;
    write_secret(&root.join(KEPT_REL), &body)
        .map_err(|e| format!("the refeed could not be kept at {KEPT_REL}: {e}"))?;
    let names = r.keys.keys().cloned().collect::<Vec<_>>().join(", ");
    log::warn!(
        "refeed: the ONE box fed this box {names} again; kept at {KEPT_REL}: {}",
        said.join("; ")
    );
    Ok(format!("kept {names}: {}", said.join("; ")))
}

/// **The kept keys** (feed keys → values). Empty when nothing is kept, or the kept file does not
/// read (said).
pub fn kept(root: &Path) -> BTreeMap<String, String> {
    let path = root.join(KEPT_REL);
    let Ok(bytes) = std::fs::read(&path) else {
        return BTreeMap::new();
    };
    match serde_json::from_slice::<Refeed>(&bytes) {
        Ok(r) => r
            .keys
            .into_iter()
            .filter(|(k, v)| {
                refeed_key::KNOWN.contains(&k.as_str())
                    && !refeed_key::AS_SETTINGS.contains(&k.as_str())
                    && (k == refeed_key::PUBLIC_REPOSITORIES || !v.trim().is_empty())
            })
            .collect(),
        Err(e) => {
            log::error!("refeed: the kept keys at {KEPT_REL} do not read ({e}): the feed the box was laid with is used");
            BTreeMap::new()
        }
    }
}

/// **The feed in force with the kept keys over it**: they are newer than what the lay fed, so
/// they win (a kept relay as a whole: a kept relay with no root drops the lay's root). Nothing
/// kept: the feed as it is.
pub fn over(mut feed: BTreeMap<String, String>, root: &Path) -> BTreeMap<String, String> {
    let k = kept(root);
    if k.contains_key(FEED_SMTP_URL) {
        feed.retain(|key, _| !RELAY.contains(&key.as_str()));
    }
    feed.extend(k);
    feed
}

/// `bytes` at `path`, 0600, written whole (temp beside it, fsync, rename).
fn write_secret(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
    use std::io::Write as _;
    use std::os::unix::fs::OpenOptionsExt as _;
    let dir = path.parent().unwrap_or(Path::new("."));
    std::fs::create_dir_all(dir)?;
    let tmp = dir.join(format!(
        ".{}.tmp",
        path.file_name().and_then(|n| n.to_str()).unwrap_or("kept")
    ));
    let _ = std::fs::remove_file(&tmp);
    let mut f = std::fs::OpenOptions::new()
        .write(true)
        .create_new(true)
        .mode(0o600)
        .open(&tmp)?;
    f.write_all(bytes)?;
    f.sync_all()?;
    drop(f);
    std::fs::rename(&tmp, path)?;
    if let Ok(d) = std::fs::File::open(dir) {
        let _ = d.sync_all();
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    const TOKEN: &str = "t0kenVALUEnever-shown";

    fn refeed(keys: &[(&str, &str)]) -> Refeed {
        Refeed {
            keys: keys
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
        }
    }

    fn served(n: &[&str]) -> Vec<String> {
        n.iter().map(|s| s.to_string()).collect()
    }

    /// ★ **A relay that does not parse is refused by name and nothing is kept**; the kept relay
    /// goes over the lay's as a whole, and the rest of the feed stays.
    #[test]
    fn a_relay_that_does_not_parse_is_refused_and_the_kept_one_goes_over_the_lays() {
        let dir = tempfile::tempdir().unwrap();
        let any = |_: &[String]| Ok(());
        let bad = refeed(&[
            (
                FEED_SMTP_URL,
                &format!("http://u:{TOKEN}@relay.mock.invalid"),
            ),
            (FEED_SMTP_FROM, "h@mock.invalid"),
        ]);
        let e = keep(dir.path(), &bad, &any).unwrap_err();
        assert!(e.contains("refeed") && !e.contains(TOKEN), "{e}");
        assert!(!dir.path().join(KEPT_REL).exists(), "nothing kept");
        assert!(kept(dir.path()).is_empty());
        let lay = BTreeMap::from([
            (
                FEED_SMTP_URL.to_string(),
                "smtp://old:old@old.mock.invalid:587".to_string(),
            ),
            (FEED_SMTP_FROM.to_string(), "old@mock.invalid".to_string()),
            (FEED_SMTP_CA.to_string(), "OLD CA".to_string()),
            ("fleet".to_string(), "f".to_string()),
        ]);
        assert_eq!(
            over(lay.clone(), dir.path()),
            lay,
            "nothing kept: the lay's feed as it is"
        );
        let url = format!("smtps://u:{TOKEN}@relay.mock.invalid:465");
        let said = keep(
            dir.path(),
            &refeed(&[(FEED_SMTP_URL, &url), (FEED_SMTP_FROM, "new@mock.invalid")]),
            &any,
        )
        .unwrap();
        assert!(
            said.contains("relay.mock.invalid:465") && !said.contains(TOKEN),
            "{said}"
        );
        let f = over(lay, dir.path());
        assert_eq!(
            (
                f[FEED_SMTP_URL].as_str(),
                f[FEED_SMTP_FROM].as_str(),
                f["fleet"].as_str()
            ),
            (url.as_str(), "new@mock.invalid", "f")
        );
        assert!(
            !f.contains_key(FEED_SMTP_CA),
            "the kept relay as a whole: the lay's root is dropped"
        );
    }

    /// ★ **RA5-26: the refeed never says the relay's sender address** (owner rule RA4-2: no
    /// addresses in logs). The answer goes to the warn log and back in the order's answer, so it
    /// names the relay's host and port and that a sender is set, never the address itself.
    #[test]
    fn the_refeed_never_says_the_relays_sender_address() {
        let dir = tempfile::tempdir().unwrap();
        let any = |_: &[String]| Ok(());
        let said = keep(
            dir.path(),
            &refeed(&[
                (FEED_SMTP_URL, "smtps://u:p@relay.mock.invalid:465"),
                (FEED_SMTP_FROM, "estate-sender@mock.invalid"),
            ]),
            &any,
        )
        .unwrap();
        assert!(said.contains("relay.mock.invalid:465"), "{said}");
        assert!(
            !said.contains("estate-sender") && !said.contains('@'),
            "the sender address is in the answer: {said}"
        );
        assert_eq!(
            over(BTreeMap::new(), dir.path())
                .get(FEED_SMTP_FROM)
                .map(String::as_str),
            Some("estate-sender@mock.invalid"),
            "the sender is still kept"
        );
    }

    /// ★ **NIGHT-PUBREPO through the refeed**: a public list is checked against what the box
    /// serves before it is kept — a name it does not serve, and a repository that gates itself,
    /// are refused by name and nothing is kept; a kept list goes over the lay's and a later relay
    /// keeps it.
    #[test]
    fn public_repositories_are_checked_before_they_are_kept_and_go_over_the_lays() {
        let dir = tempfile::tempdir().unwrap();
        let box_ = served(&["crates", "npm", "bundles", "monetize-backup"]);
        let gated = served(&["monetize-backup"]);
        let check = |names: &[String]| public_check(names, &box_, &gated);
        let e = keep(
            dir.path(),
            &refeed(&[(refeed_key::PUBLIC_REPOSITORIES, "crates,pypi")]),
            &check,
        )
        .unwrap_err();
        assert!(
            e.starts_with(crate::posture::PUBLIC_UNKNOWN) && e.contains("pypi"),
            "{e}"
        );
        let e = keep(
            dir.path(),
            &refeed(&[(refeed_key::PUBLIC_REPOSITORIES, "monetize-backup")]),
            &check,
        )
        .unwrap_err();
        assert!(e.starts_with(crate::posture::ACCESS_REFUSED), "{e}");
        assert!(!dir.path().join(KEPT_REL).exists(), "nothing kept");
        let said = keep(
            dir.path(),
            &refeed(&[(refeed_key::PUBLIC_REPOSITORIES, "crates,npm")]),
            &check,
        )
        .unwrap();
        assert!(
            said.contains("public repositories crates, npm from the next start"),
            "{said}"
        );
        let lay = BTreeMap::from([("fleet".to_string(), "f".to_string())]);
        assert_eq!(
            over(lay.clone(), dir.path())
                .get(refeed_key::PUBLIC_REPOSITORIES)
                .map(String::as_str),
            Some("crates,npm")
        );
        keep(
            dir.path(),
            &refeed(&[
                (FEED_SMTP_URL, "smtps://u:p@relay.mock.invalid:465"),
                (FEED_SMTP_FROM, "h@mock.invalid"),
            ]),
            &check,
        )
        .unwrap();
        let f = over(lay, dir.path());
        assert_eq!(
            (
                f.get(refeed_key::PUBLIC_REPOSITORIES).map(String::as_str),
                f.get(FEED_SMTP_FROM).map(String::as_str)
            ),
            (Some("crates,npm"), Some("h@mock.invalid")),
            "a later relay keeps the list"
        );
        let mode = std::os::unix::fs::PermissionsExt::mode(
            &std::fs::metadata(dir.path().join(KEPT_REL))
                .unwrap()
                .permissions(),
        ) & 0o777;
        assert_eq!(mode, 0o600, "kept as a secret");
    }
}