aauth_protocol/
identifier.rs1use url::Url;
4
5pub 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
28pub 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
36pub fn trim(url: &str) -> &str {
38 url.trim_end_matches('/')
39}
40
41pub 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}