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

nordisk/edda

crates/nornir-identity-web/src/login_js.rs

//! **The login page helpers**, one script for every console (A34-51).

/// The script: `safeLocalPath`, `nextDestination(home)`, `inviteDone(text,
/// cls)` (calls the page's `onInviteDone()` when it has one, and its
/// `show(text, cls)`), `ORPHAN_HINT` and `INVITE_SPENT`.
pub const HELPERS: &str = include_str!("../assets/login-helpers.js");

/// Where a console serves [`HELPERS`] (before its own login script).
pub const PATH: &str = "/login-helpers.js";

/// The line a page carries where [`splice`] puts [`HELPERS`] (an inline
/// script, so a CSP hash covers it).
pub const MARKER: &str = "/*@nornir-identity-web:login-helpers@*/";

/// The names the script defines; a page that defines one of them itself is
/// carrying a second copy.
pub const NAMES: &[&str] = &[
    "safeLocalPath",
    "nextDestination",
    "inviteDone",
    "ORPHAN_HINT",
    "INVITE_SPENT",
];

/// The page with [`HELPERS`] in place of its one [`MARKER`]. `None` when the
/// page carries no marker or more than one.
pub fn splice(page: &str) -> Option<String> {
    (page.matches(MARKER).count() == 1).then(|| page.replacen(MARKER, HELPERS, 1))
}

/// Whether `script` defines one of [`NAMES`] itself (a second copy).
pub fn defines_a_helper(script: &str) -> Option<&'static str> {
    NAMES.iter().copied().find(|name| {
        script.contains(&format!("function {name}("))
            || script.contains(&format!("const {name} ="))
            || script.contains(&format!("let {name} ="))
    })
}

/// **`safeLocalPath` in Rust**, for the server's own redirects: a single
/// leading slash, no authority (`//host`, `/\host`), no control character or
/// space. `None` for anything else.
pub fn safe_local_path(raw: &str) -> Option<&str> {
    let ok = raw.starts_with('/')
        && !raw.starts_with("//")
        && !raw.starts_with("/\\")
        && !raw.chars().any(|c| c <= '\u{20}' || c == '\u{7f}');
    ok.then_some(raw)
}

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

    /// ★ **One script, every helper once** (A34-51).
    #[test]
    fn the_script_defines_each_helper_once() {
        for name in NAMES {
            let defs = HELPERS.matches(&format!("function {name}(")).count()
                + HELPERS.matches(&format!("const {name} =")).count();
            assert_eq!(defs, 1, "{name} is defined {defs} times");
        }
        assert_eq!(defines_a_helper(HELPERS), Some("safeLocalPath"));
        assert_eq!(defines_a_helper("function show(t) {}"), None);
        assert!(HELPERS.contains(r"/[\u0000-\u0020\u007F]/"), "the escaped control class");
        assert!(HELPERS.is_ascii(), "no literal control byte or non-ASCII in the script");
        assert!(HELPERS.contains("if (typeof onInviteDone === 'function') onInviteDone();"));
    }

    #[test]
    fn a_page_takes_the_script_at_its_one_marker() {
        let page = format!("<script>\n{MARKER}\nfunction show() {{}}\n</script>");
        let done = splice(&page).expect("one marker");
        assert!(done.contains("function safeLocalPath(raw)") && !done.contains(MARKER));
        assert_eq!(splice("<script></script>"), None);
        assert_eq!(splice(&format!("{MARKER}{MARKER}")), None);
    }

    /// The Rust rule is the script's rule.
    #[test]
    fn the_local_path_rule() {
        for ok in ["/", "/overview", "/a/b?c=d#e", "/%2F%2Fhost"] {
            assert_eq!(safe_local_path(ok), Some(ok), "{ok}");
        }
        for bad in ["", "overview", "//evil.example", "/\\evil.example", "https://evil.example", "/a b", "/a\tb", "/a\nb", "/\u{7f}", "/\0"] {
            assert_eq!(safe_local_path(bad), None, "{bad:?}");
        }
    }
}