aven_core/sync/client/
origin.rs1use anyhow::{Context, Result, ensure};
3use url::Url;
4
5pub const MAX_SERVER_BYTES: usize = 255;
7
8const AVEN_CLOUD_HOST: &str = "sync.aventasks.dev";
10
11pub(crate) fn endpoint(origin: &str, path: &str) -> Result<Url> {
15 let (_, address) = origin
18 .split_once("://")
19 .ok_or_else(|| anyhow::anyhow!("error bootstrap-origin"))?;
20 let (authority, suffix) = address.split_once('/').unwrap_or((address, ""));
21 ensure!(
22 !authority.contains('@') && !origin.contains('\\') && suffix.is_empty(),
23 "error bootstrap-origin"
24 );
25 let mut url = Url::parse(origin).map_err(|_| anyhow::anyhow!("error bootstrap-origin"))?;
26 ensure!(
27 matches!(url.scheme(), "http" | "https")
28 && url.host_str().is_some()
29 && url.username().is_empty()
30 && url.password().is_none()
31 && url.query().is_none()
32 && url.fragment().is_none()
33 && url.path() == "/",
34 "error bootstrap-origin"
35 );
36 let host = url.host_str().unwrap_or_default();
39 ensure!(
40 url.scheme() == "https" || host.strip_suffix('.').unwrap_or(host) != AVEN_CLOUD_HOST,
41 "error sync-cloud-https-required"
42 );
43 url.set_path(path);
44 Ok(url)
45}
46
47pub fn server_origin(url: &str) -> Result<String> {
49 let url = endpoint(url, "/").context(
50 "error sync-server-url-invalid hint=\"use an HTTP or HTTPS origin with no path, query or credentials\"",
51 )?;
52 let origin = url.origin().ascii_serialization();
53 ensure!(
54 origin.len() <= MAX_SERVER_BYTES,
55 "error sync-server-url-too-long hint=\"use a server origin of at most 255 bytes\""
56 );
57 Ok(origin)
58}
59
60pub fn is_aven_cloud(origin: &str) -> bool {
62 endpoint(origin, "/").is_ok_and(|url| {
63 let host = url.host_str().unwrap_or_default();
64 host.strip_suffix('.').unwrap_or(host) == AVEN_CLOUD_HOST
65 && url.port_or_known_default() == Some(443)
66 })
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn cloud_identity_uses_the_validated_origin() {
75 for origin in [
76 "https://sync.aventasks.dev",
77 "HTTPS://SYNC.AVENTASKS.DEV:443/",
78 "https://SYNC.AVENTASKS.DEV.:443/",
79 "https://sync.aventasks.dev./",
80 "https://%73ync.aventasks.dev",
81 ] {
82 assert!(is_aven_cloud(origin), "{origin}");
83 }
84 for origin in [
85 "http://sync.aventasks.dev",
86 "https://sync.aventasks.dev:444",
87 "https://sync.aventasks.dev.evil.example",
88 "https://sync.aventasks.dev/private",
89 "https://user:secret@sync.aventasks.dev",
90 "https://sync.aventasks.dev/?x=1",
91 "https://sync.aventasks.dev/#x",
92 "https://@sync.aventasks.dev",
93 "https://sync.aventasks.dev/private/..",
94 "https://sync.aventasks.dev/.",
95 ] {
96 assert!(!is_aven_cloud(origin), "{origin}");
97 }
98 for origin in [
99 "http://SYNC.AVENTASKS.DEV:443",
100 "http://sync.aventasks.dev.:80",
101 "http://%73ync.aventasks.dev:3746",
102 ] {
103 assert!(endpoint(origin, "/e2ee/bootstrap/v1").is_err(), "{origin}");
104 }
105 }
106
107 #[test]
108 fn accepts_http_and_https_origins() {
109 for origin in [
110 "http://127.0.0.1:3746",
111 "http://100.100.20.30:3746",
112 "http://sync.private.example:3746",
113 "https://sync.example.com",
114 ] {
115 assert_eq!(server_origin(origin).unwrap(), origin);
116 }
117 }
118
119 #[test]
120 fn official_cloud_host_requires_https_without_changing_origins() {
121 for origin in [
122 "http://sync.aventasks.dev",
123 "HTTP://SYNC.AVENTASKS.DEV",
124 "http://sync.aventasks.dev:80",
125 "http://sync.aventasks.dev:443",
126 "http://sync.aventasks.dev:3746",
127 "http://sync.aventasks.dev.",
128 "http://%73ync.aventasks.dev",
129 ] {
130 let error = endpoint(origin, "/e2ee/bootstrap/v1").unwrap_err();
131 assert_eq!(
132 error.to_string(),
133 "error sync-cloud-https-required",
134 "{origin}"
135 );
136 let error = server_origin(origin).unwrap_err();
137 assert!(
138 format!("{error:#}").contains("sync-cloud-https-required"),
139 "{origin}: {error:#}"
140 );
141 }
142 for (origin, canonical) in [
143 ("https://sync.aventasks.dev", "https://sync.aventasks.dev"),
144 (
145 "HTTPS://SYNC.AVENTASKS.DEV:443/",
146 "https://sync.aventasks.dev",
147 ),
148 ("https://sync.aventasks.dev.", "https://sync.aventasks.dev."),
149 (
150 "https://sync.aventasks.dev:444",
151 "https://sync.aventasks.dev:444",
152 ),
153 (
154 "http://sync.aventasks.dev.example",
155 "http://sync.aventasks.dev.example",
156 ),
157 (
158 "http://dev-sync.aventasks.dev",
159 "http://dev-sync.aventasks.dev",
160 ),
161 ] {
162 assert_eq!(server_origin(origin).unwrap(), canonical, "{origin}");
163 }
164 }
165
166 #[test]
167 fn rejects_non_http_and_non_origin_urls() {
168 for origin in [
169 "ftp://sync.example.com",
170 "https://user:secret@sync.example.com",
171 "https://sync.example.com/private",
172 "https://sync.example.com/?token=secret",
173 "https://sync.example.com/#fragment",
174 "https://",
175 "not a URL",
176 ] {
177 let error = server_origin(origin).unwrap_err();
178 assert!(
179 format!("{error:#}").contains("sync-server-url-invalid"),
180 "{origin}: {error:#}"
181 );
182 }
183 }
184}