#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Os {
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
Macos,
#[cfg_attr(not(windows), allow(dead_code))]
Windows,
}
fn expected_origin(os: Os) -> (&'static str, &'static str) {
match os {
Os::Macos => ("aiub", "localhost"),
Os::Windows => ("http", "aiub.localhost"),
}
}
pub(crate) fn is_popover_origin(uri: &str, os: Os) -> bool {
match scheme_and_host(uri) {
Some((scheme, host)) => {
let (expected_scheme, expected_host) = expected_origin(os);
scheme.eq_ignore_ascii_case(expected_scheme) && host.eq_ignore_ascii_case(expected_host)
}
None => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Navigation {
Allow,
OpenExternal,
Deny,
}
pub(crate) fn navigation(url: &str, os: Os) -> Navigation {
if is_popover_origin(url, os) {
return Navigation::Allow;
}
if super::browse::http_url(url).is_some() {
Navigation::OpenExternal
} else {
Navigation::Deny
}
}
fn scheme_and_host(uri: &str) -> Option<(String, String)> {
let (scheme, rest) = uri.split_once(':')?;
if !valid_scheme(scheme) {
return None;
}
let rest = rest.strip_prefix("//")?;
let authority = rest.split(['/', '?', '#']).next()?;
let host = authority
.rsplit_once('@')
.map_or(authority, |(_, host)| host);
if host.is_empty() || host.contains(':') {
return None;
}
Some((scheme.to_ascii_lowercase(), host.to_ascii_lowercase()))
}
fn valid_scheme(scheme: &str) -> bool {
let mut chars = scheme.bytes();
match chars.next() {
Some(first) if first.is_ascii_alphabetic() => {
chars.all(|b| b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.')
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_the_popover_origin_of_its_own_os() {
assert!(is_popover_origin("aiub://localhost/index.html", Os::Macos));
assert!(is_popover_origin(
"http://aiub.localhost/index.html",
Os::Windows
));
assert!(is_popover_origin("aiub://localhost/", Os::Macos));
assert!(is_popover_origin("http://aiub.localhost", Os::Windows));
}
#[test]
fn rejects_other_hosts_and_schemes() {
assert!(!is_popover_origin("aiub://localhost.evil/", Os::Macos));
assert!(!is_popover_origin("aiub://evil/", Os::Macos));
assert!(!is_popover_origin(
"http://aiub.localhost.evil.com/",
Os::Windows
));
assert!(!is_popover_origin("https://aiub.localhost/", Os::Windows));
assert!(!is_popover_origin("http://aiub.localhost/", Os::Macos));
assert!(!is_popover_origin(
"aiub://localhost/index.html",
Os::Windows
));
assert!(!is_popover_origin(
"http://aiub.localhost/index.html",
Os::Macos
));
}
#[test]
fn rejects_malformed_uris() {
assert!(!is_popover_origin("", Os::Macos));
assert!(!is_popover_origin("null", Os::Macos));
assert!(!is_popover_origin("localhost/index.html", Os::Macos));
assert!(!is_popover_origin("//localhost/index.html", Os::Macos));
assert!(!is_popover_origin("aiub:localhost/index.html", Os::Macos));
assert!(!is_popover_origin("javascript:alert(1)", Os::Macos));
assert!(!is_popover_origin("file:///etc/passwd", Os::Macos));
}
#[test]
fn a_port_is_a_different_origin_and_a_userinfo_hides_the_host() {
assert!(!is_popover_origin(
"aiub://localhost:8080/index.html",
Os::Macos
));
assert!(!is_popover_origin("http://aiub.localhost:80/", Os::Windows));
assert!(!is_popover_origin("aiub://localhost@evil/", Os::Macos));
assert!(is_popover_origin(
"http://user:pass@aiub.localhost/",
Os::Windows
));
}
#[test]
fn navigation_allows_only_the_popover_origin() {
assert_eq!(
navigation("aiub://localhost/index.html", Os::Macos),
Navigation::Allow
);
assert_eq!(
navigation("http://aiub.localhost/index.html", Os::Windows),
Navigation::Allow
);
assert_ne!(
navigation("aiub://localhost/index.html", Os::Windows),
Navigation::Allow
);
}
#[test]
fn navigation_sends_other_web_urls_outside_and_denies_them() {
assert_eq!(
navigation("https://example.com", Os::Macos),
Navigation::OpenExternal
);
assert_eq!(
navigation("http://example.com", Os::Windows),
Navigation::OpenExternal
);
assert_eq!(
navigation("https://ok.example\nhttps://evil", Os::Macos),
Navigation::Deny
);
assert_eq!(
navigation("HTTPS://EXAMPLE.COM", Os::Windows),
Navigation::Deny
);
}
#[test]
fn navigation_denies_non_web_schemes_without_opening() {
assert_eq!(
navigation("file:///etc/passwd", Os::Macos),
Navigation::Deny
);
assert_eq!(
navigation("javascript:alert(1)", Os::Windows),
Navigation::Deny
);
assert_eq!(
navigation("data:text/html,alert(1)", Os::Macos),
Navigation::Deny
);
assert_eq!(navigation("", Os::Windows), Navigation::Deny);
assert_eq!(navigation("null", Os::Macos), Navigation::Deny);
}
}