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

nordisk/edda

crates/estate-clock/src/poll.rs

//! **Asking sources with no HTTP client in the build** — the poll a
//! zero-egress box runs.
//!
//! [`crate::fetch::poll`] is this poll with an HTTPS sampler plugged in, and it
//! is what a front (and a paired box built with `fetch`) runs. A box whose build
//! must link NO outbound HTTP client — an on-prem downstream behind an airgap,
//! whose only time is a signed peer on its own network (`estate-clock run` with
//! `ESTATE_CLOCK_TRUST_SYSTEM=1` on the site's clock host) — still needs the
//! SAME signed-peer protocol and the SAME quorum rule, and nothing else. That is
//! [`poll`]: feature `peer` alone (ssh-key + the OS RNG), no ureq, no rustls.
//!
//! An `https://` source configured on such a build is not skipped: it fails BY
//! NAME in the poll's answers, so the report says why it contributed nothing.

use crate::config::Source;
use crate::decide::{Poll, Sample};

/// What an `https://` source answers on a build without `fetch`.
pub const NO_HTTPS_CLIENT: &str =
    "this build links no HTTP client (estate-clock without `fetch`); an https:// clock source cannot be asked";

/// Ask every source at once and decide, with `https` as the HTTPS sampler.
/// Blocking; no clock is moved here.
///
/// One thread per source under `std::thread::scope`: samples taken within
/// milliseconds of each other are comparable, and a dead source costs one
/// timeout rather than its turn in a queue. A peer key that does not parse
/// fails the peer sources by name rather than the whole poll.
pub fn poll_with(sources: &[Source], peer_key: Option<&str>, https: &(dyn Fn(&str) -> Result<Sample, String> + Sync)) -> Poll {
    let key = peer_key.map(crate::peer::PeerKey::parse);
    let answers = std::thread::scope(|scope| {
        let handles: Vec<_> = sources
            .iter()
            .map(|source| {
                let key = &key;
                // A32-14, stays a thread: a scoped thread per time source of one poll, all joined before the poll answers.
                scope.spawn(move || {
                    let label = source.label();
                    let result = match source {
                        Source::Https(url) => https(url),
                        Source::Ntp { host, port } => crate::sntp::query(host, *port),
                        Source::Peer(addr) => match key {
                            Some(Ok(key)) => crate::peer::query(*addr, key),
                            Some(Err(why)) => Err(why.clone()),
                            None => Err("no peer key is configured".to_owned()),
                        },
                    };
                    result.map_err(|why| (label, why))
                })
            })
            .collect();
        handles
            .into_iter()
            .zip(sources)
            .map(|(handle, source)| {
                handle.join().unwrap_or_else(|_| {
                    Err((source.label(), "the sampling thread panicked".to_owned()))
                })
            })
            .collect::<Vec<_>>()
    });
    Poll::over(answers)
}

/// **The zero-egress poll**: peer and SNTP sources; an `https://` source fails
/// by name ([`NO_HTTPS_CLIENT`]).
pub fn poll(sources: &[Source], peer_key: Option<&str>) -> Poll {
    poll_with(sources, peer_key, &|_| Err(NO_HTTPS_CLIENT.to_owned()))
}

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

    #[test]
    fn a_zero_egress_poll_reads_a_signed_peer_and_names_the_https_source_it_cannot_ask() {
        let signer = crate::peer::generate_key().unwrap();
        let key_line = signer.public_key().to_openssh().unwrap();
        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();
        std::thread::spawn(move || crate::peer::serve(listener, &signer, &|| Ok(()), &|_, _| {}));
        let polled = poll(&[Source::Peer(addr)], Some(&key_line));
        assert!(matches!(polled.verdict.decision, crate::Decision::AlreadyRight { .. }), "{polled:?}");

        let polled = poll(&[Source::Https("https://api.stripe.com/".into())], None);
        let Err((_, why)) = &polled.answers[0] else { panic!("an https source on a zero-egress build must fail") };
        assert_eq!(why, NO_HTTPS_CLIENT);
    }
}