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/front_report.rs

//! ★ **The front's report, kept by main** (lane HOL-FLOW-FRONT, Henrik's Q1 of HOL-FLOW-SMART).
//!
//! The front tells main its doors, the names it serves and where each goes
//! (`holger_downstream_rules::front`): `POST /-/front/report` on main's HTTP door, with a proof
//! of its own. Main opens it under the key its feed gave it for its front ([`keep_key`]: the
//! paired box derives it from the front's offered console key and its own, the pair the
//! console's bearer comes from, under the report's own context), keeps the last one
//! ([`heard`] → `<data>/holger-front/report.ron`), and the flow unfolds the front to it.
//! Nothing that is not reported is drawn: a front that never reported (an older front, a box
//! fed no key) stays one closed box.
//!
//! An OLDER main has no such door: it answers `404`, and the front says so and carries on.
//! The door names no secret and needs no bearer (the repository route carries main's HTTP door
//! to the internet, so anybody may knock): the proof is the credential, as a downstream's report
//! proof is.

use std::path::{Path, PathBuf};

pub use holger_downstream_rules::front;
use holger_downstream_rules::front::{FrontReport, Sent};
use serde::{Deserialize, Serialize};

/// The front's folder under the data root.
pub const DIR: &str = "holger-front";
/// The proof's key, hex, 0600 (written by the pairing loop from the feed).
pub const KEY_FILE: &str = "report.key";
/// The last report heard.
pub const REPORT_FILE: &str = "report.ron";
/// The largest body the door reads.
pub const MAX_BODY: usize = 64 * 1024;

/// **What main keeps of its front's last report.**
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Kept {
    pub report: FrontReport,
    /// When main heard it (Unix ms).
    pub heard_ms: i64,
}

fn dir(data_root: &Path) -> PathBuf {
    data_root.join(DIR)
}

fn hex(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}

/// Write `bytes` to `path` 0600, staged and renamed.
fn write_private(path: &Path, bytes: &[u8]) -> Result<(), String> {
    use std::io::Write as _;
    if let Some(d) = path.parent() {
        std::fs::create_dir_all(d).map_err(|e| format!("create {}: {e}", d.display()))?;
    }
    let tmp = path.with_extension(format!("tmp-{}", std::process::id()));
    let mut o = std::fs::OpenOptions::new();
    o.write(true).create(true).truncate(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt as _;
        o.mode(0o600);
    }
    let mut f = o
        .open(&tmp)
        .map_err(|e| format!("write {}: {e}", tmp.display()))?;
    f.write_all(bytes)
        .and_then(|()| f.sync_all())
        .and_then(|()| std::fs::rename(&tmp, path))
        .map_err(|e| format!("write {}: {e}", path.display()))
}

/// ★ **Keep the key this box opens its front's reports under** (the pairing loop, every feed;
/// unchanged is a no-op): `Ok(true)` when it was written.
pub fn keep_key(data_root: &Path, key: &[u8; 32]) -> Result<bool, String> {
    let path = dir(data_root).join(KEY_FILE);
    let want = hex(key);
    if std::fs::read_to_string(&path)
        .ok()
        .as_deref()
        .map(str::trim)
        == Some(want.as_str())
    {
        return Ok(false);
    }
    write_private(&path, want.as_bytes())?;
    Ok(true)
}

fn key(data_root: &Path) -> Option<[u8; 32]> {
    let t = std::fs::read_to_string(dir(data_root).join(KEY_FILE)).ok()?;
    let t = t.trim();
    if t.len() != 64 {
        return None;
    }
    let mut k = [0u8; 32];
    for (i, b) in k.iter_mut().enumerate() {
        *b = u8::from_str_radix(t.get(i * 2..i * 2 + 2)?, 16).ok()?;
    }
    Some(k)
}

/// ★ **The front's report, heard**: the body parsed, its proof checked under the kept key,
/// near now; when the pairing names fronts, the report names one of them. A console address
/// that is no plain URL is dropped (the rest is kept). Kept, and answered with what was kept;
/// refused as `(status, words)` otherwise, nothing kept.
pub fn heard(data_root: &Path, body: &[u8], now_ms: i64) -> Result<Kept, (u16, String)> {
    let refused = |status: u16, why: String| {
        (
            status,
            holger_errcode::FRONT_REPORT_REFUSED
                .tag(format_args!("the front's report is not kept: {why}")),
        )
    };
    let sent: Sent = serde_json::from_slice(body)
        .map_err(|e| refused(400, format!("it is not a front report: {e}")))?;
    let Some(k) = key(data_root) else {
        return Err(refused(
            409,
            "this box was fed no front console key, so it has no key to open a front's report under".into(),
        ));
    };
    front::check(&k, &sent.report, &sent.proof, now_ms.div_euclid(1000))
        .map_err(|why| refused(403, why))?;
    if let Some(p) = crate::topology::pairing() {
        let fronts: Vec<&str> = p
            .boxes
            .iter()
            .filter(|b| b.role == crate::topology::ROLE_FRONT)
            .map(|b| b.name.as_str())
            .collect();
        if !fronts.is_empty() && !fronts.contains(&sent.report.front.as_str()) {
            return Err(refused(
                403,
                format!(
                    "it names the front '{}', and the pairing names {}",
                    sent.report.front,
                    fronts.join(", ")
                ),
            ));
        }
    }
    let mut report = sent.report;
    report.console_url = front::console_url(&report.console_url).unwrap_or_default();
    let kept = Kept {
        report,
        heard_ms: now_ms,
    };
    let text = ron::ser::to_string_pretty(&kept, ron::ser::PrettyConfig::default())
        .map_err(|e| (500, format!("the front's report could not be written: {e}")))?;
    let path = dir(data_root).join(REPORT_FILE);
    let tmp = path.with_extension(format!("tmp-{}", std::process::id()));
    std::fs::create_dir_all(dir(data_root))
        .and_then(|()| std::fs::write(&tmp, text))
        .and_then(|()| std::fs::rename(&tmp, &path))
        .map_err(|e| (500, format!("the front's report could not be kept: {e}")))?;
    Ok(kept)
}

/// The front's last report, as kept; `None` when it never reported.
pub fn kept(data_root: &Path) -> Option<Kept> {
    let t = std::fs::read_to_string(dir(data_root).join(REPORT_FILE)).ok()?;
    ron::from_str(&t).ok()
}

/// **This estate's console address**, as its front last reported it; empty when none.
pub fn console_url(data_root: &Path) -> String {
    kept(data_root)
        .map(|k| k.report.console_url)
        .unwrap_or_default()
}

/// Pure: **the kept report as the flow draws it**, as of `now_ms`.
pub fn view(k: &Kept, now_ms: i64) -> crate::flow::FrontView {
    use crate::flow::{FrontDoorView, FrontRouteView, FrontView};
    FrontView {
        front: k.report.front.clone(),
        last_seen_unix_ms: k.heard_ms,
        seen_ago_ms: now_ms.saturating_sub(k.heard_ms).max(0) as u64,
        doors: k
            .report
            .doors
            .iter()
            .map(|d| FrontDoorView {
                name: d.name.clone(),
                listen: d.listen.clone(),
                what: d.what.clone(),
            })
            .collect(),
        routes: k
            .report
            .routes
            .iter()
            .map(|r| FrontRouteView {
                host: r.host.clone(),
                door: r.door.clone(),
                to: r.to.clone(),
                what: r.what.clone(),
            })
            .collect(),
        console_url: k.report.console_url.clone(),
    }
}