Skip to main content

kellnr_settings/
origin.rs

1use provcfg::{ClapArgs, Configurable};
2use serde::{Deserialize, Serialize};
3
4use crate::protocol::Protocol;
5
6fn default_hostname() -> String {
7    "127.0.0.1".to_string()
8}
9
10fn default_origin_port() -> u16 {
11    std::env::var("KELLNR_ORIGIN__PORT")
12        .ok()
13        .and_then(|v| v.parse::<u16>().ok())
14        .unwrap_or(8000)
15}
16
17#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
18#[serde(default)]
19#[configurable(clap_prefix = "origin")]
20pub struct Origin {
21    /// External hostname for URLs
22    pub hostname: String,
23
24    /// External port for URLs
25    pub port: u16,
26
27    /// Protocol (http or https), not exposed on the CLI; set via TOML/env
28    #[arg(skip)]
29    pub protocol: Protocol,
30
31    /// URL path prefix
32    pub path: String,
33}
34
35impl Default for Origin {
36    fn default() -> Self {
37        Self {
38            hostname: default_hostname(),
39            port: default_origin_port(),
40            protocol: Protocol::Http,
41            path: String::new(),
42        }
43    }
44}
45
46impl Origin {
47    pub fn base_url(&self) -> String {
48        let path_prefix = self.path.trim().trim_end_matches('/');
49        if self.port == 443 || self.port == 80 {
50            format!("{}://{}{}", self.protocol, self.hostname, path_prefix)
51        } else {
52            format!(
53                "{}://{}:{}{}",
54                self.protocol, self.hostname, self.port, path_prefix
55            )
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    fn origin(port: u16, path: &str) -> Origin {
65        Origin {
66            hostname: "kellnr.example.com".to_string(),
67            port,
68            protocol: Protocol::Https,
69            path: path.to_string(),
70        }
71    }
72
73    #[test]
74    fn base_url_omits_default_ports() {
75        assert_eq!(
76            origin(443, "").base_url(),
77            "https://kellnr.example.com",
78            "443 is implied by https and must not be spelled out"
79        );
80
81        let mut http = origin(80, "");
82        http.protocol = Protocol::Http;
83        assert_eq!(http.base_url(), "http://kellnr.example.com");
84    }
85
86    #[test]
87    fn base_url_includes_non_default_port() {
88        assert_eq!(
89            origin(8000, "").base_url(),
90            "https://kellnr.example.com:8000"
91        );
92    }
93
94    #[test]
95    fn base_url_appends_path_prefix() {
96        assert_eq!(
97            origin(8000, "/registry").base_url(),
98            "https://kellnr.example.com:8000/registry"
99        );
100        assert_eq!(
101            origin(443, "/registry").base_url(),
102            "https://kellnr.example.com/registry"
103        );
104    }
105
106    /// A bare or trailing slash must not leak into the URL, otherwise callers
107    /// appending their own path produce a double slash.
108    #[test]
109    fn base_url_normalizes_slashes_in_path_prefix() {
110        for path in ["", "/", "  /  ", "/registry/", "/registry//"] {
111            let url = origin(8000, path).base_url();
112            assert!(
113                !url.ends_with('/'),
114                "base_url for path {path:?} must not end in a slash, got {url}"
115            );
116        }
117
118        assert_eq!(
119            origin(8000, "/registry/").base_url(),
120            "https://kellnr.example.com:8000/registry"
121        );
122    }
123
124    /// The `OAuth2` callback and post-logout redirect are both built by appending
125    /// to `base_url`, so the join must stay free of double slashes.
126    #[test]
127    fn base_url_composes_into_callback_url() {
128        for path in ["", "/", "/registry", "/registry/"] {
129            let url = format!("{}/api/v1/oauth2/callback", origin(443, path).base_url());
130            assert!(
131                !url.contains("com//") && !url.contains("registry//"),
132                "callback URL for path {path:?} has a double slash: {url}"
133            );
134        }
135    }
136}