#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OriginError {
NotAUrl(String),
Scheme(String),
Opaque,
}
impl std::fmt::Display for OriginError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotAUrl(why) => write!(f, "{why}"),
Self::Scheme(scheme) => write!(f, "scheme {scheme:?} is not http or https"),
Self::Opaque => f.write_str("opaque origin"),
}
}
}
impl std::error::Error for OriginError {}
pub fn origin_of(url: &str) -> Result<String, OriginError> {
let parsed = url::Url::parse(url).map_err(|err| OriginError::NotAUrl(err.to_string()))?;
if !matches!(parsed.scheme(), "http" | "https") {
return Err(OriginError::Scheme(parsed.scheme().to_owned()));
}
let origin = parsed.origin();
if !origin.is_tuple() {
return Err(OriginError::Opaque);
}
Ok(origin.ascii_serialization())
}
#[cfg(test)]
mod tests {
use super::{OriginError, origin_of};
#[test]
fn an_ordinary_url_keeps_its_origin_and_loses_everything_else() {
assert_eq!(
origin_of("https://cdn.example/theme/dark.css?v=2#top").unwrap(),
"https://cdn.example"
);
}
#[test]
fn a_non_default_port_stays_and_a_default_one_goes() {
assert_eq!(
origin_of("https://x.example:443/a").unwrap(),
"https://x.example"
);
assert_eq!(
origin_of("http://x.example:80/a").unwrap(),
"http://x.example"
);
assert_eq!(
origin_of("https://x.example:8443/a").unwrap(),
"https://x.example:8443"
);
}
#[test]
fn userinfo_never_reaches_the_output() {
assert_eq!(
origin_of("https://a:b@evil.test/x.css").unwrap(),
"https://evil.test"
);
}
#[test]
fn the_host_is_lowercased_and_punycoded() {
assert_eq!(
origin_of("https://EXAMPLE.TEST/a").unwrap(),
"https://example.test"
);
assert_eq!(
origin_of("https://münchen.test/a").unwrap(),
"https://xn--mnchen-3ya.test"
);
}
#[test]
fn a_scheme_that_is_not_http_is_refused() {
assert_eq!(
origin_of("javascript://x/"),
Err(OriginError::Scheme("javascript".to_owned()))
);
assert!(matches!(
origin_of("file:///etc/passwd"),
Err(OriginError::Scheme(_))
));
}
#[test]
fn an_opaque_origin_is_refused_rather_than_named() {
assert!(matches!(
origin_of("data:text/css,body{}"),
Err(OriginError::Scheme(_) | OriginError::Opaque)
));
}
#[test]
fn a_relative_url_is_not_an_origin() {
assert!(matches!(
origin_of("/theme.css"),
Err(OriginError::NotAUrl(_))
));
assert!(matches!(origin_of(""), Err(OriginError::NotAUrl(_))));
}
}