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:?}");
}
}
}