aven_core/sync/client/
origin.rs1use anyhow::{Context, Result, ensure};
3use url::Url;
4
5pub const MAX_SERVER_BYTES: usize = 255;
7
8pub(crate) fn endpoint(origin: &str, path: &str) -> Result<Url> {
12 let mut url = Url::parse(origin).map_err(|_| anyhow::anyhow!("error bootstrap-origin"))?;
13 ensure!(
14 matches!(url.scheme(), "http" | "https")
15 && url.host_str().is_some()
16 && url.username().is_empty()
17 && url.password().is_none()
18 && url.query().is_none()
19 && url.fragment().is_none()
20 && url.path() == "/",
21 "error bootstrap-origin"
22 );
23 url.set_path(path);
24 Ok(url)
25}
26
27pub fn server_origin(url: &str) -> Result<String> {
29 endpoint(url, "/").context(
30 "error sync-server-url-invalid hint=\"use an HTTP or HTTPS origin with no path, query or credentials\"",
31 )?;
32 let origin = Url::parse(url)?.origin().ascii_serialization();
33 ensure!(
34 origin.len() <= MAX_SERVER_BYTES,
35 "error sync-server-url-too-long hint=\"use a server origin of at most 255 bytes\""
36 );
37 Ok(origin)
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43
44 #[test]
45 fn accepts_http_and_https_origins() {
46 for origin in [
47 "http://127.0.0.1:3746",
48 "http://100.100.20.30:3746",
49 "http://sync.private.example:3746",
50 "https://sync.example.com",
51 ] {
52 assert_eq!(server_origin(origin).unwrap(), origin);
53 }
54 }
55
56 #[test]
57 fn rejects_non_http_and_non_origin_urls() {
58 for origin in [
59 "ftp://sync.example.com",
60 "https://user:secret@sync.example.com",
61 "https://sync.example.com/private",
62 "https://sync.example.com/?token=secret",
63 "https://sync.example.com/#fragment",
64 "https://",
65 "not a URL",
66 ] {
67 let error = server_origin(origin).unwrap_err();
68 assert!(
69 format!("{error:#}").contains("sync-server-url-invalid"),
70 "{origin}: {error:#}"
71 );
72 }
73 }
74}