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

nordisk/gunnar

crates/gunnar-loadbench/src/safety.rs

//! **The safety gate.** The front's Stripe key is `rk_live_`: a checkout this
//! bench opened would be a real charge on a real card. So the bench does not
//! decide at the call site whether a URL is safe — every probe is built through
//! [`SafeGet`], which refuses at CONSTRUCTION, before the target list is even
//! handed to the engine, and the refusal is a hard error that stops the run.
//!
//! Three rules, in this order:
//!
//!  1. **GET only.** There is no other constructor. A POST/PUT/PATCH/DELETE
//!     cannot be expressed by this type, so no amount of later editing can
//!     make one by accident.
//!  2. **No money word anywhere in the URL.** [`FORBIDDEN`] is matched
//!     case-insensitively against the whole URL, path and query alike.
//!  3. **No state-moving verb anywhere in the URL**, even behind GET: the
//!     console has GET routes that start work (`/auth/passkey/login/start`
//!     mints a webauthn ceremony and stores it), and a load bench must not
//!     mint ten thousand of anything.

/// Substrings that are never allowed in a benched URL. Matched
/// case-insensitively against the entire URL.
///
/// The first block is money. `buy.stripe.com` is what `/auth/status` reports as
/// `buy_url`, and `/signup` is the front's own door to it; both are one click
/// from a live charge and neither is ever measured.
///
/// The second block is state: things that create, delete, start or reserve.
/// `start` is in the list because of `/auth/passkey/login/start` specifically —
/// it answers 200 to anyone and looks exactly like a read until you notice it
/// has written a ceremony row per call.
pub const FORBIDDEN: &[&str] = &[
    // money
    "stripe",
    "checkout",
    "buy",
    "signup",
    "purchase",
    "invoice",
    "billing",
    "subscribe",
    "entitlement",
    "quota",
    "plan/set",
    // state
    "start",
    "finish",
    "create",
    "delete",
    "purge",
    "undelete",
    "reclaim",
    "restore",
    "reimage",
    "grow",
    "/gc",
    "gate",
    "revoke",
    "enroll",
    "invite",
    "logout",
    "apply",
    "sync",
    "resync",
    "flush",
    "demote",
    "abdicate",
    "set",
];

/// A URL that has passed the gate. The only way to make one.
#[derive(Debug, Clone)]
pub struct SafeGet(String);

impl SafeGet {
    /// Build a GET target, or say exactly which rule refused it.
    pub fn new(url: &str) -> Result<SafeGet, String> {
        if !(url.starts_with("https://") || url.starts_with("http://")) {
            return Err(format!("{url}: not an http(s) URL"));
        }
        let lower = url.to_ascii_lowercase();
        for bad in FORBIDDEN {
            if lower.contains(bad) {
                return Err(format!(
                    "{url}: refused by the safety gate — the URL contains {bad:?}. \
                     This bench measures READ paths only; the front's Stripe key is live."
                ));
            }
        }
        Ok(SafeGet(url.to_string()))
    }

    pub fn url(&self) -> &str {
        &self.0
    }
}

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

    #[test]
    fn the_health_endpoint_passes() {
        assert!(SafeGet::new("https://gunnar.rs/health").is_ok());
    }

    #[test]
    fn a_checkout_is_refused_and_says_why() {
        let e = SafeGet::new("https://gunnar.rs/checkout").unwrap_err();
        assert!(e.contains("checkout"), "{e}");
    }

    #[test]
    fn the_stripe_buy_url_is_refused() {
        assert!(SafeGet::new("https://buy.stripe.com/28E3cvghi1Hd1azci43VC00").is_err());
    }

    #[test]
    fn the_signup_door_is_refused() {
        assert!(SafeGet::new("https://gunnar.rs/signup").is_err());
    }

    /// The trap this list exists for: a 200-answering GET that writes.
    #[test]
    fn the_passkey_ceremony_is_refused_even_though_it_answers_200_to_anyone() {
        assert!(SafeGet::new("https://gunnar.rs/auth/passkey/login/start").is_err());
    }

    #[test]
    fn a_non_http_scheme_is_refused() {
        assert!(SafeGet::new("ssh://git.gunnar.rs:2222/nordisk/gunnar").is_err());
    }
}