Skip to main content

aven_core/sync/client/
origin.rs

1//! Server origins that encrypted sync accepts.
2use anyhow::{Context, Result, ensure};
3use url::Url;
4
5/// Longest server origin; invitations carry its length in one byte.
6pub const MAX_SERVER_BYTES: usize = 255;
7
8/// Validates `origin` for encrypted transport and returns the endpoint at
9/// `path`: HTTP or HTTPS with a host and no credentials, query, fragment or
10/// path.
11pub(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
27/// Validates a server URL for encrypted transport and returns its origin.
28pub 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}