Skip to main content

aauth_protocol/
identifier.rs

1//! Server identifiers (AAuth §Server Identifiers) and origin comparison.
2
3use url::Url;
4
5/// `https://host`: lowercase, no port, path, query, fragment or trailing slash,
6/// and a host name rather than an IP address. Two identifiers compare by
7/// exact string equality.
8pub fn is_server_identifier(value: &str) -> bool {
9    if value.is_empty() || value != value.to_lowercase() || value.ends_with('/') {
10        return false;
11    }
12    let Ok(url) = Url::parse(value) else {
13        return false;
14    };
15    url.scheme() == "https"
16        && url.port().is_none()
17        && url.query().is_none()
18        && url.fragment().is_none()
19        && url.username().is_empty()
20        && url.password().is_none()
21        && matches!(url.path(), "" | "/")
22        && matches!(url.host(), Some(url::Host::Domain(_)))
23        && url
24            .host_str()
25            .is_some_and(|host| value == format!("https://{host}"))
26}
27
28/// Whether two URLs share scheme, host and port.
29pub fn same_origin(a: &str, b: &str) -> bool {
30    match (Url::parse(a), Url::parse(b)) {
31        (Ok(a), Ok(b)) => a.origin() == b.origin(),
32        _ => false,
33    }
34}
35
36/// A URL with its trailing slashes removed.
37pub fn trim(url: &str) -> &str {
38    url.trim_end_matches('/')
39}
40
41/// The `host[:port]` a request to `url` is addressed to, lowercased.
42pub fn authority(url: &Url) -> Option<String> {
43    let host = url.host_str()?.to_ascii_lowercase();
44    Some(match url.port() {
45        Some(port) => format!("{host}:{port}"),
46        None => host,
47    })
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn server_identifiers_are_bare_https_origins() {
56        assert!(is_server_identifier("https://ps.example"));
57        for bad in [
58            "https://ps.example/",
59            "https://PS.example",
60            "http://ps.example",
61            "https://ps.example:8443",
62            "https://ps.example/ps",
63            "https://ps.example?x",
64            "https://10.0.0.1",
65            "https://[::1]",
66            "",
67        ] {
68            assert!(!is_server_identifier(bad), "{bad}");
69        }
70    }
71
72    #[test]
73    fn origins_compare_scheme_host_and_port() {
74        assert!(same_origin(
75            "https://ps.example",
76            "https://ps.example/token"
77        ));
78        assert!(!same_origin(
79            "https://ps.example",
80            "https://internal.example/token"
81        ));
82        assert!(!same_origin(
83            "https://ps.example",
84            "https://ps.example:8443/token"
85        ));
86        assert!(!same_origin(
87            "https://ps.example",
88            "http://ps.example/token"
89        ));
90    }
91}